feat(ai-cli): add @tanstack/ai-cli (ts-ai), a CLI over TanStack AI#717
feat(ai-cli): add @tanstack/ai-cli (ts-ai), a CLI over TanStack AI#717AlemTuzlak wants to merge 13 commits into
Conversation
Machine-first CLI exposing the core activities as the `ts-ai` binary: chat, image, video, audio, speech, transcribe, summarize, plus introspect, mcp, and update. - Stateless single-shot subprocess design: --json buffered output, --stream AG-UI events, strict stdout-is-payload / stderr-is-everything-else, typed exit codes (0-4), and structured error objects. - provider/model slug resolution; openai, anthropic, gemini, openrouter and fal bundled for zero-install; keys via --api-key, a conventional .env, or env vars; all options expressible via --config (file or inline JSON). - chat: tools via --mcp servers, sandboxed --code-mode, --schema structured output, stateless --messages history; rich JSON envelope via StreamProcessor. - Lazy Ink TTY layer (never loaded on the machine path): animated home menu, interactive chat REPL, inline image preview. - ts-ai introspect (machine-readable manifest) and ts-ai mcp (every command exposed as an MCP tool over stdio). - New testing/cli subprocess E2E project + unit tests, docs page, changeset.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughA new Changests-ai CLI package and contract
Estimated code review effort: 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
|
🚀 Changeset Version Preview4 package(s) bumped directly, 28 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
|
View your CI Pipeline Execution ↗ for commit e4f4d83
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗ ☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-anthropic
@tanstack/ai-cli
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-devtools-core
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-mcp
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
The adapter-construction unit tests dynamically imported provider packages and depended on the pnpm node_modules layout (e.g. whether a non-bundled provider was resolvable), which differs between platforms and broke on CI. Extract the pure factory-candidate-name derivation into `factoryCandidatesForProvider` and unit-test that instead — it still guards the OpenRouter casing/Text and fal alt-prefix regressions, with no module resolution or SDK construction, so it's platform-independent.
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/ai-cli/package.json (1)
1-83:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd explicit
packageManagerpin to satisfy repo package-manager contract.This package.json is missing
packageManager: "pnpm@10.17.0", which is required by the repo rule for package/task consistency.Suggested fix
{ "name": "`@tanstack/ai-cli`", "version": "0.1.0", + "packageManager": "pnpm@10.17.0", "description": "ts-ai — a type-safe CLI over TanStack AI: chat, image, video, audio, speech, transcribe, and summarize from the terminal or any agent harness.",As per coding guidelines, "
**/package.json: Use pnpm@10.17.0 as the package manager for all tasks and dependency management."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-cli/package.json` around lines 1 - 83, The package.json is missing the required packageManager field; add a top-level "packageManager": "pnpm@10.17.0" entry to the JSON (alongside existing keys like "name", "version", "type") so the package adheres to the repo package-manager contract; update the package.json in this package to include that exact key/value.Source: Coding guidelines
packages/ai-cli/tests/core.test.ts (1)
1-260:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMove this unit test alongside source files to match repo test placement rules.
This test currently lives under
packages/ai-cli/tests/rather than next to the source modules it validates.As per coding guidelines,
**/*.test.ts: Place unit tests alongside source code in*.test.tsfiles.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-cli/tests/core.test.ts` around lines 1 - 260, Test file is placed in the top-level tests folder instead of next to the modules it validates; move it next to the source modules and adjust imports. Relocate the test so it lives alongside the package's source (next to the core/ and manifest/ modules) and update all import paths accordingly (references to providers, config, output, cli/options, core/exit-codes, core/io, cli/mcp-clients, manifest/manifest and manifest/types should become local relative imports). Ensure the test filename remains core.test.ts and verify imports for findCommand, resolveModelSlug, instantiateAdapter, resolveApiKey, mergeOptions, resolveOutputMode, coerceFlags, CliError/ExitCode/toCliError, inferMimeType/resolvePrompt, and tokenizeCommand still resolve.Source: Coding guidelines
🟡 Minor comments (5)
packages/ai-cli/src/cli/interactive.ts-45-45 (1)
45-45:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPreserve empty prompt values when dispatching command args.
Line 45 uses a truthy check, so
''is converted to no positional args. Use anundefinedcheck so user input is passed consistently.Suggested fix
- await dispatchCommand(spec, choice.prompt ? [choice.prompt] : [], { + await dispatchCommand(spec, choice.prompt !== undefined ? [choice.prompt] : [], { model, preview: true, })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-cli/src/cli/interactive.ts` at line 45, The dispatch call currently drops empty-string prompts because it uses a truthy check; update the argument expression so it tests explicitly for undefined (e.g., choice.prompt !== undefined) and passes [choice.prompt] when prompt is an empty string, ensuring dispatchCommand(spec, ...) receives the user's empty prompt; locate the invocation of dispatchCommand with spec and choice.prompt and replace the truthy check with an undefined check to preserve '' as a valid positional arg.packages/ai-cli/src/render/repl.tsx-27-59 (1)
27-59:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAllow immediate Escape while waiting on provider response.
Line 28 returns early on
busy, so Escape at Line 56 is unreachable during in-flight requests. If a request stalls, users can’t quit from the REPL flow.Suggested fix
useInput((input, key) => { - if (busy) return + if (key.escape) { + exit() + return + } + if (busy) return if (key.return) { const text = draft.trim() setDraft('') if (!text) return @@ - if (key.escape) { - exit() - return - } if (key.backspace || key.delete) { setDraft((d) => d.slice(0, -1)) return }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-cli/src/render/repl.tsx` around lines 27 - 59, The early "if (busy) return" in the useInput handler prevents the key.escape branch from running while a request is in-flight; allow Escape to always work by either moving the "if (key.escape) { exit(); return }" check above the "if (busy) return" or by changing the busy guard to "if (busy && !key.escape) return" so that exit() can be invoked while respond(...) is pending; update the useInput handler around the busy/key.escape logic (references: useInput, busy, key.escape, exit, respond, setBusy) to ensure Escape exits immediately during stalled requests.packages/ai-cli/src/cli/activities/transcribe.ts-79-79 (1)
79-79:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPreserve zero-duration values in machine output.
Line 79 omits
durationwhen it is0. Use a nullish check so0remains serializable.Proposed fix
- ...(result.duration ? { duration: result.duration } : {}), + ...(result.duration != null ? { duration: result.duration } : {}),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-cli/src/cli/activities/transcribe.ts` at line 79, The object spread currently omits duration when it's 0 because it uses a truthy check; update the conditional to use a nullish check so zero is preserved: replace the ternary using "result.duration ? { duration: result.duration } : {}" with a nullish-aware check like "result.duration != null ? { duration: result.duration } : {}", referencing the variable/result object and the spread expression where duration is added (the line with ...(result.duration ? { duration: result.duration } : {} ) in transcribe.ts).docs/cli/overview.md-38-50 (1)
38-50:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse one flag spelling consistently:
--api-key.Line 38 and Line 50 mention
--apiKey, while the examples use--api-key(Line 45). Prefer--api-keythroughout to avoid CLI-flag confusion.Proposed fix
-Pick a model with a `provider/model` slug. The API key comes from `--apiKey` +Pick a model with a `provider/model` slug. The API key comes from `--api-key` @@ -and `--apiKey` always take precedence over `.env`. +and `--api-key` always take precedence over `.env`.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/cli/overview.md` around lines 38 - 50, Update the CLI docs to use a single flag spelling: replace every instance of the camelCase flag `--apiKey` with the kebab-case `--api-key` so it matches the examples and the actual `ts-ai` command usage; search for occurrences of `--apiKey` in the docs/cli/overview.md and change them to `--api-key`, and ensure the sample commands (e.g., the `ts-ai chat` examples) and explanatory text consistently reference `--api-key`.packages/ai-cli/src/cli/activities/audio.ts-44-49 (1)
44-49:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSanitize parsed
durationbefore passing it downstream.String parsing can yield
NaN/Infinity, which currently flows intogenerateAudiounchanged.Suggested patch
- const duration = - typeof ctx.options.duration === 'number' - ? ctx.options.duration - : typeof ctx.options.duration === 'string' - ? Number(ctx.options.duration) - : undefined + const parsedDuration = + typeof ctx.options.duration === 'number' + ? ctx.options.duration + : typeof ctx.options.duration === 'string' + ? Number(ctx.options.duration) + : undefined + const duration = + typeof parsedDuration === 'number' && Number.isFinite(parsedDuration) + ? parsedDuration + : undefined🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-cli/src/cli/activities/audio.ts` around lines 44 - 49, The parsed duration from ctx.options.duration (the block computing const duration) can be NaN or Infinity and must be validated before being passed to generateAudio; update the logic that computes duration (using ctx.options.duration) to coerce string to Number, then check Number.isFinite(value) and that it falls in an acceptable range (e.g., > 0 and <= a sane max) and otherwise set duration to undefined (or omit it) so generateAudio never receives NaN/Infinity; reference the const duration computation and the downstream generateAudio call when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/config.json`:
- Around line 223-226: The new docs entry for the "ts-ai CLI" node (the object
with "label": "ts-ai CLI" and "to": "cli/overview") is missing an updatedAt
field; add "updatedAt": "2026-06-07" to that object so it contains both addedAt
and updatedAt with the current date.
In `@packages/ai-cli/src/cli/activities/chat.ts`:
- Around line 53-58: The parsing of ctx.options.maxSteps into maxSteps (and the
similar parse at line 93) currently relies on truthiness and Number(...) which
can produce NaN or drop valid zero; change the logic in the maxSteps parsing and
the second occurrence to explicitly validate and normalize the value: if
ctx.options.maxSteps is a number use it, if it's a string attempt to parse it
with Number(...) and then check Number.isFinite(parsed) (or
!Number.isNaN(parsed)) before assigning, otherwise treat as undefined and
surface a usage error (throw or return a clear validation error); ensure
explicit 0 is accepted rather than being treated as falsy.
- Around line 173-179: parseMessages is only checking that the input is an array
then force-casting to Array<ModelMessageLike>, allowing invalid elements to
leak; update parseMessages to validate each array element against the expected
ModelMessageLike shape (e.g., ensure each item is a non-null object and has the
required properties such as role and content or other fields defined by
ModelMessageLike) and throw a CliError('USAGE', '--messages must be a JSON array
of messages with valid message objects.') if any item fails; implement a small
type-guard helper (e.g., isModelMessageLike) and use it while iterating the
array before returning the typed array.
In `@packages/ai-cli/src/cli/activities/image.ts`:
- Around line 61-64: The code currently hardcodes mimeType = 'image/png' and
looks up EXT_BY_MIME, which mislabels non-PNG outputs; change the logic in the
image handling flow (references: mediaSourceToBytes, bytes, mimeType,
EXT_BY_MIME) to determine the actual MIME type instead of hardcoding: obtain the
MIME from the media source or detect it from the returned bytes (use the
existing mediaSource or a byte-based detector), set mimeType to that detected
value, then derive ext = EXT_BY_MIME[mimeType] ?? fallback (e.g., 'bin'); apply
the same fix to the other occurrences noted around the blocks referenced (lines
handling the first output and the subsequent outputs) so file names and JSON
mimeType reflect the true format.
- Around line 65-70: The code that computes `target` (using `output`, `index`,
and `suffixPath` in packages/ai-cli/src/cli/activities/image.ts) currently sends
the first image to stdout when `output === '-'` and writes the rest to files;
change this to fail fast: before the loop/creation of `target`, check if `output
=== '-'` AND `count > 1` (or the variable that represents number of images) and
throw a usage error or exit with a clear message (e.g., "cannot write multiple
images to stdout; use a file output or single image"), so you never mix stdout
and file writes; update the place that computes `target` to assume `output ===
'-'` only when a single image is allowed.
In `@packages/ai-cli/src/cli/activities/summarize.ts`:
- Around line 36-41: The computed maxLength value (from ctx.options.maxLength)
can become NaN when converting arbitrary strings and is being passed to
summarize; validate it before the summarize call by checking Number conversion
produced a finite integer (e.g., Number.isFinite and Number.isInteger or >0 as
required) and reject invalid values by throwing a clear usage error or returning
early; update the maxLength assignment/validation near where maxLength is
defined and before the summarize invocation so summarize always receives a valid
numeric maxLength.
In `@packages/ai-cli/src/cli/activities/video.ts`:
- Around line 119-130: The infinite polling loop in the video job waiter
(involving getVideoJobStatus, POLL_INTERVAL_MS, sleep, jobId and
ctx.logger.info) must be bounded: add either a maxAttempts counter or a
maxPollingTimeMs start timestamp check and increment attempts or compute elapsed
each iteration, and if the limit is exceeded log a clear timeout/failure and
return/throw a failure status instead of looping forever; ensure the new limit
is configurable (e.g., MAX_VIDEO_POLL_ATTEMPTS or MAX_VIDEO_POLL_MS) and used to
break the for(;;) loop and surface an error when exceeded.
In `@packages/ai-cli/src/cli/artifact.ts`:
- Around line 52-60: fetchBytes currently can hang and lets fetch-level errors
escape; wrap the fetch call in an AbortController with a short configurable
timeout (e.g., 10s) and use setTimeout to abort, clear the timer on completion,
and catch any errors from fetch or abort and rethrow them as a CliError with the
'PROVIDER' code (preserve useful info like error.message and the URL). Keep the
existing non-OK response check and convert it to CliError as you already do;
ensure the function signature and returned Uint8Array behavior (function
fetchBytes) remain unchanged.
In `@packages/ai-cli/src/cli/interactive.ts`:
- Around line 35-45: The interactive flow currently returns 0 on failure: when
no model is resolved (variable model from modelOverride ??
DEFAULT_MODELS[choice.command]) and when findCommand(choice.command) returns
falsy (spec), change those return values to a non-zero exit code (e.g. return 1)
so the CLI reports failure correctly; update the two early returns in the
interactive handler that reference model and spec to return a non-zero status
instead of 0.
In `@packages/ai-cli/src/cli/mcp-clients.ts`:
- Around line 39-56: When a later mcp.createMCPClient(...) attempt fails the
already-created clients in the clients array must be closed before re-throwing
the CliError; inside the catch block that currently throws CliError, iterate the
existing clients (e.g., for (const c of clients) await c.close().catch(() => {}
) or close in reverse order) to ensure stdio subprocesses are terminated, then
throw the CliError; reference mcp.createMCPClient, the clients array, and the
catch that throws the CliError to locate where to add the cleanup.
In `@packages/ai-cli/src/cli/run.ts`:
- Around line 40-43: The error-emitter selection currently uses
resolveOutputMode({ json: argv.includes('--json'), stream:
argv.includes('--stream') }) in the catch/error path which ignores configuration
precedence; change the catch path to derive mode from the same merged options
used elsewhere (not raw argv) — e.g. load or reuse the merged options/config
object and call resolveOutputMode against its flags (or flag booleans) instead
of argv so mode reflects flags > config > env > defaults; update any references
to mode in the error handling to use this merged-derived value (keep using
resolveOutputMode and the same emitter selection logic).
In `@packages/ai-cli/src/cli/update.ts`:
- Around line 43-46: The isOnDemand() detection in
packages/ai-cli/src/cli/update.ts only checks for '_npx' and npm_command ===
'exec', so it misses other one-shot runners like pnpm dlx, yarn dlx and bunx;
update isOnDemand() to treat any exec path or npm command indicating a one-off
runner as on-demand by checking execPath for additional markers such as 'dlx'
and 'bunx' (and other common substrings used by one-shot runners) and also
consider npm_command values like 'dlx' in addition to 'exec' when deciding true;
update the function isOnDemand to include these extra checks so ephemeral
runners are detected and global install is avoided.
In `@packages/ai-cli/src/core/exit-codes.ts`:
- Around line 74-79: The toErrorObject() result currently spreads this.detail
last allowing detail.code/detail.message/detail.provider to overwrite canonical
fields; fix by sanitizing detail before merging: in toErrorObject() create a
sanitizedDetail (copy of this.detail) and remove keys "code", "message", and
"provider" (or alternatively spread sanitizedDetail before the canonical
fields), then return the object using ...(sanitizedDetail) so canonical fields
this.code, this.message, and this.provider cannot be overridden; reference the
toErrorObject() method and the this.detail property when making the change.
In `@packages/ai-cli/src/core/io.ts`:
- Around line 13-21: readStdin currently decodes all stdin as UTF-8 which
corrupts binary uploads; change readStdin to preserve raw bytes (return a Buffer
or a Uint8Array) and cache that raw buffer (stdinCache) instead of a UTF-8
string, then update callers (notably loadAttachments and the code paths around
the existing logic at lines ~85-88) to consume the raw bytes directly rather
than re-encoding a string; alternatively provide a new readStdinBuffer() that
returns the raw Buffer and keep a readStdinUtf8() wrapper that decodes when text
is required, ensuring binary stdin piped to "-" is forwarded byte-for-byte.
In `@packages/ai-cli/src/core/providers.ts`:
- Around line 221-233: importProvider currently maps every dynamic-import
failure to a PROVIDER_NOT_INSTALLED CliError; change the catch in importProvider
to inspect the thrown error (the caught "cause") and only convert it to the
CliError when the error indicates the module is missing (e.g., cause.code ===
'ERR_MODULE_NOT_FOUND' || cause.code === 'MODULE_NOT_FOUND' || the message
contains "Cannot find module"); for all other errors (module init/runtime
errors), rethrow the original error (or let it bubble) so runtime failures
aren’t misclassified; keep the existing CliError shape and include { provider,
detail: { package: entry.pkg }, cause } when you do throw the
PROVIDER_NOT_INSTALLED error.
In `@packages/ai-cli/vite.config.ts`:
- Line 12: The Vite test config's include pattern currently restricts discovery
to a separate tests tree; update the test runner config (the include property in
vite.config.ts) to use a colocated pattern like "**/*.test.ts" so unit tests
alongside source files (src/**) are discovered; locate the include:
['tests/**/*.test.ts'] entry and replace it with the glob for colocated tests
(and ensure any exclude settings still permit src/**/*.test.ts).
In `@testing/cli/package.json`:
- Around line 1-14: The package.json is missing the required packageManager pin;
add a top-level "packageManager" field set to "pnpm@10.17.0" in the package.json
(next to existing fields like "name" and "type") so pnpm versioning is enforced
for this package; ensure the entry is a top-level string property named
packageManager with the exact value pnpm@10.17.0.
---
Outside diff comments:
In `@packages/ai-cli/package.json`:
- Around line 1-83: The package.json is missing the required packageManager
field; add a top-level "packageManager": "pnpm@10.17.0" entry to the JSON
(alongside existing keys like "name", "version", "type") so the package adheres
to the repo package-manager contract; update the package.json in this package to
include that exact key/value.
In `@packages/ai-cli/tests/core.test.ts`:
- Around line 1-260: Test file is placed in the top-level tests folder instead
of next to the modules it validates; move it next to the source modules and
adjust imports. Relocate the test so it lives alongside the package's source
(next to the core/ and manifest/ modules) and update all import paths
accordingly (references to providers, config, output, cli/options,
core/exit-codes, core/io, cli/mcp-clients, manifest/manifest and manifest/types
should become local relative imports). Ensure the test filename remains
core.test.ts and verify imports for findCommand, resolveModelSlug,
instantiateAdapter, resolveApiKey, mergeOptions, resolveOutputMode, coerceFlags,
CliError/ExitCode/toCliError, inferMimeType/resolvePrompt, and tokenizeCommand
still resolve.
---
Minor comments:
In `@docs/cli/overview.md`:
- Around line 38-50: Update the CLI docs to use a single flag spelling: replace
every instance of the camelCase flag `--apiKey` with the kebab-case `--api-key`
so it matches the examples and the actual `ts-ai` command usage; search for
occurrences of `--apiKey` in the docs/cli/overview.md and change them to
`--api-key`, and ensure the sample commands (e.g., the `ts-ai chat` examples)
and explanatory text consistently reference `--api-key`.
In `@packages/ai-cli/src/cli/activities/audio.ts`:
- Around line 44-49: The parsed duration from ctx.options.duration (the block
computing const duration) can be NaN or Infinity and must be validated before
being passed to generateAudio; update the logic that computes duration (using
ctx.options.duration) to coerce string to Number, then check
Number.isFinite(value) and that it falls in an acceptable range (e.g., > 0 and
<= a sane max) and otherwise set duration to undefined (or omit it) so
generateAudio never receives NaN/Infinity; reference the const duration
computation and the downstream generateAudio call when making this change.
In `@packages/ai-cli/src/cli/activities/transcribe.ts`:
- Line 79: The object spread currently omits duration when it's 0 because it
uses a truthy check; update the conditional to use a nullish check so zero is
preserved: replace the ternary using "result.duration ? { duration:
result.duration } : {}" with a nullish-aware check like "result.duration != null
? { duration: result.duration } : {}", referencing the variable/result object
and the spread expression where duration is added (the line with
...(result.duration ? { duration: result.duration } : {} ) in transcribe.ts).
In `@packages/ai-cli/src/cli/interactive.ts`:
- Line 45: The dispatch call currently drops empty-string prompts because it
uses a truthy check; update the argument expression so it tests explicitly for
undefined (e.g., choice.prompt !== undefined) and passes [choice.prompt] when
prompt is an empty string, ensuring dispatchCommand(spec, ...) receives the
user's empty prompt; locate the invocation of dispatchCommand with spec and
choice.prompt and replace the truthy check with an undefined check to preserve
'' as a valid positional arg.
In `@packages/ai-cli/src/render/repl.tsx`:
- Around line 27-59: The early "if (busy) return" in the useInput handler
prevents the key.escape branch from running while a request is in-flight; allow
Escape to always work by either moving the "if (key.escape) { exit(); return }"
check above the "if (busy) return" or by changing the busy guard to "if (busy &&
!key.escape) return" so that exit() can be invoked while respond(...) is
pending; update the useInput handler around the busy/key.escape logic
(references: useInput, busy, key.escape, exit, respond, setBusy) to ensure
Escape exits immediately during stalled requests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 441d789a-b5b2-454a-85e5-6501c8233e0c
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (49)
.changeset/ai-cli-initial.mddocs/cli/overview.mddocs/config.jsonknip.jsonpackages/ai-cli/package.jsonpackages/ai-cli/src/cli/activities/audio.tspackages/ai-cli/src/cli/activities/chat.tspackages/ai-cli/src/cli/activities/image.tspackages/ai-cli/src/cli/activities/speech.tspackages/ai-cli/src/cli/activities/summarize.tspackages/ai-cli/src/cli/activities/transcribe.tspackages/ai-cli/src/cli/activities/video.tspackages/ai-cli/src/cli/artifact.tspackages/ai-cli/src/cli/bin.tspackages/ai-cli/src/cli/code-mode.tspackages/ai-cli/src/cli/context.tspackages/ai-cli/src/cli/dispatch.tspackages/ai-cli/src/cli/interactive.tspackages/ai-cli/src/cli/introspect.tspackages/ai-cli/src/cli/mcp-clients.tspackages/ai-cli/src/cli/mcp.tspackages/ai-cli/src/cli/options.tspackages/ai-cli/src/cli/program.tspackages/ai-cli/src/cli/run.tspackages/ai-cli/src/cli/update.tspackages/ai-cli/src/core/config.tspackages/ai-cli/src/core/emit.tspackages/ai-cli/src/core/env.tspackages/ai-cli/src/core/exit-codes.tspackages/ai-cli/src/core/io.tspackages/ai-cli/src/core/logger.tspackages/ai-cli/src/core/output.tspackages/ai-cli/src/core/providers.tspackages/ai-cli/src/index.tspackages/ai-cli/src/manifest/manifest.tspackages/ai-cli/src/manifest/types.tspackages/ai-cli/src/render/ink.tsxpackages/ai-cli/src/render/lazy.tspackages/ai-cli/src/render/menu.tsxpackages/ai-cli/src/render/repl.tsxpackages/ai-cli/tests/core.test.tspackages/ai-cli/tsconfig.jsonpackages/ai-cli/tsup.bin.config.tspackages/ai-cli/vite.config.tstesting/cli/README.mdtesting/cli/package.jsontesting/cli/tests/cli.spec.tstesting/cli/tests/mcp.spec.tstesting/cli/vitest.config.ts
| watch: false, | ||
| globals: true, | ||
| environment: 'node', | ||
| include: ['tests/**/*.test.ts'], |
There was a problem hiding this comment.
Align test discovery with the colocated test-file rule.
include: ['tests/**/*.test.ts'] enforces a separate test tree and will skip colocated unit tests under src/**, which conflicts with the repository rule for *.test.ts placement.
Suggested config change
- include: ['tests/**/*.test.ts'],
+ include: ['src/**/*.test.ts'],As per coding guidelines: **/*.test.ts files should be placed alongside source code, and discovered as such.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| include: ['tests/**/*.test.ts'], | |
| include: ['src/**/*.test.ts'], |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/ai-cli/vite.config.ts` at line 12, The Vite test config's include
pattern currently restricts discovery to a separate tests tree; update the test
runner config (the include property in vite.config.ts) to use a colocated
pattern like "**/*.test.ts" so unit tests alongside source files (src/**) are
discovered; locate the include: ['tests/**/*.test.ts'] entry and replace it with
the glob for colocated tests (and ensure any exclude settings still permit
src/**/*.test.ts).
Source: Coding guidelines
| { | ||
| "name": "@tanstack/ai-cli-tests", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "test:e2e": "vitest run", | ||
| "test:e2e:dev": "vitest" | ||
| }, | ||
| "devDependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| "@tanstack/ai-cli": "workspace:*", | ||
| "vitest": "^4.0.14" | ||
| } | ||
| } |
There was a problem hiding this comment.
Add the required packageManager pin for pnpm.
This package.json is missing the required package-manager lock (pnpm@10.17.0) defined by repo guidelines.
💡 Proposed fix
{
"name": "`@tanstack/ai-cli-tests`",
"private": true,
"type": "module",
+ "packageManager": "pnpm@10.17.0",
"scripts": {As per coding guidelines: **/package.json: “Use pnpm@10.17.0 as the package manager for all tasks and dependency management.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| { | |
| "name": "@tanstack/ai-cli-tests", | |
| "private": true, | |
| "type": "module", | |
| "scripts": { | |
| "test:e2e": "vitest run", | |
| "test:e2e:dev": "vitest" | |
| }, | |
| "devDependencies": { | |
| "@modelcontextprotocol/sdk": "^1.29.0", | |
| "@tanstack/ai-cli": "workspace:*", | |
| "vitest": "^4.0.14" | |
| } | |
| } | |
| { | |
| "name": "`@tanstack/ai-cli-tests`", | |
| "private": true, | |
| "type": "module", | |
| "packageManager": "pnpm@10.17.0", | |
| "scripts": { | |
| "test:e2e": "vitest run", | |
| "test:e2e:dev": "vitest" | |
| }, | |
| "devDependencies": { | |
| "`@modelcontextprotocol/sdk`": "^1.29.0", | |
| "`@tanstack/ai-cli`": "workspace:*", | |
| "vitest": "^4.0.14" | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@testing/cli/package.json` around lines 1 - 14, The package.json is missing
the required packageManager pin; add a top-level "packageManager" field set to
"pnpm@10.17.0" in the package.json (next to existing fields like "name" and
"type") so pnpm versioning is enforced for this package; ensure the entry is a
top-level string property named packageManager with the exact value
pnpm@10.17.0.
Source: Coding guidelines
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/ai-cli/tests/core.test.ts (1)
1-1: 🏗️ Heavy liftMove this unit test beside its source module to match test-placement policy.
This test file is under
packages/ai-cli/tests/instead of colocated*.test.tsnear the tested source files.As per coding guidelines, "
**/*.test.ts: Place unit tests alongside source code in*.test.tsfiles".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-cli/tests/core.test.ts` at line 1, The test file core.test.ts is in a top-level tests folder instead of colocated with the module it tests; move core.test.ts into the same directory as the source module it targets (so the test filename becomes adjacent to the source file), update any relative imports if needed, and ensure the Vitest imports (describe, expect, it) remain unchanged so the test runner picks it up as a sibling *.test.ts per the project's test-placement policy.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/ai-cli/tests/core.test.ts`:
- Line 1: The test file core.test.ts is in a top-level tests folder instead of
colocated with the module it tests; move core.test.ts into the same directory as
the source module it targets (so the test filename becomes adjacent to the
source file), update any relative imports if needed, and ensure the Vitest
imports (describe, expect, it) remain unchanged so the test runner picks it up
as a sibling *.test.ts per the project's test-placement policy.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 45a84a7a-79d8-4c33-809a-8f9d32697db5
📒 Files selected for processing (2)
packages/ai-cli/src/core/providers.tspackages/ai-cli/tests/core.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/ai-cli/src/core/providers.ts
- Full-width welcome screen: island logo (graphics-capable terminals) above a two-color ANSI wordmark — TANSTACK white, AI in the package pink (#EC4899) — with a sunset gradient rule and tagline. New shared theme module so the menu, chat REPL, and artifact/error renderers all use consistent brand colors. - `ts-ai mcp` now logs connection info to stderr before listening: a ready-to-paste MCP client config, transport, and tool list (stdout stays the clean JSON-RPC channel). - `--output-dir <dir>` for generations (image/video/audio/speech): default is the current directory; --output-dir sets the directory (created if missing, cross-platform via node:path); -o/--output sets an exact path and wins. - Tests: resolveOutputPath precedence, describeMcpServer content, and an introspect assertion for the --output-dir flag. Docs + changeset updated.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/ai-cli/package.json (1)
69-69: ⚡ Quick winClarify whether
react-devtools-coreshould be a runtime dependency.Developer tools are typically
devDependenciesrather than runtime dependencies. If this is needed for Ink UI debugging or inspection features in the CLI, that's fine—but if it's only used during development, consider moving it todevDependenciesto reduce bundle size.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-cli/package.json` at line 69, The package.json currently lists "react-devtools-core" as a runtime dependency; determine whether it's only used for development (e.g., Ink UI debugging) by searching for references to "react-devtools-core" in the codebase, and if it's not required at runtime move it from dependencies into devDependencies in package.json, then run the package manager to update lockfiles; if it is used at runtime, keep it but add a clear comment in package.json explaining why it must remain a runtime dependency.packages/ai-cli/src/render/theme.ts (1)
36-36: 💤 Low valueOptional: Remove unnecessary fallback.
The
?? SUNSET[0]fallback on Line 36 is unreachable becauseslotis explicitly bounded to0..SUNSET.length-1on lines 32-35, soSUNSET[slot]will never beundefined.♻️ Simplify by removing the dead fallback
- return SUNSET[slot] ?? SUNSET[0] + return SUNSET[slot]!🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-cli/src/render/theme.ts` at line 36, The return expression includes an unreachable fallback—remove the "?? SUNSET[0]" and return SUNSET[slot] directly because "slot" is already clamped to 0..SUNSET.length-1 above; update the return in the function that references SUNSET and slot so it simply returns SUNSET[slot].
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ai-cli/tests/core.test.ts`:
- Line 13: Move the Node.js built-in import for join (import { join } from
'node:path') above the external package imports (the vitest imports) to satisfy
ESLint's import order rule: locate the import statement for join and place it
before the vitest import lines so built-in modules come first.
---
Nitpick comments:
In `@packages/ai-cli/package.json`:
- Line 69: The package.json currently lists "react-devtools-core" as a runtime
dependency; determine whether it's only used for development (e.g., Ink UI
debugging) by searching for references to "react-devtools-core" in the codebase,
and if it's not required at runtime move it from dependencies into
devDependencies in package.json, then run the package manager to update
lockfiles; if it is used at runtime, keep it but add a clear comment in
package.json explaining why it must remain a runtime dependency.
In `@packages/ai-cli/src/render/theme.ts`:
- Line 36: The return expression includes an unreachable fallback—remove the "??
SUNSET[0]" and return SUNSET[slot] directly because "slot" is already clamped to
0..SUNSET.length-1 above; update the return in the function that references
SUNSET and slot so it simply returns SUNSET[slot].
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 017e9656-b5f8-43da-a1c0-8fc34d6ad593
⛔ Files ignored due to path filters (1)
packages/ai-cli/assets/logo.pngis excluded by!**/*.png
📒 Files selected for processing (18)
.changeset/ai-cli-initial.mddocs/cli/overview.mdpackages/ai-cli/package.jsonpackages/ai-cli/src/cli/activities/audio.tspackages/ai-cli/src/cli/activities/image.tspackages/ai-cli/src/cli/activities/speech.tspackages/ai-cli/src/cli/activities/video.tspackages/ai-cli/src/cli/artifact.tspackages/ai-cli/src/cli/mcp.tspackages/ai-cli/src/cli/run.tspackages/ai-cli/src/manifest/manifest.tspackages/ai-cli/src/render/ink.tsxpackages/ai-cli/src/render/menu.tsxpackages/ai-cli/src/render/repl.tsxpackages/ai-cli/src/render/theme.tspackages/ai-cli/src/render/welcome.tsxpackages/ai-cli/tests/core.test.tstesting/cli/tests/cli.spec.ts
✅ Files skipped from review due to trivial changes (2)
- .changeset/ai-cli-initial.md
- docs/cli/overview.md
🚧 Files skipped from review as they are similar to previous changes (9)
- packages/ai-cli/src/cli/activities/audio.ts
- packages/ai-cli/src/render/ink.tsx
- packages/ai-cli/src/cli/run.ts
- packages/ai-cli/src/cli/activities/image.ts
- packages/ai-cli/src/cli/mcp.ts
- packages/ai-cli/src/cli/activities/speech.ts
- packages/ai-cli/src/cli/activities/video.ts
- packages/ai-cli/src/manifest/manifest.ts
- testing/cli/tests/cli.spec.ts
…unch `ts-ai` (no command) now clears the terminal first for a clean splash, then a pink band sweeps left→right across the wordmark — starting at TANSTACK and landing on AI, which stays pink while TANSTACK settles to white. Run-length segmented per line so the per-frame node count stays small; the sweep runs once then stops. Narrow terminals and non-TTY output skip the animation.
Ships skills/ai-cli/SKILL.md so coding agents (Claude Code, Cursor, Copilot) learn to drive ts-ai correctly: the machine-mode contract (--json/--stream, stdout-is-payload, exit codes, structured errors), provider/model slugs, --config + modelOptions, --output-dir, stateless chat via --messages, --mcp / --code-mode, and introspect / mcp. Adds the `tanstack-intent` keyword and `skills` to files[] for discovery, and lists the skill in the Agent Skills doc.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
docs/config.json (1)
223-227:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd
updatedAtto the new docs entry.The new
cli/overviewnode hasaddedAtbut noupdatedAt. For this new page, setupdatedAtto today's date (2026-06-07) as well.Proposed fix
{ "label": "ts-ai CLI", "to": "cli/overview", - "addedAt": "2026-06-07" + "addedAt": "2026-06-07", + "updatedAt": "2026-06-07" }As per coding guidelines, maintain
addedAtandupdatedAtindocs/config.json, setting/refreshingupdatedAtwhen adding or changing docs content.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/config.json` around lines 223 - 227, The new docs entry for "ts-ai CLI" (to: "cli/overview") is missing an updatedAt field; update the object in docs/config.json for the "ts-ai CLI" node by adding "updatedAt": "2026-06-07" alongside the existing "addedAt" so both timestamps are present.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@docs/config.json`:
- Around line 223-227: The new docs entry for "ts-ai CLI" (to: "cli/overview")
is missing an updatedAt field; update the object in docs/config.json for the
"ts-ai CLI" node by adding "updatedAt": "2026-06-07" alongside the existing
"addedAt" so both timestamps are present.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8253fac9-19a5-4c02-877a-8f69d9f17e86
📒 Files selected for processing (5)
.changeset/ai-cli-initial.mddocs/config.jsondocs/getting-started/agent-skills.mdpackages/ai-cli/package.jsonpackages/ai-cli/skills/ai-cli/SKILL.md
✅ Files skipped from review due to trivial changes (2)
- docs/getting-started/agent-skills.md
- packages/ai-cli/skills/ai-cli/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (2)
- .changeset/ai-cli-initial.md
- packages/ai-cli/package.json
Add a workspace:* override in pnpm-workspace.yaml for every library under packages/ so any transitive or example dependency that references a published @tanstack/ai-* version resolves to the local workspace copy — the monorepo never builds or tests against an npm-published version. Document the convention (add an override for each new packages/ library) in CLAUDE.md and AGENTS.md.
Image preview / pixelation: - Only render inline image previews on graphics-capable terminals (iTerm2/ Kitty/WezTerm); elsewhere show just the saved path instead of muddy ANSI block-art. Detect the real image format from magic bytes instead of assuming PNG, and fail fast on `-o -` with multiple images. Interactive menu is now a hub: - `ts-ai` (no command) loops back to the menu after each action; Esc inside a sub-flow (prompt input or chat REPL) returns to the menu, while `ts-ai chat` run directly still exits on Esc. The splash only animates on first show. CodeRabbit fixes: - Validate --max-steps / --max-length as positive integers; validate --messages item shapes. - Bound the video poll loop with a timeout; add a fetch timeout + normalized CliError for artifact downloads. - Close already-opened MCP clients when a later --mcp spec fails. - Only classify a genuinely-missing package as PROVIDER_NOT_INSTALLED; surface real load errors as RUNTIME. - Read --attachment - as raw bytes (no UTF-8 corruption of binary stdin). - Prevent CliError detail from overriding canonical error fields. - Broaden on-demand (npx/dlx) detection in `update`. - docs/config.json: add updatedAt to the cli/overview entry; tidy import order. Unit tests cover resolveOutputPath, describeMcpServer, factory candidates, and tokenizeCommand; all green (35 unit, 15 e2e).
|
Addressed the CodeRabbit feedback in Fixed
Skipped (with reasons)
Also unrelated to CodeRabbit: fixed pixelated image previews (now graphics-terminal-only, path otherwise) and made the interactive home screen a hub (Esc returns to the menu). |
- Revert the graphics-only gate on image previews: render the inline preview everywhere (crisp in iTerm2/Kitty/WezTerm, ANSI block-art otherwise) — a blocky preview beats none. Keep the magic-byte format detection. - Result views (image/text/artifact) now stay on screen until the user presses Esc/Enter on an interactive terminal, then unmount. This fixes results being instantly cleared when returning to the interactive hub (and a latent hang where these views never exited); non-interactive output unmounts immediately. - Add a stderr progress spinner (quiet- and TTY-aware) around every generation (chat, image, video, audio, speech, transcribe, summarize) so there's a clear loading indicator; stdout stays the clean machine payload.
Previously, inside the interactive hub, Ctrl+C in the chat REPL or a result view only unmounted that Ink app and looped back to the menu. Disable Ink's default Ctrl+C handling (exitOnCtrlC: false) and handle it explicitly in the menu, REPL, and result views: restore the terminal (show cursor, leave raw mode) and exit the process with code 130. Esc keeps its return-to-menu / dismiss behavior.
What
A new
@tanstack/ai-clipackage that installs thets-aibinary — a type-safe, machine-first CLI over the core TanStack AI activities. Designed so the same binary serves both one-off human use and agent harnesses.Commands
chat · image · video · audio · speech · transcribe · summarize+introspect(machine-readable manifest) +mcp(every command exposed as an MCP tool over stdio) +update.Design
--json= buffered result,--stream= AG-UI event stream; strict stdout-is-payload / stderr-is-everything-else; typed exit codes (0ok,1runtime,2usage,3provider/output,4provider-not-installed); structured{ error }objects on stdout in machine mode.provider/modelslug → dynamic import → adapter.openai, anthropic, gemini, openrouter, falbundled for zero-installnpx; others resolved-if-present (exit 4 otherwise). Keys via--api-key, a conventional.env, or env vars. Every option expressible via--config(file or inline JSON), with precedence flags > config > env > defaults.--messages, tools via--mcpservers, sandboxed--code-mode,--schemastructured output, rich JSON envelope (text/toolCalls/finishReason/threaded messages) viaStreamProcessor.ts-aiwith no command), interactive chat REPL (ts-ai chatwith no prompt), inline image preview.ts-ai introspect --jsonemits a versioned manifest of every command/flag/type/exit-code;ts-ai mcpexposes the commands as MCP tools.Testing
packages/ai-cli: slug/key/config resolution, output-mode, flag coercion, exit-code mapping, provider factory-candidate resolution,--mcpcommand tokenizing.testing/clisubprocess E2E (15): spawns the built binary — version, introspect, error/exit-code contract, stdout purity, kebab-flag parsing, argv-injection guard, and a real MCP client drivingts-ai mcp.--mcptool round-trip,--code-modesandbox orchestration,ts-ai mcp).--end-of-options terminator); a code review's high-confidence findings (stdout backpressure, stdin double-consume, command tokenizing) are also fixed.Includes a docs page (
docs/cli/overview.md+ nav entry) and a changeset.Notes / not yet covered
videois experimental and blocks until the job completes (--no-waitreturns the job id;ts-ai video status <jobId>polls).summarize+openai/gpt-5.5fails upstream (the OpenAI summarize adapter sendstemperature, which gpt-5.5 rejects) — not a CLI bug; works on temperature-accepting models.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests