@pentoshi/clai 4.10.2 → 4.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +40 -7
  2. package/dist/agent/runner.js +114 -25
  3. package/dist/agent/runner.js.map +1 -1
  4. package/dist/agent/task-evidence.js +0 -63
  5. package/dist/agent/task-evidence.js.map +1 -1
  6. package/dist/app/adapters/current-store-adapter.js +8 -2
  7. package/dist/app/adapters/current-store-adapter.js.map +1 -1
  8. package/dist/app/commands/catalog.js +19 -2
  9. package/dist/app/commands/catalog.js.map +1 -1
  10. package/dist/app/controllers/session-controller.d.ts +4 -2
  11. package/dist/app/controllers/session-controller.js +26 -38
  12. package/dist/app/controllers/session-controller.js.map +1 -1
  13. package/dist/app/controllers/session-loop-recovery.d.ts +13 -0
  14. package/dist/app/controllers/session-loop-recovery.js +43 -0
  15. package/dist/app/controllers/session-loop-recovery.js.map +1 -0
  16. package/dist/app/ports/persistence-port.d.ts +3 -1
  17. package/dist/classic/bootstrap/start-classic.d.ts +1 -0
  18. package/dist/classic/bootstrap/start-classic.js +13 -4
  19. package/dist/classic/bootstrap/start-classic.js.map +1 -1
  20. package/dist/commands/providers.js +2 -0
  21. package/dist/commands/providers.js.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/llm/capabilities.js +13 -0
  25. package/dist/llm/capabilities.js.map +1 -1
  26. package/dist/llm/free-default-model.d.ts +7 -0
  27. package/dist/llm/free-default-model.js +41 -0
  28. package/dist/llm/free-default-model.js.map +1 -0
  29. package/dist/llm/free.js +39 -0
  30. package/dist/llm/free.js.map +1 -1
  31. package/dist/llm/merge-gateway.d.ts +6 -0
  32. package/dist/llm/merge-gateway.js +157 -0
  33. package/dist/llm/merge-gateway.js.map +1 -0
  34. package/dist/llm/meta.d.ts +1 -2
  35. package/dist/llm/meta.js +31 -1140
  36. package/dist/llm/meta.js.map +1 -1
  37. package/dist/llm/provider-model-layers.js +6 -0
  38. package/dist/llm/provider-model-layers.js.map +1 -1
  39. package/dist/llm/provider-profile-layers.js +21 -0
  40. package/dist/llm/provider-profile-layers.js.map +1 -1
  41. package/dist/llm/provider.js +68 -0
  42. package/dist/llm/provider.js.map +1 -1
  43. package/dist/llm/reasoning-capability.js +8 -0
  44. package/dist/llm/reasoning-capability.js.map +1 -1
  45. package/dist/llm/responses-complete.d.ts +4 -0
  46. package/dist/llm/responses-complete.js +28 -0
  47. package/dist/llm/responses-complete.js.map +1 -0
  48. package/dist/llm/responses-config.d.ts +22 -0
  49. package/dist/llm/responses-config.js +19 -0
  50. package/dist/llm/responses-config.js.map +1 -0
  51. package/dist/llm/responses-dialect.d.ts +6 -0
  52. package/dist/llm/responses-dialect.js +6 -0
  53. package/dist/llm/responses-dialect.js.map +1 -0
  54. package/dist/llm/responses-http.d.ts +13 -0
  55. package/dist/llm/responses-http.js +88 -0
  56. package/dist/llm/responses-http.js.map +1 -0
  57. package/dist/llm/responses-parse.d.ts +42 -0
  58. package/dist/llm/responses-parse.js +242 -0
  59. package/dist/llm/responses-parse.js.map +1 -0
  60. package/dist/llm/responses-request.d.ts +15 -0
  61. package/dist/llm/responses-request.js +209 -0
  62. package/dist/llm/responses-request.js.map +1 -0
  63. package/dist/llm/responses-stream-accumulator.d.ts +38 -0
  64. package/dist/llm/responses-stream-accumulator.js +102 -0
  65. package/dist/llm/responses-stream-accumulator.js.map +1 -0
  66. package/dist/llm/responses-stream-events.d.ts +6 -0
  67. package/dist/llm/responses-stream-events.js +398 -0
  68. package/dist/llm/responses-stream-events.js.map +1 -0
  69. package/dist/llm/responses-stream-watchdog.d.ts +14 -0
  70. package/dist/llm/responses-stream-watchdog.js +64 -0
  71. package/dist/llm/responses-stream-watchdog.js.map +1 -0
  72. package/dist/llm/responses-stream.d.ts +4 -0
  73. package/dist/llm/responses-stream.js +264 -0
  74. package/dist/llm/responses-stream.js.map +1 -0
  75. package/dist/llm/router.js +3 -0
  76. package/dist/llm/router.js.map +1 -1
  77. package/dist/llm/tool-protocol.js +35 -3
  78. package/dist/llm/tool-protocol.js.map +1 -1
  79. package/dist/mcp/auth/host-credentials.d.ts +11 -0
  80. package/dist/mcp/auth/host-credentials.js +54 -0
  81. package/dist/mcp/auth/host-credentials.js.map +1 -0
  82. package/dist/mcp/auth/loopback.d.ts +16 -0
  83. package/dist/mcp/auth/loopback.js +126 -0
  84. package/dist/mcp/auth/loopback.js.map +1 -0
  85. package/dist/mcp/auth/metadata.d.ts +9 -0
  86. package/dist/mcp/auth/metadata.js +102 -0
  87. package/dist/mcp/auth/metadata.js.map +1 -0
  88. package/dist/mcp/auth/pkce.d.ts +3 -0
  89. package/dist/mcp/auth/pkce.js +17 -0
  90. package/dist/mcp/auth/pkce.js.map +1 -0
  91. package/dist/mcp/auth/provider.d.ts +24 -0
  92. package/dist/mcp/auth/provider.js +372 -0
  93. package/dist/mcp/auth/provider.js.map +1 -0
  94. package/dist/mcp/auth/registration.d.ts +12 -0
  95. package/dist/mcp/auth/registration.js +38 -0
  96. package/dist/mcp/auth/registration.js.map +1 -0
  97. package/dist/mcp/auth/security.d.ts +2 -0
  98. package/dist/mcp/auth/security.js +29 -0
  99. package/dist/mcp/auth/security.js.map +1 -0
  100. package/dist/mcp/auth/token-exchange.d.ts +24 -0
  101. package/dist/mcp/auth/token-exchange.js +66 -0
  102. package/dist/mcp/auth/token-exchange.js.map +1 -0
  103. package/dist/mcp/auth/token-store.d.ts +3 -0
  104. package/dist/mcp/auth/token-store.js +170 -0
  105. package/dist/mcp/auth/token-store.js.map +1 -0
  106. package/dist/mcp/auth/types.d.ts +62 -0
  107. package/dist/mcp/auth/types.js +2 -0
  108. package/dist/mcp/auth/types.js.map +1 -0
  109. package/dist/mcp/auth/www-authenticate.d.ts +2 -0
  110. package/dist/mcp/auth/www-authenticate.js +42 -0
  111. package/dist/mcp/auth/www-authenticate.js.map +1 -0
  112. package/dist/mcp/discovery.js +48 -1
  113. package/dist/mcp/discovery.js.map +1 -1
  114. package/dist/mcp/manager.d.ts +19 -0
  115. package/dist/mcp/manager.js +102 -10
  116. package/dist/mcp/manager.js.map +1 -1
  117. package/dist/mcp/mentions.js +2 -2
  118. package/dist/mcp/mentions.js.map +1 -1
  119. package/dist/mcp/runtime.d.ts +12 -0
  120. package/dist/mcp/runtime.js +146 -8
  121. package/dist/mcp/runtime.js.map +1 -1
  122. package/dist/mcp/transport-http.d.ts +12 -1
  123. package/dist/mcp/transport-http.js +160 -45
  124. package/dist/mcp/transport-http.js.map +1 -1
  125. package/dist/mcp/types.d.ts +18 -0
  126. package/dist/mcp/types.js.map +1 -1
  127. package/dist/mcp/validation.d.ts +10 -1
  128. package/dist/mcp/validation.js +111 -1
  129. package/dist/mcp/validation.js.map +1 -1
  130. package/dist/noninteractive/cancellation.d.ts +15 -0
  131. package/dist/noninteractive/cancellation.js +49 -0
  132. package/dist/noninteractive/cancellation.js.map +1 -0
  133. package/dist/noninteractive/readline-prompts.d.ts +1 -0
  134. package/dist/noninteractive/readline-prompts.js +14 -0
  135. package/dist/noninteractive/readline-prompts.js.map +1 -1
  136. package/dist/noninteractive/start-noninteractive.js +11 -10
  137. package/dist/noninteractive/start-noninteractive.js.map +1 -1
  138. package/dist/os/screen-sequences.d.ts +4 -0
  139. package/dist/os/screen-sequences.js +9 -0
  140. package/dist/os/screen-sequences.js.map +1 -1
  141. package/dist/os/terminal-rescue.js +2 -2
  142. package/dist/os/terminal-rescue.js.map +1 -1
  143. package/dist/os/terminal-write.d.ts +5 -0
  144. package/dist/os/terminal-write.js +45 -0
  145. package/dist/os/terminal-write.js.map +1 -0
  146. package/dist/safety/classifier.js +7 -2
  147. package/dist/safety/classifier.js.map +1 -1
  148. package/dist/session-runtime/alt-screen.js +2 -1
  149. package/dist/session-runtime/alt-screen.js.map +1 -1
  150. package/dist/session-runtime/child-bridge.d.ts +1 -1
  151. package/dist/session-runtime/child-bridge.js +7 -2
  152. package/dist/session-runtime/child-bridge.js.map +1 -1
  153. package/dist/session-runtime/client.js +13 -5
  154. package/dist/session-runtime/client.js.map +1 -1
  155. package/dist/session-runtime/host.d.ts +2 -0
  156. package/dist/session-runtime/host.js +19 -2
  157. package/dist/session-runtime/host.js.map +1 -1
  158. package/dist/session-runtime/reaper.d.ts +4 -0
  159. package/dist/session-runtime/reaper.js +64 -0
  160. package/dist/session-runtime/reaper.js.map +1 -0
  161. package/dist/session-runtime/terminal-modes.d.ts +8 -0
  162. package/dist/session-runtime/terminal-modes.js +51 -0
  163. package/dist/session-runtime/terminal-modes.js.map +1 -0
  164. package/dist/session-runtime/types.d.ts +2 -0
  165. package/dist/store/config.js +1 -0
  166. package/dist/store/config.js.map +1 -1
  167. package/dist/store/history.d.ts +9 -3
  168. package/dist/store/history.js +39 -8
  169. package/dist/store/history.js.map +1 -1
  170. package/dist/store/session-model.d.ts +29 -0
  171. package/dist/store/session-model.js +212 -0
  172. package/dist/store/session-model.js.map +1 -0
  173. package/dist/tools/definitions.d.ts +2 -0
  174. package/dist/tools/definitions.js +54 -0
  175. package/dist/tools/definitions.js.map +1 -1
  176. package/dist/tui-v2/bootstrap/renderer-handle.d.ts +2 -1
  177. package/dist/tui-v2/bootstrap/renderer-handle.js +2 -1
  178. package/dist/tui-v2/bootstrap/renderer-handle.js.map +1 -1
  179. package/dist/tui-v2/bootstrap/resize-repaint.d.ts +3 -1
  180. package/dist/tui-v2/bootstrap/resize-repaint.js +18 -2
  181. package/dist/tui-v2/bootstrap/resize-repaint.js.map +1 -1
  182. package/dist/tui-v2/bootstrap/start-tui-v2.d.ts +1 -0
  183. package/dist/tui-v2/bootstrap/start-tui-v2.js +21 -17
  184. package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
  185. package/dist/tui-v2/components/transcript/intro-card.js +2 -2
  186. package/dist/tui-v2/components/transcript/intro-card.js.map +1 -1
  187. package/dist/tui-v2/composer/composer-editor.js +4 -2
  188. package/dist/tui-v2/composer/composer-editor.js.map +1 -1
  189. package/dist/types.d.ts +1 -1
  190. package/dist/types.js +1 -0
  191. package/dist/types.js.map +1 -1
  192. package/dist/ui-core/bootstrap/composition-root.d.ts +2 -2
  193. package/dist/ui-core/bootstrap/composition-root.js +3 -1
  194. package/dist/ui-core/bootstrap/composition-root.js.map +1 -1
  195. package/dist/ui-core/bootstrap/exit-epilogue.d.ts +2 -2
  196. package/dist/ui-core/bootstrap/exit-epilogue.js +1 -1
  197. package/dist/ui-core/bootstrap/exit-epilogue.js.map +1 -1
  198. package/dist/ui-core/bootstrap/session-resume.js +10 -0
  199. package/dist/ui-core/bootstrap/session-resume.js.map +1 -1
  200. package/dist/ui-core/commands/mcp-commands.js +182 -70
  201. package/dist/ui-core/commands/mcp-commands.js.map +1 -1
  202. package/dist/ui-core/commands/picker-commands.js +10 -8
  203. package/dist/ui-core/commands/picker-commands.js.map +1 -1
  204. package/dist/ui-core/commands/session-commands.js +41 -8
  205. package/dist/ui-core/commands/session-commands.js.map +1 -1
  206. package/dist/version.generated.d.ts +2 -2
  207. package/dist/version.generated.js +2 -2
  208. package/package.json +1 -1
