Skip to content

feat: auto-generate skill.ts from Pastel/Zod command metadata#1

Open
cosmicallycooked wants to merge 74 commits intomainfrom
feat/auto-generate-skill-definition
Open

feat: auto-generate skill.ts from Pastel/Zod command metadata#1
cosmicallycooked wants to merge 74 commits intomainfrom
feat/auto-generate-skill-definition

Conversation

@cosmicallycooked
Copy link
Copy Markdown
Owner

Summary

  • Adds scripts/generate-skill.ts: walks src/commands/**/*.tsx, introspects each command's exported Zod options schema (flag names, types, descriptions, defaults), and generates src/lib/skill.ts with SKILL_CONTENT embedded automatically
  • Adds scripts/check-skill-drift.mjs: runs generator in --dry-run mode and compares output against committed skill.ts; fails with a clear message if they differ
  • Integrates generation into pnpm build (tsx scripts/generate-skill.ts && tsc) and prepublishOnly
  • Adds drift:skill:check script and includes it in drift:check
  • Regenerated src/lib/skill.ts from current 30-command surface

Test plan

  • pnpm build succeeds and regenerates skill.ts
  • pnpm drift:skill:check passes with no changes
  • Adding a new command file causes pnpm drift:skill:check to fail until pnpm generate:skill is run and committed
  • pnpm generate:skill produces functionally equivalent skill content covering all commands and their flags
  • pnpm drift:check passes end-to-end

Closes AES-7

🤖 Generated with Claude Code

cosmicallycooked and others added 30 commits February 22, 2026 21:08
…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.
1a35e1 and others added 30 commits April 8, 2026 21:47
…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
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants