{"record":{"id":"ddd19bea3a8635da","repo":"jackwener/OpenCLI","slug":"twitter-search-query-is-empty","errorCode":null,"errorMessage":"twitter search query is empty","messagePattern":"twitter search query is empty","errorType":"validation","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/twitter/search.js","lineNumber":282,"sourceCode":"    description: 'Search Twitter/X for tweets, with optional --from / --has / --exclude / --product filters mapped to X\\'s search operators',\n    domain: 'x.com',\n    strategy: Strategy.COOKIE,\n    browser: true,\n    args: [\n        { name: 'query', type: 'string', required: true, positional: true, help: 'Search query. Raw X operators (e.g. \"exact phrase\", #tag, OR, lang:en, since:YYYY-MM-DD, from:, since:) are passed through unchanged.' },\n        { name: 'filter', type: 'string', default: 'top', choices: ['top', 'live'], help: 'Legacy alias for --product. Kept for backwards compatibility; if --product is set it wins.' },\n        { name: 'product', type: 'string', choices: PRODUCT_CHOICES, help: 'Which X search tab to read: top (default), live (Latest), photos, videos. Maps to the f= URL param.' },\n        { name: 'from', type: 'string', help: 'Restrict to tweets authored by <user>. Leading @ is stripped. Equivalent to appending `from:<user>` to the query.' },\n        { name: 'has', type: 'string', choices: HAS_CHOICES, help: 'Restrict to tweets that have media|images|videos|links|replies. Maps to X\\'s `filter:<has>` operator.' },\n        { name: 'exclude', type: 'string', choices: EXCLUDE_CHOICES, help: 'Exclude tweets matching <type>: replies|retweets|media|links. Maps to X\\'s `-filter:<x>` operator (retweets → -filter:nativeretweets).' },\n        { name: 'limit', type: 'int', default: 15, help: 'Maximum number of tweets to return (default 15). Result count after server-side filtering.' },\n        { name: 'top-by-engagement', type: 'int', default: 0, help: 'When set to N>0, re-rank the results by weighted engagement (likes×1 + retweets×3 + replies×2 + bookmarks×5 + log10(views+1)×0.5) and return the top N. Default 0 keeps X\\'s native ordering.' },\n    ],\n    columns: ['id', 'author', 'bio', 'text', 'created_at', 'likes', 'views', 'url', 'has_media', 'media_urls', 'media_posters', 'card', 'quoted_tweet'],\n    func: async (page, kwargs) => {\n        const finalQuery = buildSearchQuery(kwargs.query, kwargs);\n        if (!finalQuery) {\n            throw new ArgumentError('twitter search query is empty', 'Provide a non-empty <query>, or use at least one of --from / --has / --exclude.');\n        }\n        if (!Number.isInteger(Number(kwargs.limit)) || Number(kwargs.limit) <= 0) {\n            throw new ArgumentError('twitter search --limit must be a positive integer', 'Example: opencli twitter search opencli --limit 15');\n        }\n        const cookies = await page.getCookies({ url: 'https://x.com' });\n        const ct0 = cookies.find((c) => c.name === 'ct0')?.value || null;\n        if (!ct0) throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');\n        await page.goto('https://x.com/home', { waitUntil: 'load', settleMs: 1000 });\n        const operation = await resolveTwitterOperationMetadata(page, 'SearchTimeline', SEARCH_TIMELINE_OPERATION);\n        const headers = JSON.stringify({\n            'Authorization': `Bearer ${decodeURIComponent(TWITTER_BEARER_TOKEN)}`,\n            'X-Csrf-Token': ct0,\n            'X-Twitter-Auth-Type': 'OAuth2Session',\n            'X-Twitter-Active-User': 'yes',\n            'Content-Type': 'application/json',\n        });\n        const product = resolveSearchProduct(kwargs);\n        const results = [];","sourceCodeStart":264,"sourceCodeEnd":300,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/search.js#L264-L300","documentation":"ArgumentError thrown by the search command when the final assembled query string is empty — i.e. the positional <query> was empty AND no --from/--has/--exclude options contributed any terms. X's search endpoint requires a non-empty query, so the library fails fast.","triggerScenarios":"`opencli twitter search ''`, calling search programmatically with kwargs.query = null/undefined and no filter flags, or quoting issues so the shell passes an empty string as the query.","commonSituations":"Script variables that expand to empty strings, forgetting the positional query while intending a --from-only search but misspelling the flag name, or piping empty input.","solutions":["Pass a non-empty positional <query>","Or supply at least one filter: --from, --has, or --exclude","Check shell quoting/variable expansion so the query is not stripped to an empty string"],"exampleFix":"// before\nopencli twitter search ''\n// after\nopencli twitter search 'opencli' --limit 15\n// or flag-only search:\nopencli twitter search '' --from jack  # if supported spelling of flags is used","handlingStrategy":"validation","validationCode":"function hasSearchInput(kwargs) {\n  return Boolean(String(kwargs.query ?? '').trim() || kwargs.from || kwargs.has || kwargs.exclude);\n}\nif (!hasSearchInput(kwargs)) throw new Error('Provide a query or --from/--has/--exclude');","typeGuard":"function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; }","tryCatchPattern":"try {\n  await opencli.twitter.search(query);\n} catch (e) {\n  if (e.name === 'ArgumentError' && /empty/.test(e.message)) {\n    console.error('Query was empty; supply <query> or filter flags');\n  } else throw e;\n}","preventionTips":["Always pass a non-empty positional query","Check flag spelling (--from, --has, --exclude) when doing flag-only searches","Guard against empty shell/CI variables before interpolation","Trim and validate user input before invoking"],"tags":["validation","argument","twitter","search","empty-input"],"backgroundTag":"missing-required-argument","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}