@pentoshi/clai 4.10.1 → 4.11.0
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.
- package/README.md +40 -7
- package/dist/agent/runner.js +114 -25
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/task-evidence.js +0 -63
- package/dist/agent/task-evidence.js.map +1 -1
- package/dist/app/adapters/current-store-adapter.js +8 -2
- package/dist/app/adapters/current-store-adapter.js.map +1 -1
- package/dist/app/commands/catalog.js +19 -2
- package/dist/app/commands/catalog.js.map +1 -1
- package/dist/app/controllers/session-controller.d.ts +4 -2
- package/dist/app/controllers/session-controller.js +26 -38
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/controllers/session-loop-recovery.d.ts +13 -0
- package/dist/app/controllers/session-loop-recovery.js +43 -0
- package/dist/app/controllers/session-loop-recovery.js.map +1 -0
- package/dist/app/ports/persistence-port.d.ts +3 -1
- package/dist/classic/bootstrap/start-classic.d.ts +1 -0
- package/dist/classic/bootstrap/start-classic.js +13 -3
- package/dist/classic/bootstrap/start-classic.js.map +1 -1
- package/dist/commands/providers.js +2 -0
- package/dist/commands/providers.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interactive-session/transport-node-pty.js +37 -0
- package/dist/interactive-session/transport-node-pty.js.map +1 -1
- package/dist/llm/capabilities.js +13 -0
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/free-default-model.d.ts +6 -0
- package/dist/llm/free-default-model.js +32 -0
- package/dist/llm/free-default-model.js.map +1 -0
- package/dist/llm/free.js +39 -0
- package/dist/llm/free.js.map +1 -1
- package/dist/llm/merge-gateway.d.ts +6 -0
- package/dist/llm/merge-gateway.js +157 -0
- package/dist/llm/merge-gateway.js.map +1 -0
- package/dist/llm/meta.d.ts +1 -2
- package/dist/llm/meta.js +31 -1140
- package/dist/llm/meta.js.map +1 -1
- package/dist/llm/provider-model-layers.js +6 -0
- package/dist/llm/provider-model-layers.js.map +1 -1
- package/dist/llm/provider-profile-layers.js +21 -0
- package/dist/llm/provider-profile-layers.js.map +1 -1
- package/dist/llm/provider.js +68 -0
- package/dist/llm/provider.js.map +1 -1
- package/dist/llm/reasoning-capability.js +8 -0
- package/dist/llm/reasoning-capability.js.map +1 -1
- package/dist/llm/responses-complete.d.ts +4 -0
- package/dist/llm/responses-complete.js +28 -0
- package/dist/llm/responses-complete.js.map +1 -0
- package/dist/llm/responses-config.d.ts +22 -0
- package/dist/llm/responses-config.js +19 -0
- package/dist/llm/responses-config.js.map +1 -0
- package/dist/llm/responses-dialect.d.ts +6 -0
- package/dist/llm/responses-dialect.js +6 -0
- package/dist/llm/responses-dialect.js.map +1 -0
- package/dist/llm/responses-http.d.ts +13 -0
- package/dist/llm/responses-http.js +88 -0
- package/dist/llm/responses-http.js.map +1 -0
- package/dist/llm/responses-parse.d.ts +42 -0
- package/dist/llm/responses-parse.js +242 -0
- package/dist/llm/responses-parse.js.map +1 -0
- package/dist/llm/responses-request.d.ts +15 -0
- package/dist/llm/responses-request.js +209 -0
- package/dist/llm/responses-request.js.map +1 -0
- package/dist/llm/responses-stream-accumulator.d.ts +38 -0
- package/dist/llm/responses-stream-accumulator.js +102 -0
- package/dist/llm/responses-stream-accumulator.js.map +1 -0
- package/dist/llm/responses-stream-events.d.ts +6 -0
- package/dist/llm/responses-stream-events.js +398 -0
- package/dist/llm/responses-stream-events.js.map +1 -0
- package/dist/llm/responses-stream-watchdog.d.ts +14 -0
- package/dist/llm/responses-stream-watchdog.js +64 -0
- package/dist/llm/responses-stream-watchdog.js.map +1 -0
- package/dist/llm/responses-stream.d.ts +4 -0
- package/dist/llm/responses-stream.js +264 -0
- package/dist/llm/responses-stream.js.map +1 -0
- package/dist/llm/router.js +3 -0
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/tool-protocol.js +35 -3
- package/dist/llm/tool-protocol.js.map +1 -1
- package/dist/mcp/auth/host-credentials.d.ts +11 -0
- package/dist/mcp/auth/host-credentials.js +54 -0
- package/dist/mcp/auth/host-credentials.js.map +1 -0
- package/dist/mcp/auth/loopback.d.ts +16 -0
- package/dist/mcp/auth/loopback.js +126 -0
- package/dist/mcp/auth/loopback.js.map +1 -0
- package/dist/mcp/auth/metadata.d.ts +9 -0
- package/dist/mcp/auth/metadata.js +102 -0
- package/dist/mcp/auth/metadata.js.map +1 -0
- package/dist/mcp/auth/pkce.d.ts +3 -0
- package/dist/mcp/auth/pkce.js +17 -0
- package/dist/mcp/auth/pkce.js.map +1 -0
- package/dist/mcp/auth/provider.d.ts +24 -0
- package/dist/mcp/auth/provider.js +372 -0
- package/dist/mcp/auth/provider.js.map +1 -0
- package/dist/mcp/auth/registration.d.ts +12 -0
- package/dist/mcp/auth/registration.js +38 -0
- package/dist/mcp/auth/registration.js.map +1 -0
- package/dist/mcp/auth/security.d.ts +2 -0
- package/dist/mcp/auth/security.js +29 -0
- package/dist/mcp/auth/security.js.map +1 -0
- package/dist/mcp/auth/token-exchange.d.ts +24 -0
- package/dist/mcp/auth/token-exchange.js +66 -0
- package/dist/mcp/auth/token-exchange.js.map +1 -0
- package/dist/mcp/auth/token-store.d.ts +3 -0
- package/dist/mcp/auth/token-store.js +170 -0
- package/dist/mcp/auth/token-store.js.map +1 -0
- package/dist/mcp/auth/types.d.ts +62 -0
- package/dist/mcp/auth/types.js +2 -0
- package/dist/mcp/auth/types.js.map +1 -0
- package/dist/mcp/auth/www-authenticate.d.ts +2 -0
- package/dist/mcp/auth/www-authenticate.js +42 -0
- package/dist/mcp/auth/www-authenticate.js.map +1 -0
- package/dist/mcp/discovery.js +48 -1
- package/dist/mcp/discovery.js.map +1 -1
- package/dist/mcp/manager.d.ts +19 -0
- package/dist/mcp/manager.js +102 -10
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/mentions.js +2 -2
- package/dist/mcp/mentions.js.map +1 -1
- package/dist/mcp/runtime.d.ts +12 -0
- package/dist/mcp/runtime.js +146 -8
- package/dist/mcp/runtime.js.map +1 -1
- package/dist/mcp/transport-http.d.ts +12 -1
- package/dist/mcp/transport-http.js +160 -45
- package/dist/mcp/transport-http.js.map +1 -1
- package/dist/mcp/types.d.ts +18 -0
- package/dist/mcp/types.js.map +1 -1
- package/dist/mcp/validation.d.ts +10 -1
- package/dist/mcp/validation.js +111 -1
- package/dist/mcp/validation.js.map +1 -1
- package/dist/noninteractive/cancellation.d.ts +15 -0
- package/dist/noninteractive/cancellation.js +49 -0
- package/dist/noninteractive/cancellation.js.map +1 -0
- package/dist/noninteractive/readline-prompts.d.ts +1 -0
- package/dist/noninteractive/readline-prompts.js +14 -0
- package/dist/noninteractive/readline-prompts.js.map +1 -1
- package/dist/noninteractive/start-noninteractive.js +11 -10
- package/dist/noninteractive/start-noninteractive.js.map +1 -1
- package/dist/os/screen-sequences.d.ts +6 -0
- package/dist/os/screen-sequences.js +12 -0
- package/dist/os/screen-sequences.js.map +1 -0
- package/dist/os/terminal-rescue.js +2 -2
- package/dist/os/terminal-rescue.js.map +1 -1
- package/dist/os/terminal-write.d.ts +5 -0
- package/dist/os/terminal-write.js +45 -0
- package/dist/os/terminal-write.js.map +1 -0
- package/dist/safety/classifier.js +7 -2
- package/dist/safety/classifier.js.map +1 -1
- package/dist/session-runtime/alt-screen.d.ts +9 -0
- package/dist/session-runtime/alt-screen.js +32 -0
- package/dist/session-runtime/alt-screen.js.map +1 -0
- package/dist/session-runtime/child-bridge.d.ts +1 -1
- package/dist/session-runtime/child-bridge.js +7 -2
- package/dist/session-runtime/child-bridge.js.map +1 -1
- package/dist/session-runtime/client.d.ts +1 -0
- package/dist/session-runtime/client.js +26 -12
- package/dist/session-runtime/client.js.map +1 -1
- package/dist/session-runtime/host.d.ts +2 -0
- package/dist/session-runtime/host.js +20 -7
- package/dist/session-runtime/host.js.map +1 -1
- package/dist/session-runtime/reaper.d.ts +4 -0
- package/dist/session-runtime/reaper.js +64 -0
- package/dist/session-runtime/reaper.js.map +1 -0
- package/dist/session-runtime/terminal-modes.d.ts +8 -0
- package/dist/session-runtime/terminal-modes.js +51 -0
- package/dist/session-runtime/terminal-modes.js.map +1 -0
- package/dist/session-runtime/types.d.ts +2 -0
- package/dist/store/config.js +1 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/history.d.ts +9 -3
- package/dist/store/history.js +39 -8
- package/dist/store/history.js.map +1 -1
- package/dist/store/session-model.d.ts +28 -0
- package/dist/store/session-model.js +212 -0
- package/dist/store/session-model.js.map +1 -0
- package/dist/tools/definitions.d.ts +2 -0
- package/dist/tools/definitions.js +54 -0
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tui-v2/bootstrap/renderer-handle.d.ts +19 -0
- package/dist/tui-v2/bootstrap/renderer-handle.js +35 -0
- package/dist/tui-v2/bootstrap/renderer-handle.js.map +1 -0
- package/dist/tui-v2/bootstrap/resize-repaint.d.ts +16 -0
- package/dist/tui-v2/bootstrap/resize-repaint.js +37 -0
- package/dist/tui-v2/bootstrap/resize-repaint.js.map +1 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.d.ts +1 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js +37 -37
- package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
- package/dist/tui-v2/components/transcript/intro-card.js +2 -2
- package/dist/tui-v2/components/transcript/intro-card.js.map +1 -1
- package/dist/tui-v2/composer/composer-editor.js +4 -2
- package/dist/tui-v2/composer/composer-editor.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/ui-core/bootstrap/composition-root.d.ts +2 -2
- package/dist/ui-core/bootstrap/composition-root.js +3 -1
- package/dist/ui-core/bootstrap/composition-root.js.map +1 -1
- package/dist/ui-core/bootstrap/exit-epilogue.d.ts +3 -2
- package/dist/ui-core/bootstrap/exit-epilogue.js +6 -2
- package/dist/ui-core/bootstrap/exit-epilogue.js.map +1 -1
- package/dist/ui-core/bootstrap/session-resume.js +10 -0
- package/dist/ui-core/bootstrap/session-resume.js.map +1 -1
- package/dist/ui-core/commands/mcp-commands.js +182 -70
- package/dist/ui-core/commands/mcp-commands.js.map +1 -1
- package/dist/ui-core/commands/picker-commands.js +10 -8
- package/dist/ui-core/commands/picker-commands.js.map +1 -1
- package/dist/ui-core/commands/session-commands.js +41 -8
- package/dist/ui-core/commands/session-commands.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- 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.**
|
|
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
|
|
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`.
|
|
550
|
-
|
|
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/ #
|
|
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
|
package/dist/agent/runner.js
CHANGED
|
@@ -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) => [
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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"
|