feat: auto-generate skill.ts from Pastel/Zod command metadata#1
Open
cosmicallycooked wants to merge 74 commits intomainfrom
Open
feat: auto-generate skill.ts from Pastel/Zod command metadata#1cosmicallycooked wants to merge 74 commits intomainfrom
cosmicallycooked wants to merge 74 commits intomainfrom
Conversation
…te (#1) * Suppress spinner in --json mode for interests create/update * Handle --json errors via stderr and simplify update label --------- Co-authored-by: cosmicallycooked <190560893+cosmicallycooked@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ands (1a35e1#3) The root problem: fetch() has no built-in timeout. If the Sonar server accepts a TCP connection but stalls before sending a response the spinner renders indefinitely and the process must be killed manually. Changes: src/lib/client.ts - gql() now wraps every request in an AbortController with a configurable timeoutMs (default 20 s). AbortError is caught and rethrown as a human- readable message that names the timeout, explains the likely cause, and directs the operator to check SONAR_API_URL and retry. src/commands/ingest/tweets.tsx src/commands/ingest/bookmarks.tsx - Added a component-level 15 s wall-clock deadline (useRef + setTimeout) that fires independently of the fetch timeout. This catches the edge case where the request itself times out inside the gql() call but React has not yet had a chance to surface the error. - Timeout state is tracked separately so the UI can render a yellow warning (rather than a red error) with a note to run 'sonar ingest monitor' — the server may have queued the job even if the response was lost. - When the mutation returns false (job not queued) a follow-up hint directs the operator to check their account status. src/commands/monitor.tsx - The raw fetch() call for /indexing/status now uses its own AbortController (10 s). In --watch mode this prevents a single hung poll from freezing the entire watch loop. Co-authored-by: cosmicallycooked <190560893+cosmicallycooked@users.noreply.github.com>
…5e1#4) * fix: add timeout handling to --from-prompt AI calls with actionable errors The root problem: callOpenAI() and callAnthropic() in src/lib/ai.ts call fetch() with no timeout. The OpenAI path uses the web_search_preview tool which can take 30-60 s even on a healthy connection; any network hiccup or provider slowdown causes the spinner to hang indefinitely. Changes: src/lib/ai.ts - Added fetchWithTimeout() helper that wraps every AI fetch in an AbortController. Deadlines are set per-vendor: OpenAI 90 s (web_search_preview adds latency) Anthropic 60 s - AbortError is caught and rethrown as a structured message that names the vendor, the elapsed timeout, three likely causes, and the suggestion to retry or switch vendors with --vendor. - Applied to all four call sites: callOpenAI, callAnthropic, callOpenAIReply, callAnthropicReply. src/commands/interests/create.tsx src/commands/interests/update.tsx - Spinner label for --from-prompt now includes the max expected wait time so operators know the long wait is normal and not a hang: 'Generating interest via openai... (may take up to 90s with web search)' * fix: address all Copilot review comments on from-prompt timeout - fetchWithTimeout now accepts a processResponse callback that wraps both the fetch() call and the body consumption (res.json()). The AbortController timer stays active until processResponse resolves or rejects, ensuring a stalled body download is caught by the same deadline as a stalled connection. clearTimeout moved to finally so the timer is always cleaned up. - Timeout error message is now vendor-aware: the OpenAI web_search bullet is only appended when vendorLabel includes 'openai', avoiding misleading output when the failing vendor is Anthropic. - OPENAI_TIMEOUT_MS and ANTHROPIC_TIMEOUT_MS are now exported from ai.ts. Spinner labels in create.tsx and update.tsx import these constants instead of hard-coding '90'/'60', and compute vendor via a single getVendor() call. The 'with web search' qualifier in the spinner is now conditional on vendor === 'openai' so Anthropic labels no longer mention web search. --------- Co-authored-by: cosmicallycooked <190560893+cosmicallycooked@users.noreply.github.com>
* feat(config): add sqlite backup/restore/verify commands * fix: address all Copilot review comments on data backup/restore/verify - Extract shared integrityCheck() + copyDbWithSidecars() helpers into utils.ts, eliminating the triplication across backup/restore/verify and ensuring the DB handle is always closed via try/finally (fixes handle leak on the error path in all three commands) - backup.tsx: use flags.out.trim() as the actual output path so leading/ trailing whitespace cannot cause silent filesystem errors - backup.tsx: replace plain copyFileSync with better-sqlite3's db.backup() (wraps sqlite3_backup_* C API) which produces a safe online backup under concurrent writes without requiring an exclusive lock or prior WAL checkpoint - restore.tsx: add resolve()-based same-path guard before any file operations so 'from === to' is rejected with a clear error before any data is touched - restore.tsx: pre-restore snapshot now uses copyDbWithSidecars() so WAL/SHM sidecars are included — the snapshot is a complete, restorable point-in-time image of the target DB - restore.tsx: if the post-restore integrity check fails, automatically roll back to the pre-restore snapshot so a corrupted backup cannot leave the user with a broken local database * fix: address new Copilot review comments on db backup/restore - restore.tsx: replace regex path-stripping with dirname(dst) for cross-platform correctness. The previous expression used a /\/ regex which only strips POSIX-style separators; on Windows path.resolve() returns backslash-separated paths so mkdirSync would attempt to create a directory named after the full destination file path. dirname() handles both / and \ correctly on all platforms. - PR description: remove stale claims about WAL/SHM sidecar copying in the backup command. The current implementation uses db.backup(out) (SQLite online backup API) which produces a consistent page-level snapshot without requiring sidecar file handling. Description updated to accurately reflect the implementation. --------- Co-authored-by: cosmicallycooked <190560893+cosmicallycooked@users.noreply.github.com>
…35e1#5) The root problem: 'No tweets found in this window.' and 'Inbox is empty.' give no guidance on why the result is empty or what to do next. For agents consuming --json output there is also no signal that the empty result might indicate a configuration or data-pipeline problem vs. a genuine quiet period. Changes: src/commands/feed.tsx - Terminal (non-json) empty state now renders a yellow header + numbered checklist tailored to the --kind flag: bookmarks → remind to run 'sonar ingest bookmarks' default / followers / following → widen window, check interests, trigger ingest, run matching, check account - JSON mode: when result is empty, a structured diagnostic is written to stderr (stdout still receives the valid empty JSON array []). This lets piped agents distinguish an empty result from an error while still giving a human operator reading stderr actionable next steps. src/commands/inbox/index.tsx - Terminal empty state renders a yellow header + numbered checklist that adapts to the active --status filter: • If a specific status is set, first step is 'try --all' • Then: check interests, ingest, match, monitor, account - JSON mode: same stderr-diagnostic pattern as feed — empty array on stdout, structured hint on stderr with status label, causes, and remediation commands. Co-authored-by: cosmicallycooked <190560893+cosmicallycooked@users.noreply.github.com>
Co-authored-by: cosmicallycooked <190560893+cosmicallycooked@users.noreply.github.com>
…r' (1a35e1#9) The monitor subcommand lives at the top-level (sonar monitor), not under the ingest namespace (sonar ingest monitor). Fix all user-facing references in diagnostic tips, error messages, and the README. Co-authored-by: cosmicallycooked <190560893+cosmicallycooked@users.noreply.github.com>
* feat: add sonar quickstart command
First-run setup wizard that guides new users through:
1. Auth check — friendly exit with setup instructions if no API key
2. Bootstraps in a single GraphQL call (me + projects)
3. If interests already exist, jumps straight to inbox display
4. If no interests: proposes 3 starter interest drafts based on the
user's X profile (sensible defaults for the tech/AI crowd)
5. Interactive Y/n confirmation prompt via Ink useInput
6. Creates each interest sequentially with live progress display
7. Triggers indexTweets ingest mutation
8. Shows current inbox items, or an empty-state message with
actionable next steps if indexing hasn't completed yet
Also adds 'quickstart' to the top-level command list in src/commands/index.tsx.
* fix: address CodeRabbit review comments on quickstart PR
- CHANGELOG.md: add sonar quickstart command to 0.2.0 release notes
- quickstart.tsx: distinguish 'created' vs 'pre-existing' interests in
InboxView; 'Interests created and indexing triggered!' now only shows
when interests were just created; pre-existing interests path shows
'Your interests are set up — indexing is in progress.' instead
* fix: replace process.exit(0) with useApp().exit() in handleAbort
Avoids skipping Ink's cleanup which can leave the terminal in a bad
state. useApp() is called at component level per React hooks rules and
exit() is destructured for use inside the handleAbort callback.
* fix: address CodeRabbit review comments on quickstart command
- Fix non-exhaustive switch: add 'inbox-empty' render case and wire up
the phase in handleConfirm when inbox returns empty results
- Fix useInput confirm guard: remove empty-string match so non-printable
keys (arrows, Tab, etc.) no longer accidentally trigger onConfirm
- Fix Spinner in CreatingView: pass label="" to suppress default 'Loading…' text
- Remove dead code: inbox-empty union variant is now fully wired up
* refactor(quickstart): remove redundant inbox-empty phase
inbox-empty was a dead variant that rendered <InboxView items={[]} created={true} />
— identical to what the inbox phase already produces when items is an empty array.
Collapse the two by passing items: [] directly into the inbox phase.
Closes CodeRabbit nitpick on PR 1a35e1#7.
* fix(quickstart): trim BOOTSTRAP_QUERY PII, NaN guard in relativeTime, re-entrancy guard in handleConfirm
- BOOTSTRAP_QUERY: trim to xHandle + projects.id only (removes email, xid,
isPayingCustomer, and all other unused me fields; projects trimmed to id only
since we only check .length)
- relativeTime(): add isNaN guard — invalid date strings now return '?' instead
of 'NaNd'
- handleConfirm: add confirmedRef to prevent double-invoke on rapid keypresses
- Drop now-unused Account and Interest type imports
* fix: address CodeRabbit review comments on quickstart command
- clamp relativeTime diff to Math.max(0, diff) to prevent negative
values when passed a future date
- trim SONAR_API_KEY and config.token in hasToken() so whitespace-only
strings are treated as unauthenticated
- trim createOrUpdateProject selection set to only the consumed nanoId
field (return value is unused)
---------
Co-authored-by: cosmicallycooked <190560893+cosmicallycooked@users.noreply.github.com>
Co-authored-by: Prime <263221252+cosmicallycooked@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New default view (sonar): merged feed + inbox ranked by score - sonar interests add: create interest from natural language prompt - sonar interests edit: renamed from update, same flags - sonar refresh: single command to trigger ingest + match pipeline - sonar status: combined account, plan, inbox counts, and job queues - sonar archive/later/skip: top-level triage actions - Move TweetCard + FeedTweet types to src/components/TweetCard.tsx - Preserve all legacy commands in src/commands-legacy/ for reference
The backend renamed projects to topics but the CLI queries were still referencing the old field name.
1 day produced only 7 matches vs 27+ with 3 days in experiments. The plan cap still applies server-side.
Remove deprecated topic fields from local sync queries and make interest persistence tolerant of missing legacy fields to prevent schema drift breakage.
Align nuke/deleteDatabase with ~/.sonar/data.db and report exactly what was removed so cleanup is reliable and explicit.
Replace stale interests/inbox/ingest/account references with the actual topics/default-view/status/config/sync command surface so generated skill docs match runtime behavior.
Add command surface snapshots, docs parity checks, data compatibility checks, and schema validation scripts, then enforce them in CI to catch CLI drift before release.
…lse spinner - sonar account add/switch/remove for managing multiple API keys - ~/.sonar/accounts.json with auto-migration from config.json - SONAR_API_KEY env var still takes priority - Help banner with spaced SONAR header and version - Switched spinner to unicode-animations pulse
…th random default
…ests to topics in schema
- Add scripts/generate-skill.ts: walks src/commands/**/*.tsx, introspects Zod option schemas (names, types, descriptions, defaults), and generates src/lib/skill.ts with SKILL_CONTENT embedded as a template literal - Add scripts/check-skill-drift.mjs: runs generator in --dry-run mode and compares output against the committed skill.ts; fails if they differ - Update build to run generate:skill before tsc so skill.ts is always in sync - Add drift:skill:check script and include it in drift:check - Regenerate src/lib/skill.ts from current command surface (30 commands) Closes: AES-7 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
scripts/generate-skill.ts: walkssrc/commands/**/*.tsx, introspects each command's exported Zodoptionsschema (flag names, types, descriptions, defaults), and generatessrc/lib/skill.tswithSKILL_CONTENTembedded automaticallyscripts/check-skill-drift.mjs: runs generator in--dry-runmode and compares output against committedskill.ts; fails with a clear message if they differpnpm build(tsx scripts/generate-skill.ts && tsc) andprepublishOnlydrift:skill:checkscript and includes it indrift:checksrc/lib/skill.tsfrom current 30-command surfaceTest plan
pnpm buildsucceeds and regeneratesskill.tspnpm drift:skill:checkpasses with no changespnpm drift:skill:checkto fail untilpnpm generate:skillis run and committedpnpm generate:skillproduces functionally equivalent skill content covering all commands and their flagspnpm drift:checkpasses end-to-endCloses AES-7
🤖 Generated with Claude Code