Skip to content

feat(ai-cli): add @tanstack/ai-cli (ts-ai), a CLI over TanStack AI#717

Open
AlemTuzlak wants to merge 13 commits into
mainfrom
feat/ai-cli
Open

feat(ai-cli): add @tanstack/ai-cli (ts-ai), a CLI over TanStack AI#717
AlemTuzlak wants to merge 13 commits into
mainfrom
feat/ai-cli

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

@AlemTuzlak AlemTuzlak commented Jun 7, 2026

What

A new @tanstack/ai-cli package that installs the ts-ai binary — 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

  • Machine-first: every command is a stateless single-shot subprocess. --json = buffered result, --stream = AG-UI event stream; strict stdout-is-payload / stderr-is-everything-else; typed exit codes (0 ok, 1 runtime, 2 usage, 3 provider/output, 4 provider-not-installed); structured { error } objects on stdout in machine mode.
  • Providers: provider/model slug → dynamic import → adapter. openai, anthropic, gemini, openrouter, fal bundled for zero-install npx; 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.
  • chat: stateless multi-turn via --messages, tools via --mcp servers, sandboxed --code-mode, --schema structured output, rich JSON envelope (text/toolCalls/finishReason/threaded messages) via StreamProcessor.
  • Human TTY layer (Ink), lazy-imported only on the pretty path so the machine path never loads React: animated home menu (ts-ai with no command), interactive chat REPL (ts-ai chat with no prompt), inline image preview.
  • Self-describing for harnesses: ts-ai introspect --json emits a versioned manifest of every command/flag/type/exit-code; ts-ai mcp exposes the commands as MCP tools.

