clauderipple 0.6.0 → 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 +15 -0
- package/README.ko.md +28 -1
- package/README.md +37 -1
- package/dist/app/package.json +1 -1
- package/dist/cli/src/index.js +43 -0
- package/dist/router/src/admin.js +86 -2
- package/dist/router/src/agents.js +28 -3
- package/dist/router/src/config.js +2 -0
- package/dist/router/src/index.js +1 -0
- package/dist/router/src/providers/chatgpt/index.js +112 -0
- package/dist/router/src/proxy.js +14 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +17 -0
- package/dist/ui/i18n.js +12 -0
- package/dist/ui/index.html +5 -0
- package/docs/ARCHITECTURE.md +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 — 2026-09-29
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Worker tools** (`cli.limitWorkerTools`, off by default; a toggle in the dashboard). Generated
|
|
8
|
+
workers get only the read, edit, run, search and skill tools instead of inheriting every MCP
|
|
9
|
+
server and plugin tool of the session. One worker's first request fell from 32,942 to 8,823
|
|
10
|
+
tokens. Such a worker cannot use a browser or the iOS Simulator; keep those in an agent file of
|
|
11
|
+
your own.
|
|
12
|
+
- **Images from the ChatGPT subscription.** `clauderipple image "<prompt>" -o file.png`, with
|
|
13
|
+
`--aspect square|landscape|portrait`, `--transparent`, `--format` and `--ref FILE` for reference
|
|
14
|
+
images. Underneath is `POST /api/image` on the admin port, which answers with the image itself.
|
|
15
|
+
Generated workers are told the `curl` form, so a worker with only Bash can make one. The backend
|
|
16
|
+
chooses resolution and quality; about 30 seconds an image.
|
|
17
|
+
|
|
3
18
|
## 0.6.0 — 2026-09-28
|
|
4
19
|
|
|
5
20
|
Both suggested in #8 by @artisthanbohee-del.
|
package/README.ko.md
CHANGED
|
@@ -144,6 +144,13 @@ ClaudeRipple이 Claude에 하는 일은 작고 확인할 수 있습니다. Claud
|
|
|
144
144
|
Claude 모델이 이름 그대로 뜹니다. Claude Code 로그인이나 Anthropic API 키로 답합니다.
|
|
145
145
|
[Codex 앱과 Codex CLI](#codex-앱과-codex-cli) 절을 보세요.
|
|
146
146
|
|
|
147
|
+
### 이미지도 만들 수 있나요?
|
|
148
|
+
|
|
149
|
+
됩니다. API 키 없이 ChatGPT 구독으로 만듭니다: `clauderipple image "빨간 종이배" -o boat.png --aspect square`.
|
|
150
|
+
`--ref 파일`로 참고 이미지를, `--transparent`로 투명 배경을 줄 수 있습니다. 한 장에 30초쯤 걸리고, 해상도와 품질은
|
|
151
|
+
구독 쪽이 정합니다. ClaudeRipple이 만든 서브에이전트에도 같은 방법이 적혀 있어 워커도 이미지를 만듭니다.
|
|
152
|
+
[Claude Code](#claude-code-터미널-remote-control-서브에이전트) 절을 보세요.
|
|
153
|
+
|
|
147
154
|
### Claude Code 안에서는 GPT 성능이 떨어지지 않나요?
|
|
148
155
|
|
|
149
156
|
Claude Code의 시스템 프롬프트와 도구 설명은 Claude에 맞춰 쓰여 있어서, 다른 모델은 남에게 맞춘 지시를 읽는 셈이고
|
|
@@ -219,6 +226,10 @@ ClaudeRipple은 Claude Code 프로세스만 신뢰하는 작은 HTTPS 프록시
|
|
|
219
226
|
서버 측 스레드 처리, 도구 호출과 이미지 왕복, 모델이 받는 범위로 추론 강도 클램프, 호환 벤더에는 Anthropic 전용 필드 제거.
|
|
220
227
|
- **제대로 된 요청 로그.** 누가 물었고 어떤 모델이 답했는지, 입력·캐시·출력 토큰, 지연, 상태를 요청마다. 한 시간 요약 포함.
|
|
221
228
|
- **서브에이전트 이름표.** 백그라운드 작업 패널에 "Agent" 대신 `Terra·high · Review`가 보입니다.
|
|
229
|
+
- **ChatGPT 구독으로 이미지 생성.** `clauderipple image "<프롬프트>"`. 참고 이미지와 투명 배경도 됩니다. 서브에이전트에도
|
|
230
|
+
방법이 적혀 있어, GPT나 DeepSeek 워커가 이미지 도구 없이 이미지를 만들고 직접 확인합니다.
|
|
231
|
+
- **원하면 가벼운 워커.** 스위치 하나로, 생성된 서브에이전트에 세션의 MCP·플러그인 도구 전부 대신 워커가 쓰는 도구만
|
|
232
|
+
줍니다. 워커 첫 요청이 3.3만에서 0.9만 토큰으로 줄었습니다.
|
|
222
233
|
- **터미널이 싫은 사람을 위해.** 원클릭 연결 확인과 모델 자동 검색이 붙은 프로바이더 프리셋, 드롭다운 모델 매핑, 자동 저장,
|
|
223
234
|
한국어·영어 UI. 런타임을 품고 첫 실행에 스스로 설치하는 메뉴 막대 앱. 서명·공증 완료.
|
|
224
235
|
|
|
@@ -310,7 +321,7 @@ node packages/cli/src/index.ts ui # 브라우저에서 로컬 GUI 열기
|
|
|
310
321
|
|
|
311
322
|
`uninstall`은 전부 되돌리고 `~/.claude/settings.json`을 백업에서 복원합니다. 그 밖의 명령: `status`, `start`, `stop`,
|
|
312
323
|
`restart`, `logs -f`, `login`, `logout`, `claude-login`, `claude-logout`, `picker on|off`, `agent-title on|off`,
|
|
313
|
-
`codex on|off`.
|
|
324
|
+
`codex on|off`, `update`, `image "<프롬프트>"`.
|
|
314
325
|
</details>
|
|
315
326
|
|
|
316
327
|
## 클라이언트
|
|
@@ -355,6 +366,22 @@ model: gpt-5.6-sol@medium
|
|
|
355
366
|
알 수 없는 에러로 죽게 두지 않고 이름을 대고 거부합니다(`400 ClaudeRipple: no provider declares "…"`).
|
|
356
367
|
Claude 모델은 언제나 그대로 통과합니다. 생성을 끄려면 `"cli": { "agentFiles": false }`.
|
|
357
368
|
|
|
369
|
+
생성된 워커는 기본적으로 세션의 도구를 전부 물려받아, MCP 서버·플러그인 도구가 워커 요청마다 실립니다.
|
|
370
|
+
`"cli": { "limitWorkerTools": true }`(대시보드의 **워커 도구 제한**)를 켜면 읽기·편집·실행·검색·스킬 도구만
|
|
371
|
+
주어 워커 첫 요청이 약 3.3만에서 0.9만 토큰으로 줄었습니다. 대신 그 워커는 브라우저·시뮬레이터 도구를 못 씁니다.
|
|
372
|
+
그런 도구가 필요한 워커는 직접 만든 에이전트 파일로 두면 되고, 그 파일은 건드리지 않습니다.
|
|
373
|
+
|
|
374
|
+
ChatGPT 프로바이더가 있으면 같은 구독으로 이미지를 만들 수 있습니다.
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
clauderipple image "빨간 종이배, 플랫 일러스트" -o boat.png --aspect square
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
`--transparent`, `--format png|jpeg|webp`, `--ref 파일`(참고 이미지, 여러 번 가능)은 선택입니다. 한 장에 30초쯤 걸립니다.
|
|
381
|
+
구독 백엔드가 해상도와 품질을 직접 정하므로, 모양은 `--aspect`로 말로 요청합니다. 이 명령은 관리 포트의
|
|
382
|
+
`POST /api/image`(응답이 이미지 바이트)를 부르는 클라이언트입니다. 생성된 워커에는 그 `curl` 형태가 적혀 있어,
|
|
383
|
+
Bash만 있는 워커도 이미지를 만들고 Read로 확인할 수 있습니다.
|
|
384
|
+
|
|
358
385
|
Claude Code의 `WebSearch`는 작은 모델에 별도 요청을 보냅니다. ChatGPT로 라우팅한 구성에서 그 요청만
|
|
359
386
|
Anthropic 한도를 쓰지 않게 하려면 `config.json`에서 같은 ChatGPT 프로바이더를 검색 백엔드로 지정합니다:
|
|
360
387
|
|
package/README.md
CHANGED
|
@@ -165,6 +165,15 @@ Yes. `clauderipple codex on` adds ClaudeRipple as a provider, and Claude models
|
|
|
165
165
|
the Codex app's and CLI's model list under their own names. They answer through your
|
|
166
166
|
Claude Code login or an Anthropic API key. See [Codex app and Codex CLI](#codex-app-and-codex-cli).
|
|
167
167
|
|
|
168
|
+
### Can it generate images?
|
|
169
|
+
|
|
170
|
+
Yes, with a ChatGPT subscription and no API key:
|
|
171
|
+
`clauderipple image "a red paper boat" -o boat.png --aspect square`. `--ref FILE` adds
|
|
172
|
+
a reference image and `--transparent` a transparent background. One image takes about
|
|
173
|
+
30 seconds, and the subscription chooses the resolution and quality. Subagents
|
|
174
|
+
ClaudeRipple generates are told the same call, so they can make images too. See
|
|
175
|
+
[Claude Code](#claude-code-terminal-remote-control-subagents).
|
|
176
|
+
|
|
168
177
|
### Does GPT do worse inside Claude Code?
|
|
169
178
|
|
|
170
179
|
Claude Code's system prompt and tool descriptions are written for Claude, so another model
|
|
@@ -263,6 +272,12 @@ you map go to your provider, everything else goes to Anthropic byte for byte.
|
|
|
263
272
|
latency, status, per request, with a one-hour summary.
|
|
264
273
|
- **Subagent name tags.** The background-task panel shows `Terra·high · Review`
|
|
265
274
|
instead of a generic "Agent".
|
|
275
|
+
- **Images from your ChatGPT subscription.** `clauderipple image "<prompt>"`, with
|
|
276
|
+
reference images and transparent backgrounds. Subagents are told how, so a GPT or
|
|
277
|
+
DeepSeek worker can make an image and look at it without an image tool.
|
|
278
|
+
- **Lean workers, if you want them.** One switch gives generated subagents only the
|
|
279
|
+
tools a worker uses instead of every MCP server and plugin of the session: one
|
|
280
|
+
worker's first request fell from 33k to 9k tokens.
|
|
266
281
|
- **Made for people who don't want a terminal.** Provider presets with one-click
|
|
267
282
|
connection tests and model discovery, drop-down model mapping, auto-save, Korean
|
|
268
283
|
and English UI. A menu-bar app that carries its own runtime and sets itself up on
|
|
@@ -360,7 +375,7 @@ node packages/cli/src/index.ts ui # open the local GUI in your browser
|
|
|
360
375
|
`uninstall` reverses everything and restores `~/.claude/settings.json` from a backup.
|
|
361
376
|
Other commands: `status`, `start`, `stop`, `restart`, `logs -f`, `login`, `logout`,
|
|
362
377
|
`claude-login`, `claude-logout`, `picker on|off`, `agent-title on|off`,
|
|
363
|
-
`codex on|off`.
|
|
378
|
+
`codex on|off`, `update`, `image "<prompt>"`.
|
|
364
379
|
</details>
|
|
365
380
|
|
|
366
381
|
## Clients
|
|
@@ -413,6 +428,27 @@ by name (`400 ClaudeRipple: no provider declares "…"`) instead of being sent o
|
|
|
413
428
|
to fail somewhere less legible; native Claude models always pass through.
|
|
414
429
|
Set `"cli": { "agentFiles": false }` to turn generation off.
|
|
415
430
|
|
|
431
|
+
By default a generated worker inherits every tool of the session, so each MCP
|
|
432
|
+
server and plugin rides along on every worker request. `"cli": {
|
|
433
|
+
"limitWorkerTools": true }` (or **Worker tools** in the dashboard) gives it only
|
|
434
|
+
the read, edit, run, search and skill tools, which cut one worker's first
|
|
435
|
+
request from about 33k to 9k tokens. Such a worker has no browser or simulator
|
|
436
|
+
tools. A worker that needs them belongs in an agent file of your own, which is
|
|
437
|
+
never touched.
|
|
438
|
+
|
|
439
|
+
With a ChatGPT provider configured, images come from the same subscription:
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
clauderipple image "a red paper boat, flat illustration" -o boat.png --aspect square
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`--transparent`, `--format png|jpeg|webp` and `--ref FILE` (a reference image,
|
|
446
|
+
repeatable) are optional. One image takes about 30 seconds. The subscription
|
|
447
|
+
backend picks the resolution and quality itself, so the shape is asked for in
|
|
448
|
+
words (`--aspect`). The command is a client of `POST /api/image` on the admin
|
|
449
|
+
port, which answers with the image bytes. Generated workers are told the `curl`
|
|
450
|
+
form of it, so a worker with only Bash can make an image and read it back.
|
|
451
|
+
|
|
416
452
|
Claude Code runs `WebSearch` as a separate small-model request. To keep a
|
|
417
453
|
ChatGPT-routed setup from spending Anthropic quota for that request, select the
|
|
418
454
|
same ChatGPT provider as the search backend in `config.json`:
|
package/dist/app/package.json
CHANGED
package/dist/cli/src/index.js
CHANGED
|
@@ -120,6 +120,44 @@ function starterConfig(port) {
|
|
|
120
120
|
log: DEFAULTS.log,
|
|
121
121
|
}, null, 2) + "\n";
|
|
122
122
|
}
|
|
123
|
+
const IMAGE_USAGE = 'usage: clauderipple image "<prompt>" [-o FILE] [--aspect square|landscape|portrait] [--format png|jpeg|webp] [--transparent] [--ref FILE ...]';
|
|
124
|
+
/**
|
|
125
|
+
* One image through the running router's ChatGPT subscription (`POST /api/image`), written to a
|
|
126
|
+
* file whose path is printed — so a worker with nothing but Bash can make and then look at one.
|
|
127
|
+
*/
|
|
128
|
+
async function image() {
|
|
129
|
+
const prompt = args[1];
|
|
130
|
+
if (!prompt || prompt.startsWith("-"))
|
|
131
|
+
throw new Error(IMAGE_USAGE);
|
|
132
|
+
const images = args.flatMap((a, i) => (a === "--ref" && args[i + 1] ? [args[i + 1]] : [])).map((file) => {
|
|
133
|
+
const ext = path.extname(file).slice(1).toLowerCase();
|
|
134
|
+
return `data:image/${ext === "jpg" ? "jpeg" : ext};base64,${fs.readFileSync(file).toString("base64")}`;
|
|
135
|
+
});
|
|
136
|
+
const format = opt("format") ?? "png";
|
|
137
|
+
const out = path.resolve(opt("o") ?? `image-${new Date().toISOString().replace(/[:.]/g, "-")}.${format === "jpeg" ? "jpg" : format}`);
|
|
138
|
+
const body = {
|
|
139
|
+
prompt,
|
|
140
|
+
format,
|
|
141
|
+
...(opt("aspect") ? { aspect: opt("aspect") } : {}),
|
|
142
|
+
...(flag("transparent") ? { background: "transparent" } : {}),
|
|
143
|
+
...(images.length ? { images } : {}),
|
|
144
|
+
};
|
|
145
|
+
const port = adminPort(new ConfigStore(configPath()).get());
|
|
146
|
+
let res;
|
|
147
|
+
try {
|
|
148
|
+
res = await fetch(`http://127.0.0.1:${port}/api/image`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
throw new Error(`the router is not answering on 127.0.0.1:${port}; start it with: clauderipple start`);
|
|
152
|
+
}
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
const answer = (await res.json().catch(() => ({})));
|
|
155
|
+
throw new Error(answer.error ?? `HTTP ${res.status}`);
|
|
156
|
+
}
|
|
157
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
158
|
+
fs.writeFileSync(out, Buffer.from(await res.arrayBuffer()));
|
|
159
|
+
console.log(out);
|
|
160
|
+
}
|
|
123
161
|
/** The version the router on this admin port reports, or null when it does not answer. */
|
|
124
162
|
async function runningVersion(port) {
|
|
125
163
|
try {
|
|
@@ -420,6 +458,8 @@ function help() {
|
|
|
420
458
|
logout forget every ChatGPT account added with "login" (the Codex CLI's own login is left alone)
|
|
421
459
|
claude-login connect a Claude subscription in the browser (--setup-token: via \`claude setup-token\`; --manual: paste the code)
|
|
422
460
|
claude-logout remove every Claude subscription added to ClaudeRipple
|
|
461
|
+
image "<prompt>" [-o FILE] [--aspect square|landscape|portrait] [--transparent] [--ref FILE ...]
|
|
462
|
+
generate an image with your ChatGPT subscription and print the file's path
|
|
423
463
|
picker on|off show your mapped models by name in the Claude Desktop picker (trusts the CA in your login keychain, routes the app through ClaudeRipple)
|
|
424
464
|
codex on|off add/remove ClaudeRipple's local OpenAI provider and selection profile for Codex CLI
|
|
425
465
|
agent-title on|off|status
|
|
@@ -552,6 +592,9 @@ try {
|
|
|
552
592
|
case "claude-logout":
|
|
553
593
|
console.log(claudeLogout(homeDir()) ? "✓ ClaudeRipple Claude accounts removed" : "no ClaudeRipple Claude accounts stored");
|
|
554
594
|
break;
|
|
595
|
+
case "image":
|
|
596
|
+
await image();
|
|
597
|
+
break;
|
|
555
598
|
case "ui":
|
|
556
599
|
ui();
|
|
557
600
|
break;
|
package/dist/router/src/admin.js
CHANGED
|
@@ -33,7 +33,10 @@ import { ClaudeOAuthSession } from "./providers/claude-oauth.js";
|
|
|
33
33
|
import { readClaudeAuthFile } from "./providers/anthropic-token-file.js";
|
|
34
34
|
import { listClaudeAccounts, removeClaudeAccount, renameClaudeAccount } from "./providers/anthropic-accounts.js";
|
|
35
35
|
import { readChatGptAccounts, removeChatGptAccount, summarize as summarizeChatGptAccount, updateChatGptAccount } from "./providers/chatgpt/accounts.js";
|
|
36
|
+
import { IMAGE_ASPECTS, IMAGE_BACKGROUNDS, IMAGE_FORMATS } from "./providers/chatgpt/index.js";
|
|
36
37
|
const MAX_BODY = 1024 * 1024;
|
|
38
|
+
/** `/api/image` carries reference images inline, so it gets more room than a config save. */
|
|
39
|
+
const MAX_IMAGE_BODY = 32 * 1024 * 1024;
|
|
37
40
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
38
41
|
const STARTED_AT = new Date().toISOString();
|
|
39
42
|
// packages/router/src → packages/ui in a checkout; dist/router/src → dist/ui in an npm install,
|
|
@@ -444,13 +447,51 @@ function syncModelSlotsForGui(cfg) {
|
|
|
444
447
|
return `model slots were not written to settings.json: ${message}`;
|
|
445
448
|
}
|
|
446
449
|
}
|
|
447
|
-
|
|
450
|
+
/**
|
|
451
|
+
* The body of `POST /api/image`, checked: `{prompt, aspect?, format?, background?, images?}`,
|
|
452
|
+
* `images` being `data:image/...;base64,` URLs. A string is the reason it was refused.
|
|
453
|
+
*/
|
|
454
|
+
export function parseImageRequest(raw) {
|
|
455
|
+
if (!raw || typeof raw !== "object")
|
|
456
|
+
return "expected a JSON object";
|
|
457
|
+
const body = raw;
|
|
458
|
+
if (typeof body.prompt !== "string" || !body.prompt.trim())
|
|
459
|
+
return "prompt is required";
|
|
460
|
+
const out = { prompt: body.prompt };
|
|
461
|
+
const pick = (key, allowed) => {
|
|
462
|
+
const value = body[key];
|
|
463
|
+
if (value === undefined)
|
|
464
|
+
return null;
|
|
465
|
+
if (typeof value !== "string" || !allowed.includes(value))
|
|
466
|
+
return `${key} must be one of ${allowed.join(", ")}`;
|
|
467
|
+
out[key] = value;
|
|
468
|
+
return null;
|
|
469
|
+
};
|
|
470
|
+
const refused = pick("aspect", IMAGE_ASPECTS) ?? pick("format", IMAGE_FORMATS) ?? pick("background", IMAGE_BACKGROUNDS);
|
|
471
|
+
if (refused)
|
|
472
|
+
return refused;
|
|
473
|
+
if (body.images !== undefined) {
|
|
474
|
+
if (!Array.isArray(body.images))
|
|
475
|
+
return "images must be a list of data: URLs";
|
|
476
|
+
const images = [];
|
|
477
|
+
for (const url of body.images) {
|
|
478
|
+
const m = typeof url === "string" ? /^data:(image\/[a-z0-9.+-]+);base64,([A-Za-z0-9+/=\s]+)$/i.exec(url) : null;
|
|
479
|
+
if (!m)
|
|
480
|
+
return "each image must be a data:image/...;base64, URL";
|
|
481
|
+
images.push({ mediaType: m[1].toLowerCase(), data: m[2].replace(/\s/g, "") });
|
|
482
|
+
}
|
|
483
|
+
if (images.length)
|
|
484
|
+
out.images = images;
|
|
485
|
+
}
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
function readBody(req, limit = MAX_BODY) {
|
|
448
489
|
return new Promise((resolveP, reject) => {
|
|
449
490
|
const chunks = [];
|
|
450
491
|
let n = 0;
|
|
451
492
|
req.on("data", (c) => {
|
|
452
493
|
n += c.length;
|
|
453
|
-
if (n >
|
|
494
|
+
if (n > limit) {
|
|
454
495
|
reject(new Error("body too large"));
|
|
455
496
|
req.destroy();
|
|
456
497
|
return;
|
|
@@ -1409,6 +1450,49 @@ export function startAdmin(deps) {
|
|
|
1409
1450
|
sendJson(res, r.ok ? 200 : 500, { ok: r.ok, output: r.output });
|
|
1410
1451
|
return;
|
|
1411
1452
|
}
|
|
1453
|
+
if (pathname === "/api/image" && method === "POST") {
|
|
1454
|
+
// The image itself is the response body, so `curl -o out.png` is the whole client. An error
|
|
1455
|
+
// is JSON with a non-2xx status (`curl --fail-with-body` writes it to the same file).
|
|
1456
|
+
if (!deps.image) {
|
|
1457
|
+
sendJson(res, 501, { error: "image generation not available" });
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
let parsed;
|
|
1461
|
+
try {
|
|
1462
|
+
parsed = parseImageRequest(JSON.parse((await readBody(req, MAX_IMAGE_BODY)).toString("utf8")));
|
|
1463
|
+
}
|
|
1464
|
+
catch (e) {
|
|
1465
|
+
sendJson(res, 400, { error: /too large/.test(e.message) ? "body too large (reference images over 32 MB)" : "invalid JSON" });
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
if (typeof parsed === "string") {
|
|
1469
|
+
sendJson(res, 400, { error: parsed });
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
// A caller that gives up should not leave the generation running on the subscription.
|
|
1473
|
+
const abort = new AbortController();
|
|
1474
|
+
res.on("close", () => { if (!res.writableFinished)
|
|
1475
|
+
abort.abort(); });
|
|
1476
|
+
const started = Date.now();
|
|
1477
|
+
try {
|
|
1478
|
+
const image = await deps.image(parsed, abort.signal);
|
|
1479
|
+
deps.log.info(`admin: image ${image.size ?? "?"} ${image.quality ?? "?"} ${image.format}, ${parsed.images?.length ?? 0} reference(s), ${image.data.length} bytes in ${Date.now() - started} ms`);
|
|
1480
|
+
res.writeHead(200, {
|
|
1481
|
+
"content-type": `image/${image.format}`,
|
|
1482
|
+
"content-length": String(image.data.length),
|
|
1483
|
+
...(image.size ? { "x-image-size": image.size } : {}),
|
|
1484
|
+
...(image.quality ? { "x-image-quality": image.quality } : {}),
|
|
1485
|
+
...(image.revisedPrompt ? { "x-revised-prompt": encodeURIComponent(image.revisedPrompt) } : {}),
|
|
1486
|
+
});
|
|
1487
|
+
res.end(image.data);
|
|
1488
|
+
}
|
|
1489
|
+
catch (e) {
|
|
1490
|
+
deps.log.warn(`admin: image failed after ${Date.now() - started} ms: ${e.message}`);
|
|
1491
|
+
if (!res.headersSent)
|
|
1492
|
+
sendJson(res, 502, { error: e.message });
|
|
1493
|
+
}
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1412
1496
|
if (pathname === "/api/agent-title" && method === "POST") {
|
|
1413
1497
|
// Registers/removes the PreToolUse hook that prefixes subagent titles with the real model.
|
|
1414
1498
|
let enabled;
|
|
@@ -14,6 +14,7 @@ import os from "node:os";
|
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { homeDir } from "./config.js";
|
|
16
16
|
import { declaredBy } from "./routing.js";
|
|
17
|
+
import { adminPort } from "./admin.js";
|
|
17
18
|
/** Where Claude Code's Agent tool reads its worker definitions. */
|
|
18
19
|
export function defaultAgentDir() {
|
|
19
20
|
return path.join(os.homedir(), ".claude", "agents");
|
|
@@ -34,6 +35,27 @@ const BODY = [
|
|
|
34
35
|
"**작업 디렉터리(cwd)에 어떤 파일도 만들지 않는다.** 조사·감사처럼 \"읽기만\" 하는 일이어도 마찬가지다.",
|
|
35
36
|
"임시 파일이 필요하면 `/tmp` 아래에만 만들고, 끝나면 지운다. 사본을 작업 폴더에 떨구지 마라.",
|
|
36
37
|
].join("\n");
|
|
38
|
+
/**
|
|
39
|
+
* Added to the body when a ChatGPT provider is configured: how a worker makes an image. A worker has
|
|
40
|
+
* no image tool, and `clauderipple` is not on PATH for everyone (a checkout runs from source), so
|
|
41
|
+
* the instruction is the admin endpoint itself, which curl reaches on every platform.
|
|
42
|
+
*/
|
|
43
|
+
function imageLine(port) {
|
|
44
|
+
return [
|
|
45
|
+
"이미지가 필요하면 ChatGPT 구독으로 만든다(한 장 30초~수 분):",
|
|
46
|
+
`\`curl -sS --fail-with-body -o <절대경로>.png -H 'content-type: application/json' -d '{"prompt":"…"}' http://127.0.0.1:${port}/api/image\``,
|
|
47
|
+
"선택 키: aspect(square·landscape·portrait), background(transparent), format(png·jpeg·webp), images(참고 이미지 data: URL 목록). 해상도·품질은 지정할 수 없다. 실패하면 그 파일에 오류 JSON이 담긴다. 만든 뒤 Read로 확인한다.",
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The tools a generated worker is given when `cli.limitWorkerTools` is on. Without a `tools:` line a
|
|
52
|
+
* subagent inherits every tool of the session — each MCP server, plugin agent and deferred-tool name —
|
|
53
|
+
* and a general worker's first request measured 43,598 tokens (2026-09-20), resent on every turn;
|
|
54
|
+
* limited, a deepseek worker's first request fell from 32,942 to 8,823 (2026-09-29). A worker reads,
|
|
55
|
+
* edits, searches, runs commands and uses skills; it does not drive browsers, simulators or mail.
|
|
56
|
+
* Off by default: a user whose workers do need those tools would otherwise lose them silently.
|
|
57
|
+
*/
|
|
58
|
+
const TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "WebFetch", "WebSearch", "Skill"];
|
|
37
59
|
/**
|
|
38
60
|
* Parsed `*.md` frontmatter per directory, keyed by file mtime so a request never re-reads the whole
|
|
39
61
|
* directory. Only the `---` block's own `key: value` lines are read: no YAML library, because the
|
|
@@ -134,10 +156,12 @@ function effortSuffix(provider, modelId) {
|
|
|
134
156
|
const levels = modelLevels !== undefined ? modelLevels : capsEffortLevels(provider);
|
|
135
157
|
return levels?.includes("medium") ? "@medium" : "";
|
|
136
158
|
}
|
|
137
|
-
function fileContent(name, modelId, provider, suffix) {
|
|
159
|
+
function fileContent(name, modelId, provider, suffix, limitTools, imagePort) {
|
|
138
160
|
const description = `${modelId} via ${provider}. Generated by ClaudeRipple from config.json — edits are overwritten; ` +
|
|
139
161
|
`to customise, copy to another name. Set effort with [[ripple: ${name}@<level>]] on the first line.`;
|
|
140
|
-
|
|
162
|
+
const tools = limitTools ? `tools: ${TOOLS.join(", ")}\n` : "";
|
|
163
|
+
const body = imagePort === null ? BODY : `${BODY}\n${imageLine(imagePort)}`;
|
|
164
|
+
return `---\nname: ${name}\ndescription: ${description}\nmodel: ${modelId}${suffix}\n${tools}---\n${body}\n`;
|
|
141
165
|
}
|
|
142
166
|
function readManifest(file) {
|
|
143
167
|
try {
|
|
@@ -199,6 +223,7 @@ export function syncAgentFiles(cfg, dir, log, opts = {}) {
|
|
|
199
223
|
targets.set(name, { modelId: model.id, provider: providerName, suffix: effortSuffix(provider, model.id) });
|
|
200
224
|
}
|
|
201
225
|
}
|
|
226
|
+
const imagePort = Object.values(cfg.providers).some((p) => p.type === "chatgpt") ? adminPort(cfg) : null;
|
|
202
227
|
const nextOwned = [];
|
|
203
228
|
for (const [name, t] of targets) {
|
|
204
229
|
if (handNames.has(name)) {
|
|
@@ -206,7 +231,7 @@ export function syncAgentFiles(cfg, dir, log, opts = {}) {
|
|
|
206
231
|
continue;
|
|
207
232
|
}
|
|
208
233
|
const file = path.join(dir, `${name}.md`);
|
|
209
|
-
const content = fileContent(name, t.modelId, t.provider, t.suffix);
|
|
234
|
+
const content = fileContent(name, t.modelId, t.provider, t.suffix, cfg.cli.limitWorkerTools === true, imagePort);
|
|
210
235
|
const existing = scanned.get(`${name}.md`);
|
|
211
236
|
if (existing) {
|
|
212
237
|
let current = null;
|
|
@@ -381,6 +381,8 @@ export function validate(c) {
|
|
|
381
381
|
errors.push("listen.port out of range");
|
|
382
382
|
if (c.cli.agentFiles !== undefined && typeof c.cli.agentFiles !== "boolean")
|
|
383
383
|
errors.push("cli.agentFiles must be true or false");
|
|
384
|
+
if (c.cli.limitWorkerTools !== undefined && typeof c.cli.limitWorkerTools !== "boolean")
|
|
385
|
+
errors.push("cli.limitWorkerTools must be true or false");
|
|
384
386
|
if (c.listen.openaiPort !== undefined && !(c.listen.openaiPort >= 0 && c.listen.openaiPort < 65536))
|
|
385
387
|
errors.push("listen.openaiPort out of range");
|
|
386
388
|
return errors;
|
package/dist/router/src/index.js
CHANGED
|
@@ -154,6 +154,7 @@ proxy
|
|
|
154
154
|
picker: () => ({ enabled: !!store.get().picker?.enabled, hosts: terminateHosts(store.get()).slice(1), last: proxy.lastPickerInjection }),
|
|
155
155
|
observedClaudeCodeAuth,
|
|
156
156
|
claudeUsage: () => proxy.claudeUsage.snapshot(),
|
|
157
|
+
image: (req, signal) => proxy.generateImage(req, signal),
|
|
157
158
|
shutdown: () => beginDrain("shutdown requested"),
|
|
158
159
|
});
|
|
159
160
|
log.info(`clauderipple admin GUI on http://127.0.0.1:${admin.port}/`);
|
|
@@ -24,6 +24,17 @@ const USAGE_TIMEOUT_MS = 10_000;
|
|
|
24
24
|
const MODELS_PATH = "/codex/models";
|
|
25
25
|
const MODELS_TIMEOUT_MS = 15_000;
|
|
26
26
|
const MODELS_CACHE_MS = 60 * 60 * 1000;
|
|
27
|
+
/** One image took about 30 s at low quality (2026-09-29); high quality and references take longer. */
|
|
28
|
+
const IMAGE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
29
|
+
/**
|
|
30
|
+
* The shape, asked for in words. The subscription backend accepts the tool's `size` and `quality`
|
|
31
|
+
* and ignores them — even "999x1" and "bogus" came back 200 as a 1536x1024 image (2026-09-29) —
|
|
32
|
+
* while the prompt steers it: "square composition" gave 1254x1254. So the shape is a prompt hint,
|
|
33
|
+
* and quality is the backend's to choose.
|
|
34
|
+
*/
|
|
35
|
+
export const IMAGE_ASPECTS = ["square", "landscape", "portrait"];
|
|
36
|
+
export const IMAGE_FORMATS = ["png", "jpeg", "webp"];
|
|
37
|
+
export const IMAGE_BACKGROUNDS = ["auto", "transparent", "opaque"];
|
|
27
38
|
function anthropicError(status, type, message) {
|
|
28
39
|
return { status, body: JSON.stringify({ type: "error", error: { type, message } }) };
|
|
29
40
|
}
|
|
@@ -394,6 +405,107 @@ export class ChatGptAdapter {
|
|
|
394
405
|
const prose = text.trim();
|
|
395
406
|
return prose ? { hits: selected, text: prose } : { hits: selected };
|
|
396
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Hosted image generation, the same way `searchWeb` runs a hosted search: one Responses turn with
|
|
410
|
+
* the `image_generation` tool required. Wire measured 2026-09-29 against the live backend with a
|
|
411
|
+
* ChatGPT subscription: an `image_generation_call` output item whose `result` is the base64 image,
|
|
412
|
+
* beside the `size`, `quality` and `output_format` it chose and a `revised_prompt`; about 30 s.
|
|
413
|
+
*/
|
|
414
|
+
async generateImage(model, req, signal) {
|
|
415
|
+
const tokens = await this.anyCredential();
|
|
416
|
+
if (tokens instanceof Error)
|
|
417
|
+
throw tokens;
|
|
418
|
+
const id = crypto.randomUUID();
|
|
419
|
+
const tool = {
|
|
420
|
+
type: "image_generation",
|
|
421
|
+
output_format: req.format ?? "png",
|
|
422
|
+
...(req.background ? { background: req.background } : {}),
|
|
423
|
+
};
|
|
424
|
+
const prompt = req.aspect ? `${req.prompt}\n\nCompose it as a ${req.aspect} image.` : req.prompt;
|
|
425
|
+
const content = [{ type: "input_text", text: prompt }];
|
|
426
|
+
for (const image of req.images ?? [])
|
|
427
|
+
content.push({ type: "input_image", image_url: `data:${image.mediaType};base64,${image.data}` });
|
|
428
|
+
const body = {
|
|
429
|
+
model,
|
|
430
|
+
instructions: "Generate the requested image. Use any attached images as references.",
|
|
431
|
+
input: [{ type: "message", role: "user", content }],
|
|
432
|
+
tools: [tool],
|
|
433
|
+
tool_choice: "required",
|
|
434
|
+
reasoning: { effort: "low", summary: "auto" },
|
|
435
|
+
store: false,
|
|
436
|
+
stream: true,
|
|
437
|
+
prompt_cache_key: id,
|
|
438
|
+
client_metadata: { session_id: id, thread_id: id, turn_id: crypto.randomUUID(), "x-codex-window-id": `${id}:0` },
|
|
439
|
+
};
|
|
440
|
+
const timeout = AbortSignal.timeout(IMAGE_TIMEOUT_MS);
|
|
441
|
+
const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
442
|
+
const res = await fetch(`${(this.cfg.url ?? DEFAULT_BASE).replace(/\/$/, "")}/codex/responses`, {
|
|
443
|
+
method: "POST",
|
|
444
|
+
headers: {
|
|
445
|
+
"content-type": "application/json",
|
|
446
|
+
accept: "text/event-stream",
|
|
447
|
+
authorization: `Bearer ${tokens.accessToken}`,
|
|
448
|
+
"chatgpt-account-id": tokens.accountId,
|
|
449
|
+
"OpenAI-Beta": "responses=experimental",
|
|
450
|
+
originator: "codex_cli_rs",
|
|
451
|
+
"session-id": id,
|
|
452
|
+
"thread-id": id,
|
|
453
|
+
"x-client-request-id": id,
|
|
454
|
+
"x-codex-window-id": `${id}:0`,
|
|
455
|
+
},
|
|
456
|
+
body: JSON.stringify(body),
|
|
457
|
+
signal: requestSignal,
|
|
458
|
+
});
|
|
459
|
+
this.noteRateLimits(tokens, rateLimitsFromHeaders(res.headers));
|
|
460
|
+
if (!res.ok || !res.body) {
|
|
461
|
+
const text = await res.text().catch(() => "");
|
|
462
|
+
if (res.status === 401)
|
|
463
|
+
void this.accounts.forceRefresh(tokens.ownerId);
|
|
464
|
+
throw new Error(`ChatGPT image generation: HTTP ${res.status}${text ? ` ${redactErrorText(text, [tokens.accessToken], 200)}` : ""}`);
|
|
465
|
+
}
|
|
466
|
+
const parser = new SseParser();
|
|
467
|
+
const reader = res.body.getReader();
|
|
468
|
+
const decoder = new TextDecoder();
|
|
469
|
+
let image;
|
|
470
|
+
let text = "";
|
|
471
|
+
try {
|
|
472
|
+
for (;;) {
|
|
473
|
+
const { done, value } = await reader.read();
|
|
474
|
+
if (done)
|
|
475
|
+
break;
|
|
476
|
+
for (const event of parser.feed(decoder.decode(value, { stream: true }))) {
|
|
477
|
+
if (event.type === "response.output_text.delta")
|
|
478
|
+
text += String(event.delta ?? "");
|
|
479
|
+
if (event.type === "response.output_item.done") {
|
|
480
|
+
const item = event.item;
|
|
481
|
+
if (item?.type === "image_generation_call" && typeof item.result === "string" && item.result)
|
|
482
|
+
image = item;
|
|
483
|
+
}
|
|
484
|
+
if (event.type === "error" || event.type === "response.failed") {
|
|
485
|
+
const error = (event.error ?? event.response?.error);
|
|
486
|
+
throw new Error(`ChatGPT image generation: ${String(error?.message ?? "backend error")}`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
try {
|
|
493
|
+
await reader.cancel();
|
|
494
|
+
}
|
|
495
|
+
catch { /* already closed */ }
|
|
496
|
+
}
|
|
497
|
+
// A refusal comes back as prose and no image; that prose is the only explanation there is.
|
|
498
|
+
if (!image)
|
|
499
|
+
throw new Error(`ChatGPT image generation: no image returned${text.trim() ? `: ${text.trim().slice(0, 300)}` : ""}`);
|
|
500
|
+
const str = (v) => (typeof v === "string" && v ? v : undefined);
|
|
501
|
+
return {
|
|
502
|
+
data: Buffer.from(image.result, "base64"),
|
|
503
|
+
format: str(image.output_format) ?? req.format ?? "png",
|
|
504
|
+
...(str(image.size) ? { size: str(image.size) } : {}),
|
|
505
|
+
...(str(image.quality) ? { quality: str(image.quality) } : {}),
|
|
506
|
+
...(str(image.revised_prompt) ? { revisedPrompt: str(image.revised_prompt) } : {}),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
397
509
|
/** One in-flight lookup shared by every caller (the status route and the startup refresh). */
|
|
398
510
|
rateLimitsInFlight = null;
|
|
399
511
|
/**
|
package/dist/router/src/proxy.js
CHANGED
|
@@ -253,6 +253,20 @@ export class Proxy {
|
|
|
253
253
|
return this.chatgpt(first[0], first[1]);
|
|
254
254
|
return this.chatgpt("codex-login", { type: "chatgpt", auth: "borrow-codex" });
|
|
255
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* One image for `/api/image`, on the first chatgpt provider and the first model it declares. The
|
|
258
|
+
* model only runs the tool call — the image comes from the backend's own image model — so which
|
|
259
|
+
* one it is barely matters.
|
|
260
|
+
*/
|
|
261
|
+
generateImage(req, signal) {
|
|
262
|
+
const first = Object.entries(this.deps.config().providers).find(([, p]) => p.type === "chatgpt");
|
|
263
|
+
if (!first || first[1].type !== "chatgpt")
|
|
264
|
+
return Promise.reject(new Error("image generation needs a ChatGPT provider; add one in the dashboard"));
|
|
265
|
+
const model = first[1].models?.[0]?.id;
|
|
266
|
+
if (!model)
|
|
267
|
+
return Promise.reject(new Error(`provider ${first[0]} declares no models`));
|
|
268
|
+
return this.chatgpt(first[0], first[1]).generateImage(model, req, signal);
|
|
269
|
+
}
|
|
256
270
|
/** Dashboard action: put one resting ChatGPT account back into rotation now. */
|
|
257
271
|
chatgptClearCooldown(ownerId) {
|
|
258
272
|
for (const [name, p] of Object.entries(this.deps.config().providers)) {
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
// from the manifests. Until 0.1.1 the router and the CLI each carried their own literal, and both
|
|
6
6
|
// still said "0.1.0" in the 0.1.1 release: after an update nobody could tell which router was
|
|
7
7
|
// running, and the tray app had nothing to compare (2026-09-15, reported from a Windows install).
|
|
8
|
-
export const VERSION = "0.
|
|
8
|
+
export const VERSION = "0.7.0";
|
package/dist/ui/app.js
CHANGED
|
@@ -377,6 +377,12 @@ function renderClients() {
|
|
|
377
377
|
agentButton.className = agentEnabled ? "btn secondary" : "btn";
|
|
378
378
|
agentButton.disabled = agentTitleBusy;
|
|
379
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);
|
|
380
386
|
void renderCodexClient();
|
|
381
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 })}`);
|
|
382
388
|
$("#client-claude-code-rows").replaceChildren(el("div", { class: "small", text: mapped.join(" · ") || t("slots.noChanges") }));
|
|
@@ -537,6 +543,17 @@ async function toggleAgentTitle(enabled) {
|
|
|
537
543
|
void refreshHealth();
|
|
538
544
|
}
|
|
539
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
|
+
}
|
|
540
557
|
async function renderCodexClient() {
|
|
541
558
|
const rows = $("#client-codex-rows");
|
|
542
559
|
const button = $("#client-codex-toggle");
|
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/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
|
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": [
|