clauderipple 0.9.4 → 0.10.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 +80 -0
- package/README.md +31 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/hooks/agent-title.js +7 -4
- package/dist/cli/src/index.js +43 -0
- package/dist/cli/src/remote.js +263 -0
- package/dist/cli/src/schtasks.js +48 -40
- package/dist/cli/src/settings.js +2 -1
- package/dist/mod/hooks/i18n.ts +252 -6
- package/dist/mod/hooks/judge.ts +35 -17
- package/dist/mod/hooks/modes.ts +56 -1
- package/dist/mod/hooks/register.tsx +238 -32
- package/dist/mod/hooks/session.ts +98 -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 +858 -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 +116 -3
- package/dist/mod/hooks/workers.ts +33 -6
- package/dist/mod/tests/register.test.tsx +155 -11
- package/dist/mod/tests/ui.test.tsx +375 -27
- package/dist/mod/types/index.d.ts +68 -2
- package/dist/remote/package.json +28 -0
- package/dist/remote/src/away.js +85 -0
- package/dist/remote/src/bridge-client.js +295 -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 +227 -4
- package/dist/router/src/config.js +30 -0
- package/dist/router/src/index.js +39 -0
- package/dist/router/src/ingress/translate.js +6 -2
- package/dist/router/src/phone.js +82 -0
- package/dist/router/src/presets.js +3 -0
- package/dist/router/src/providers/anthropic-usage.js +33 -1
- package/dist/router/src/providers/chatgpt/index.js +41 -11
- package/dist/router/src/providers/chatgpt/translate.js +147 -7
- package/dist/router/src/providers/google/index.js +26 -0
- package/dist/router/src/providers/google/prompt-refusal.js +29 -0
- package/dist/router/src/providers/google/usage.js +257 -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/index.js +29 -5
- package/dist/router/src/providers/openai/translate.js +260 -27
- package/dist/router/src/providers/opencode-usage.js +116 -0
- package/dist/router/src/proxy.js +339 -258
- package/dist/router/src/reasoning.js +157 -0
- 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/thinking.js +20 -9
- package/dist/router/src/usage-history.js +227 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +436 -59
- package/dist/ui/i18n.js +184 -0
- package/dist/ui/index.html +24 -5
- package/dist/ui/style.css +39 -0
- package/docs/ARCHITECTURE.md +164 -12
- package/package.json +7 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const HOUR_MS = 3_600_000;
|
|
4
|
+
export function emptyTotals() {
|
|
5
|
+
return { count: 0, ok: 0, failed: 0, input: 0, cached: 0, cacheWrite: 0, output: 0, ms: 0 };
|
|
6
|
+
}
|
|
7
|
+
/** The same counting rules as `RequestLog.summary`, for one record. */
|
|
8
|
+
export function recordTotals(record) {
|
|
9
|
+
const totals = emptyTotals();
|
|
10
|
+
totals.count = 1;
|
|
11
|
+
if (record.ok)
|
|
12
|
+
totals.ok = 1;
|
|
13
|
+
else if (!record.resent)
|
|
14
|
+
totals.failed = 1;
|
|
15
|
+
totals.ms = record.ms;
|
|
16
|
+
if (record.usage) {
|
|
17
|
+
totals.input = record.usage.input;
|
|
18
|
+
totals.cached = record.usage.cached;
|
|
19
|
+
totals.cacheWrite = record.usage.cacheWrite ?? 0;
|
|
20
|
+
totals.output = record.usage.output;
|
|
21
|
+
}
|
|
22
|
+
return totals;
|
|
23
|
+
}
|
|
24
|
+
export function addTotals(target, source) {
|
|
25
|
+
target.count += source.count;
|
|
26
|
+
target.ok += source.ok;
|
|
27
|
+
target.failed += source.failed;
|
|
28
|
+
target.input += source.input;
|
|
29
|
+
target.cached += source.cached;
|
|
30
|
+
target.cacheWrite += source.cacheWrite;
|
|
31
|
+
target.output += source.output;
|
|
32
|
+
target.ms += source.ms;
|
|
33
|
+
}
|
|
34
|
+
/** Derives the rate columns from summed totals. A cache write is input the model read fresh, so it
|
|
35
|
+
* counts against the hit rate (leaving it out read a turn that wrote 2,218 tokens and uncached 2 as
|
|
36
|
+
* a 100% hit, 2026-09-23). */
|
|
37
|
+
export function finishTotals(totals) {
|
|
38
|
+
const base = totals.input + totals.cached + totals.cacheWrite;
|
|
39
|
+
return {
|
|
40
|
+
count: totals.count,
|
|
41
|
+
ok: totals.ok,
|
|
42
|
+
failed: totals.failed,
|
|
43
|
+
input: totals.input,
|
|
44
|
+
cached: totals.cached,
|
|
45
|
+
cacheWrite: totals.cacheWrite,
|
|
46
|
+
output: totals.output,
|
|
47
|
+
cacheHitPercent: base > 0 ? Math.round((totals.cached / base) * 1000) / 10 : 0,
|
|
48
|
+
avgMs: totals.count > 0 ? Math.round(totals.ms / totals.count) : 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function bucketKey(h, provider, model) {
|
|
52
|
+
return `${h}\u0000${provider}\u0000${model}`;
|
|
53
|
+
}
|
|
54
|
+
function modelKey(provider, model) {
|
|
55
|
+
return `${provider}\u0000${model}`;
|
|
56
|
+
}
|
|
57
|
+
/** Models with the most tokens read first (input + cached + cacheWrite), then the busiest. */
|
|
58
|
+
export function sortByVolume(models) {
|
|
59
|
+
const volume = (bucket) => bucket.input + bucket.cached + bucket.cacheWrite;
|
|
60
|
+
models.sort((a, b) => volume(b) - volume(a) || b.count - a.count);
|
|
61
|
+
}
|
|
62
|
+
function isHourBucket(value) {
|
|
63
|
+
if (!value || typeof value !== "object")
|
|
64
|
+
return false;
|
|
65
|
+
const b = value;
|
|
66
|
+
return typeof b.h === "number" && Number.isFinite(b.h) && Number.isInteger(b.h) &&
|
|
67
|
+
typeof b.p === "string" && typeof b.m === "string" &&
|
|
68
|
+
["count", "ok", "failed", "input", "cached", "cacheWrite", "output", "ms"].every((key) => {
|
|
69
|
+
const n = b[key];
|
|
70
|
+
return typeof n === "number" && Number.isFinite(n);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function toLine(bucket) {
|
|
74
|
+
return JSON.stringify({
|
|
75
|
+
h: bucket.h,
|
|
76
|
+
p: bucket.p,
|
|
77
|
+
m: bucket.m,
|
|
78
|
+
count: bucket.count,
|
|
79
|
+
ok: bucket.ok,
|
|
80
|
+
failed: bucket.failed,
|
|
81
|
+
input: bucket.input,
|
|
82
|
+
cached: bucket.cached,
|
|
83
|
+
cacheWrite: bucket.cacheWrite,
|
|
84
|
+
output: bucket.output,
|
|
85
|
+
ms: bucket.ms,
|
|
86
|
+
}) + "\n";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Durable hourly usage history, appended to `usage-hourly.jsonl` next to `requests.jsonl`.
|
|
90
|
+
*
|
|
91
|
+
* Invariant: every hour below the open hour is fully on disk (possibly as several additive lines
|
|
92
|
+
* for the same (h,p,m)); only the open hour lives in memory. A late record (its hour is below the
|
|
93
|
+
* open hour because a request started earlier and finished later) is appended immediately as its
|
|
94
|
+
* own line. On startup the file's highest hour is the watermark, and request records above it are
|
|
95
|
+
* replayed, so an hour never counted twice yet the open hour survives a restart.
|
|
96
|
+
*/
|
|
97
|
+
export class UsageHistory {
|
|
98
|
+
file;
|
|
99
|
+
buckets = new Map();
|
|
100
|
+
open = new Map();
|
|
101
|
+
openHour = null;
|
|
102
|
+
highestHour = null;
|
|
103
|
+
earliestHour = null;
|
|
104
|
+
constructor(file) {
|
|
105
|
+
this.file = file;
|
|
106
|
+
this.load();
|
|
107
|
+
}
|
|
108
|
+
/** Highest hour already on disk, or null when nothing was persisted yet. */
|
|
109
|
+
get watermark() {
|
|
110
|
+
return this.highestHour;
|
|
111
|
+
}
|
|
112
|
+
/** Feeds startup replay: records at or below the watermark are already on disk. */
|
|
113
|
+
replay(records) {
|
|
114
|
+
const floor = this.highestHour;
|
|
115
|
+
for (const record of records) {
|
|
116
|
+
const hour = hourOf(record);
|
|
117
|
+
if (hour === null)
|
|
118
|
+
continue;
|
|
119
|
+
if (floor !== null && hour <= floor)
|
|
120
|
+
continue;
|
|
121
|
+
this.add(record);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
add(record) {
|
|
125
|
+
const hour = hourOf(record);
|
|
126
|
+
if (hour === null)
|
|
127
|
+
return;
|
|
128
|
+
this.observe(hour);
|
|
129
|
+
const bucket = { h: hour, p: record.provider, m: record.target, ...recordTotals(record) };
|
|
130
|
+
if (this.openHour !== null && hour < this.openHour) {
|
|
131
|
+
this.append([bucket]);
|
|
132
|
+
this.merge(this.buckets, bucket);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (this.openHour === null || hour > this.openHour) {
|
|
136
|
+
this.flush();
|
|
137
|
+
this.openHour = hour;
|
|
138
|
+
}
|
|
139
|
+
this.merge(this.open, bucket);
|
|
140
|
+
}
|
|
141
|
+
/** Buckets whose hour start lies in [fromMs, toMs), summed across providers and models. */
|
|
142
|
+
summary(fromMs, toMs) {
|
|
143
|
+
const fromHour = Math.floor(fromMs / HOUR_MS);
|
|
144
|
+
const total = emptyTotals();
|
|
145
|
+
const providers = new Map();
|
|
146
|
+
const models = new Map();
|
|
147
|
+
for (const bucket of [...this.buckets.values(), ...this.open.values()]) {
|
|
148
|
+
if (bucket.h < fromHour || bucket.h * HOUR_MS >= toMs)
|
|
149
|
+
continue;
|
|
150
|
+
addTotals(total, bucket);
|
|
151
|
+
const provider = providers.get(bucket.p) ?? providers.set(bucket.p, emptyTotals()).get(bucket.p);
|
|
152
|
+
addTotals(provider, bucket);
|
|
153
|
+
const key = modelKey(bucket.p, bucket.m);
|
|
154
|
+
const model = models.get(key) ?? (models.set(key, { totals: emptyTotals(), provider: bucket.p, model: bucket.m }).get(key));
|
|
155
|
+
addTotals(model.totals, bucket);
|
|
156
|
+
}
|
|
157
|
+
const modelList = [...models.values()].map(({ totals, provider, model }) => ({ ...finishTotals(totals), provider, model }));
|
|
158
|
+
sortByVolume(modelList);
|
|
159
|
+
return {
|
|
160
|
+
total: finishTotals(total),
|
|
161
|
+
providers: Object.fromEntries([...providers].map(([name, totals]) => [name, finishTotals(totals)])),
|
|
162
|
+
models: modelList,
|
|
163
|
+
earliest: this.earliestHour === null ? null : new Date(this.earliestHour * HOUR_MS).toISOString(),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
observe(hour) {
|
|
167
|
+
if (this.earliestHour === null || hour < this.earliestHour)
|
|
168
|
+
this.earliestHour = hour;
|
|
169
|
+
}
|
|
170
|
+
merge(target, bucket) {
|
|
171
|
+
const key = bucketKey(bucket.h, bucket.p, bucket.m);
|
|
172
|
+
const existing = target.get(key);
|
|
173
|
+
if (existing)
|
|
174
|
+
addTotals(existing, bucket);
|
|
175
|
+
else
|
|
176
|
+
target.set(key, { ...bucket });
|
|
177
|
+
if (target === this.buckets && (this.highestHour === null || bucket.h > this.highestHour))
|
|
178
|
+
this.highestHour = bucket.h;
|
|
179
|
+
}
|
|
180
|
+
flush() {
|
|
181
|
+
if (this.open.size === 0)
|
|
182
|
+
return;
|
|
183
|
+
const lines = [...this.open.values()];
|
|
184
|
+
this.open.clear();
|
|
185
|
+
this.append(lines);
|
|
186
|
+
for (const line of lines)
|
|
187
|
+
this.merge(this.buckets, line);
|
|
188
|
+
}
|
|
189
|
+
append(lines) {
|
|
190
|
+
if (lines.length === 0)
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
194
|
+
fs.appendFileSync(this.file, lines.map(toLine).join(""), "utf8");
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Usage history is best-effort: it must never alter the proxy response path.
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
load() {
|
|
201
|
+
let text;
|
|
202
|
+
try {
|
|
203
|
+
text = fs.readFileSync(this.file, "utf8");
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (const line of text.split("\n")) {
|
|
209
|
+
if (line === "")
|
|
210
|
+
continue;
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(line);
|
|
213
|
+
if (isHourBucket(parsed)) {
|
|
214
|
+
this.observe(parsed.h);
|
|
215
|
+
this.merge(this.buckets, parsed);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// A torn write stays behind; the neighbouring lines still count.
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function hourOf(record) {
|
|
225
|
+
const at = Date.parse(record.at);
|
|
226
|
+
return Number.isFinite(at) ? Math.floor(at / HOUR_MS) : null;
|
|
227
|
+
}
|
|
@@ -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.10.0";
|