Testing

  • Unit (32) in packages/ai-cli: slug/key/config resolution, output-mode, flag coercion, exit-code mapping, provider factory-candidate resolution, --mcp command tokenizing.
  • New testing/cli subprocess 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 driving ts-ai mcp.
  • Live-verified against real OpenAI / Anthropic / OpenRouter / fal APIs across the whole surface (chat, stream, structured output, image, speech, transcribe round-trip, audio, video, --mcp tool round-trip, --code-mode sandbox orchestration, ts-ai mcp).
  • A security review flagged and fixed an argv-injection vector in the MCP server (now uses a -- 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

  • video is experimental and blocks until the job completes (--no-wait returns the job id; ts-ai video status <jobId> polls).
  • Not live-tested: Gemini (no key available during dev) and the Ink menu/REPL/preview (need an interactive TTY, not a piped harness). All build/typecheck/lint clean.
  • summarize + openai/gpt-5.5 fails upstream (the OpenAI summarize adapter sends temperature, which gpt-5.5 rejects) — not a CLI bug; works on temperature-accepting models.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduces ts-ai CLI: generation commands (chat, image, video, audio, speech, transcribe, summarize), interactive TTY home/menu and chat REPL, lazy Ink UI, code-mode tooling, machine-first JSON/stream modes, structured schema output, attachments, artifact file writing/streaming, dotenv/API-key/config precedence, MCP tool server, introspect manifest, and self-update.
  • Documentation

    • Adds CLI overview, site navigation entry, skill doc, and testing README covering usage, output modes, config precedence, and harness integration.
  • Tests

    • Adds unit and E2E suites validating manifest, machine contract, MCP, CLI behaviors, output formats, and exit-code contracts.

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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 7, 2026

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A new @tanstack/ai-cli package adds the ts-ai binary with manifest-driven commands (chat, image, video, audio, speech, transcribe, summarize), provider/model resolution and adapter instantiation, machine vs TTY output modes, interactive Ink UI and REPL, MCP stdio server, artifact IO, update flow, tests, and docs.

Changes

ts-ai CLI package and contract

Layer / File(s) Summary
Package scaffold and build tooling
packages/ai-cli/package.json, packages/ai-cli/tsconfig.json, packages/ai-cli/tsup.bin.config.ts, packages/ai-cli/vite.config.ts, knip.json, .changeset/ai-cli-initial.md, testing/cli/package.json, testing/cli/vitest.config.ts, testing/cli/README.md
Adds package manifest, TypeScript config, tsup CLI bundle config with shebang, Vite/Vitest config for tests/coverage, Knip workspace ignore rules for bundled provider packages, initial changeset and testing harness metadata.
Manifest types and command metadata
packages/ai-cli/src/manifest/types.ts, packages/ai-cli/src/manifest/manifest.ts, packages/ai-cli/src/index.ts, packages/ai-cli/src/cli/introspect.ts
Defines manifest schema/types (Activity, FlagSpec, CommandSpec, CliManifest), COMMON_FLAGS and COMMANDS, kebab-flag utilities, buildManifest/findCommand, and exposes introspect and public CLI exports.
Core runtime contracts, emitters, config, and IO
packages/ai-cli/src/core/exit-codes.ts, packages/ai-cli/src/core/emit.ts, packages/ai-cli/src/core/env.ts, packages/ai-cli/src/core/config.ts, packages/ai-cli/src/core/io.ts, packages/ai-cli/src/core/logger.ts, packages/ai-cli/src/core/output.ts
Implements ExitCode/CliError mapping and toCliError, emitJson/emitEvent/emitBytes/emitError, loadDotEnv, loadConfig/loadJsonInput/mergeOptions, readStdin/resolvePrompt/loadAttachments/inferMimeType, CliLogger, and resolveOutputMode/isMachine.
Provider registry and adapter instantiation
packages/ai-cli/src/core/providers.ts
PROVIDERS registry mapping slugs→packages/factory prefixes/env keys; factoryCandidatesForProvider, resolveModelSlug (provider/model or inference), resolveApiKey (flag then env), instantiateAdapter with dynamic import and factory discovery, import errors converted to PROVIDER_NOT_INSTALLED, and bundledProviders().
Run context assembly and flag coercion
packages/ai-cli/src/cli/context.ts, packages/ai-cli/src/cli/options.ts
createRunContext merges flags+config, resolves output mode/logger/now; resolveAdapterContext validates model and apiKey and normalizes modelOptions; coerceFlags coercion rules for number/json/string[] and validation errors.
CLI entrypoint, program builder, and dispatch
packages/ai-cli/src/cli/bin.ts, packages/ai-cli/src/cli/program.ts, packages/ai-cli/src/cli/dispatch.ts
bin loads .env and forwards argv/version to run(); buildProgram registers manifest commands with applyFlag, introspect/mcp/update top-level commands, and TTY-only interactive home; dispatchCommand routes to activity handlers with prompt resolution and chat REPL fallback.
CLI run loop, error handling, update command
packages/ai-cli/src/cli/run.ts, packages/ai-cli/src/cli/update.ts
run() parses Commander without exiting, maps errors to ExitCode, emits structured JSON errors in machine mode; runUpdate detects package-manager and spawns upgrade subprocess, throwing CliError on failure.
MCP client, server, and Code Mode wiring
packages/ai-cli/src/cli/mcp-clients.ts, packages/ai-cli/src/cli/mcp.ts, packages/ai-cli/src/cli/code-mode.ts
buildMcpClients selects HTTP vs stdio transports, tokenize/parse command strings, runMcpServer exposes COMMANDS as MCP tools invoking ts-ai subprocess with --json and inline --config, and buildCodeMode lazily wires ai-code-mode and isolate driver.
Artifact utilities for media persistence
packages/ai-cli/src/cli/artifact.ts
Artifact shape, resolveOutputPath, writeArtifact to file or stdout, fetchBytes from URL, and mediaSourceToBytes for {b64Json
Chat activity with message building and structured output
packages/ai-cli/src/cli/activities/chat.ts
runChat builds messages from prompt/--messages/attachments, optional schema validation, optional MCP/code-mode tooling, supports pretty/stream/buffered outputs via StreamProcessor, and ensures MCP client cleanup.
Image, audio, speech generation handlers
packages/ai-cli/src/cli/activities/image.ts, packages/ai-cli/src/cli/activities/audio.ts, packages/ai-cli/src/cli/activities/speech.ts
runImage generates images (count/size), writes multiple artifacts with suffixed paths and optional preview; runAudio handles generateAudio and artifact ext/mime; runSpeech decodes base64 audio and writes artifact with format/voice/speed options.
Transcription, summarization, video handlers
packages/ai-cli/src/cli/activities/transcribe.ts, packages/ai-cli/src/cli/activities/summarize.ts, packages/ai-cli/src/cli/activities/video.ts
runTranscribe reads audio file or stdin attachment, converts to base64 and calls generateTranscription; runSummarize derives focus/maxLength and calls summarize; runVideo/job status supports --no-wait, polling to completion, download and artifact writing, plus status subcommand.
Lazy render boundary and Ink components
packages/ai-cli/src/render/lazy.ts, packages/ai-cli/src/render/menu.tsx, packages/ai-cli/src/render/repl.tsx, packages/ai-cli/src/render/ink.tsx, packages/ai-cli/src/render/theme.ts, packages/ai-cli/src/render/welcome.tsx
Lazy-load Ink renderers to defer deps; Ink menu with logo and prompt entry; REPL with message history, busy state, /exit//quit//clear commands; Ink renderers for text/image/artifact outputs and UI theme utilities.
Interactive home menu and chat REPL
packages/ai-cli/src/cli/interactive.ts
runHome renders menu and routes selections to dispatchCommand or runChatRepl; runChatRepl resolves model/apiKey, instantiates adapter, and streams chat responses into the REPL UI.
Unit tests for core resolution and contracts
packages/ai-cli/tests/core.test.ts
Unit tests for resolveModelSlug, factoryCandidatesForProvider, resolveApiKey, bundledProviders, mergeOptions, resolveOutputMode, coerceFlags, CliError/toCliError/toErrorObject, inferMimeType, resolvePrompt, findCommand, resolveOutputPath, describeMcpServer, and tokenizeCommand.
E2E and MCP integration tests
testing/cli/package.json, testing/cli/vitest.config.ts, testing/cli/tests/cli.spec.ts, testing/cli/tests/mcp.spec.ts
Subprocess E2E tests for --version, introspect manifest shape, machine-mode USAGE errors, flag parsing and coercion, argv injection safety, MCP server exposing tools and returning structured errors.
Documentation and changesets
docs/cli/overview.md, docs/config.json, .changeset/ai-cli-initial.md, testing/cli/README.md, packages/ai-cli/skills/ai-cli/SKILL.md, docs/getting-started/agent-skills.md
Adds CLI overview, docs navigation entry, changeset for minor release, skill documentation and testing README describing E2E harness.

Estimated code review effort: 🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • TanStack/ai#700: MCP client and stdio transport work referenced by the new MCP modules.

Suggested reviewers

  • tannerlinsley
  • tombeckenham
  • jherr

"I hopped through code with nimble paws,
Ts-ai now chats and paints and draws.
Streams and schemas, menus bright,
Ink and artifacts glowing light,
I munched a carrot at CI's applause! 🥕🐇"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ai-cli

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 7, 2026

🚀 Changeset Version Preview

4 package(s) bumped directly, 28 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-event-client 0.5.4 → 1.0.0 Changeset
@tanstack/ai-fal 0.7.23 → 1.0.0 Changeset
@tanstack/ai-anthropic 0.15.1 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.2.5 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.2.5 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.20 → 1.0.0 Dependent
@tanstack/ai-gemini 0.15.1 → 1.0.0 Dependent
@tanstack/ai-grok 0.11.2 → 1.0.0 Dependent
@tanstack/ai-groq 0.4.2 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.30 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.30 → 1.0.0 Dependent
@tanstack/ai-ollama 0.8.1 → 1.0.0 Dependent
@tanstack/ai-openai 0.14.1 → 1.0.0 Dependent
@tanstack/ai-openrouter 0.13.1 → 1.0.0 Dependent
@tanstack/ai-preact 0.9.4 → 1.0.0 Dependent
@tanstack/ai-react 0.15.4 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.6 → 1.0.0 Dependent
@tanstack/ai-solid 0.13.4 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.6 → 1.0.0 Dependent
@tanstack/ai-svelte 0.13.4 → 1.0.0 Dependent
@tanstack/ai-vue 0.13.4 → 1.0.0 Dependent
@tanstack/openai-base 0.8.1 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.28.0 → 0.29.0 Changeset
@tanstack/ai-cli 0.1.0 → 0.2.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-client 0.16.3 → 0.16.4 Dependent
@tanstack/ai-devtools-core 0.4.8 → 0.4.9 Dependent
@tanstack/ai-isolate-cloudflare 0.2.21 → 0.2.22 Dependent
@tanstack/ai-mcp 0.1.0 → 0.1.1 Dependent
@tanstack/ai-vue-ui 0.2.16 → 0.2.17 Dependent
@tanstack/preact-ai-devtools 0.1.51 → 0.1.52 Dependent
@tanstack/react-ai-devtools 0.2.51 → 0.2.52 Dependent
@tanstack/solid-ai-devtools 0.2.51 → 0.2.52 Dependent

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Jun 7, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedterminal-image@​4.3.09310010084100
Addedink@​7.0.59810010095100

View full report

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Jun 7, 2026

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.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm strtok3 is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/terminal-image@4.3.0npm/strtok3@10.3.5

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/strtok3@10.3.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Jun 7, 2026

View your CI Pipeline Execution ↗ for commit e4f4d83

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 5s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-08 11:58:20 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 7, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@717

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@717

@tanstack/ai-cli

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-cli@717

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@717

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@717

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-skills@717

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@717

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@717

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@717

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@717

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@717

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@717

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@717

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@717

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@717

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@717

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@717

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@717

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@717

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@717

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@717

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@717

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@717

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@717

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@717

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@717

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@717

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@717

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@717

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@717

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@717

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@717

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@717

commit: 87a99a9

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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Add explicit packageManager pin 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 win

Move 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.ts files.

🤖 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 win

Preserve empty prompt values when dispatching command args.

Line 45 uses a truthy check, so '' is converted to no positional args. Use an undefined check 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 win

Allow 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 win

Preserve zero-duration values in machine output.

Line 79 omits duration when it is 0. Use a nullish check so 0 remains 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 win

Use one flag spelling consistently: --api-key.

Line 38 and Line 50 mention --apiKey, while the examples use --api-key (Line 45). Prefer --api-key throughout 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 win

Sanitize parsed duration before passing it downstream.

String parsing can yield NaN/Infinity, which currently flows into generateAudio unchanged.

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

📥 Commits

Reviewing files that changed from the base of the PR and between afb2960 and 329f56a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (49)
  • .changeset/ai-cli-initial.md
  • docs/cli/overview.md
  • docs/config.json
  • knip.json
  • packages/ai-cli/package.json
  • packages/ai-cli/src/cli/activities/audio.ts
  • packages/ai-cli/src/cli/activities/chat.ts
  • packages/ai-cli/src/cli/activities/image.ts
  • packages/ai-cli/src/cli/activities/speech.ts
  • packages/ai-cli/src/cli/activities/summarize.ts
  • packages/ai-cli/src/cli/activities/transcribe.ts
  • packages/ai-cli/src/cli/activities/video.ts
  • packages/ai-cli/src/cli/artifact.ts
  • packages/ai-cli/src/cli/bin.ts
  • packages/ai-cli/src/cli/code-mode.ts
  • packages/ai-cli/src/cli/context.ts
  • packages/ai-cli/src/cli/dispatch.ts
  • packages/ai-cli/src/cli/interactive.ts
  • packages/ai-cli/src/cli/introspect.ts
  • packages/ai-cli/src/cli/mcp-clients.ts
  • packages/ai-cli/src/cli/mcp.ts
  • packages/ai-cli/src/cli/options.ts
  • packages/ai-cli/src/cli/program.ts
  • packages/ai-cli/src/cli/run.ts
  • packages/ai-cli/src/cli/update.ts
  • packages/ai-cli/src/core/config.ts
  • packages/ai-cli/src/core/emit.ts
  • packages/ai-cli/src/core/env.ts
  • packages/ai-cli/src/core/exit-codes.ts
  • packages/ai-cli/src/core/io.ts
  • packages/ai-cli/src/core/logger.ts
  • packages/ai-cli/src/core/output.ts
  • packages/ai-cli/src/core/providers.ts
  • packages/ai-cli/src/index.ts
  • packages/ai-cli/src/manifest/manifest.ts
  • packages/ai-cli/src/manifest/types.ts
  • packages/ai-cli/src/render/ink.tsx
  • packages/ai-cli/src/render/lazy.ts
  • packages/ai-cli/src/render/menu.tsx
  • packages/ai-cli/src/render/repl.tsx
  • packages/ai-cli/tests/core.test.ts
  • packages/ai-cli/tsconfig.json
  • packages/ai-cli/tsup.bin.config.ts
  • packages/ai-cli/vite.config.ts
  • testing/cli/README.md
  • testing/cli/package.json
  • testing/cli/tests/cli.spec.ts
  • testing/cli/tests/mcp.spec.ts
  • testing/cli/vitest.config.ts

Comment thread docs/config.json
Comment thread packages/ai-cli/src/cli/activities/chat.ts
Comment thread packages/ai-cli/src/cli/activities/chat.ts
Comment thread packages/ai-cli/src/cli/activities/image.ts
Comment thread packages/ai-cli/src/cli/activities/image.ts
Comment thread packages/ai-cli/src/core/exit-codes.ts
Comment thread packages/ai-cli/src/core/io.ts Outdated
Comment thread packages/ai-cli/src/core/providers.ts
watch: false,
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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

Comment thread testing/cli/package.json
Comment on lines +1 to +14
{
"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"
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
{
"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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/ai-cli/tests/core.test.ts (1)

1-1: 🏗️ Heavy lift

Move 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.ts near the tested source files.

As per coding guidelines, "**/*.test.ts: Place unit tests alongside source code in *.test.ts files".

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 329f56a and efb4c54.

📒 Files selected for processing (2)
  • packages/ai-cli/src/core/providers.ts
  • packages/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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/ai-cli/package.json (1)

69-69: ⚡ Quick win

Clarify whether react-devtools-core should be a runtime dependency.

Developer tools are typically devDependencies rather 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 to devDependencies to 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 value

Optional: Remove unnecessary fallback.

The ?? SUNSET[0] fallback on Line 36 is unreachable because slot is explicitly bounded to 0..SUNSET.length-1 on lines 32-35, so SUNSET[slot] will never be undefined.

♻️ 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

📥 Commits

Reviewing files that changed from the base of the PR and between efb4c54 and 5e4af8b.

⛔ Files ignored due to path filters (1)
  • packages/ai-cli/assets/logo.png is excluded by !**/*.png
📒 Files selected for processing (18)
  • .changeset/ai-cli-initial.md
  • docs/cli/overview.md
  • packages/ai-cli/package.json
  • packages/ai-cli/src/cli/activities/audio.ts
  • packages/ai-cli/src/cli/activities/image.ts
  • packages/ai-cli/src/cli/activities/speech.ts
  • packages/ai-cli/src/cli/activities/video.ts
  • packages/ai-cli/src/cli/artifact.ts
  • packages/ai-cli/src/cli/mcp.ts
  • packages/ai-cli/src/cli/run.ts
  • packages/ai-cli/src/manifest/manifest.ts
  • packages/ai-cli/src/render/ink.tsx
  • packages/ai-cli/src/render/menu.tsx
  • packages/ai-cli/src/render/repl.tsx
  • packages/ai-cli/src/render/theme.ts
  • packages/ai-cli/src/render/welcome.tsx
  • packages/ai-cli/tests/core.test.ts
  • testing/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

Comment thread packages/ai-cli/tests/core.test.ts Outdated
AlemTuzlak and others added 3 commits June 8, 2026 12:08
…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.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
docs/config.json (1)

223-227: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add updatedAt to the new docs entry.

The new cli/overview node has addedAt but no updatedAt. For this new page, set updatedAt to 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 addedAt and updatedAt in docs/config.json, setting/refreshing updatedAt when 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

📥 Commits

Reviewing files that changed from the base of the PR and between cffa541 and 9ccc029.

📒 Files selected for processing (5)
  • .changeset/ai-cli-initial.md
  • docs/config.json
  • docs/getting-started/agent-skills.md
  • packages/ai-cli/package.json
  • packages/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).
@AlemTuzlak
Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit feedback in cd555067 (plus two issues found while testing).

Fixed

  • docs/config.json: added updatedAt to the cli/overview entry.
  • chat: validate --max-steps (positive integer) and each --messages item shape; use maxSteps !== undefined so an explicit value isn't dropped.
  • summarize: validate --max-length.
  • image: detect the real image format from magic bytes instead of hardcoding PNG; fail fast on -o - with multiple images.
  • video: bound the poll loop with a timeout (points the user at ts-ai video status).
  • artifact: fetch timeout + normalized CliError for downloads.
  • mcp-clients: close already-opened clients when a later --mcp spec fails.
  • providers: only classify a genuinely-missing package as PROVIDER_NOT_INSTALLED; surface real load errors as RUNTIME.
  • io: read --attachment - as raw bytes (no UTF-8 corruption of binary stdin).
  • exit-codes: spread detail first so it can't override canonical error fields.
  • update: broadened on-demand (npx/dlx) detection.
  • tests: import order.

Skipped (with reasons)

  • packageManager pin on package.json (ai-cli + testing/cli): no leaf package in this repo pins packageManager — it's root-only. Adding it would be inconsistent with every sibling package.
  • vite.config test discovery / colocated tests: tests/**/*.test.ts matches the established convention in ai-code-mode, ai-mcp, and ai-openai.
  • interactive.ts non-zero exit codes: failures from dispatched commands already propagate as thrown CliErrors → correct exit code; the explicit 0 returns are intentional informational/quit paths.
  • run.ts error-mode from argv: this runs only after commander throws (no parsed options available); non-TTY already routes to JSON via resolveOutputMode, so the only gap is --config '{"json":true}' on a TTY at error time — an edge not worth pre-parsing for.

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).

autofix-ci Bot and others added 4 commits June 8, 2026 11:05
- 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.
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.

1 participant