clauderipple 0.9.5 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.es.md +45 -0
- package/README.ja.md +17 -0
- package/README.ko.md +31 -0
- package/README.md +70 -0
- package/README.pt-BR.md +45 -0
- package/README.zh-CN.md +17 -0
- package/dist/app/assets/icon.icns +0 -0
- package/dist/app/assets/icon.ico +0 -0
- package/dist/app/assets/icon.png +0 -0
- package/dist/app/assets/trayDirectTemplate.png +0 -0
- package/dist/app/assets/trayDirectTemplate@2x.png +0 -0
- package/dist/app/assets/trayDownTemplate.png +0 -0
- package/dist/app/assets/trayDownTemplate@2x.png +0 -0
- package/dist/app/assets/trayTemplate.png +0 -0
- package/dist/app/assets/trayTemplate@2x.png +0 -0
- package/dist/app/assets/trayWarnTemplate.png +0 -0
- package/dist/app/assets/trayWarnTemplate@2x.png +0 -0
- package/dist/app/assets/trayWin.png +0 -0
- package/dist/app/assets/trayWin@2x.png +0 -0
- package/dist/app/assets/trayWinDirect.png +0 -0
- package/dist/app/assets/trayWinDirect@2x.png +0 -0
- package/dist/app/assets/trayWinDown.png +0 -0
- package/dist/app/assets/trayWinDown@2x.png +0 -0
- package/dist/app/assets/trayWinWarn.png +0 -0
- package/dist/app/assets/trayWinWarn@2x.png +0 -0
- package/dist/app/dist/claude-desktop.js +67 -0
- package/dist/app/dist/main.js +423 -110
- package/dist/app/dist/preload.js +21 -0
- package/dist/app/dist/protocol.js +121 -0
- package/dist/app/dist/status-model.js +87 -0
- package/dist/app/dist/window.js +133 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/awake-setup.js +314 -0
- package/dist/cli/src/bundle.js +9 -5
- package/dist/cli/src/direct.js +159 -0
- package/dist/cli/src/doctor.js +205 -0
- package/dist/cli/src/hooks/agent-title.js +7 -4
- package/dist/cli/src/index.js +227 -31
- package/dist/cli/src/launchd.js +29 -20
- package/dist/cli/src/launcher.js +215 -0
- package/dist/cli/src/power.js +182 -0
- package/dist/cli/src/registration.js +157 -0
- package/dist/cli/src/remote.js +263 -0
- package/dist/cli/src/schtasks.js +50 -40
- package/dist/cli/src/settings.js +26 -1
- package/dist/cli/src/tray.js +5 -2
- package/dist/mod/hooks/i18n.ts +473 -227
- package/dist/mod/hooks/judge.ts +35 -17
- package/dist/mod/hooks/modes.ts +56 -1
- package/dist/mod/hooks/register.tsx +237 -32
- package/dist/mod/hooks/session.ts +102 -1
- package/dist/mod/hooks/settings.js +7 -3
- package/dist/mod/hooks/settings.ts +9 -4
- package/dist/mod/hooks/stats.ts +25 -1
- package/dist/mod/hooks/ui/agents.tsx +903 -79
- package/dist/mod/hooks/ui/alerts.tsx +20 -17
- package/dist/mod/hooks/ui/art.ts +33 -7
- package/dist/mod/hooks/ui/band.tsx +262 -81
- package/dist/mod/hooks/ui/flat.ts +265 -0
- package/dist/mod/hooks/ui/kit.tsx +70 -55
- package/dist/mod/hooks/ui/look.ts +129 -0
- package/dist/mod/hooks/ui/settings.tsx +71 -46
- package/dist/mod/hooks/ui/setup.tsx +15 -13
- package/dist/mod/hooks/ui/theme.ts +3 -0
- package/dist/mod/hooks/ui/tools.ts +124 -0
- package/dist/mod/hooks/ui/view.ts +120 -4
- package/dist/mod/hooks/workers.ts +33 -6
- package/dist/mod/tests/register.test.tsx +155 -11
- package/dist/mod/tests/ui.test.tsx +408 -27
- package/dist/mod/types/index.d.ts +66 -2
- package/dist/remote/package.json +28 -0
- package/dist/remote/src/away.js +85 -0
- package/dist/remote/src/bridge-client.js +336 -0
- package/dist/remote/src/composer-menu.js +11 -0
- package/dist/remote/src/config.js +102 -0
- package/dist/remote/src/desktop.js +1018 -0
- package/dist/remote/src/e2ee.js +126 -0
- package/dist/remote/src/e2ee.ts +126 -0
- package/dist/remote/src/eventlog.js +298 -0
- package/dist/remote/src/expo-push.js +127 -0
- package/dist/remote/src/folders.js +79 -0
- package/dist/remote/src/fsutil.js +39 -0
- package/dist/remote/src/handoff.js +38 -0
- package/dist/remote/src/host-service.js +168 -0
- package/dist/remote/src/images.js +215 -0
- package/dist/remote/src/local-control.js +138 -0
- package/dist/remote/src/main.js +264 -0
- package/dist/remote/src/permissions.js +124 -0
- package/dist/remote/src/power.js +76 -0
- package/dist/remote/src/processes.js +24 -0
- package/dist/remote/src/protocol.js +32 -0
- package/dist/remote/src/protocol.ts +284 -0
- package/dist/remote/src/push-content.js +32 -0
- package/dist/remote/src/push.js +134 -0
- package/dist/remote/src/receipts.js +111 -0
- package/dist/remote/src/relay.js +438 -0
- package/dist/remote/src/router.js +208 -0
- package/dist/remote/src/sdk-launcher.js +18 -0
- package/dist/remote/src/server.js +706 -0
- package/dist/remote/src/service.js +157 -0
- package/dist/remote/src/sessions.js +1800 -0
- package/dist/remote/src/sidebar-storage.js +211 -0
- package/dist/remote/src/sidebar.js +173 -0
- package/dist/remote/src/tool-details.js +205 -0
- package/dist/remote/src/tool-details.ts +153 -0
- package/dist/remote/src/worktree.js +99 -0
- package/dist/remote/web/app.ts +746 -0
- package/dist/remote/web/conn.ts +193 -0
- package/dist/remote/web/demo.ts +134 -0
- package/dist/remote/web/dom.ts +124 -0
- package/dist/remote/web/esc.ts +4 -0
- package/dist/remote/web/i18n.ts +374 -0
- package/dist/remote/web/icon.svg +1 -0
- package/dist/remote/web/index.html +24 -0
- package/dist/remote/web/manifest.webmanifest +10 -0
- package/dist/remote/web/mascot.ts +60 -0
- package/dist/remote/web/md.ts +78 -0
- package/dist/remote/web/notify.ts +55 -0
- package/dist/remote/web/pushstate.ts +27 -0
- package/dist/remote/web/secure.ts +109 -0
- package/dist/remote/web/sidebar.ts +106 -0
- package/dist/remote/web/style.css +311 -0
- package/dist/remote/web/sw.ts +48 -0
- package/dist/remote/web/timeline.ts +224 -0
- package/dist/remote/web/views.ts +323 -0
- package/dist/router/src/admin.js +331 -5
- package/dist/router/src/awake-win.js +590 -0
- package/dist/router/src/awake.js +432 -0
- package/dist/router/src/config.js +94 -0
- package/dist/router/src/health.js +86 -3
- package/dist/router/src/hold.js +107 -0
- package/dist/router/src/index.js +108 -6
- package/dist/router/src/ingress/server.js +26 -4
- package/dist/router/src/ingress/translate.js +6 -2
- package/dist/router/src/phone.js +82 -0
- package/dist/router/src/power.js +107 -0
- package/dist/router/src/providers/anthropic-usage.js +33 -1
- package/dist/router/src/providers/google/antigravity.js +73 -10
- package/dist/router/src/providers/google/index.js +191 -14
- package/dist/router/src/providers/google/prompt-refusal.js +29 -0
- package/dist/router/src/providers/google/thoughtSignatures.js +13 -0
- package/dist/router/src/providers/google/transport.js +1 -1
- package/dist/router/src/providers/google/usage.js +302 -0
- package/dist/router/src/providers/grok/index.js +18 -0
- package/dist/router/src/providers/grok/usage.js +77 -0
- package/dist/router/src/providers/openai/translate.js +13 -2
- package/dist/router/src/providers/opencode-usage.js +116 -0
- package/dist/router/src/providers/retry.js +4 -1
- package/dist/router/src/proxy.js +371 -261
- package/dist/router/src/remote-host.js +320 -0
- package/dist/router/src/remote-tunnel.js +320 -0
- package/dist/router/src/requestlog.js +48 -39
- package/dist/router/src/tether-win.js +211 -0
- package/dist/router/src/tether.js +712 -0
- package/dist/router/src/usage-history.js +227 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +1083 -59
- package/dist/ui/i18n.js +670 -69
- package/dist/ui/index.html +33 -6
- package/dist/ui/style.css +177 -0
- package/docs/ARCHITECTURE.md +402 -14
- package/package.json +8 -2
package/dist/router/src/admin.js
CHANGED
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
// PATCH/DELETE /api/google-accounts/:id rename, pause/resume, clear a cooldown; remove
|
|
15
15
|
// GET /api/update whether a newer version is published, and how this copy was installed
|
|
16
16
|
// POST /api/update start `clauderipple update` in the background (it restarts this router)
|
|
17
|
+
// GET /api/power lid-closed awake + hotspot failover state (power.ts)
|
|
18
|
+
// PUT /api/power change awake mode/until, tether on/off (saved), pin (in memory)
|
|
19
|
+
// POST /api/power/setup start `clauderipple awake setup [--ssid]` (macOS password dialog); GET polls it
|
|
17
20
|
// GET /* static files from packages/ui (the GUI itself)
|
|
18
21
|
import crypto from "node:crypto";
|
|
22
|
+
import { phoneRequest } from "./phone.js";
|
|
19
23
|
import fs from "node:fs";
|
|
20
24
|
import net from "node:net";
|
|
21
25
|
import os from "node:os";
|
|
@@ -23,7 +27,7 @@ import path from "node:path";
|
|
|
23
27
|
import http from "node:http";
|
|
24
28
|
import { execFile, spawn } from "node:child_process";
|
|
25
29
|
import { fileURLToPath } from "node:url";
|
|
26
|
-
import { adminPort, headerValueProblem, homeDir, searchProviderFor, validate } from "./config.js";
|
|
30
|
+
import { adminPort, headerValueProblem, homeDir, searchProviderFor, ssidProblem, validate } from "./config.js";
|
|
27
31
|
export { adminPort } from "./config.js";
|
|
28
32
|
import { generatedAgents } from "./agents.js";
|
|
29
33
|
import { isJudgeOnlyModel } from "./model-policy.js";
|
|
@@ -51,12 +55,15 @@ import { readClaudeAuthFile } from "./providers/anthropic-token-file.js";
|
|
|
51
55
|
import { listClaudeAccounts, removeClaudeAccount, renameClaudeAccount, setPreferredClaudeAccount } from "./providers/anthropic-accounts.js";
|
|
52
56
|
import { readChatGptAccounts, removeChatGptAccount, summarize as summarizeChatGptAccount, updateChatGptAccount } from "./providers/chatgpt/accounts.js";
|
|
53
57
|
import { exhaustedForMs, IMAGE_ASPECTS, IMAGE_BACKGROUNDS, IMAGE_FORMATS } from "./providers/chatgpt/index.js";
|
|
58
|
+
import { googleWindowsFromGroups } from "./providers/google/usage.js";
|
|
59
|
+
import { isOpenCodeGo } from "./providers/opencode-usage.js";
|
|
54
60
|
import { ANTIGRAVITY_WARNING, staticAntigravityModels } from "./providers/google/antigravity.js";
|
|
55
61
|
import { readGoogleAccounts, removeGoogleAccount, summarize as summarizeGoogleAccount, updateGoogleAccount } from "./providers/google/accounts.js";
|
|
56
62
|
import { GoogleOAuthSession } from "./providers/google/login.js";
|
|
57
63
|
import { GROK_AUTH_HINT, grokAuthPath, grokHome, readGrokToken, sharedGrokAuth } from "./providers/grok/auth.js";
|
|
58
64
|
import { GROK_DEFAULT_URL, GROK_EFFORT_LEVELS, GROK_FALLBACK_MODELS, grokModelsFromListing } from "./providers/grok/catalog.js";
|
|
59
65
|
import { GROK_WARNING, grokHeaders } from "./providers/grok/index.js";
|
|
66
|
+
import { parsePowerChange, persistPowerChange } from "./power.js";
|
|
60
67
|
const MAX_BODY = 1024 * 1024;
|
|
61
68
|
/** `/api/image` carries reference images inline, so it gets more room than a config save. */
|
|
62
69
|
const MAX_IMAGE_BODY = 32 * 1024 * 1024;
|
|
@@ -100,6 +107,145 @@ async function claudeAccountUsage(deps) {
|
|
|
100
107
|
}
|
|
101
108
|
return rows;
|
|
102
109
|
}
|
|
110
|
+
/** A `{used_percent, window_minutes, reset_at}` snapshot window as `/api/limits` reads it, or null. */
|
|
111
|
+
function limitWindowFrom(w) {
|
|
112
|
+
if (!w || typeof w !== "object" || Array.isArray(w))
|
|
113
|
+
return null;
|
|
114
|
+
const o = w;
|
|
115
|
+
const used = o.used_percent;
|
|
116
|
+
if (typeof used !== "number" || !Number.isFinite(used))
|
|
117
|
+
return null;
|
|
118
|
+
const minutes = typeof o.window_minutes === "number" && o.window_minutes > 0 ? o.window_minutes : undefined;
|
|
119
|
+
// A reset already in milliseconds would otherwise park a window for decades; the other readers
|
|
120
|
+
// normalise the same way (chatgpt windowResetMs).
|
|
121
|
+
const reset = typeof o.reset_at === "number" && Number.isFinite(o.reset_at) ? Math.round(o.reset_at > 1e12 ? o.reset_at / 1000 : o.reset_at) : undefined;
|
|
122
|
+
return { used_percent: used, ...(minutes !== undefined ? { window_minutes: minutes } : {}), ...(reset !== undefined ? { reset_at: reset } : {}) };
|
|
123
|
+
}
|
|
124
|
+
/** The kind a five-hour/weekly/other window takes, by its length in minutes. */
|
|
125
|
+
function windowKind(minutes) {
|
|
126
|
+
return minutes === 300 ? "five_hour" : minutes === 10080 ? "seven_day" : "window";
|
|
127
|
+
}
|
|
128
|
+
/** One Claude subscription account's plan windows: the two main windows and the scoped weeklies. */
|
|
129
|
+
function claudeLimitWindows(quota) {
|
|
130
|
+
const out = [];
|
|
131
|
+
const limits = quota.rate_limits;
|
|
132
|
+
for (const w of [limits?.primary, limits?.secondary]) {
|
|
133
|
+
const win = limitWindowFrom(w);
|
|
134
|
+
if (win)
|
|
135
|
+
out.push({ kind: windowKind(win.window_minutes), used_percent: win.used_percent, ...(win.window_minutes !== undefined ? { window_minutes: win.window_minutes } : {}), ...(win.reset_at !== undefined ? { reset_at: win.reset_at } : {}) });
|
|
136
|
+
}
|
|
137
|
+
const scoped = quota.scoped;
|
|
138
|
+
if (Array.isArray(scoped))
|
|
139
|
+
for (const s of scoped) {
|
|
140
|
+
if (!s || typeof s !== "object" || Array.isArray(s))
|
|
141
|
+
continue;
|
|
142
|
+
const entry = s;
|
|
143
|
+
const win = limitWindowFrom(entry);
|
|
144
|
+
if (!win || typeof entry.name !== "string" || !entry.name)
|
|
145
|
+
continue;
|
|
146
|
+
out.push({ kind: "scoped", label: entry.name, used_percent: win.used_percent, ...(win.window_minutes !== undefined ? { window_minutes: win.window_minutes } : {}), ...(win.reset_at !== undefined ? { reset_at: win.reset_at } : {}) });
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
/** The Claude Code login's plan windows as one account (the row shows Claude as a single group). */
|
|
151
|
+
async function anthropicLimitAccounts(deps) {
|
|
152
|
+
let usage;
|
|
153
|
+
try {
|
|
154
|
+
usage = (await deps.claudeUsage?.()) ?? {};
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const quota = usage.current ?? Object.values(usage)[0];
|
|
160
|
+
if (!quota)
|
|
161
|
+
return [];
|
|
162
|
+
const windows = claudeLimitWindows(quota);
|
|
163
|
+
if (!windows.length)
|
|
164
|
+
return [];
|
|
165
|
+
const at = typeof quota.at === "number" ? quota.at : Date.now();
|
|
166
|
+
return [{ account: 0, windows, at }];
|
|
167
|
+
}
|
|
168
|
+
/** Every ChatGPT account's windows, paused and needs-login accounts left out and unnumbered. */
|
|
169
|
+
function chatgptLimitAccounts(accounts) {
|
|
170
|
+
const kept = accounts.filter((a) => a.state !== "paused" && a.state !== "needs-login");
|
|
171
|
+
const out = [];
|
|
172
|
+
kept.forEach((account, index) => {
|
|
173
|
+
const quota = account.quota;
|
|
174
|
+
if (!quota || typeof quota.at !== "number")
|
|
175
|
+
return;
|
|
176
|
+
const limits = quota.rate_limits;
|
|
177
|
+
const windows = [];
|
|
178
|
+
for (const w of [limits?.primary, limits?.secondary]) {
|
|
179
|
+
const win = limitWindowFrom(w);
|
|
180
|
+
if (win)
|
|
181
|
+
windows.push({ kind: windowKind(win.window_minutes), used_percent: win.used_percent, ...(win.window_minutes !== undefined ? { window_minutes: win.window_minutes } : {}), ...(win.reset_at !== undefined ? { reset_at: win.reset_at } : {}) });
|
|
182
|
+
}
|
|
183
|
+
if (windows.length)
|
|
184
|
+
out.push({ account: index, windows, at: quota.at });
|
|
185
|
+
});
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
/** Every Antigravity account's windows (Gemini's own plus the third-party ones), paused left out. */
|
|
189
|
+
async function googleLimitAccounts(deps, name) {
|
|
190
|
+
const google = deps.google?.();
|
|
191
|
+
if (!google)
|
|
192
|
+
return [];
|
|
193
|
+
const statuses = google.accounts()[name] ?? [];
|
|
194
|
+
const groups = (await google.usage?.(name)) ?? {};
|
|
195
|
+
const kept = statuses.filter((a) => a.state !== "paused" && a.state !== "needs-login");
|
|
196
|
+
const at = Date.now();
|
|
197
|
+
// The third-party (Claude and GPT) windows only when this provider has such a model selected:
|
|
198
|
+
// beside Gemini's own they made four figures, two of them a pool nobody here spends (2026-10-20).
|
|
199
|
+
const provider = deps.config().providers[name];
|
|
200
|
+
const models = provider && "models" in provider && Array.isArray(provider.models) ? provider.models : [];
|
|
201
|
+
const thirdParty = models.some((m) => /^(claude-|gpt-oss(-|$))/i.test(typeof m === "string" ? m : m.id));
|
|
202
|
+
const out = [];
|
|
203
|
+
kept.forEach((account, index) => {
|
|
204
|
+
const quotient = groups[account.id];
|
|
205
|
+
if (!quotient || !quotient.length)
|
|
206
|
+
return;
|
|
207
|
+
const windows = googleWindowsFromGroups(quotient).filter((w) => thirdParty || w.kind !== "scoped");
|
|
208
|
+
if (windows.length)
|
|
209
|
+
out.push({ account: index, windows, at });
|
|
210
|
+
});
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
/** Every provider whose quota can be read, in one shape, for `GET /api/limits`. */
|
|
214
|
+
async function buildLimits(deps) {
|
|
215
|
+
const providers = [];
|
|
216
|
+
const chatgpt = deps.chatgpt?.();
|
|
217
|
+
const accounts = chatgpt?.accounts?.() ?? {};
|
|
218
|
+
for (const [name, p] of Object.entries(deps.config().providers)) {
|
|
219
|
+
if (p.limits === false)
|
|
220
|
+
continue;
|
|
221
|
+
if (p.type === "anthropic" && p.auth === "claude-code") {
|
|
222
|
+
const rows = await anthropicLimitAccounts(deps);
|
|
223
|
+
if (rows.length)
|
|
224
|
+
providers.push({ provider: name, type: "anthropic", title: "Claude", accounts: rows });
|
|
225
|
+
}
|
|
226
|
+
else if (p.type === "chatgpt") {
|
|
227
|
+
const rows = chatgptLimitAccounts(accounts[name] ?? []);
|
|
228
|
+
if (rows.length)
|
|
229
|
+
providers.push({ provider: name, type: "chatgpt", title: "GPT", accounts: rows });
|
|
230
|
+
}
|
|
231
|
+
else if (p.type === "google" && p.auth === "antigravity") {
|
|
232
|
+
const rows = await googleLimitAccounts(deps, name);
|
|
233
|
+
if (rows.length)
|
|
234
|
+
providers.push({ provider: name, type: "google", title: "Gemini", accounts: rows });
|
|
235
|
+
}
|
|
236
|
+
else if (p.type === "openai-compatible" && isOpenCodeGo(p)) {
|
|
237
|
+
const windows = (await deps.opencodeUsage?.(name)) ?? [];
|
|
238
|
+
if (windows.length)
|
|
239
|
+
providers.push({ provider: name, type: "opencode-go", title: "Go", accounts: [{ account: 0, windows: windows.map((w) => ({ ...w })), at: Date.now() }] });
|
|
240
|
+
}
|
|
241
|
+
else if (p.type === "grok") {
|
|
242
|
+
const windows = (await deps.grokUsage?.(name)) ?? [];
|
|
243
|
+
if (windows.length)
|
|
244
|
+
providers.push({ provider: name, type: "grok", title: "Grok", accounts: [{ account: 0, windows: windows.map((w) => ({ ...w })), at: Date.now() }] });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return { providers };
|
|
248
|
+
}
|
|
103
249
|
// The CLI's `--effort` takes the same five; Opus, Sonnet and Haiku 5.5 answered `xhigh` (requests.jsonl, 2026-10-14, #56).
|
|
104
250
|
const ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
105
251
|
const CHATGPT_DEFAULT_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
@@ -1030,10 +1176,12 @@ async function probeProvider(body, probeFetch = fetchWithTimeout) {
|
|
|
1030
1176
|
const checkModel = presetProbeModel(body.preset) ?? models[0]?.id ?? body.probeModel ?? "test";
|
|
1031
1177
|
// 16 tokens, as on Responses: some vendors refuse a smaller cap outright (B.ai: "max_tokens must be
|
|
1032
1178
|
// greater than 2", #57), which read as a failed connection though the key and URL were right.
|
|
1179
|
+
// Streamed, as every translated turn is: the status arrives with the headers, while a reasoning
|
|
1180
|
+
// model answering whole can take past the timeout (Hive: 0.3s streamed, 3–23s whole, 2026-10-18).
|
|
1033
1181
|
const checkBody = responses
|
|
1034
|
-
? { model: checkModel, max_output_tokens: 16, stream:
|
|
1182
|
+
? { model: checkModel, max_output_tokens: 16, stream: true, store: false, input: "hi" }
|
|
1035
1183
|
: openai
|
|
1036
|
-
? { model: checkModel, max_tokens: 16, stream:
|
|
1184
|
+
? { model: checkModel, max_tokens: 16, stream: true, messages: [{ role: "user", content: "hi" }] }
|
|
1037
1185
|
: { model: checkModel, max_tokens: 16, messages: [{ role: "user", content: "hi" }] };
|
|
1038
1186
|
const label = responses ? "responses endpoint" : openai ? "chat completions endpoint" : "messages endpoint";
|
|
1039
1187
|
try {
|
|
@@ -1051,8 +1199,10 @@ async function probeProvider(body, probeFetch = fetchWithTimeout) {
|
|
|
1051
1199
|
const auth = response.status === 403 && refusedByPlan(refusal) ? "not-entitled" : "bad-key";
|
|
1052
1200
|
return { ok: false, auth, models, error: `${label} returned ${response.status}: ${refusal}` };
|
|
1053
1201
|
}
|
|
1054
|
-
if (response.ok)
|
|
1202
|
+
if (response.ok) {
|
|
1203
|
+
await response.body?.cancel().catch(() => { });
|
|
1055
1204
|
return { ok: true, auth: "ok", models, ...(modelsError ? { error: modelsError } : {}) };
|
|
1205
|
+
}
|
|
1056
1206
|
const detail = snippet(await response.text());
|
|
1057
1207
|
// A model-validation refusal still demonstrates that the endpoint reached the provider and the key was accepted.
|
|
1058
1208
|
if (refusedForModel(response.status, detail)) {
|
|
@@ -1441,6 +1591,28 @@ async function pickerModels(deps) {
|
|
|
1441
1591
|
}
|
|
1442
1592
|
return { models: await claudeModelList(deps), source: "fallback" };
|
|
1443
1593
|
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Each model's context window, for a client that reads a session's context use without the CLI's own
|
|
1596
|
+
* figure (the phone, for a Claude app session). Claude's from Anthropic's catalog, the CLI's injected
|
|
1597
|
+
* models from their config (else `cli.autoCompactWindow`), providers' from theirs. Unknown is absent.
|
|
1598
|
+
*/
|
|
1599
|
+
async function contextWindows(deps) {
|
|
1600
|
+
const cfg = deps.config();
|
|
1601
|
+
const windows = {};
|
|
1602
|
+
const put = (id, value) => {
|
|
1603
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0 && windows[id] === undefined)
|
|
1604
|
+
windows[id] = Math.floor(value);
|
|
1605
|
+
};
|
|
1606
|
+
for (const model of await claudeModelList(deps))
|
|
1607
|
+
put(model.id, model.contextWindow);
|
|
1608
|
+
for (const model of cfg.cli.extraModels)
|
|
1609
|
+
put(model.model, model.contextWindow ?? cfg.cli.autoCompactWindow);
|
|
1610
|
+
for (const provider of Object.values(cfg.providers)) {
|
|
1611
|
+
for (const model of ("models" in provider && provider.models) || [])
|
|
1612
|
+
put(model.id, model.contextWindow);
|
|
1613
|
+
}
|
|
1614
|
+
return { windows };
|
|
1615
|
+
}
|
|
1444
1616
|
function tailLines(file, n) {
|
|
1445
1617
|
if (!fs.existsSync(file))
|
|
1446
1618
|
return "";
|
|
@@ -1502,6 +1674,8 @@ export function startAdmin(deps) {
|
|
|
1502
1674
|
const scores = new ModelScoreCatalog({ home: homeDir(), warn: message => deps.log.warn(message), ...(deps.modelScoresFetch ? { fetch: deps.modelScoresFetch } : {}) });
|
|
1503
1675
|
// Set again once the socket is bound (port 0 in tests); the Origin check below compares against it.
|
|
1504
1676
|
let boundPort = adminPort(deps.config());
|
|
1677
|
+
/** The dashboard's `awake setup` run in progress (or the last one), for GET /api/power/setup. */
|
|
1678
|
+
let powerSetup = { running: false };
|
|
1505
1679
|
const server = http.createServer((req, res) => {
|
|
1506
1680
|
void handle(req, res);
|
|
1507
1681
|
});
|
|
@@ -1535,6 +1709,28 @@ export function startAdmin(deps) {
|
|
|
1535
1709
|
sendJson(res, 403, { error: "cross-site request refused" });
|
|
1536
1710
|
return;
|
|
1537
1711
|
}
|
|
1712
|
+
if (pathname.startsWith("/api/phone/")) {
|
|
1713
|
+
res.setHeader("cache-control", "no-store");
|
|
1714
|
+
// Pairing is a credential read: guard GETs too, including DNS-rebinding Host values.
|
|
1715
|
+
if (crossSitePost(req) || req.headers["sec-fetch-site"] === "cross-site" ||
|
|
1716
|
+
!new Set([`127.0.0.1:${boundPort}`, `localhost:${boundPort}`]).has(req.headers.host ?? "")) {
|
|
1717
|
+
sendJson(res, 403, { error: "same-origin phone control required" });
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
let body;
|
|
1721
|
+
if (method === "POST" && pathname === "/api/phone/settings") {
|
|
1722
|
+
try {
|
|
1723
|
+
body = (await readBody(req, 4096, new Error("phone settings too large"))).toString("utf8");
|
|
1724
|
+
}
|
|
1725
|
+
catch {
|
|
1726
|
+
sendJson(res, 413, { error: "phone settings too large" });
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const result = await phoneRequest(pathname.slice("/api/phone/".length), method, body, deps.phone);
|
|
1731
|
+
sendJson(res, result.status, result.body);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1538
1734
|
if (pathname === "/api/shutdown" && method === "POST") {
|
|
1539
1735
|
if (!deps.shutdown) {
|
|
1540
1736
|
sendJson(res, 501, { error: "shutdown not available" });
|
|
@@ -1546,6 +1742,114 @@ export function startAdmin(deps) {
|
|
|
1546
1742
|
deps.shutdown();
|
|
1547
1743
|
return;
|
|
1548
1744
|
}
|
|
1745
|
+
if (pathname === "/api/remote" && method === "GET") {
|
|
1746
|
+
if (!deps.remote) {
|
|
1747
|
+
sendJson(res, 501, { error: "remote tunnels not available" });
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
sendJson(res, 200, { hosts: deps.remote.status() });
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
if (pathname === "/api/remote/reload" && method === "POST") {
|
|
1754
|
+
if (!deps.remote) {
|
|
1755
|
+
sendJson(res, 501, { error: "remote tunnels not available" });
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
sendJson(res, 200, { hosts: deps.remote.reload() });
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
if (pathname === "/api/power" && (method === "GET" || method === "PUT")) {
|
|
1762
|
+
if (!deps.power) {
|
|
1763
|
+
sendJson(res, 501, { error: "power control not available" });
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (method === "GET") {
|
|
1767
|
+
sendJson(res, 200, deps.power.status());
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
let parsed;
|
|
1771
|
+
try {
|
|
1772
|
+
parsed = JSON.parse((await readBody(req, 4096)).toString("utf8"));
|
|
1773
|
+
}
|
|
1774
|
+
catch {
|
|
1775
|
+
sendJson(res, 400, { error: "invalid JSON" });
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
const change = parsePowerChange(parsed);
|
|
1779
|
+
if (typeof change === "string") {
|
|
1780
|
+
sendJson(res, 400, { error: change });
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (persistPowerChange(deps.configFile, change)) {
|
|
1784
|
+
deps.log.info(`admin: power saved (${JSON.stringify({ ...(change.awake ? { awake: change.awake } : {}), ...(change.tether?.enabled !== undefined ? { tether: { enabled: change.tether.enabled } } : {}) })})`);
|
|
1785
|
+
}
|
|
1786
|
+
if (change.tether?.pin !== undefined)
|
|
1787
|
+
deps.power.pin(change.tether.pin);
|
|
1788
|
+
await deps.power.poke();
|
|
1789
|
+
sendJson(res, 200, deps.power.status());
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
// The one privileged step, from the dashboard: the CLI's `awake setup`, which shows macOS's own
|
|
1793
|
+
// password dialog. It runs in the background and is polled, like the ChatGPT sign-in: the user
|
|
1794
|
+
// may take minutes to type, and the app's window gives up on a response with no headers after
|
|
1795
|
+
// 4s (packages/app/src/protocol.ts) — a reply held open until the end would read as a dead router.
|
|
1796
|
+
if (pathname === "/api/power/setup" && (method === "GET" || method === "POST")) {
|
|
1797
|
+
if (!deps.power) {
|
|
1798
|
+
sendJson(res, 501, { error: "power control not available" });
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
if (method === "GET") {
|
|
1802
|
+
sendJson(res, 200, powerSetup);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
let parsed;
|
|
1806
|
+
try {
|
|
1807
|
+
const text = (await readBody(req, 4096)).toString("utf8");
|
|
1808
|
+
parsed = text.trim() ? JSON.parse(text) : {};
|
|
1809
|
+
}
|
|
1810
|
+
catch {
|
|
1811
|
+
sendJson(res, 400, { error: "invalid JSON" });
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1815
|
+
sendJson(res, 400, { error: "expected an object" });
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const extra = Object.keys(parsed).find((key) => key !== "ssid");
|
|
1819
|
+
if (extra) {
|
|
1820
|
+
sendJson(res, 400, { error: `unknown field "${extra}"` });
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
const ssid = parsed.ssid;
|
|
1824
|
+
if (ssid !== undefined) {
|
|
1825
|
+
// Checked here too so a bad name is refused before a password dialog, not after it.
|
|
1826
|
+
const problem = ssidProblem(ssid);
|
|
1827
|
+
if (problem) {
|
|
1828
|
+
sendJson(res, 400, { error: problem });
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
if (powerSetup.running) {
|
|
1833
|
+
sendJson(res, 409, { error: "setup is already running", running: true });
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
powerSetup = { running: true, startedAt: new Date().toISOString() };
|
|
1837
|
+
const args = ["awake", "setup", ...(typeof ssid === "string" ? ["--ssid", ssid] : [])];
|
|
1838
|
+
deps.log.info(`admin: awake setup via GUI -> started${typeof ssid === "string" ? ` (hotspot "${ssid}")` : ""}`);
|
|
1839
|
+
const power = deps.power;
|
|
1840
|
+
// The CLI's own dialog times out at 5 min; this leaves it room to say so itself.
|
|
1841
|
+
void (deps.runCli ?? runCli)(args, 330_000).then((result) => ({ ok: result.ok, output: result.output }), (error) => ({ ok: false, output: errorText(error) })).then(async (result) => {
|
|
1842
|
+
powerSetup = { ...powerSetup, running: false, finishedAt: new Date().toISOString(), ...result };
|
|
1843
|
+
deps.log.info(`admin: awake setup via GUI -> ${result.ok ? "ok" : "failed"}`);
|
|
1844
|
+
// Re-read whether the rule is in place now, so the next GET /api/power says so.
|
|
1845
|
+
try {
|
|
1846
|
+
await power.poke();
|
|
1847
|
+
}
|
|
1848
|
+
catch { /* the next tick does it */ }
|
|
1849
|
+
});
|
|
1850
|
+
sendJson(res, 202, { started: true });
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1549
1853
|
// Liveness is answering at all; readiness is 200 only when a request can actually go through.
|
|
1550
1854
|
if (pathname === "/readyz" && method === "GET") {
|
|
1551
1855
|
const readiness = (await buildStatus(deps)).readiness;
|
|
@@ -1559,6 +1863,11 @@ export function startAdmin(deps) {
|
|
|
1559
1863
|
sendJson(res, 200, await buildStatus(deps, { refresh }));
|
|
1560
1864
|
return;
|
|
1561
1865
|
}
|
|
1866
|
+
// One shape for every provider whose quota can be read, for the status bar's limits row.
|
|
1867
|
+
if (pathname === "/api/limits" && method === "GET") {
|
|
1868
|
+
sendJson(res, 200, await buildLimits(deps));
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1562
1871
|
if (pathname === "/api/mod-settings" && method === "GET") {
|
|
1563
1872
|
const snapshot = readModSettings();
|
|
1564
1873
|
// Filled here as well as on save, so a ladder with empty rungs (Auto migrated, or never set) reads whole to every surface.
|
|
@@ -1597,6 +1906,10 @@ export function startAdmin(deps) {
|
|
|
1597
1906
|
sendJson(res, 200, await pickerModels(deps));
|
|
1598
1907
|
return;
|
|
1599
1908
|
}
|
|
1909
|
+
if (pathname === "/api/context-windows" && method === "GET") {
|
|
1910
|
+
sendJson(res, 200, await contextWindows(deps));
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1600
1913
|
if (pathname === "/api/effort-levels" && method === "GET") {
|
|
1601
1914
|
sendJson(res, 200, effortLevels(deps.config()));
|
|
1602
1915
|
return;
|
|
@@ -2294,7 +2607,20 @@ export function startAdmin(deps) {
|
|
|
2294
2607
|
return;
|
|
2295
2608
|
}
|
|
2296
2609
|
if (pathname === "/api/requests/summary" && method === "GET") {
|
|
2297
|
-
const
|
|
2610
|
+
const query = new URL(url, "http://x").searchParams;
|
|
2611
|
+
const fromParam = query.get("from");
|
|
2612
|
+
if (fromParam !== null) {
|
|
2613
|
+
const from = Number(fromParam);
|
|
2614
|
+
const toParam = query.get("to");
|
|
2615
|
+
const to = toParam === null ? Date.now() : Number(toParam);
|
|
2616
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || from > to) {
|
|
2617
|
+
sendJson(res, 400, { error: "from and to must be epoch milliseconds with from <= to" });
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
sendJson(res, 200, { ...deps.requests.usage(from, to), from, to });
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
const seconds = Number(query.get("since") ?? 3600);
|
|
2298
2624
|
const safeSeconds = Number.isFinite(seconds) ? Math.min(31_536_000, Math.max(0, seconds)) : 3600;
|
|
2299
2625
|
sendJson(res, 200, deps.requests.summary(Date.now() - safeSeconds * 1000));
|
|
2300
2626
|
return;
|