@pentoshi/clai 4.3.0 → 4.5.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.
Files changed (88) hide show
  1. package/README.md +45 -7
  2. package/dist/agent/runner.js +12 -13
  3. package/dist/agent/runner.js.map +1 -1
  4. package/dist/app/commands/catalog.js +4 -5
  5. package/dist/app/commands/catalog.js.map +1 -1
  6. package/dist/app/commands/registry.js +0 -1
  7. package/dist/app/commands/registry.js.map +1 -1
  8. package/dist/app/controllers/session-controller.d.ts +3 -0
  9. package/dist/app/controllers/session-controller.js +11 -25
  10. package/dist/app/controllers/session-controller.js.map +1 -1
  11. package/dist/app/controllers/session-persistence.d.ts +4 -1
  12. package/dist/app/controllers/session-persistence.js +23 -5
  13. package/dist/app/controllers/session-persistence.js.map +1 -1
  14. package/dist/app/controllers/session-responder.d.ts +1 -0
  15. package/dist/app/controllers/session-responder.js +8 -0
  16. package/dist/app/controllers/session-responder.js.map +1 -1
  17. package/dist/app/controllers/session-usage-ledger.d.ts +63 -0
  18. package/dist/app/controllers/session-usage-ledger.js +232 -0
  19. package/dist/app/controllers/session-usage-ledger.js.map +1 -0
  20. package/dist/classic/app/ClassicApp.js +2 -1
  21. package/dist/classic/app/ClassicApp.js.map +1 -1
  22. package/dist/classic/chrome/toast-rows.d.ts +1 -0
  23. package/dist/classic/chrome/toast-rows.js +18 -0
  24. package/dist/classic/chrome/toast-rows.js.map +1 -1
  25. package/dist/commands/update-install.d.ts +2 -0
  26. package/dist/commands/update-install.js +47 -4
  27. package/dist/commands/update-install.js.map +1 -1
  28. package/dist/commands/update.js +34 -2
  29. package/dist/commands/update.js.map +1 -1
  30. package/dist/index.js +6 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/llm/capabilities.d.ts +2 -1
  33. package/dist/llm/capabilities.js +13 -95
  34. package/dist/llm/capabilities.js.map +1 -1
  35. package/dist/llm/custom-provider-profile.js +2 -2
  36. package/dist/llm/custom-provider-profile.js.map +1 -1
  37. package/dist/llm/http.js +33 -0
  38. package/dist/llm/http.js.map +1 -1
  39. package/dist/llm/key-rotation.d.ts +1 -0
  40. package/dist/llm/key-rotation.js +8 -0
  41. package/dist/llm/key-rotation.js.map +1 -1
  42. package/dist/llm/provider-profile-layers.d.ts +1 -0
  43. package/dist/llm/provider-profile-layers.js +44 -15
  44. package/dist/llm/provider-profile-layers.js.map +1 -1
  45. package/dist/llm/provider-profile.d.ts +1 -0
  46. package/dist/llm/provider-profile.js +5 -0
  47. package/dist/llm/provider-profile.js.map +1 -1
  48. package/dist/llm/provider-profiles.js +9 -6
  49. package/dist/llm/provider-profiles.js.map +1 -1
  50. package/dist/llm/provider.js +3 -3
  51. package/dist/llm/provider.js.map +1 -1
  52. package/dist/llm/reasoning-capability.d.ts +3 -0
  53. package/dist/llm/reasoning-capability.js +96 -0
  54. package/dist/llm/reasoning-capability.js.map +1 -0
  55. package/dist/llm/stream-terminal.js +2 -1
  56. package/dist/llm/stream-terminal.js.map +1 -1
  57. package/dist/llm/system-messages.d.ts +1 -0
  58. package/dist/llm/system-messages.js +7 -0
  59. package/dist/llm/system-messages.js.map +1 -1
  60. package/dist/store/history.d.ts +1 -0
  61. package/dist/store/history.js +7 -0
  62. package/dist/store/history.js.map +1 -1
  63. package/dist/store/scope.d.ts +8 -0
  64. package/dist/store/scope.js +138 -1
  65. package/dist/store/scope.js.map +1 -1
  66. package/dist/tools/interactive-session-tools.js +5 -5
  67. package/dist/tools/interactive-session-tools.js.map +1 -1
  68. package/dist/tools/registry.js +2 -2
  69. package/dist/tools/registry.js.map +1 -1
  70. package/dist/ui-core/bootstrap/composition-root.js +8 -2
  71. package/dist/ui-core/bootstrap/composition-root.js.map +1 -1
  72. package/dist/ui-core/commands/command-handlers.js +2 -1
  73. package/dist/ui-core/commands/command-handlers.js.map +1 -1
  74. package/dist/ui-core/commands/config-commands.js +17 -14
  75. package/dist/ui-core/commands/config-commands.js.map +1 -1
  76. package/dist/ui-core/commands/picker-commands.js +3 -3
  77. package/dist/ui-core/commands/picker-commands.js.map +1 -1
  78. package/dist/ui-core/commands/session-commands.d.ts +1 -0
  79. package/dist/ui-core/commands/session-commands.js +20 -0
  80. package/dist/ui-core/commands/session-commands.js.map +1 -1
  81. package/dist/ui-core/rendering/format-help.js +2 -2
  82. package/dist/ui-core/rendering/format-help.js.map +1 -1
  83. package/dist/ui-core/rendering/format-usage.d.ts +6 -0
  84. package/dist/ui-core/rendering/format-usage.js +116 -0
  85. package/dist/ui-core/rendering/format-usage.js.map +1 -0
  86. package/dist/version.generated.d.ts +2 -2
  87. package/dist/version.generated.js +2 -2
  88. 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-1/deepseek-v4-flash-free`) so a fresh install runs at no cost with zero setup — no API key required.
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.
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.
@@ -31,17 +31,17 @@ Two things make it practical for everyday use:
31
31
  ```sh
