clauderipple 0.5.1 → 0.7.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/dist/ui/app.js CHANGED
@@ -335,6 +335,16 @@ function renderHealthProviders() {
335
335
  ].filter(Boolean));
336
336
  const quota = quotaLine(name);
337
337
  if (quota) line.appendChild(el("div", { class: "small", text: quota }));
338
+ // Claude subscription accounts, one line each: the current login and every account added here.
339
+ // Before the config has loaded the rows come from the status, which names the sign-in source
340
+ // rather than the auth mode.
341
+ if (provider.type === "anthropic" && (provider.auth === "claude-code" || (live && live.authSource))) {
342
+ const usage = ((status && status.claude && status.claude.accounts) || [])
343
+ .map((account) => ({ account, text: chatgptQuotaText(account.quota) }))
344
+ .filter(({ text }) => text)
345
+ .map(({ account, text }) => el("div", { text: `${claudeAccountLabel(account)} · ${text}` }));
346
+ if (usage.length) line.appendChild(el("div", { class: "small account-usage" }, usage));
347
+ }
338
348
  const pool = credentialLine(name);
339
349
  if (pool) line.appendChild(pool);
340
350
  return line;
@@ -367,6 +377,12 @@ function renderClients() {
367
377
  agentButton.className = agentEnabled ? "btn secondary" : "btn";
368
378
  agentButton.disabled = agentTitleBusy;
369
379
  agentButton.onclick = () => toggleAgentTitle(!agentEnabled);
380
+ const toolsLimited = Boolean(currentConfig.cli && currentConfig.cli.limitWorkerTools);
381
+ $("#client-worker-tools-rows").replaceChildren(el("div", { class: "row" }, [el("span", { class: "k", text: t("picker.state") }), toolsLimited ? badge("ok", t("workerTools.on")) : el("span", { class: "small", text: t("workerTools.off") })]));
382
+ const toolsButton = $("#client-worker-tools-toggle");
383
+ toolsButton.textContent = toolsLimited ? t("workerTools.turnOff") : t("workerTools.turnOn");
384
+ toolsButton.className = toolsLimited ? "btn secondary" : "btn";
385
+ toolsButton.onclick = () => toggleWorkerTools(!toolsLimited);
370
386
  void renderCodexClient();
371
387
  const mapped = Object.entries(currentConfig.routes || {}).map(([source, route]) => `${labelOf(claudeModels.find((model) => model.id === source) || { id: source })} → ${labelOf((groupedModels(currentConfig).find((group) => group.name === route.provider) || { models: [] }).models.find((model) => model.id === route.model) || { id: route.model })}`);
372
388
  $("#client-claude-code-rows").replaceChildren(el("div", { class: "small", text: mapped.join(" · ") || t("slots.noChanges") }));
@@ -527,6 +543,17 @@ async function toggleAgentTitle(enabled) {
527
543
  void refreshHealth();
528
544
  }
529
545
  }
546
+ async function toggleWorkerTools(enabled) {
547
+ if (!currentConfig) return;
548
+ const next = clone(currentConfig);
549
+ next.cli = { ...(next.cli || {}), limitWorkerTools: enabled };
550
+ try {
551
+ await configRequest(next);
552
+ currentConfig = next;
553
+ toast(t("common.saved"));
554
+ } catch (error) { toast(t("common.saveFailed"), true, error.message); }
555
+ renderClients();
556
+ }
530
557
  async function renderCodexClient() {
531
558
  const rows = $("#client-codex-rows");
532
559
  const button = $("#client-codex-toggle");
@@ -1002,11 +1029,22 @@ async function saveAnthropicRotation(name, provider, enabled, control, message)
1002
1029
  } finally { control.disabled = false; }
1003
1030
  }
1004
1031
 
