From 3df7969b168e8d97ed10b2bd81e819d159cbded4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 26 Dec 2025 12:23:00 +0100 Subject: [PATCH] feat(cli): mentions target user --- CHANGELOG.md | 3 ++ src/index.ts | 55 ++++++++++++++++++++++++++++++---- src/lib/normalize-handle.ts | 13 ++++++++ tests/normalize-handle.test.ts | 24 +++++++++++++++ 4 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 src/lib/normalize-handle.ts create mode 100644 tests/normalize-handle.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f64566..597affc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Fixed - CLI `--version`: read version from `package.json`/`VERSION` (no hardcoded string) + append git sha when available. +### Changed +- `mentions`: no hardcoded user; defaults to authenticated user or accepts `--user @handle`. + ## 0.1.1 — 2025-12-26 ### Changed diff --git a/src/index.ts b/src/index.ts index 6d6d396..bec5c41 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,6 +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 { SweetisticsClient } from './lib/sweetistics-client.js'; import { type TweetData, TwitterClient } from './lib/twitter-client.js'; import { getCliVersion } from './lib/version.js'; @@ -804,10 +805,11 @@ program // Mentions command - shortcut to search for @username mentions program .command('mentions') - .description('Find tweets mentioning @clawdbot') + .description('Find tweets mentioning a user (defaults to current user)') + .option('-u, --user ', 'User handle (e.g. @steipete)') .option('-n, --count ', 'Number of tweets to fetch', '10') .option('--json', 'Output as JSON') - .action(async (cmdOpts: { count?: string; json?: boolean }) => { + .action(async (cmdOpts: { user?: string; count?: string; json?: boolean }) => { const opts = program.opts(); const timeoutMs = resolveTimeoutFromOptions(opts); const count = Number.parseInt(cmdOpts.count || '10', 10); @@ -815,6 +817,9 @@ 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; + if (useSweetistics) { if (!sweetistics.apiKey) { console.error('❌ Sweetistics engine selected but no API key provided.'); @@ -825,7 +830,18 @@ program apiKey: sweetistics.apiKey, timeoutMs, }); - const result = await client.search('@clawdbot', count); + + if (!query) { + const who = await client.getCurrentUser(); + const handle = normalizeHandle(who.user?.username); + if (!handle) { + console.error(`❌ Could not determine current user (${who.error ?? 'Unknown error'}). Use --user .`); + process.exit(1); + } + query = `@${handle}`; + } + + const result = await client.search(query, count); if (result.success && result.tweets) { printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' }); return; @@ -850,7 +866,36 @@ program } const client = new TwitterClient({ cookies, timeoutMs }); - const result = await client.search('@clawdbot', count); + + if (!query) { + const who = await client.getCurrentUser(); + const handle = normalizeHandle(who.user?.username); + if (!handle) { + if (sweetistics.apiKey) { + console.error( + `⚠️ Could not determine current user (${who.error ?? 'Unknown error'}); trying Sweetistics fallback...`, + ); + const fallbackWho = await new SweetisticsClient({ + baseUrl: sweetistics.baseUrl, + apiKey: sweetistics.apiKey, + timeoutMs, + }).getCurrentUser(); + const fallbackHandle = normalizeHandle(fallbackWho.user?.username); + if (!fallbackHandle) { + console.error(`❌ Could not determine current user. Use --user .`); + process.exit(1); + } + query = `@${fallbackHandle}`; + } else { + console.error(`❌ Could not determine current user (${who.error ?? 'Unknown error'}). Use --user .`); + process.exit(1); + } + } else { + query = `@${handle}`; + } + } + + const result = await client.search(query, count); if (result.success && result.tweets) { printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' }); @@ -860,7 +905,7 @@ program baseUrl: sweetistics.baseUrl, apiKey: sweetistics.apiKey, timeoutMs, - }).search('@clawdbot', count); + }).search(query, count); if (fallback.success && fallback.tweets) { printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' }); } else { diff --git a/src/lib/normalize-handle.ts b/src/lib/normalize-handle.ts new file mode 100644 index 0000000..f093399 --- /dev/null +++ b/src/lib/normalize-handle.ts @@ -0,0 +1,13 @@ +export function normalizeHandle(input?: string | null): string | null { + const raw = (input ?? '').trim(); + if (!raw) return null; + + const withoutAt = raw.startsWith('@') ? raw.slice(1) : raw; + const handle = withoutAt.trim(); + if (!handle) return null; + + // X/Twitter handles are traditionally max 15 chars; keep strict to avoid surprising queries. + if (!/^[A-Za-z0-9_]{1,15}$/.test(handle)) return null; + + return handle; +} diff --git a/tests/normalize-handle.test.ts b/tests/normalize-handle.test.ts new file mode 100644 index 0000000..31d48b4 --- /dev/null +++ b/tests/normalize-handle.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { normalizeHandle } from '../src/lib/normalize-handle.js'; + +describe('normalizeHandle', () => { + it('accepts bare handle', () => { + expect(normalizeHandle('steipete')).toBe('steipete'); + }); + + it('accepts @handle and trims whitespace', () => { + expect(normalizeHandle(' @steipete ')).toBe('steipete'); + }); + + it('rejects empty input', () => { + expect(normalizeHandle('')).toBeNull(); + expect(normalizeHandle(' ')).toBeNull(); + expect(normalizeHandle(null)).toBeNull(); + }); + + it('rejects invalid characters and too-long handles', () => { + expect(normalizeHandle('@stei-pete')).toBeNull(); + expect(normalizeHandle('@steipete!')).toBeNull(); + expect(normalizeHandle('a'.repeat(16))).toBeNull(); + }); +});