portable-agent-layer 0.17.0 → 0.18.1

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.
@@ -15,27 +15,9 @@ import { parseArgs } from "node:util";
15
15
  import { MODEL_PRICING } from "../hooks/lib/models";
16
16
  import { palHome } from "../hooks/lib/paths";
17
17
 
18
- // ── Args ──
19
-
20
- const { values: args } = parseArgs({
21
- options: {
22
- today: { type: "boolean", default: false },
23
- week: { type: "boolean", default: false },
24
- month: { type: "boolean", default: false },
25
- all: { type: "boolean", default: false },
26
- project: { type: "string" },
27
- },
28
- strict: false,
29
- });
30
-
31
- const now = new Date();
32
- const todayPrefix = now.toISOString().slice(0, 10);
33
- const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
34
- const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
35
-
36
18
  // ── Types ──
37
19
 
38
- interface Bucket {
20
+ export interface Bucket {
39
21
  input: number;
40
22
  output: number;
41
23
  cacheWrite: number;
@@ -44,14 +26,30 @@ interface Bucket {
44
26
  calls: number;
45
27
  }
46
28
 
47
- function emptyBucket(): Bucket {
29
+ export function emptyBucket(): Bucket {
48
30
  return { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, cost: 0, calls: 0 };
49
31
  }
50
32
 
33
+ export interface TimeBuckets {
34
+ today: Bucket;
35
+ week: Bucket;
36
+ month: Bucket;
37
+ total: Bucket;
38
+ }
39
+
40
+ export function emptyTimeBuckets(): TimeBuckets {
41
+ return {
42
+ today: emptyBucket(),
43
+ week: emptyBucket(),
44
+ month: emptyBucket(),
45
+ total: emptyBucket(),
46
+ };
47
+ }
48
+
49
+ // ── Helpers ──
50
+
51
51
  function findPricing(model: string): (typeof MODEL_PRICING)[string] | null {
52
- // Exact match first
53
52
  if (MODEL_PRICING[model]) return MODEL_PRICING[model];
54
- // Prefix match (e.g. "claude-sonnet-4-5-20250929" matches "claude-sonnet-4-5")
55
53
  for (const key of Object.keys(MODEL_PRICING)) {
56
54
  if (model.startsWith(key)) return MODEL_PRICING[key];
57
55
  }
@@ -76,7 +74,7 @@ function costForUsage(
76
74
  );
77
75
  }
78
76
 
79
- function addToBucket(
77
+ export function addToBucket(
80
78
  bucket: Bucket,
81
79
  model: string,
82
80
  input: number,
@@ -120,22 +118,6 @@ function printDetailed(label: string, b: Bucket, labelWidth = 14): void {
120
118
 
121
119
  // ── Claude Code transcripts ──
122
120
 
123
- interface TimeBuckets {
124
- today: Bucket;
125
- week: Bucket;
126
- month: Bucket;
127
- total: Bucket;
128
- }
129
-
130
- function emptyTimeBuckets(): TimeBuckets {
131
- return {
132
- today: emptyBucket(),
133
- week: emptyBucket(),
134
- month: emptyBucket(),
135
- total: emptyBucket(),
136
- };
137
- }
138
-
139
121
  function addToTimeBuckets(
140
122
  tb: TimeBuckets,
141
123
  ts: string,
@@ -143,7 +125,10 @@ function addToTimeBuckets(
143
125
  input: number,
144
126
  output: number,
145
127
  cacheWrite: number,
146
- cacheRead: number
128
+ cacheRead: number,
129
+ todayPrefix: string,
130
+ weekAgo: string,
131
+ monthAgo: string
147
132
  ): void {
148
133
  addToBucket(tb.total, model, input, output, cacheWrite, cacheRead);
149
134
  if (ts >= monthAgo) addToBucket(tb.month, model, input, output, cacheWrite, cacheRead);
@@ -152,11 +137,16 @@ function addToTimeBuckets(
152
137
  addToBucket(tb.today, model, input, output, cacheWrite, cacheRead);
153
138
  }
154
139
 
155
- function readClaudeCode(): {
140
+ export function readClaudeCode(projectFilter?: string): {
156
141
  buckets: TimeBuckets;
157
142
  byModel: Record<string, Bucket>;
158
143
  byProject: Record<string, TimeBuckets>;
159
144
  } {
145
+ const now = new Date();
146
+ const todayPrefix = now.toISOString().slice(0, 10);
147
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
148
+ const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
149
+
160
150
  const buckets = emptyTimeBuckets();
161
151
  const byModel: Record<string, Bucket> = {};
162
152
  const byProject: Record<string, TimeBuckets> = {};
@@ -170,20 +160,17 @@ function readClaudeCode(): {
170
160
 
171
161
  for (const projDir of projectDirs) {
172
162
  const projPath = resolve(claudeDir, projDir);
173
- // Project dir is like "-Users-rico-Development-git-myproject" — extract last meaningful segment
174
163
  const segments = projDir.replace(/^-/, "").split("-");
175
164
  const projName = segments.length > 1 ? segments.slice(-1)[0] : projDir;
176
165
 
177
- if (typeof args.project === "string" && !projName.includes(args.project)) continue;
166
+ if (typeof projectFilter === "string" && !projName.includes(projectFilter)) continue;
178
167
 
179
- // Collect all JSONL files: top-level + subagent directories
180
168
  const jsonlFiles: string[] = [];
181
169
 
182
170
  for (const entry of readdirSync(projPath, { withFileTypes: true })) {
183
171
  if (entry.isFile() && entry.name.endsWith(".jsonl")) {
184
172
  jsonlFiles.push(resolve(projPath, entry.name));
185
173
  } else if (entry.isDirectory()) {
186
- // Check for subagent transcripts inside session directories
187
174
  const subagentsDir = resolve(projPath, entry.name, "subagents");
188
175
  try {
189
176
  for (const sub of readdirSync(subagentsDir)) {
@@ -232,13 +219,35 @@ function readClaudeCode(): {
232
219
  const cw = usage.cache_creation_input_tokens ?? 0;
233
220
  const cr = usage.cache_read_input_tokens ?? 0;
234
221
 
235
- addToTimeBuckets(buckets, ts, model, input, output, cw, cr);
222
+ addToTimeBuckets(
223
+ buckets,
224
+ ts,
225
+ model,
226
+ input,
227
+ output,
228
+ cw,
229
+ cr,
230
+ todayPrefix,
231
+ weekAgo,
232
+ monthAgo
233
+ );
236
234
 
237
235
  if (!byModel[model]) byModel[model] = emptyBucket();
238
236
  addToBucket(byModel[model], model, input, output, cw, cr);
239
237
 
240
238
  if (!byProject[projName]) byProject[projName] = emptyTimeBuckets();
241
- addToTimeBuckets(byProject[projName], ts, model, input, output, cw, cr);
239
+ addToTimeBuckets(
240
+ byProject[projName],
241
+ ts,
242
+ model,
243
+ input,
244
+ output,
245
+ cw,
246
+ cr,
247
+ todayPrefix,
248
+ weekAgo,
249
+ monthAgo
250
+ );
242
251
  } catch {
243
252
  /* skip */
244
253
  }
@@ -251,7 +260,15 @@ function readClaudeCode(): {
251
260
 
252
261
  // ── PAL Haiku inference ──
253
262
 
254
- function readPalInference(): { buckets: TimeBuckets; byCaller: Record<string, Bucket> } {
263
+ export function readPalInference(): {
264
+ buckets: TimeBuckets;
265
+ byCaller: Record<string, Bucket>;
266
+ } {
267
+ const now = new Date();
268
+ const todayPrefix = now.toISOString().slice(0, 10);
269
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
270
+ const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
271
+
255
272
  const buckets = emptyTimeBuckets();
256
273
  const byCaller: Record<string, Bucket> = {};
257
274
 
@@ -270,7 +287,18 @@ function readPalInference(): { buckets: TimeBuckets; byCaller: Record<string, Bu
270
287
  inputTokens: number;
271
288
  outputTokens: number;
272
289
  };
273
- addToTimeBuckets(buckets, e.ts, e.model, e.inputTokens, e.outputTokens, 0, 0);
290
+ addToTimeBuckets(
291
+ buckets,
292
+ e.ts,
293
+ e.model,
294
+ e.inputTokens,
295
+ e.outputTokens,
296
+ 0,
297
+ 0,
298
+ todayPrefix,
299
+ weekAgo,
300
+ monthAgo
301
+ );
274
302
  if (!byCaller[e.caller]) byCaller[e.caller] = emptyBucket();
275
303
  addToBucket(byCaller[e.caller], e.model, e.inputTokens, e.outputTokens, 0, 0);
276
304
  } catch {
@@ -281,54 +309,68 @@ function readPalInference(): { buckets: TimeBuckets; byCaller: Record<string, Bu
281
309
  return { buckets, byCaller };
282
310
  }
283
311
 
284
- // ── Main ──
285
-
286
- const cc = readClaudeCode();
287
- const pal = readPalInference();
288
-
289
- console.log("\n Claude Code Usage\n");
290
- printRow("Today", cc.buckets.today);
291
- printRow("7d", cc.buckets.week);
292
- printRow("30d", cc.buckets.month);
293
- printRow("Total", cc.buckets.total);
312
+ // ── CLI ──
313
+
314
+ function run() {
315
+ parseArgs({
316
+ options: {
317
+ today: { type: "boolean", default: false },
318
+ week: { type: "boolean", default: false },
319
+ month: { type: "boolean", default: false },
320
+ all: { type: "boolean", default: false },
321
+ project: { type: "string" },
322
+ },
323
+ strict: false,
324
+ });
325
+
326
+ const cc = readClaudeCode();
327
+ const pal = readPalInference();
328
+
329
+ console.log("\n Claude Code Usage\n");
330
+ printRow("Today", cc.buckets.today);
331
+ printRow("7d", cc.buckets.week);
332
+ printRow("30d", cc.buckets.month);
333
+ printRow("Total", cc.buckets.total);
334
+
335
+ if (Object.keys(cc.byModel).length > 0) {
336
+ console.log("\n By Model (all time)\n");
337
+ const sorted = Object.entries(cc.byModel).sort((a, b) => b[1].cost - a[1].cost);
338
+ const modelNames = sorted.map(([m]) => m.replace("claude-", ""));
339
+ const modelWidth = Math.max(14, ...modelNames.map((n) => n.length + 2));
340
+ for (let i = 0; i < sorted.length; i++) {
341
+ printDetailed(modelNames[i], sorted[i][1], modelWidth);
342
+ }
343
+ }
294
344
 
295
- if (Object.keys(cc.byModel).length > 0) {
296
- console.log("\n By Model (all time)\n");
297
- const sorted = Object.entries(cc.byModel).sort((a, b) => b[1].cost - a[1].cost);
298
- const modelNames = sorted.map(([m]) => m.replace("claude-", ""));
299
- const modelWidth = Math.max(14, ...modelNames.map((n) => n.length + 2));
300
- for (let i = 0; i < sorted.length; i++) {
301
- printDetailed(modelNames[i], sorted[i][1], modelWidth);
345
+ if (Object.keys(cc.byProject).length > 1) {
346
+ console.log("\n By Project (all time)\n");
347
+ const sorted = Object.entries(cc.byProject).sort(
348
+ (a, b) => b[1].total.cost - a[1].total.cost
349
+ );
350
+ for (const [proj, tb] of sorted) {
351
+ printRow(proj, tb.total);
352
+ }
302
353
  }
303
- }
304
354
 
305
- if (Object.keys(cc.byProject).length > 1) {
306
- console.log("\n By Project (all time)\n");
307
- const sorted = Object.entries(cc.byProject).sort(
308
- (a, b) => b[1].total.cost - a[1].total.cost
309
- );
310
- for (const [proj, tb] of sorted) {
311
- printRow(proj, tb.total);
355
+ if (pal.buckets.total.calls > 0) {
356
+ console.log("\n PAL Inference (Haiku)\n");
357
+ printRow("Today", pal.buckets.today);
358
+ printRow("7d", pal.buckets.week);
359
+ printRow("30d", pal.buckets.month);
360
+ printRow("Total", pal.buckets.total);
312
361
  }
313
- }
314
362
 
315
- if (pal.buckets.total.calls > 0) {
316
- console.log("\n PAL Inference (Haiku)\n");
317
- printRow("Today", pal.buckets.today);
318
- printRow("7d", pal.buckets.week);
319
- printRow("30d", pal.buckets.month);
320
- printRow("Total", pal.buckets.total);
321
- }
363
+ const grand = emptyBucket();
364
+ for (const b of [cc.buckets.total, pal.buckets.total]) {
365
+ grand.input += b.input;
366
+ grand.output += b.output;
367
+ grand.cacheWrite += b.cacheWrite;
368
+ grand.cacheRead += b.cacheRead;
369
+ grand.cost += b.cost;
370
+ grand.calls += b.calls;
371
+ }
322
372
 
323
- // Grand total
324
- const grand = emptyBucket();
325
- for (const b of [cc.buckets.total, pal.buckets.total]) {
326
- grand.input += b.input;
327
- grand.output += b.output;
328
- grand.cacheWrite += b.cacheWrite;
329
- grand.cacheRead += b.cacheRead;
330
- grand.cost += b.cost;
331
- grand.calls += b.calls;
373
+ console.log(`\n Grand Total: ${fmtCost(grand.cost)}\n`);
332
374
  }
333
375
 
334
- console.log(`\n Grand Total: ${fmtCost(grand.cost)}\n`);
376
+ if (import.meta.main) run();
@@ -1,152 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Unified Learning Analysis — graduation patterns + ratings summary.
4
- *
5
- * Reads failures and session learnings, finds recurring patterns,
6
- * summarizes ratings, and generates recommendations.
7
- *
8
- * Usage: bun run tool:analyze
9
- */
10
-
11
- import { parseArgs } from "node:util";
12
- import { analyze } from "../hooks/lib/graduation";
13
-
14
- // ── ANSI Colors ──
15
-
16
- const c = {
17
- bold: (s: string) => `\x1b[1m${s}\x1b[0m`,
18
- dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
19
- cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
20
- yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
21
- green: (s: string) => `\x1b[32m${s}\x1b[0m`,
22
- red: (s: string) => `\x1b[31m${s}\x1b[0m`,
23
- magenta: (s: string) => `\x1b[35m${s}\x1b[0m`,
24
- };
25
-
26
- const { values } = parseArgs({
27
- args: Bun.argv.slice(2),
28
- options: {
29
- help: { type: "boolean", short: "h" },
30
- actionable: { type: "boolean", short: "a" },
31
- },
32
- });
33
-
34
- if (values.help) {
35
- console.log(`
36
- PAL Learning Analysis — unified graduation + ratings report
37
-
38
- Reads all captured failures (rating ≤3) and session learnings,
39
- groups recurring patterns via Dice similarity on context text,
40
- and summarizes rating trends.
41
-
42
- Sections:
43
- Ratings Overall average, low/high counts
44
- Graduation Patterns with 3+ occurrences → ready to crystallize
45
- Emerging Patterns with 2 occurrences → one more to graduate
46
-
47
- Flags:
48
- --actionable, -a Generate actionable recommendations via Haiku inference
49
-
50
- To crystallize a graduated pattern, add it to the target wisdom frame:
51
- - Your principle here [CRYSTAL: 85%]
52
-
53
- Usage: bun run tool:analyze [--actionable] [--help]
54
- `);
55
- process.exit(0);
56
- }
57
-
58
- const result = await analyze({ actionable: values.actionable });
59
-
60
- const hasPatterns = result.candidates.length > 0 || result.emerging.length > 0;
61
- const hasRatings = result.ratings !== null;
62
-
63
- if (!hasPatterns && !hasRatings) {
64
- console.log("\n No patterns or ratings data found.\n");
65
- process.exit(0);
66
- }
67
-
68
- // ── Ratings Summary ──
69
-
70
- if (result.ratings) {
71
- const r = result.ratings;
72
- const avgColor = r.average >= 7 ? c.green : r.average <= 4 ? c.red : c.yellow;
73
- console.log(
74
- `\n ${c.bold("Ratings:")} ${avgColor(`${r.average.toFixed(1)}/10`)} avg (${r.total} total)`
75
- );
76
- console.log(
77
- ` ${c.red(`Low (≤4): ${r.low.count}`)} | ${c.green(`High (≥7): ${r.high.count}`)}`
78
- );
79
- }
80
-
81
- // ── Graduation Candidates ──
82
-
83
- if (result.candidates.length > 0) {
84
- console.log(
85
- `\n ${c.bold(c.green(`Graduation Report — ${result.candidates.length} pattern(s) detected`))}\n`
86
- );
87
- console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
88
-
89
- for (const candidate of result.candidates) {
90
- console.log(
91
- ` ${c.cyan(`[${candidate.domain}]`)} ${c.bold(`${candidate.entries.length}x`)} occurrences`
92
- );
93
- console.log("");
94
-
95
- for (const entry of candidate.entries) {
96
- const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
97
- const tag =
98
- sourceType === "failure" ? c.red(`[${sourceType}]`) : c.yellow(`[${sourceType}]`);
99
- console.log(
100
- ` ${c.dim(entry.date || "unknown")} ${tag} ${entry.text.slice(0, 100)}`
101
- );
102
- }
103
-
104
- console.log(`\n ${c.dim("Files:")}`);
105
- for (const entry of candidate.entries) {
106
- console.log(` ${c.dim(entry.path)}`);
107
- }
108
-
109
- console.log("");
110
- console.log(
111
- ` Target frame: ${c.magenta(`memory/wisdom/frames/${candidate.domain}.md`)}`
112
- );
113
- console.log(` ${c.dim("─────────────────────────────────────────────────")}\n`);
114
- }
115
- }
116
-
117
- // ── Emerging Patterns ──
118
-
119
- if (result.emerging.length > 0) {
120
- console.log(` ${c.bold(c.yellow("Emerging (2x — one more to graduate)"))}\n`);
121
- for (const group of result.emerging) {
122
- console.log(` ${c.cyan(`[${group.domain}]`)} ${c.bold(`${group.entries.length}x`)}`);
123
- for (const entry of group.entries) {
124
- const sourceType = entry.source.startsWith("failure:") ? "failure" : "learning";
125
- const tag =
126
- sourceType === "failure" ? c.red(`[${sourceType}]`) : c.yellow(`[${sourceType}]`);
127
- console.log(
128
- ` ${c.dim(entry.date || "unknown")} ${tag} ${entry.text.slice(0, 80)}`
129
- );
130
- }
131
- console.log(" Files:");
132
- for (const entry of group.entries) {
133
- console.log(` ${c.dim(entry.path)}`);
134
- }
135
- console.log("");
136
- }
137
- }
138
-
139
- // ── Recommendations ──
140
-
141
- if (result.recommendations.length > 0) {
142
- console.log(` ${c.bold("Recommendations:")}\n`);
143
- for (const rec of result.recommendations) {
144
- console.log(` ${rec}`);
145
- }
146
- console.log("");
147
- }
148
-
149
- if (result.candidates.length > 0) {
150
- console.log(` To crystallize: add a line to the wisdom frame file.`);
151
- console.log(` Format: ${c.green("- Your principle here [CRYSTAL: 85%]")}\n`);
152
- }