diff --git a/README.md b/README.md index da515ed..9daf9b1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ bird replies 1234567890123456789 # Search + mentions bird search "from:steipete" -n 5 bird mentions -n 5 +bird mentions --user @steipete -n 5 ``` ## Commands @@ -38,7 +39,7 @@ bird mentions -n 5 - `bird replies [--json]` — list replies to a tweet. - `bird thread [--json]` — show the full conversation thread. - `bird search "" [-n count] [--json]` — search for tweets matching a query. -- `bird mentions [-n count] [--json]` — find tweets mentioning @clawdbot. +- `bird mentions [-n count] [--user @handle] [--json]` — find tweets mentioning a user (defaults to the authenticated user). - `bird whoami` — print which Twitter account your cookies belong to. - `bird check` — show which credentials are available and where they were sourced from. @@ -92,6 +93,10 @@ Environment shortcuts: - `--json` prints raw tweet objects for read/replies/thread/search/mentions. - `read` returns full text for Notes and Articles when present. +## Version + +`bird --version` prints `package.json` version plus current git sha when available, e.g. `0.1.1 (3df7969b)`. + ## Media uploads (Sweetistics only) - Attach media with `--media` (repeatable) and optional `--alt` per item. diff --git a/src/index.ts b/src/index.ts index bec5c41..dec6ef4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ import { resolveCliInvocation } from './lib/cli-args.js'; import { resolveCredentials } from './lib/cookies.js'; import { type EngineMode, resolveEngineMode, shouldUseSweetistics } from './lib/engine.js'; import { extractTweetId } from './lib/extract-tweet-id.js'; -import { normalizeHandle } from './lib/normalize-handle.js'; +import { mentionsQueryFromUserOption, normalizeHandle } from './lib/normalize-handle.js'; import { SweetisticsClient } from './lib/sweetistics-client.js'; import { type TweetData, TwitterClient } from './lib/twitter-client.js'; import { getCliVersion } from './lib/version.js'; @@ -817,8 +817,13 @@ program const engine = resolveEngineMode(opts.engine); const useSweetistics = shouldUseSweetistics(engine, Boolean(sweetistics.apiKey)); - const explicitHandle = normalizeHandle(cmdOpts.user); - let query: string | null = explicitHandle ? `@${explicitHandle}` : null; + const fromUserOpt = mentionsQueryFromUserOption(cmdOpts.user); + if (fromUserOpt.error) { + console.error(`❌ ${fromUserOpt.error}`); + process.exit(2); + } + + let query: string | null = fromUserOpt.query; if (useSweetistics) { if (!sweetistics.apiKey) { diff --git a/src/lib/normalize-handle.ts b/src/lib/normalize-handle.ts index f093399..2d6572e 100644 --- a/src/lib/normalize-handle.ts +++ b/src/lib/normalize-handle.ts @@ -11,3 +11,20 @@ export function normalizeHandle(input?: string | null): string | null { return handle; } + +export function mentionsQueryFromUserOption(userOption?: string | null): { + query: string | null; + error: string | null; +} { + if (typeof userOption === 'undefined') return { query: null, error: null }; + + const handle = normalizeHandle(userOption); + if (!handle) { + return { + query: null, + error: 'Invalid --user handle. Expected something like @steipete (letters, digits, underscore; max 15).', + }; + } + + return { query: `@${handle}`, error: null }; +} diff --git a/tests/normalize-handle.test.ts b/tests/normalize-handle.test.ts index 31d48b4..89de4a6 100644 --- a/tests/normalize-handle.test.ts +++ b/tests/normalize-handle.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { normalizeHandle } from '../src/lib/normalize-handle.js'; +import { mentionsQueryFromUserOption, normalizeHandle } from '../src/lib/normalize-handle.js'; describe('normalizeHandle', () => { it('accepts bare handle', () => { @@ -22,3 +22,20 @@ describe('normalizeHandle', () => { expect(normalizeHandle('a'.repeat(16))).toBeNull(); }); }); + +describe('mentionsQueryFromUserOption', () => { + it('returns null query when option omitted', () => { + expect(mentionsQueryFromUserOption(undefined)).toEqual({ query: null, error: null }); + }); + + it('returns normalized @query for valid handle', () => { + expect(mentionsQueryFromUserOption('@steipete')).toEqual({ query: '@steipete', error: null }); + expect(mentionsQueryFromUserOption(' steipete ')).toEqual({ query: '@steipete', error: null }); + }); + + it('returns error for invalid handle', () => { + const result = mentionsQueryFromUserOption('@stei-pete'); + expect(result.query).toBeNull(); + expect(result.error).toMatch(/Invalid --user handle/); + }); +});