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/CHANGELOG.md +45 -0
- package/README.ko.md +33 -4
- package/README.md +42 -4
- package/dist/app/dist/main.js +124 -4
- package/dist/app/package.json +1 -0
- package/dist/cli/src/index.js +110 -1
- package/dist/cli/src/tray.js +21 -7
- package/dist/cli/src/update.js +104 -0
- package/dist/router/src/admin.js +118 -4
- package/dist/router/src/agents.js +28 -3
- package/dist/router/src/config.js +2 -0
- package/dist/router/src/index.js +2 -0
- package/dist/router/src/providers/anthropic-usage.js +98 -0
- package/dist/router/src/providers/chatgpt/index.js +112 -0
- package/dist/router/src/proxy.js +21 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +40 -2
- package/dist/ui/i18n.js +12 -0
- package/dist/ui/index.html +5 -0
- package/dist/ui/style.css +1 -0
- package/docs/ARCHITECTURE.md +15 -1
- package/package.json +1 -1
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)",
|
package/dist/ui/index.html
CHANGED
|
@@ -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); }
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
|
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.
|
|
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": [
|