32
32
  brew tap pentoshi007/clai && brew install clai
33
33
  # or
34
- curl -fsSL https://raw.githubusercontent.com/pentoshi007/clai/main/install/install.sh | sh
34
+ curl -fsSL https://downloads.clai.aniketpandey.website/install/install.sh | sh
35
35
  ```
36
36
 
37
37
  ### Linux
38
38
  ```sh
39
- curl -fsSL https://raw.githubusercontent.com/pentoshi007/clai/main/install/install.sh | sh
39
+ curl -fsSL https://downloads.clai.aniketpandey.website/install/install.sh | sh
40
40
  ```
41
41
 
42
42
  ### Windows
43
43
  ```powershell
44
- irm https://raw.githubusercontent.com/pentoshi007/clai/main/install/install.ps1 | iex
44
+ irm https://downloads.clai.aniketpandey.website/install/install.ps1 | iex
45
45
  # or
46
46
  scoop bucket add clai https://github.com/pentoshi007/clai && scoop install clai
47
47
  ```
@@ -93,6 +93,44 @@ The full-screen TUI uses mouse tracking and hover events via OpenTUI's Zig FFI r
93
93
 
94
94
  ---
95
95
 
96
+ ## Fast downloads (Cloudflare R2 mirror)
97
+
98
+ Release binaries are mirrored to a **Cloudflare R2** bucket served through Cloudflare's global CDN — significantly faster than GitHub's release hosting in most regions, and free for this project's traffic (R2 has **zero egress fees**).
99
+
100
+ Mirror layout at `https://downloads.clai.aniketpandey.website`:
101
+
102
+ | Path | Contents |
103
+ |---|---|
104
+ | `/vX.Y.Z/` | Binaries + `.sha256` sidecars for release vX.Y.Z |
105
+ | `/latest/` | Same files, refreshed on every release |
106
+ | `/install/` | `install.sh` / `install.ps1` used by the one-liners above |
107
+ | `/version.json` | Latest released version (update-check fallback) |
108
+
109
+ **Security model:** installers and `clai update` try the R2 mirror first for speed and fall back to GitHub Releases automatically. The SHA256 checksum is fetched from GitHub Releases (falling back to the mirror) and verified locally before anything is installed — the trust anchor stays with GitHub, so a bad mirror can only slow you down, never feed you tampered bytes. `CLAI_SKIP_CHECKSUM=1` still bypasses verification, at your own risk. If the mirror is unreachable but raw.githubusercontent.com works, grab the installer from `https://raw.githubusercontent.com/pentoshi007/clai/main/install/` instead — it makes the same mirror-first/fallback choices when it runs.
110
+
111
+ User knobs:
112
+
113
+ ```sh
114
+ CLAI_DOWNLOAD_BASE=https://your-mirror.example
115
+ CLAI_NO_MIRROR=1
116
+ ```
117
+
118
+ ### Maintainer setup (one-time, ~10 minutes)
119
+
120
+ 1. **Create the bucket.** Cloudflare dashboard → R2 → Create bucket, e.g. `clai-releases`. Any location hint works.
121
+ 2. **Make it publicly readable.** Bucket → Settings → Public access:
122
+ - **Option A — `r2.dev` (zero DNS config):** enable the managed "r2.dev URL" → you get `https://pub-<hash>.r2.dev`.
123
+ - **Option B — custom domain (the default everywhere in this repo):** add `downloads.clai.aniketpandey.website` as a custom domain (requires the zone's DNS on Cloudflare). Served over the edge cache with free SSL.
124
+ If you end up on a different hostname, update `DEFAULT_MIRROR_BASE` in `src/commands/update-install.ts`, the default base in `install/install.sh` and `install/install.ps1`, and the URLs in `manifests/` — or export `CLAI_DOWNLOAD_BASE` until a release ships the change.
125
+ 3. **Create an API token.** R2 → Manage R2 API Tokens → Create API token: permission **Object Read & Write**, scoped to the `clai-releases` bucket only. Keep the **Access Key ID**, **Secret Access Key**, and the **Account ID** shown on the token page.
126
+ 4. **Add repo secrets** (GitHub repo → Settings → Secrets and variables → Actions → Secrets): `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET_NAME`.
127
+ 5. **Add repo variable** (Variables tab): `R2_PUBLIC_URL` = your public base URL, e.g. `https://downloads.clai.aniketpandey.website`, no trailing slash.
128
+ 6. **Done.** On every `v*.*.*` tag, the `publish-r2` job in `.github/workflows/release.yml` uploads binaries + checksums to `/vX.Y.Z/` and `/latest/`, refreshes `/install/` and `/version.json`, then re-downloads every asset through the public URL and re-verifies its SHA256 against the sidecars. Missing secrets → the job skips without failing the release; missing `R2_PUBLIC_URL` → only the public-URL verification step is skipped.
129
+
130
+ **Cost:** the R2 free tier covers 10 GB-months of storage, 1M Class A and 10M Class B operations per month, and egress is always free — assets served via the custom domain ride the edge cache, so this stays $0 at clai's scale.
131
+
132
+ ---
133
+
96
134
 
97
135
  ## Quick start
98
136
 
@@ -136,7 +174,7 @@ This is the core of clai's design: assemble capacity from free tiers, then survi
136
174
 
137
175
  | Provider | Default model | Tier | Env var |
138
176
  |----------|---------------|------|---------|
139
- | Free (keyless) | `free-1/deepseek-v4-flash-free` | Free · keyless | — (no key needed) |
177
+ | Free (keyless) | `free-2/kilo-auto/free` | Free · keyless | — (no key needed) |
140
178
  | NVIDIA NIM | `openai/gpt-oss-20b` | Free | `NVIDIA_API_KEY` |
141
179
  | Google Gemini | `gemini-3.5-flash` | Free | `GEMINI_API_KEY` |
142
180
  | OpenRouter | `meta-llama/llama-3.3-70b-instruct:free` | Free | `OPENROUTER_API_KEY` |
@@ -159,7 +197,7 @@ Several "paid" providers also expose limited free allowances — the tier label
159
197
 
160
198
  #### Free — keyless, zero setup
161
199
 
162
- clai's default provider needs **no account, no API key, and no config**. It bundles **two keyless gateways** under one provider, namespaced by source so overlapping model names never collide: `free-1/…` ids come from the primary gateway and `free-2/…` ids from the [Kilo gateway](https://api.kilo.ai/api/gateway). Both serve rotating catalogs of free OpenAI-compatible models — `free-1/deepseek-v4-flash-free` (default), `free-1/mimo-v2.5-free`, `free-1/hy3-free`, `free-2/kilo-auto/free`, `free-2/stepfun/step-3.7-flash:free`, `free-2/nvidia/nemotron-3-ultra-550b-a55b:free` — with SSE streaming, native tool calling, and `reasoning_content` thinking folded into the normal `/think` block.
200
+ clai's default provider needs **no account, no API key, and no config**. It bundles **two keyless gateways** under one provider, namespaced by source so overlapping model names never collide: `free-1/…` ids come from the primary gateway and `free-2/…` ids from the [Kilo gateway](https://api.kilo.ai/api/gateway). Both serve rotating catalogs of free OpenAI-compatible models — `free-2/kilo-auto/free` (default), `free-1/mimo-v2.5-free`, `free-1/hy3-free`, `free-1/x-preview-f-free`, `free-2/stepfun/step-3.7-flash:free`, `free-2/nvidia/nemotron-3-ultra-550b-a55b:free` — with SSE streaming, native tool calling, `/effort` reasoning levels, and `reasoning_content` thinking folded into the normal `/think` block.
163
201
 
164
202
  ```sh
165
203
  clai # already on free — nothing to set up
@@ -412,7 +450,7 @@ Tool cards show the command/input clearly, with a live elapsed timer next to the
412
450
  |---------|------|
413
451
  | `/ask` · `/agent` · `/plan` | Switch mode (plan = design-then-approve) |
414
452
  | `/implement` · `/discard` | Approve+execute or drop the current plan |
415
- | `/model [name\|#]` · `/provider [name]` · `/use <provider>` | Pick model / switch provider |
453
+ | `/model [name\|#]` · `/provider [name]` | Pick model / switch provider |
416
454
  | `/set [provider]` · `/unset [provider]` · `/keys` · `/info [provider]` | Manage API keys, endpoint URLs, and view provider setup / pricing info |
417
455
  | `/effort [level]` · `/reasoning [level]` | Thinking / reasoning effort |
418
456
  | `/freeonly [on\|off]` · `/fallback [on\|off]` | Free-only filter · cross-provider fallback |
@@ -1,5 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { streamWithProvider } from "../llm/router.js";
3
+ import { isProviderFailureStatus } from "../llm/key-rotation.js";
4
+ import { REQUEST_CONTEXT_PREFIX, upsertRequestContextMessage, } from "../llm/system-messages.js";
3
5
  import { operationUsageFromError } from "../llm/operation-ledger.js";
4
6
  import { contextAttemptFromOperationUsage } from "../llm/context-snapshot.js";
5
7
  import { modelContextWindow } from "../llm/token-usage.js";
@@ -50,7 +52,7 @@ import { accountAssembledRequest, RequestOverLimitError, } from "./request-accou
50
52
  import { autoCompactTriggerTokens, dedupeToolContextOutput, freeTierGuardNotices, getReliabilityPolicy, resolveStepMaxTokens, } from "./reliability-policy.js";
51
53
  import { auditLog } from "../store/logs.js";
52
54
  import { loadProjectContext } from "../store/project.js";
53
- import { loadScope, isScopeActive } from "../store/scope.js";
55
+ import { loadScopeForSession, isScopeActive } from "../store/scope.js";
54
56
  import { ensureProviderConfigured } from "../commands/providers.js";
55
57
  import { createThinkingStreamParser, rememberThinking, stripThinking, } from "../ui/thinking.js";
56
58
  import { hasReasoningMarker } from "../llm/reasoning-marker.js";
@@ -640,7 +642,7 @@ export async function runAgentTurn(prompt, options = {}) {
640
642
  systemSections.push(pentestNoLocalServerDirective());
641
643
  }
642
644
  {
643
- const engScope = await loadScope().catch(() => undefined);
645
+ const engScope = await loadScopeForSession(session.sessionId).catch(() => undefined);
644
646
  const scopeBlock = scopeContextMessage(engScope);
645
647
  if (scopeBlock && (pentestSession || pentestLikeTurn) && !idleOrSocialPrompt) {
646
648
  systemSections.push(scopeBlock);
@@ -733,14 +735,12 @@ export async function runAgentTurn(prompt, options = {}) {
733
735
  if (options.images && options.images.length > 0) {
734
736
  userMessage.images = options.images;
735
737
  }
738
+ const requestContextMessage = `${REQUEST_CONTEXT_PREFIX}\n${requestContext}`;
736
739
  const messages = [
737
740
  { role: "system", content: fullSystemPrompt },
738
741
  ...(options.history ?? []),
739
- // Keep all per-request authority after prior history. This preserves the
740
- // longest shared prefix for APC providers while single-system dialects
741
- // retain it in place as a marked user turn.
742
- { role: "system", content: `REQUEST CONTEXT\n${requestContext}` },
743
742
  userMessage,
743
+ { role: "system", content: requestContextMessage },
744
744
  ];
745
745
  liveMessages = messages;
746
746
  if (activePlan) {
@@ -1063,6 +1063,7 @@ export async function runAgentTurn(prompt, options = {}) {
1063
1063
  : undefined,
1064
1064
  };
1065
1065
  snap.nextHint = inferNextHint(snap);
1066
+ upsertRequestContextMessage(messages, requestContextMessage);
1066
1067
  // One live plan copy, refreshed at the same protocol-safe points as
1067
1068
  // SESSION STATE so advancing tasks are never contradicted by a stale copy.
1068
1069
  if (p) {
@@ -1567,7 +1568,7 @@ export async function runAgentTurn(prompt, options = {}) {
1567
1568
  };
1568
1569
  }
1569
1570
  }
1570
- const scope = await loadScope();
1571
+ const scope = await loadScopeForSession(session.sessionId);
1571
1572
  const decision = classifyToolCall(call, { scope });
1572
1573
  await auditLog("tool.classified", {
1573
1574
  call,
@@ -3261,11 +3262,9 @@ export async function runAgentTurn(prompt, options = {}) {
3261
3262
  }, {
3262
3263
  onStatus: (status) => {
3263
3264
  writeStatus(status);
3264
- // Toast only on key *switch* after a failure — never on sticky
3265
- // "using" or retry countdown ticks (those stay in composer status).
3266
- if (/^switching /i.test(status.trim())) {
3267
- writeNotice("warn", status.trim());
3268
- }
3265
+ const full = status.replace(/\s+/g, " ").trim();
3266
+ if (isProviderFailureStatus(full))
3267
+ writeNotice("warn", full);
3269
3268
  },
3270
3269
  onStreamEvent: (event) => {
3271
3270
  if (event.type !== "reasoning_delta")
@@ -4421,7 +4420,7 @@ export async function runAgentTurn(prompt, options = {}) {
4421
4420
  .join("\n\n");
4422
4421
  pushAssistantHistory(standardizedContent, completion);
4423
4422
  }
4424
- const scopeForBatch = await loadScope().catch(() => undefined);
4423
+ const scopeForBatch = await loadScopeForSession(session.sessionId).catch(() => undefined);
4425
4424
  const isParallelSafe = (c) => {
4426
4425
  if (c.name === "pentest.recon" ||
4427
4426
  c.name === "net.context" ||