package/README.md CHANGED
@@ -13,7 +13,7 @@ Two things make it practical for everyday use:
13
13
 
14
14
  ## Highlights
15
15
 
16
- - **Free-tier first.** 18 providers wired in, 6 cloud free tiers + local Ollama. The default provider is the **keyless Free** gateway (`free-2/kilo-auto/free`) so a fresh install runs at no cost with zero setup — no API key required.
16
+ - **Free-tier first.** 19 providers wired in, 6 cloud free tiers + local Ollama. The default provider is the **keyless Free** gateway (`free-2/kilo-auto/free`) so a fresh install runs at no cost with zero setup — no API key required.
17
17
  - **Multi-key smart switching.** Up to 10 keys per provider with a *sticky* active key and circular rotation on rate-limit / auth / quota / transient / 5xx / empty-response errors. Disable any key or endpoint row to skip it without deleting it. Optional cross-provider fallback and a free-only filter.
18
18
  - **Bring your own endpoint.** Deploy Kimi K3 (or Qwen / DeepSeek / GLM / GPT-OSS / your own fine-tune) to a [Modal](#modal--run-kimi-k3-on-your-own-endpoint-on-30month-of-free-credit) endpoint and drive it from clai on **$30/month of free compute credit**. Modal, Lightning AI and TokenRouter each keep a list of up to 10 base URLs with a sticky active one, so several deployments live side by side and you switch with one command — same editor, same ★ active row as keys.
19
19
  - **Scope-based pentesting.** Opt-in engagement scope with authorized/excluded targets, allowed phases, rate and concurrency ceilings, redirect and DNS-rebinding escape detection, and out-of-scope flagging — designed for authorized pentests and bug-bounty programs.
@@ -205,6 +205,7 @@ This is the core of clai's design: assemble capacity from free tiers, then survi
205
205
  | Fireworks | `accounts/fireworks/models/kimi-k2p6` | Paid (per token) | `FIREWORKS_API_KEY` |
206
206
  | Hetzner | `Qwen/Qwen3.6-35B-A3B-FP8` | Free (experiment — no billing yet) | `HETZNER_API_KEY` |
207
207
  | OrcaRouter | `openai/gpt-4o-mini` | Paid (per token, zero markup) | `ORCAROUTER_API_KEY` |
208
+ | Merge Gateway | `openai/gpt-5.2` | Paid (per token, free tier budget) | `MERGE_GATEWAY_API_KEY` |
208
209
 
209
210
  Several "paid" providers also expose limited free allowances — the tier label reflects what the default keys usually buy you. Flip `freeOnly` off to include paid providers in fallback.
210
211
 
@@ -313,6 +314,33 @@ clai use orcarouter # model defaults to openai/gpt-4o-mini
313
314
 
314
315
  Streaming, native tool calling, structured outputs (`response_format`), vision via `image_url` and prompt caching all work. Reasoning uses one unified `reasoning_effort` knob (low/medium/high, plus minimal/max on some models) that the gateway translates to each upstream's native shape — `/think` and `/effort` map onto it, and thinking arrives as `reasoning_content` in the usual thinking block. `/model` reads the live catalog (cached 1h), filtered to Chat-Completions-reachable models so image/video/tts ids stay out of the picker. Keys are multi-key with rotation like everywhere else; env var is `ORCAROUTER_API_KEY`.
315
316
 
317
+ #### Merge Gateway (one key, many vendors, OpenAI-compatible)
318
+
319
+ [Merge Gateway](https://gateway.merge.dev) fronts OpenAI, Anthropic, Google and other
320
+ upstreams behind a single key. It exposes two surfaces: its own Responses-style API at
321
+ `/v1` and a drop-in OpenAI-compatible surface at `/v1/openai`. clai drives the
322
+ OpenAI-compatible surface, so streaming, native tool calling, prompt caching, reasoning
323
+ effort, multi-key rotation and cross-provider fallback all behave exactly as they do for
324
+ every other OpenAI-compatible provider.
325
+
326
+ ```sh
327
+ clai set merge-gateway mg_your-key # key: https://gateway.merge.dev (starts with mg_)
328
+ clai use merge-gateway # model defaults to openai/gpt-5.2
329
+ /model anthropic/claude-sonnet-4-6 # or any id from the live /models catalog
330
+ ```
331
+
332
+ Model ids are vendor-prefixed (`openai/gpt-5.2`, `anthropic/claude-sonnet-4-6`,
333
+ `google/gemini-3.5-flash`, `deepseek/deepseek-reasoner`). `/model` reads the live catalog
334
+ (cached 1h) filtered to ids reachable over Chat Completions, so embedding, image and audio
335
+ entries stay out of the picker; if the catalog cannot be fetched, a documented offline
336
+ subset is shown. Reasoning uses one `reasoning_effort` knob (low/medium/high) that the
337
+ gateway translates per upstream, and thinking arrives as `reasoning_content` in the usual
338
+ `/think` block. Requests carry both `Authorization: Bearer` and `X-API-Key`, matching the
339
+ two shapes the gateway accepts. The free tier has a budget: once exhausted the gateway
340
+ answers `402`, which clai treats as a quota error and rotates to the next key or provider.
341
+ Aliases: `merge-gateway`, `mergegateway`, `merge`, `mg`. Env var `MERGE_GATEWAY_API_KEY`.
342
+ `/info merge-gateway` prints the full walkthrough.
343
+
316
344
  ### Manage keys
317
345
 
318
346
  ```sh
@@ -328,6 +356,7 @@ clai set tokenrouter sk-your-key # TokenRouter
328
356
  clai set fireworks fw_your_key # Fireworks
329
357
  clai set hetzner your-token # Hetzner Inference (experiments.hetzner.com)
330
358
  clai set orcarouter sk-your-key # OrcaRouter (orcarouter.ai/console)
359
+ clai set merge-gateway mg_your-key # Merge Gateway (gateway.merge.dev)
331
360
  clai set free <key> # optional: unlock premium models (free is keyless by default)
332
361
  clai unset modal --url # drop stored endpoint URLs, keep the keys
333
362
  clai keys # providers + masked keys (★ active) + endpoint URLs
@@ -477,6 +506,7 @@ Optional controls:
477
506
 
478
507
  ```sh
479
508
  CLAI_SESSION_RUNTIME_IDLE_MS=1800000 # detached idle lifetime (1 minute–24 hours)
509
+ CLAI_SESSION_RUNTIME_MAX_IDLE=6 # idle-detached LRU cap (1–256; 0 disables)
480
510
  CLAI_DISABLE_SESSION_RUNTIME=1 # force legacy direct foreground ownership
481
511
  ```
482
512
 
@@ -493,7 +523,7 @@ CLAI_DISABLE_SESSION_RUNTIME=1 # force legacy direct foreground ownership
493
523
  | `/effort [level]` · `/reasoning [level]` | Thinking / reasoning effort |
494
524
  | `/freeonly [on\|off]` · `/fallback [on\|off]` | Free-only filter · cross-provider fallback |
495
525
  | `/search [provider]` · `/search-provider` | Choose web-search backend |
496
- | `/mcp [server\|all\|off\|list\|status\|tools\|locations\|refresh]` | Browse and select MCP servers; add/reconnect servers and inspect project/inherited configuration |
526
+ | `/mcp [server\|all\|off\|list\|status\|tools\|locations\|refresh\|login <server>\|add notion]` | Browse/select MCP servers, sign in with OAuth, or connect official Notion MCP |
497
527
  | `/scope [show\|add\|new\|clear]` | Engagement scope |
498
528
  | `/output [last\|id\|list]` | Open full tool output (also `Ctrl+O`) |
499
529
  | `/jobs` | Background jobs (also `Ctrl+J`) |
@@ -546,8 +576,9 @@ suppresses progress while retaining the final answer on stdout, and `--verbose`
546
576
  output and diff hunks. `--no-history` disables persistence for that run.
547
577
 
548
578
  A completed, partially completed, blocked, or failed turn returns exit code `0`; an aborted
549
- turn returns `130`; an unhandled or loader error returns `1`. `--show-thinking` can also be
550
- enabled with `CLAI_SHOW_THINKING=1`.
579
+ turn returns `130`; an unhandled or loader error returns `1`. While one-shot work is running,
580
+ press `Esc` or `Ctrl+C` to abort it, including commands launched through `bun run dev`.
581
+ `--show-thinking` can also be enabled with `CLAI_SHOW_THINKING=1`.
551
582
 
552
583
  ---
553
584
 
@@ -579,7 +610,7 @@ The native project file is `.clai/mcp.json`. It accepts JSON or JSONC and the co
579
610
  }
580
611
  ```
581
612
 
582
- A `command` entry uses stdio. A `url` entry uses Streamable HTTP by default; set `"type": "sse"` for a legacy SSE endpoint. `${workspaceFolder}`, `${env:NAME}` / `${env.NAME}`, and compatible `${input:name}` substitutions are resolved at discovery time, and resolved secret values are redacted from diagnostics.
613
+ A `command` entry uses stdio. A `url` entry uses Streamable HTTP by default; set `"type": "sse"` for a legacy SSE endpoint. Remote HTTP/SSE entries with no `auth` block use OAuth discovery when the server requests authorization; set `"auth": {"kind": "none"}` to opt out explicitly for a public server. `${workspaceFolder}`, `${env:NAME}` / `${env.NAME}`, and compatible `${input:name}` substitutions are resolved at discovery time, and resolved secret values are redacted from diagnostics.
583
614
 
584
615
  The picker can add one server without hand-editing the file: choose **+ add MCP server** and a full multiline editor opens — visible caret, arrow-key and word/line navigation, mid-text edits, selection, and multi-line paste. `Enter` inserts a newline, `Ctrl+S` saves, `Esc` cancels. If the JSON does not validate, the editor reopens with your text intact and the parse error in the header, so a long paste is never retyped. You can also supply the fragment inline:
585
616
 
@@ -604,6 +635,8 @@ clai also inherits compatible configuration from `CLAI_MCP_CONFIG` (an OS-delimi
604
635
  /mcp locations # project and inherited configuration paths
605
636
  /mcp refresh # rediscover configs and live tools
606
637
  /mcp reconnect docs # restart one server connection
638
+ /mcp login docs # run OAuth browser sign-in, store the token, reconnect
639
+ /mcp add notion # add https://mcp.notion.com/mcp and sign in
607
640
  ```
608
641
 
609
642
  Static built-in tools always remain first; selected MCP definitions are appended in deterministic order as `mcp.<server>.<tool>`. A tool explicitly annotated read-only can run under the normal safe/parallel policy. Unmarked, mutating, or destructive MCP tools require the usual confirmation, and ask mode exposes only safe tools. Server descriptions and results are treated as untrusted data, secrets are redacted, response sizes and lifecycles are bounded, and HTTP credentials are never forwarded through redirects.
@@ -689,7 +722,7 @@ clai model <name> # default model for the active provider
689
722
  /privacy clear-all # wipe history, logs, and artifacts
690
723
  ```
691
724
 
692
- Config lives under your OS user config dir (e.g. `~/.config/clai/`). Keys are stored locally and shown only masked.
725
+ Config lives under your OS user config dir (e.g. `~/.config/clai/`). Keys are stored locally and shown only masked. Inside an interactive console, `/provider`, `/model`, and `/models` change only that session and are restored with it; the explicit `clai use`, `clai provider`, and `clai model` CLI commands continue to set global defaults.
693
726
 
694
727
  ---
695
728
 
@@ -732,7 +765,7 @@ clai/
732
765
  ├─ src/
733
766
  │ ├─ index.ts # CLI entry + subcommands
734
767
  │ ├─ agent/ # loop, plans, compaction, resume orientation, tool parsing
735
- │ ├─ llm/ # 18 providers, streaming, native tools, key rotation + fallback
768
+ │ ├─ llm/ # 19 providers, streaming, native tools, key rotation + fallback
736
769
  │ ├─ mcp/ # discovery, validation, transports, lifecycle, and tool dispatch
737
770
  │ ├─ tools/ # fs, shell, net, http, web, pentest, batch, plan
738
771
  │ ├─ safety/ # risk classifier + engagement (scope) policy
@@ -44,7 +44,7 @@ function safeEngagementActionsForToolCall(call) {
44
44
  }
45
45
  }
46
46
  import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, } from "../tools/registry.js";
47
- import { getToolDefinitions, getCompactToolDefinitions, RUNNER_META_TOOL_NAMES, } from "../tools/definitions.js";
47
+ import { getToolDefinitions, getCompactToolDefinitions, RUNNER_META_TOOL_NAMES, MCP_AGENT_TOOL_NAMES, mcpAgentToolNames, } from "../tools/definitions.js";
48
48
  import { elidedStubReuseMessage, findElidedStubArg, } from "./message-slim.js";
49
49
  import { appendAssistantWithTools, ensureUniqueToolCallIds, toolCallIdsInHistory, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
50
50
  import { legacyReasoningBlockFromArtifacts, reasoningArtifactsForPersistence, } from "../llm/reasoning-artifacts.js";
@@ -108,6 +108,36 @@ import { createTurnOutcome, normalizeTurnOutcomeInput, renderTurnOutcome, } from
108
108
  import { beginEngagementAction, finishEngagementAction, recordEngagementCheckpoint, reconcileEngagementJob, openEngagement, saveEngagement, } from "../store/engagement.js";
109
109
  export * from "./tool-call-parser.js";
110
110
  export { createSessionPolicy, isPreApprovalAllowedTool, isPlanApprovedByStatus, planHasOpenWork, shouldEnableImageOcr, } from "./session-policy.js";
111
+ function mcpAgentToolTarget(args) {
112
+ if (Array.isArray(args.servers)) {
113
+ return args.servers.filter((entry) => typeof entry === "string");
114
+ }
115
+ if (typeof args.server === "string")
116
+ return args.server;
117
+ return undefined;
118
+ }
119
+ async function runMcpAgentTool(mcp, call) {
120
+ const args = call.args ?? {};
121
+ if (call.name === "mcp.list")
122
+ return mcp.agentList();
123
+ if (call.name === "mcp.tools") {
124
+ return mcp.agentTools(typeof args.server === "string" ? args.server : undefined);
125
+ }
126
+ if (call.name === "mcp.enable")
127
+ return mcp.agentEnable(mcpAgentToolTarget(args));
128
+ if (call.name === "mcp.connect") {
129
+ return mcp.agentConnect(typeof args.server === "string" ? args.server : "");
130
+ }
131
+ return mcp.agentLogin(typeof args.server === "string" ? args.server : "");
132
+ }
133
+ function mcpAgentOutput(call, result) {
134
+ const text = result.output.trim();
135
+ if (text.length > 0)
136
+ return text;
137
+ return result.ok
138
+ ? `${call.name} completed successfully with no textual output.`
139
+ : `${call.name} failed with no textual output (exit ${result.exitCode ?? 1}).`;
140
+ }
111
141
  /**
112
142
  * A foreground task waits for a responder child only when the plan
113
143
  * Declares that dependency. Report titles carry no scheduling meaning: any
@@ -417,7 +447,11 @@ export async function runAgentTurn(prompt, options = {}) {
417
447
  // image.view is different from optimistic user-attachment handling: once
418
448
  // the tool succeeds, the model must actually receive and inspect its bytes.
419
449
  // Offer it only with affirmative capability evidence for the active route.
420
- const routeToolNames = (routeProvider, routeModel) => [...availableToolNames(), ...mcpToolNames].filter((name) => {
450
+ const routeToolNames = (routeProvider, routeModel) => [
451
+ ...availableToolNames(),
452
+ ...mcpToolNames,
453
+ ...(mcpRuntime ? mcpAgentToolNames(agentMode === "ask") : []),
454
+ ].filter((name) => {
421
455
  if (name === "image.ocr")
422
456
  return imageOcrEnabled;
423
457
  if (name === "image.view") {
@@ -1256,14 +1290,58 @@ export async function runAgentTurn(prompt, options = {}) {
1256
1290
  return release;
1257
1291
  },
1258
1292
  };
1259
- /**
1260
- * Apply a salvaged partial write through the NORMAL tool path so the
1261
- * classifier, scope/engagement gates, confirmation prompt, and receipts
1262
- * all apply exactly as they would for a model-emitted call. Salvage must
1263
- * never mutate a file with `confirmed: true`, and an `fs.append` that was
1264
- * cut off must stay an append (with its precondition) instead of becoming
1265
- * a full overwrite.
1266
- */
1293
+ function showMcpAgentCall(toolEventId, call) {
1294
+ if (alreadyPrintedIds.has(toolEventId))
1295
+ return;
1296
+ writeToolCall(toolEventId, call);
1297
+ alreadyPrintedIds.add(toolEventId);
1298
+ }
1299
+ function failMcpAgentCall(toolEventId, call, reason, cancelled = false) {
1300
+ showMcpAgentCall(toolEventId, call);
1301
+ const result = { ok: false, output: reason, exitCode: 1 };
1302
+ const body = cancelled ? `cancelled: ${reason}` : reason;
1303
+ writeToolOutput(toolEventId, `${body}\n`, { replace: true });
1304
+ emitToolResult(toolEventId, result, reason);
1305
+ return {
1306
+ ok: false,
1307
+ call,
1308
+ result,
1309
+ contextOutput: reason,
1310
+ ...(cancelled ? { blockOrCancel: true } : {}),
1311
+ };
1312
+ }
1313
+ async function executeMcpAgentCall(runtime, call, toolEventId) {
1314
+ const readOnly = call.name === "mcp.list" || call.name === "mcp.tools";
1315
+ if (!readOnly && agentMode === "ask") {
1316
+ return failMcpAgentCall(toolEventId, call, `${call.name} is not available in ask mode because it changes MCP session state. Switch to agent mode.`);
1317
+ }
1318
+ if (!readOnly) {
1319
+ const releasePrompt = await promptMutex.acquire();
1320
+ let confirmed = true;
1321
+ try {
1322
+ confirmed = await confirmToolExecution(call, Boolean(options.autoConfirm), session, confirmPort);
1323
+ restoreInteractiveStdin();
1324
+ }
1325
+ finally {
1326
+ releasePrompt();
1327
+ }
1328
+ if (!confirmed) {
1329
+ return failMcpAgentCall(toolEventId, call, "Cancelled.", true);
1330
+ }
1331
+ }
1332
+ showMcpAgentCall(toolEventId, call);
1333
+ const result = await runMcpAgentTool(runtime, call);
1334
+ const shown = mcpAgentOutput(call, result);
1335
+ loopGuard.recordAttempt(step, call.name, call.args, result.ok, 0, shown);
1336
+ writeToolOutput(toolEventId, `${shown}\n`, { replace: true });
1337
+ emitToolResult(toolEventId, { ...result, output: shown }, shown);
1338
+ return {
1339
+ ok: result.ok,
1340
+ call,
1341
+ result: { ...result, output: shown },
1342
+ contextOutput: shown,
1343
+ };
1344
+ }
1267
1345
  async function applySalvagedWrite(salvaged) {
1268
1346
  const args = {
1269
1347
  path: salvaged.path,
@@ -1299,6 +1377,20 @@ export async function runAgentTurn(prompt, options = {}) {
1299
1377
  bytesOnDisk,
1300
1378
  };
1301
1379
  }
1380
+ function invalidToolCall(call) {
1381
+ if (call.args?.__nativeParseError) {
1382
+ const raw = String(call.args._raw ?? "").slice(0, 200);
1383
+ const reason = "Tool call arguments were not valid JSON (truncated or malformed). " +
1384
+ "Retry with smaller content, or use fs.writeMany / fs.append continuation. " +
1385
+ (raw ? `Partial: ${raw}` : "");
1386
+ return { reason, result: { ok: false, output: reason, exitCode: 1 } };
1387
+ }
1388
+ const elidedStub = findElidedStubArg(call.args);
1389
+ if (!elidedStub)
1390
+ return undefined;
1391
+ const reason = elidedStubReuseMessage(elidedStub.key);
1392
+ return { reason, result: { ok: false, output: reason, exitCode: 1 } };
1393
+ }
1302
1394
  async function executeSingleTool(rawCall, toolEventId, parentSignal) {
1303
1395
  const scratchDir = scratchDirFor(safeCwd());
1304
1396
  const normalizedCall = normalizeToolCall(rawCall);
@@ -1323,21 +1415,15 @@ export async function runAgentTurn(prompt, options = {}) {
1323
1415
  let engagementLease;
1324
1416
  let engagementGraph;
1325
1417
  let engagementRecord;
1326
- if (call.args?.__nativeParseError) {
1327
- const raw = String(call.args._raw ?? "").slice(0, 200);
1328
- const reason = "Tool call arguments were not valid JSON (truncated or malformed). " +
1329
- "Retry with smaller content, or use fs.writeMany / fs.append continuation. " +
1330
- (raw ? `Partial: ${raw}` : "");
1331
- const result = { ok: false, output: reason, exitCode: 1 };
1332
- emitToolResult(toolEventId, result, reason);
1333
- return { ok: false, call, result, contextOutput: reason };
1334
- }
1335
- const elidedStub = findElidedStubArg(call.args);
1336
- if (elidedStub) {
1337
- const reason = elidedStubReuseMessage(elidedStub.key);
1338
- const result = { ok: false, output: reason, exitCode: 1 };
1339
- emitToolResult(toolEventId, result, reason);
1340
- return { ok: false, call, result, contextOutput: reason };
1418
+ const invalid = invalidToolCall(call);
1419
+ if (invalid) {
1420
+ emitToolResult(toolEventId, invalid.result, invalid.reason);
1421
+ return {
1422
+ ok: false,
1423
+ call,
1424
+ result: invalid.result,
1425
+ contextOutput: invalid.reason,
1426
+ };
1341
1427
  }
1342
1428
  if (call.name === "image.ocr" && !imageOcrEnabled) {
1343
1429
  writeNotice("info", "skipped OCR because the original image is attached to the vision model");
@@ -1436,6 +1522,9 @@ export async function runAgentTurn(prompt, options = {}) {
1436
1522
  loopGuard.recordAttempt(step, call.name, call.args, true, 0, output);
1437
1523
  return { ok: true, call, result, contextOutput: output };
1438
1524
  }
1525
+ if (mcpRuntime && MCP_AGENT_TOOL_NAMES.has(call.name)) {
1526
+ return executeMcpAgentCall(mcpRuntime, call, toolEventId);
1527
+ }
1439
1528
  if (RUNNER_META_TOOL_NAMES.has(call.name)) {
1440
1529
  if (call.name === "job.read" || call.name === "task.read") {
1441
1530
  const requestedNotificationId = typeof call.args.notificationId === "string"