1032
+ /** The current login is named by the router in English; everything else carries the user's own label. */
1033
+ function claudeAccountLabel(account) {
1034
+ return account.id === "current" ? t("providers.anthropicCurrent") : account.label;
1035
+ }
1036
+
1037
+ /** "5h 14% · weekly 10%" under a Claude account, or nothing while its usage is unknown. */
1038
+ function claudeQuotaNode(account) {
1039
+ const text = chatgptQuotaText(account && account.quota);
1040
+ return text ? el("p", { class: "small account-quota", text }) : null;
1041
+ }
1042
+
1005
1043
  function renderClaudeAccountRows(target, data, name, generation) {
1006
1044
  if (generation !== providerDetailGeneration) return;
1007
1045
  const rows = [];
1008
1046
  if (data.current) rows.push(el("article", { class: "account-card current" }, [
1009
- el("div", { class: "account-card-copy" }, [el("strong", { text: data.current.label }), el("span", { class: "small", text: anthropicSourceText(data.current.source) }), hint(t("providers.currentAccountHelp"))]),
1047
+ el("div", { class: "account-card-copy" }, [el("strong", { text: data.current.label }), el("span", { class: "small", text: anthropicSourceText(data.current.source) }), claudeQuotaNode(data.current), hint(t("providers.currentAccountHelp"))].filter(Boolean)),
1010
1048
  el("span", { class: "badge ok", text: t("providers.anthropicCurrent") }),
1011
1049
  ]));
1012
1050
  for (const account of Array.isArray(data.accounts) ? data.accounts : []) {
@@ -1031,7 +1069,7 @@ function renderClaudeAccountRows(target, data, name, generation) {
1031
1069
  actions.unshift(reauth);
1032
1070
  }
1033
1071
  rows.push(el("article", { class: "account-card" }, [
1034
- el("div", { class: "account-card-copy" }, [el("strong", { text: account.label }), account.email && account.email !== account.label ? el("span", { class: "small", text: account.email }) : null, hint(t("providers.addedAccountHelp"))]),
1072
+ el("div", { class: "account-card-copy" }, [el("strong", { text: account.label }), account.email && account.email !== account.label ? el("span", { class: "small", text: account.email }) : null, claudeQuotaNode(account), hint(t("providers.addedAccountHelp"))].filter(Boolean)),
1035
1073
  el("span", { class: `badge ${unavailable ? "bad" : "ok"}`, text: unavailable ? t("providers.anthropicReauth") : t("pool.ready") }),
1036
1074
  el("div", { class: "account-card-actions" }, actions),
1037
1075
  ]));
package/dist/ui/i18n.js CHANGED
@@ -24,6 +24,12 @@ const I18N = {
24
24
  "agentTitle.off": "꺼짐",
25
25
  "agentTitle.turnOn": "이름표 켜기",
26
26
  "agentTitle.turnOff": "이름표 끄기",
27
+ "workerTools.title": "워커 도구 제한",
28
+ "workerTools.help": "ClaudeRipple이 만든 워커에 읽기·편집·명령 실행·검색·스킬 도구만 줍니다. 세션의 MCP 서버·플러그인 도구가 워커 요청마다 실리지 않아 입력이 크게 줄지만, 브라우저·iOS 시뮬레이터 같은 도구는 쓸 수 없습니다. 그런 도구가 필요한 워커는 ~/.claude/agents에 직접 만든 에이전트 파일로 두세요(ClaudeRipple은 그 파일을 건드리지 않습니다). 새 세션부터 적용됩니다.",
29
+ "workerTools.on": "켜짐",
30
+ "workerTools.off": "꺼짐 (세션 도구 전부 상속)",
31
+ "workerTools.turnOn": "제한 켜기",
32
+ "workerTools.turnOff": "제한 끄기",
27
33
  "providers.probeNoCredits": "키는 맞습니다. 다만 이 계정에 크레딧(잔액)이 없어 응답은 받지 못합니다. 프로바이더 사이트에서 충전하세요.",
28
34
  "providers.probeBadKey": "API 키가 거부됐습니다. 키를 다시 확인하세요.",
29
35
  "providers.searchModels": "모델 검색 (이름 또는 id)",
@@ -57,6 +63,12 @@ const I18N = {
57
63
  "agentTitle.off": "Off",
58
64
  "agentTitle.turnOn": "Turn on",
59
65
  "agentTitle.turnOff": "Turn off",
66
+ "workerTools.title": "Worker tools",
67
+ "workerTools.help": "Gives the workers ClaudeRipple generates only the read, edit, run, search and skill tools. The session's MCP servers and plugin tools no longer ride along on every worker request, which cuts input sharply, but those workers cannot use a browser or the iOS Simulator. A worker that needs them belongs in an agent file of your own in ~/.claude/agents, which ClaudeRipple never touches. Applies to new sessions.",
68
+ "workerTools.on": "On",
69
+ "workerTools.off": "Off (inherits every session tool)",
70
+ "workerTools.turnOn": "Limit tools",
71
+ "workerTools.turnOff": "Stop limiting",
60
72
  "providers.probeNoCredits": "The key is valid, but this account has no credits, so no answer came back. Top up on the provider site.",
61
73
  "providers.probeBadKey": "The API key was rejected. Check the key.",
62
74
  "providers.searchModels": "Search models (name or id)",
@@ -67,6 +67,11 @@
67
67
  <p class="hint" data-i18n="agentTitle.help"></p>
68
68
  <div class="rows" id="client-agent-title-rows"></div>
69
69
  <div class="actions"><button class="btn" id="client-agent-title-toggle" type="button"></button></div>
70
+ <div class="client-divider"></div>
71
+ <h3 data-i18n="workerTools.title">Worker tools</h3>
72
+ <p class="hint" data-i18n="workerTools.help"></p>
73
+ <div class="rows" id="client-worker-tools-rows"></div>
74
+ <div class="actions"><button class="btn" id="client-worker-tools-toggle" type="button"></button></div>
70
75
  </div>
71
76
  <div class="card">
72
77
  <h2 data-i18n="clients.codex.title">Codex CLI</h2>
package/dist/ui/style.css CHANGED
@@ -355,6 +355,7 @@ label.check input { width: auto; }
355
355
  button.btn.compact { padding: 4px 9px; font-size: 14px; }
356
356
  .provider-status { display: grid; grid-template-columns: minmax(120px, 1fr) auto; gap: 8px; align-items: center; padding: 7px 0; border-bottom: 1px solid var(--border); }
357
357
  .provider-status:last-child { border-bottom: none; }
358
+ .provider-status .account-usage { grid-column: 1 / -1; display: grid; gap: 2px; }
358
359
  .empty-card { color: var(--muted); padding: 28px 16px; border: 1px dashed var(--border); border-radius: 8px; text-align: center; }
359
360
  .toast-raw { margin-top: 5px; color: var(--muted); font: 10.5px/1.35 var(--mono); word-break: break-word; }
360
361
  .modal-backdrop { position: fixed; inset: 0; z-index: 20; display: grid; place-items: center; padding: 18px; background: color-mix(in srgb, #000 38%, transparent); }
@@ -166,7 +166,19 @@ chat is out of reach for every approach, ours included.
166
166
  records what it wrote in `<home>/generated-agents.json` and touches **only**
167
167
  those files: a hand-written agent of the same name wins and is left alone, an
168
168
  unticked model removes its generated file and nothing else, identical content is
169
- not rewritten. `cli.agentFiles: false` turns generation off. The marker alias
169
+ not rewritten. `cli.agentFiles: false` turns generation off;
170
+ `cli.limitWorkerTools: true` adds a `tools:` line so a worker does not inherit
171
+ every MCP and plugin tool of the session (first request 32,942 → 8,823 tokens,
172
+ 2026-09-29), at the cost of browser and simulator tools. It is opt-in for that
173
+ reason. With a chatgpt provider configured the body also tells a worker how to
174
+ make an image: `POST /api/image` on the admin port, which runs one Codex
175
+ Responses turn with the hosted `image_generation` tool required and answers
176
+ with the image bytes (`clauderipple image` is the same call). Measured
177
+ 2026-09-29 on a subscription: the image is the `image_generation_call` output
178
+ item's base64 `result`, about 30 s; `output_format` and `background` are
179
+ honoured, `size` and `quality` are accepted and ignored (even "999x1" and
180
+ "bogus" returned 200), and the shape follows the prompt ("square composition"
181
+ → 1254x1254). So the API takes `aspect` and says it in words. The marker alias
170
182
  table is the union of `aliases` (explicit, wins) and every agent file's
171
183
  `name → model` — so `[[ripple: <agent>@<effort>]]` resolves for any agent that
172
184
  exists, generated or not, with no second registry to keep in step. Why: on
@@ -836,6 +848,8 @@ what we do not have yet — so that adding a provider does not start with readin
836
848
  | The same relay 403 kept reaching the user after that fix, because it was the wrong layer: DeepSeek runs on `openai-compatible`, whose adapter was **never** retried at all — the in-turn retry lives only in the anthropic-compatible wire path (`nextCredential`), and `credentialsOf` gives a translated provider exactly one credential and no fallback. So a relay hiccup had nowhere to go but the user (2026-09-21, root cause of the day's repeated "authentication failed") | Every translated adapter retries the `fetch` **before** any status or byte is written, so the turn is still replaceable (§4 "the first byte commits the turn"). `fetchWithRetry` retries only `transient` verdicts — connect failures, 5xx, and a 403 reporting a broken upstream — to the ceiling of 10 attempts the CLI itself uses, with a 250ms→2s backoff. A 401/429 is **not** retried: the adapter holds one credential, so asking the key that just refused the identical request again only delays the error. `classify` is reused for the judgement, with `kind === "transient"` narrowing a pool's answer to an adapter's. |
837
849
  | Workers on muse-spark-1.3 "stalled for minutes, then died": 84 of 1,815 turns (2026-09-18..22) waited a median 58s — five exactly 300s — and then the upstream closed the stream with **no** `response.completed`, no usage, no text. Both translated adapters finished such a stream as `end_turn` with whatever had arrived (the ChatGPT one by a comment saying so), so Claude Code received an empty, successful turn, did not retry, and the subagent ended with nothing. Same shape on `chatgpt` (gpt-6-astra, 12 turns, median 105s) and DeepSeek. A non-streaming failure was likewise answered 200 with the fragment | A turn is finished only when the vendor says so: `response.completed`/`response.incomplete` on Responses, a `finish_reason` or `[DONE]` on Chat. A stream that closes without one, or breaks mid-read, ends in an SSE `overloaded_error` — the one mid-stream error Claude Code retries on its own (CLI 2.1.278 matches `"type":"overloaded_error"` in the message; `api_error` is final). Non-streaming turns answer 529 with the same error, and the request record says the turn failed. The upstream stall itself is the vendor's; what must not happen here is turning it into an answer. |
838
850
  | A session switched from gpt-6-astra to claude-opus-5-5 failed every turn with `400 messages.2.content.0.thinking: each thinking block must contain thinking`: the translated adapters hand the CLI reasoning as `thinking` blocks with no signature (and no text when the reasoning item had no summary), the CLI keeps them, and passthrough replayed them to Anthropic (2026-09-28) | Before a request reaches Anthropic (`PASS` and the account-pool provider), assistant `thinking` blocks without a signature are dropped (`thinking.ts`) — Anthropic's own always carry one — and a turn left empty goes with them. Passthrough stays byte-exact when there is nothing to drop. |
851
+ | Re-running the installer — the documented update — replaced the files and left the router running the old code: `install` treats a running router as done, and on Windows and Linux an unchanged supervisor definition is not re-registered, so nothing restarted it until the next login (found 2026-09-28 while building `update`; the tray's own restart covered only the packaged app) | `install` asks a router that is already running for its version and restarts one that differs. Measured in the Windows 11 VM: 0.5.1 running, installer run with 0.6.0 → "restarting it on 0.6.0", status 0.6.0. |
852
+ | The tray's Electron (~270MB) was fetched into the installed package's own `node_modules`, and `npm install -g` of any other version deleted it with the old files (measured 2026-09-28): every update silently took the tray away, and on Windows a running tray held `electron.exe` inside the folder npm had to replace | Electron lives in `<home>/tray-runtime`, outside anything an update replaces. Both install scripts move one found in the old place there before installing (Windows first stops a tray running from it, and starts it again after setup) — checked on macOS and in the VM with a locked `electron.exe`. |
839
853
 
840
854
  ## 6. Blocked paths (measured, do not retry)
841
855
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clauderipple",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "Use GPT (ChatGPT Plus/Pro), DeepSeek and 400+ other models in the Claude Desktop Code tab and Claude Code while staying signed in to your Claude subscription — no third-party gateway mode. Several ChatGPT accounts that take over when one runs out, in Codex too.",
5
5
  "license": "GPL-3.0-only",
6
6
  "workspaces": [