portable-agent-layer 0.70.0 → 0.72.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/README.md +5 -1
- package/assets/schema/pal-settings.schema.json +4 -0
- package/assets/skills/onboarding/SKILL.md +109 -0
- package/assets/skills/projects/SKILL.md +11 -2
- package/assets/templates/pal-settings.json +1 -0
- package/package.json +5 -1
- package/src/cli/index.ts +39 -12
- package/src/cli/migrate.ts +1 -1
- package/src/cli/personal-context.ts +67 -0
- package/src/cli/server.ts +13 -7
- package/src/cli/setup-identity.ts +13 -1
- package/src/cli/skill.ts +1 -1
- package/src/hooks/CompactRecover.ts +28 -86
- package/src/hooks/LedgerUnapplied.ts +3 -28
- package/src/hooks/LoadContext.ts +33 -60
- package/src/hooks/SecurityValidator.ts +16 -109
- package/src/hooks/handlers/agenda.ts +223 -0
- package/src/hooks/handlers/failure-principle.ts +19 -44
- package/src/hooks/handlers/inject-retrieval.ts +6 -2
- package/src/hooks/handlers/session-intelligence.ts +13 -70
- package/src/hooks/lib/agenda-store.ts +41 -0
- package/src/hooks/lib/capture-store.ts +103 -0
- package/src/hooks/lib/compact-recall.ts +89 -0
- package/src/hooks/lib/failure-principle.ts +98 -0
- package/src/hooks/lib/ledger-hook.ts +35 -0
- package/src/hooks/lib/ledger.ts +48 -1
- package/src/hooks/lib/paths.ts +0 -1
- package/src/hooks/lib/projects.ts +16 -1
- package/src/hooks/lib/security-gate.ts +159 -0
- package/src/hooks/lib/serves.ts +60 -0
- package/src/hooks/lib/session-context.ts +74 -0
- package/src/hooks/lib/stop.ts +14 -0
- package/src/hooks/lib/telos-goals.ts +144 -0
- package/src/hooks/lib/telos-topics.ts +68 -0
- package/src/hooks/lib/token-usage.ts +3 -1
- package/src/hooks/lib/wall-clock.ts +58 -0
- package/src/tools/agent/algorithm-reflect.ts +28 -97
- package/src/tools/agent/analyze.ts +19 -120
- package/src/tools/agent/handoff-note.ts +40 -70
- package/src/tools/agent/project.ts +47 -136
- package/src/tools/agent/relationship-note.ts +27 -46
- package/src/tools/agent/synthesize.ts +1 -1
- package/src/tools/agent/thread.ts +43 -123
- package/src/tools/control-room/data.ts +332 -0
- package/src/tools/control-room/matrix.ts +182 -0
- package/src/tools/control-room/server.ts +150 -0
- package/src/tools/control-room/ui/agenda.tsx +43 -0
- package/src/tools/control-room/ui/agents.tsx +67 -0
- package/src/tools/control-room/ui/app.css +857 -0
- package/src/tools/control-room/ui/app.tsx +74 -0
- package/src/tools/control-room/ui/board.tsx +82 -0
- package/src/tools/control-room/ui/format.ts +31 -0
- package/src/tools/control-room/ui/handoffs.tsx +37 -0
- package/src/tools/control-room/ui/index.html +19 -0
- package/src/tools/control-room/ui/ledger.tsx +137 -0
- package/src/tools/control-room/ui/matrix.tsx +117 -0
- package/src/tools/control-room/ui/panel.tsx +60 -0
- package/src/tools/control-room/ui/signal.tsx +161 -0
- package/src/tools/ledger/view.ts +3 -0
- package/src/tools/lib/algorithm-reflect.ts +84 -0
- package/src/tools/lib/analyze-report.ts +120 -0
- package/src/tools/lib/handoff-note.ts +88 -0
- package/src/tools/lib/note-flags.ts +59 -0
- package/src/tools/lib/project-isc.ts +151 -0
- package/src/tools/lib/relationship-reflect.ts +402 -0
- package/src/tools/lib/self-model.ts +499 -0
- package/src/tools/lib/session-usage.ts +216 -0
- package/src/tools/lib/skill-doctor.ts +457 -0
- package/src/tools/lib/thread.ts +119 -0
- package/src/tools/lib/token-report.ts +173 -0
- package/src/tools/lib/transcript-usage.ts +42 -0
- package/src/tools/lib/usage-buckets.ts +329 -0
- package/src/tools/relationship-reflect.ts +48 -412
- package/src/tools/self-model.ts +76 -558
- package/src/tools/session-summary.ts +8 -215
- package/src/tools/skill-doctor.ts +9 -444
- package/src/tools/token-cost.ts +18 -428
- package/assets/templates/ledger-page.html +0 -213
- package/src/cli/setup-telos.ts +0 -52
- package/src/hooks/lib/setup.ts +0 -60
- package/src/tools/ledger/server.ts +0 -111
package/src/tools/token-cost.ts
CHANGED
|
@@ -9,389 +9,29 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
|
-
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
13
12
|
import { homedir } from "node:os";
|
|
14
13
|
import { resolve } from "node:path";
|
|
15
14
|
import { parseArgs } from "node:util";
|
|
16
|
-
import { costOfUsage } from "../hooks/lib/models";
|
|
17
15
|
import { palHome } from "../hooks/lib/paths";
|
|
18
16
|
import { findBinaryOnPath } from "../hooks/lib/which";
|
|
17
|
+
import { parseRtkSummary, type RtkGain, usageLines } from "./lib/token-report";
|
|
18
|
+
import { readClaudeCode, readPalInference } from "./lib/usage-buckets";
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
interface Bucket {
|
|
23
|
-
input: number;
|
|
24
|
-
output: number;
|
|
25
|
-
cacheWrite5m: number;
|
|
26
|
-
cacheWrite1h: number;
|
|
27
|
-
cacheRead: number;
|
|
28
|
-
cost: number;
|
|
29
|
-
calls: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function emptyBucket(): Bucket {
|
|
33
|
-
return {
|
|
34
|
-
input: 0,
|
|
35
|
-
output: 0,
|
|
36
|
-
cacheWrite5m: 0,
|
|
37
|
-
cacheWrite1h: 0,
|
|
38
|
-
cacheRead: 0,
|
|
39
|
-
cost: 0,
|
|
40
|
-
calls: 0,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface TimeBuckets {
|
|
45
|
-
today: Bucket;
|
|
46
|
-
week: Bucket;
|
|
47
|
-
month: Bucket;
|
|
48
|
-
total: Bucket;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function emptyTimeBuckets(): TimeBuckets {
|
|
52
|
-
return {
|
|
53
|
-
today: emptyBucket(),
|
|
54
|
-
week: emptyBucket(),
|
|
55
|
-
month: emptyBucket(),
|
|
56
|
-
total: emptyBucket(),
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ── Helpers ──
|
|
61
|
-
|
|
62
|
-
function addToBucket(
|
|
63
|
-
bucket: Bucket,
|
|
64
|
-
model: string,
|
|
65
|
-
input: number,
|
|
66
|
-
output: number,
|
|
67
|
-
cacheWrite5m: number,
|
|
68
|
-
cacheWrite1h: number,
|
|
69
|
-
cacheRead: number
|
|
70
|
-
): void {
|
|
71
|
-
bucket.input += input;
|
|
72
|
-
bucket.output += output;
|
|
73
|
-
bucket.cacheWrite5m += cacheWrite5m;
|
|
74
|
-
bucket.cacheWrite1h += cacheWrite1h;
|
|
75
|
-
bucket.cacheRead += cacheRead;
|
|
76
|
-
bucket.cost += costOfUsage(model, {
|
|
77
|
-
input,
|
|
78
|
-
output,
|
|
79
|
-
cacheWrite5m,
|
|
80
|
-
cacheWrite1h,
|
|
81
|
-
cacheRead,
|
|
82
|
-
});
|
|
83
|
-
bucket.calls++;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ── Formatting ──
|
|
87
|
-
|
|
88
|
-
function fmt(n: number): string {
|
|
89
|
-
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
90
|
-
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
|
91
|
-
return n.toLocaleString("en-US");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function fmtCost(n: number): string {
|
|
95
|
-
if (n >= 1) return `$${n.toFixed(2)}`;
|
|
96
|
-
return `$${n.toFixed(4)}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function printRow(label: string, b: Bucket, labelWidth = 14): void {
|
|
100
|
-
const tokens = b.input + b.output + b.cacheWrite5m + b.cacheWrite1h + b.cacheRead;
|
|
101
|
-
console.log(
|
|
102
|
-
` ${label.padEnd(labelWidth)} ${fmt(tokens).padStart(8)} tok ${fmt(b.calls).padStart(5)} calls ${fmtCost(b.cost).padStart(8)}`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function printDetailed(label: string, b: Bucket, labelWidth = 14): void {
|
|
107
|
-
console.log(
|
|
108
|
-
` ${label.padEnd(labelWidth)} ${fmt(b.input).padStart(8)} in ${fmt(b.output).padStart(8)} out ${fmt(b.cacheWrite5m).padStart(7)} cw5m ${fmt(b.cacheWrite1h).padStart(7)} cw1h ${fmt(b.cacheRead).padStart(8)} cr ${fmtCost(b.cost).padStart(8)}`
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ── Claude Code transcripts ──
|
|
113
|
-
|
|
114
|
-
function addToTimeBuckets(
|
|
115
|
-
tb: TimeBuckets,
|
|
116
|
-
ts: string,
|
|
117
|
-
model: string,
|
|
118
|
-
input: number,
|
|
119
|
-
output: number,
|
|
120
|
-
cacheWrite5m: number,
|
|
121
|
-
cacheWrite1h: number,
|
|
122
|
-
cacheRead: number,
|
|
123
|
-
todayPrefix: string,
|
|
124
|
-
weekAgo: string,
|
|
125
|
-
monthAgo: string
|
|
126
|
-
): void {
|
|
127
|
-
addToBucket(tb.total, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
|
|
128
|
-
if (ts >= monthAgo)
|
|
129
|
-
addToBucket(tb.month, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
|
|
130
|
-
if (ts >= weekAgo)
|
|
131
|
-
addToBucket(tb.week, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
|
|
132
|
-
if (ts.startsWith(todayPrefix))
|
|
133
|
-
addToBucket(tb.today, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function readClaudeCode(projectFilter?: string): {
|
|
137
|
-
buckets: TimeBuckets;
|
|
138
|
-
byModel: Record<string, Bucket>;
|
|
139
|
-
byProject: Record<string, TimeBuckets>;
|
|
140
|
-
} {
|
|
141
|
-
const now = new Date();
|
|
142
|
-
const todayPrefix = now.toISOString().slice(0, 10);
|
|
143
|
-
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
144
|
-
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
145
|
-
|
|
146
|
-
const buckets = emptyTimeBuckets();
|
|
147
|
-
const byModel: Record<string, Bucket> = {};
|
|
148
|
-
const byProject: Record<string, TimeBuckets> = {};
|
|
149
|
-
|
|
150
|
-
const claudeDir = resolve(homedir(), ".claude", "projects");
|
|
151
|
-
if (!existsSync(claudeDir)) return { buckets, byModel, byProject };
|
|
152
|
-
|
|
153
|
-
const projectDirs = readdirSync(claudeDir, { withFileTypes: true })
|
|
154
|
-
.filter((d) => d.isDirectory())
|
|
155
|
-
.map((d) => d.name);
|
|
156
|
-
|
|
157
|
-
for (const projDir of projectDirs) {
|
|
158
|
-
const projPath = resolve(claudeDir, projDir);
|
|
159
|
-
const segments = projDir.replace(/^-/, "").split("-");
|
|
160
|
-
const projName = segments.length > 1 ? segments.slice(-1)[0] : projDir;
|
|
161
|
-
|
|
162
|
-
if (typeof projectFilter === "string" && !projName.includes(projectFilter)) continue;
|
|
163
|
-
|
|
164
|
-
const jsonlFiles: string[] = [];
|
|
165
|
-
|
|
166
|
-
for (const entry of readdirSync(projPath, { withFileTypes: true })) {
|
|
167
|
-
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
168
|
-
jsonlFiles.push(resolve(projPath, entry.name));
|
|
169
|
-
} else if (entry.isDirectory()) {
|
|
170
|
-
const subagentsDir = resolve(projPath, entry.name, "subagents");
|
|
171
|
-
try {
|
|
172
|
-
for (const sub of readdirSync(subagentsDir)) {
|
|
173
|
-
if (sub.endsWith(".jsonl")) {
|
|
174
|
-
jsonlFiles.push(resolve(subagentsDir, sub));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
} catch {
|
|
178
|
-
/* no subagents dir */
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
for (const filepath of jsonlFiles) {
|
|
184
|
-
let content: string;
|
|
185
|
-
try {
|
|
186
|
-
content = readFileSync(filepath, "utf-8");
|
|
187
|
-
} catch {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
for (const line of content.split("\n")) {
|
|
192
|
-
if (!line.includes('"usage"')) continue;
|
|
193
|
-
try {
|
|
194
|
-
const d = JSON.parse(line) as {
|
|
195
|
-
type?: string;
|
|
196
|
-
timestamp?: string;
|
|
197
|
-
message?: {
|
|
198
|
-
model?: string;
|
|
199
|
-
usage?: {
|
|
200
|
-
input_tokens?: number;
|
|
201
|
-
output_tokens?: number;
|
|
202
|
-
cache_creation_input_tokens?: number;
|
|
203
|
-
cache_read_input_tokens?: number;
|
|
204
|
-
cache_creation?: {
|
|
205
|
-
ephemeral_5m_input_tokens?: number;
|
|
206
|
-
ephemeral_1h_input_tokens?: number;
|
|
207
|
-
};
|
|
208
|
-
};
|
|
209
|
-
};
|
|
210
|
-
};
|
|
211
|
-
if (d.type !== "assistant") continue;
|
|
212
|
-
const usage = d.message?.usage;
|
|
213
|
-
const model = d.message?.model;
|
|
214
|
-
const ts = d.timestamp;
|
|
215
|
-
if (!usage || !model || !ts) continue;
|
|
216
|
-
|
|
217
|
-
const input = usage.input_tokens ?? 0;
|
|
218
|
-
const output = usage.output_tokens ?? 0;
|
|
219
|
-
const cr = usage.cache_read_input_tokens ?? 0;
|
|
220
|
-
const cw5m = usage.cache_creation?.ephemeral_5m_input_tokens;
|
|
221
|
-
const cw1h = usage.cache_creation?.ephemeral_1h_input_tokens;
|
|
222
|
-
// Older transcripts only have the summed cache_creation_input_tokens — bill as 5m.
|
|
223
|
-
const hasBreakdown = cw5m !== undefined || cw1h !== undefined;
|
|
224
|
-
const cacheWrite5m = hasBreakdown
|
|
225
|
-
? (cw5m ?? 0)
|
|
226
|
-
: (usage.cache_creation_input_tokens ?? 0);
|
|
227
|
-
const cacheWrite1h = cw1h ?? 0;
|
|
228
|
-
|
|
229
|
-
addToTimeBuckets(
|
|
230
|
-
buckets,
|
|
231
|
-
ts,
|
|
232
|
-
model,
|
|
233
|
-
input,
|
|
234
|
-
output,
|
|
235
|
-
cacheWrite5m,
|
|
236
|
-
cacheWrite1h,
|
|
237
|
-
cr,
|
|
238
|
-
todayPrefix,
|
|
239
|
-
weekAgo,
|
|
240
|
-
monthAgo
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
byModel[model] ??= emptyBucket();
|
|
244
|
-
addToBucket(
|
|
245
|
-
byModel[model],
|
|
246
|
-
model,
|
|
247
|
-
input,
|
|
248
|
-
output,
|
|
249
|
-
cacheWrite5m,
|
|
250
|
-
cacheWrite1h,
|
|
251
|
-
cr
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
byProject[projName] ??= emptyTimeBuckets();
|
|
255
|
-
addToTimeBuckets(
|
|
256
|
-
byProject[projName],
|
|
257
|
-
ts,
|
|
258
|
-
model,
|
|
259
|
-
input,
|
|
260
|
-
output,
|
|
261
|
-
cacheWrite5m,
|
|
262
|
-
cacheWrite1h,
|
|
263
|
-
cr,
|
|
264
|
-
todayPrefix,
|
|
265
|
-
weekAgo,
|
|
266
|
-
monthAgo
|
|
267
|
-
);
|
|
268
|
-
} catch {
|
|
269
|
-
/* skip */
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return { buckets, byModel, byProject };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ── PAL inference ──
|
|
279
|
-
|
|
280
|
-
function readPalInference(): {
|
|
281
|
-
buckets: TimeBuckets;
|
|
282
|
-
byModel: Record<string, TimeBuckets>;
|
|
283
|
-
byCaller: Record<string, Bucket>;
|
|
284
|
-
} {
|
|
285
|
-
const now = new Date();
|
|
286
|
-
const todayPrefix = now.toISOString().slice(0, 10);
|
|
287
|
-
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
288
|
-
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
289
|
-
|
|
290
|
-
const buckets = emptyTimeBuckets();
|
|
291
|
-
const byModel: Record<string, TimeBuckets> = {};
|
|
292
|
-
const byCaller: Record<string, Bucket> = {};
|
|
293
|
-
|
|
294
|
-
const filepath = resolve(palHome(), "memory", "signals", "token-usage.jsonl");
|
|
295
|
-
if (!existsSync(filepath)) return { buckets, byModel, byCaller };
|
|
296
|
-
|
|
297
|
-
const content = readFileSync(filepath, "utf-8").trim();
|
|
298
|
-
if (!content) return { buckets, byModel, byCaller };
|
|
299
|
-
|
|
300
|
-
for (const line of content.split("\n")) {
|
|
301
|
-
try {
|
|
302
|
-
const e = JSON.parse(line) as {
|
|
303
|
-
ts: string;
|
|
304
|
-
caller: string;
|
|
305
|
-
model: string;
|
|
306
|
-
inputTokens: number;
|
|
307
|
-
outputTokens: number;
|
|
308
|
-
};
|
|
309
|
-
addToTimeBuckets(
|
|
310
|
-
buckets,
|
|
311
|
-
e.ts,
|
|
312
|
-
e.model,
|
|
313
|
-
e.inputTokens,
|
|
314
|
-
e.outputTokens,
|
|
315
|
-
0,
|
|
316
|
-
0,
|
|
317
|
-
0,
|
|
318
|
-
todayPrefix,
|
|
319
|
-
weekAgo,
|
|
320
|
-
monthAgo
|
|
321
|
-
);
|
|
322
|
-
byModel[e.model] ??= emptyTimeBuckets();
|
|
323
|
-
addToTimeBuckets(
|
|
324
|
-
byModel[e.model],
|
|
325
|
-
e.ts,
|
|
326
|
-
e.model,
|
|
327
|
-
e.inputTokens,
|
|
328
|
-
e.outputTokens,
|
|
329
|
-
0,
|
|
330
|
-
0,
|
|
331
|
-
0,
|
|
332
|
-
todayPrefix,
|
|
333
|
-
weekAgo,
|
|
334
|
-
monthAgo
|
|
335
|
-
);
|
|
336
|
-
byCaller[e.caller] ??= emptyBucket();
|
|
337
|
-
addToBucket(byCaller[e.caller], e.model, e.inputTokens, e.outputTokens, 0, 0, 0);
|
|
338
|
-
} catch {
|
|
339
|
-
/* skip */
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return { buckets, byModel, byCaller };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// ── rtk compression savings ──
|
|
347
|
-
|
|
348
|
-
interface RtkSummary {
|
|
349
|
-
total_commands: number;
|
|
350
|
-
total_saved: number;
|
|
351
|
-
avg_savings_pct: number;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Query rtk's own savings ledger. `installed: false` means rtk isn't on PATH;
|
|
356
|
-
* `summary: null` with `installed: true` means rtk is present but has no data
|
|
357
|
-
* (or errored) — the two cases print differently in the usage report.
|
|
358
|
-
*/
|
|
359
|
-
function readRtkGain(): { installed: boolean; summary: RtkSummary | null } {
|
|
20
|
+
function rtkGain(): RtkGain {
|
|
360
21
|
const rtk = findBinaryOnPath("rtk");
|
|
361
22
|
if (!rtk) return { installed: false, summary: null };
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
} catch {
|
|
371
|
-
return { installed: true, summary: null };
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function printRtkGain(): void {
|
|
376
|
-
const { installed, summary } = readRtkGain();
|
|
377
|
-
console.log("\n rtk Compression\n");
|
|
378
|
-
if (!installed) {
|
|
379
|
-
console.log(" rtk not installed");
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
if (!summary || summary.total_commands === 0) {
|
|
383
|
-
console.log(" rtk installed — no savings recorded yet");
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
console.log(
|
|
387
|
-
` Tokens saved ${fmt(summary.total_saved).padStart(8)} tok ${summary.avg_savings_pct.toFixed(1)}% avg across ${fmt(summary.total_commands)} commands`
|
|
388
|
-
);
|
|
23
|
+
const result = spawnSync(rtk, ["gain", "--format", "json"], {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
installed: true,
|
|
29
|
+
summary: parseRtkSummary(result.status, result.stdout ?? ""),
|
|
30
|
+
};
|
|
389
31
|
}
|
|
390
32
|
|
|
391
|
-
// ── CLI ──
|
|
392
|
-
|
|
393
33
|
export function usage() {
|
|
394
|
-
parseArgs({
|
|
34
|
+
const { values } = parseArgs({
|
|
395
35
|
options: {
|
|
396
36
|
today: { type: "boolean", default: false },
|
|
397
37
|
week: { type: "boolean", default: false },
|
|
@@ -402,62 +42,12 @@ export function usage() {
|
|
|
402
42
|
strict: false,
|
|
403
43
|
});
|
|
404
44
|
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
printRow("30d", cc.buckets.month);
|
|
412
|
-
printRow("Total", cc.buckets.total);
|
|
413
|
-
|
|
414
|
-
if (Object.keys(cc.byModel).length > 0) {
|
|
415
|
-
console.log("\n By Model (all time)\n");
|
|
416
|
-
const sorted = Object.entries(cc.byModel).sort((a, b) => b[1].cost - a[1].cost);
|
|
417
|
-
const modelNames = sorted.map(([m]) => m.replace("claude-", ""));
|
|
418
|
-
const modelWidth = Math.max(14, ...modelNames.map((n) => n.length + 2));
|
|
419
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
420
|
-
printDetailed(modelNames[i], sorted[i][1], modelWidth);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (Object.keys(cc.byProject).length > 1) {
|
|
425
|
-
console.log("\n By Project (all time)\n");
|
|
426
|
-
const sorted = Object.entries(cc.byProject).sort(
|
|
427
|
-
(a, b) => b[1].total.cost - a[1].total.cost
|
|
428
|
-
);
|
|
429
|
-
for (const [proj, tb] of sorted) {
|
|
430
|
-
printRow(proj, tb.total);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
for (const [model, tb] of Object.entries(pal.byModel)) {
|
|
435
|
-
if (tb.total.calls === 0) continue;
|
|
436
|
-
let label: string;
|
|
437
|
-
if (model.includes("haiku")) label = "Haiku";
|
|
438
|
-
else if (model.includes("sonnet")) label = "Sonnet";
|
|
439
|
-
else label = model.replace("claude-", "");
|
|
440
|
-
console.log(`\n PAL Inference (${label})\n`);
|
|
441
|
-
printRow("Today", tb.today);
|
|
442
|
-
printRow("7d", tb.week);
|
|
443
|
-
printRow("30d", tb.month);
|
|
444
|
-
printRow("Total", tb.total);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
printRtkGain();
|
|
448
|
-
|
|
449
|
-
const grand = emptyBucket();
|
|
450
|
-
for (const b of [cc.buckets.total, pal.buckets.total]) {
|
|
451
|
-
grand.input += b.input;
|
|
452
|
-
grand.output += b.output;
|
|
453
|
-
grand.cacheWrite5m += b.cacheWrite5m;
|
|
454
|
-
grand.cacheWrite1h += b.cacheWrite1h;
|
|
455
|
-
grand.cacheRead += b.cacheRead;
|
|
456
|
-
grand.cost += b.cost;
|
|
457
|
-
grand.calls += b.calls;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
console.log(`\n Grand Total: ${fmtCost(grand.cost)}\n`);
|
|
45
|
+
const lines = usageLines(
|
|
46
|
+
readClaudeCode(resolve(homedir(), ".claude", "projects"), values.project as string),
|
|
47
|
+
readPalInference(resolve(palHome(), "memory", "signals", "token-usage.jsonl")),
|
|
48
|
+
rtkGain()
|
|
49
|
+
);
|
|
50
|
+
for (const line of lines) console.log(line);
|
|
461
51
|
}
|
|
462
52
|
|
|
463
53
|
if (import.meta.main) usage();
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>Action Ledger</title>
|
|
7
|
-
<style>
|
|
8
|
-
:root {
|
|
9
|
-
--bg: #eef0f2;
|
|
10
|
-
--panel: #ffffff;
|
|
11
|
-
--ink: #1b2026;
|
|
12
|
-
--muted: #6b7480;
|
|
13
|
-
--line: #d5dae0;
|
|
14
|
-
--accent: #0f6e6e;
|
|
15
|
-
--accent-ink: #ffffff;
|
|
16
|
-
--block: #b4471c;
|
|
17
|
-
--block-bg: #fbf0ea;
|
|
18
|
-
--ok: #2f7a3e;
|
|
19
|
-
--ok-bg: #edf6ee;
|
|
20
|
-
--pending: #8a6d00;
|
|
21
|
-
--pending-bg: #fbf5df;
|
|
22
|
-
--agent: #4a3fa3;
|
|
23
|
-
--agent-bg: #efedf9;
|
|
24
|
-
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
25
|
-
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
26
|
-
}
|
|
27
|
-
* { box-sizing: border-box; }
|
|
28
|
-
html, body { height: 100%; }
|
|
29
|
-
body { margin: 0; background: var(--bg); color: var(--ink); font: 13px/1.45 var(--sans); }
|
|
30
|
-
header {
|
|
31
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
32
|
-
padding: 10px 16px; background: var(--panel); border-bottom: 1px solid var(--line);
|
|
33
|
-
}
|
|
34
|
-
header h1 { font-size: 14px; margin: 0; font-weight: 600; letter-spacing: .01em; }
|
|
35
|
-
header .model { font-family: var(--mono); font-size: 11px; color: var(--muted); }
|
|
36
|
-
header .who { font-size: 12px; color: var(--muted); }
|
|
37
|
-
header .who b { color: var(--ink); font-weight: 600; }
|
|
38
|
-
|
|
39
|
-
main { background: var(--panel); min-height: calc(100% - 45px); }
|
|
40
|
-
h2 {
|
|
41
|
-
font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
|
|
42
|
-
margin: 0; padding: 12px 14px 8px; font-weight: 600;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.filters { display: flex; gap: 10px; padding: 0 14px 12px; flex-wrap: wrap; align-items: end; }
|
|
46
|
-
.filters label { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
|
|
47
|
-
select, input { font: inherit; padding: 4px 6px; border: 1px solid var(--line); border-radius: 3px; }
|
|
48
|
-
|
|
49
|
-
.stats { display: grid; grid-template-columns: 1.4fr repeat(4, 1fr); gap: 1px; background: var(--line); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
50
|
-
.stat { background: var(--panel); padding: 12px 14px; }
|
|
51
|
-
.stat label { display: block; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
|
|
52
|
-
.stat .n { font-size: 26px; font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.2; }
|
|
53
|
-
.stat.headline { background: var(--block-bg); }
|
|
54
|
-
.stat.headline .n { font-size: 40px; color: var(--block); }
|
|
55
|
-
.stat.applied .n { color: var(--ok); }
|
|
56
|
-
.stat.failed .n { color: var(--pending); }
|
|
57
|
-
.stat.denied .n, .stat.blocked .n { color: var(--block); }
|
|
58
|
-
.st { font-size: 11px; color: var(--muted); }
|
|
59
|
-
|
|
60
|
-
table { width: 100%; border-collapse: collapse; }
|
|
61
|
-
td, th { text-align: left; padding: 7px 14px; border-top: 1px solid var(--line); vertical-align: top; font-variant-numeric: tabular-nums; }
|
|
62
|
-
th { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); border-top: 0; font-weight: 600; }
|
|
63
|
-
td.ts { font-family: var(--mono); font-size: 11px; color: var(--muted); white-space: nowrap; }
|
|
64
|
-
td.target { font-family: var(--mono); font-size: 11px; word-break: break-all; }
|
|
65
|
-
td.diff { font-family: var(--mono); font-size: 11px; }
|
|
66
|
-
.actor { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; }
|
|
67
|
-
.actor.human { background: #eef0f2; }
|
|
68
|
-
.actor.agent { background: var(--agent-bg); color: var(--agent); }
|
|
69
|
-
.out { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 11px; font-weight: 600; }
|
|
70
|
-
.out.applied { background: var(--ok-bg); color: var(--ok); }
|
|
71
|
-
.out.failed { background: var(--pending-bg); color: var(--pending); }
|
|
72
|
-
.out.denied { background: var(--block-bg); color: var(--block); }
|
|
73
|
-
.out.blocked { background: var(--block-bg); color: var(--block); }
|
|
74
|
-
footer { font-family: var(--mono); font-size: 10.5px; color: var(--muted); padding: 10px 14px; border-top: 1px solid var(--line); }
|
|
75
|
-
</style>
|
|
76
|
-
</head>
|
|
77
|
-
<body>
|
|
78
|
-
<header>
|
|
79
|
-
<h1>Action Ledger</h1>
|
|
80
|
-
<span class="model" id="modelLine"></span>
|
|
81
|
-
<span class="who" id="who"></span>
|
|
82
|
-
</header>
|
|
83
|
-
|
|
84
|
-
<main>
|
|
85
|
-
<h2>Window</h2>
|
|
86
|
-
<div class="filters">
|
|
87
|
-
<div><label for="project">Project</label><select id="project"><option value="">all projects</option></select></div>
|
|
88
|
-
<div><label for="since">From</label><input type="date" id="since"></div>
|
|
89
|
-
<div><label for="until">To</label><input type="date" id="until"></div>
|
|
90
|
-
</div>
|
|
91
|
-
|
|
92
|
-
<div class="stats" id="stats"></div>
|
|
93
|
-
|
|
94
|
-
<h2>Action log</h2>
|
|
95
|
-
<table>
|
|
96
|
-
<thead><tr><th>When</th><th>Actor</th><th>Action</th><th>Target</th><th>Change</th><th>Outcome</th></tr></thead>
|
|
97
|
-
<tbody id="log"></tbody>
|
|
98
|
-
</table>
|
|
99
|
-
<footer id="footer">Every row is one action an agent tried to make. The ledger saw each from both sides, before and after. Nothing here was written by hand.</footer>
|
|
100
|
-
</main>
|
|
101
|
-
|
|
102
|
-
<script>
|
|
103
|
-
const $ = id => document.getElementById(id);
|
|
104
|
-
const OUTCOMES = ['applied', 'failed', 'denied', 'blocked'];
|
|
105
|
-
let ledgerFiles = 0;
|
|
106
|
-
|
|
107
|
-
function el(tag, cls, text) {
|
|
108
|
-
const node = document.createElement(tag);
|
|
109
|
-
if (cls) node.className = cls;
|
|
110
|
-
if (text !== undefined) node.textContent = text;
|
|
111
|
-
return node;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const pad = n => String(n).padStart(2, '0');
|
|
115
|
-
function when(ts) {
|
|
116
|
-
const d = new Date(ts);
|
|
117
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const authorityWord = key => key === 'user' ? 'human' : key;
|
|
121
|
-
function tally(counts, word = k => k) {
|
|
122
|
-
const parts = Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${word(k)} ${v}`);
|
|
123
|
-
return parts.length ? parts.join(' · ') : '—';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function query() {
|
|
127
|
-
const p = new URLSearchParams();
|
|
128
|
-
if ($('project').value) p.set('project', $('project').value);
|
|
129
|
-
if ($('since').value) p.set('since', $('since').value);
|
|
130
|
-
if ($('until').value) p.set('until', `${$('until').value}T23:59:59.999Z`);
|
|
131
|
-
return p.toString();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function windowText() {
|
|
135
|
-
const from = $('since').value, to = $('until').value;
|
|
136
|
-
if (!from && !to) return 'all time';
|
|
137
|
-
return `${from || 'start'} → ${to || 'now'}`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function renderStats(s) {
|
|
141
|
-
const strip = $('stats');
|
|
142
|
-
strip.replaceChildren();
|
|
143
|
-
const headline = el('div', 'stat headline');
|
|
144
|
-
headline.append(el('label', '', 'Refusals'), el('div', 'n', String(s.refusals)),
|
|
145
|
-
el('div', 'st', `denied ${s.outcomes.denied.total} · blocked ${s.outcomes.blocked.total}`));
|
|
146
|
-
strip.append(headline);
|
|
147
|
-
for (const key of OUTCOMES) {
|
|
148
|
-
const o = s.outcomes[key];
|
|
149
|
-
const card = el('div', `stat ${key}`);
|
|
150
|
-
card.append(el('label', '', key), el('div', 'n', String(o.total)),
|
|
151
|
-
el('div', 'st', tally(o.byAuthority, authorityWord)), el('div', 'st', tally(o.byRuntime)));
|
|
152
|
-
strip.append(card);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function actorCell(r) {
|
|
157
|
-
const td = el('td');
|
|
158
|
-
const agent = r.authority === 'agent';
|
|
159
|
-
td.append(el('span', `actor ${agent ? 'agent' : 'human'}`, agent ? `agent · ${r.runtime}` : r.actor));
|
|
160
|
-
td.append(el('div', 'st', agent ? `on behalf of ${r.actor}` : `via ${r.runtime}`));
|
|
161
|
-
return td;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function changeCell(r) {
|
|
165
|
-
const td = el('td', 'diff');
|
|
166
|
-
td.append(r.reason ? el('span', 'st', r.reason) : document.createTextNode(r.change));
|
|
167
|
-
return td;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function renderRows(rows) {
|
|
171
|
-
const body = $('log');
|
|
172
|
-
body.replaceChildren();
|
|
173
|
-
if (!rows.length) {
|
|
174
|
-
const td = el('td', 'st', 'No actions in this window.');
|
|
175
|
-
td.colSpan = 6;
|
|
176
|
-
const tr = el('tr'); tr.append(td); body.append(tr);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
for (const r of rows) {
|
|
180
|
-
const tr = el('tr');
|
|
181
|
-
const outcome = el('td'); outcome.append(el('span', `out ${r.outcome}`, r.outcome));
|
|
182
|
-
tr.append(el('td', 'ts', when(r.ts)), actorCell(r), el('td', '', r.tool), el('td', 'target', r.target), changeCell(r), outcome);
|
|
183
|
-
body.append(tr);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async function load() {
|
|
188
|
-
const res = await fetch(`/api/ledger?${query()}`);
|
|
189
|
-
const body = await res.json();
|
|
190
|
-
if (!res.ok) { $('modelLine').textContent = body.error; return; }
|
|
191
|
-
renderStats(body.stats);
|
|
192
|
-
renderRows(body.rows);
|
|
193
|
-
$('modelLine').textContent = `${body.stats.total} actions · ${windowText()} · ${ledgerFiles} ledger file(s)`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function loadProjects() {
|
|
197
|
-
const list = await (await fetch('/api/projects')).json();
|
|
198
|
-
for (const { slug } of list) {
|
|
199
|
-
const o = el('option', '', slug); o.value = slug; $('project').append(o);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function loadStatus() {
|
|
204
|
-
const s = await (await fetch('/api/status')).json();
|
|
205
|
-
ledgerFiles = s.ledgerFiles;
|
|
206
|
-
$('who').replaceChildren('machine ', Object.assign(el('b', '', s.machine)));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
for (const id of ['project', 'since', 'until']) $(id).addEventListener('change', load);
|
|
210
|
-
Promise.all([loadProjects(), loadStatus()]).then(load);
|
|
211
|
-
</script>
|
|
212
|
-
</body>
|
|
213
|
-
</html>
|