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.
@@ -13,53 +13,21 @@ import { createInterface } from "node:readline";
13
13
  import AdmZip from "adm-zip";
14
14
  import { palHome } from "../hooks/lib/paths";
15
15
 
16
- const repoRoot = palHome();
17
- const args = process.argv.slice(2);
18
- const dryRun = args.includes("--dry-run");
19
- const pathArg = args.find((a) => a !== "--dry-run");
16
+ export function findLatestExport(root: string): string | null {
17
+ const candidates: string[] = [];
20
18
 
21
- async function confirm(message: string): Promise<boolean> {
22
- const rl = createInterface({ input: process.stdin, output: process.stdout });
23
- return new Promise((res) => {
24
- rl.question(`${message} [y/N] `, (answer) => {
25
- rl.close();
26
- res(answer.trim().toLowerCase() === "y");
27
- });
28
- });
29
- }
30
-
31
- function findLatestExport(): string | null {
32
- const files = readdirSync(repoRoot)
33
- .filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
34
- .sort()
35
- .reverse();
36
-
37
- // Also check backups/
38
19
  try {
39
- const backupDir = resolve(repoRoot, "backups");
40
- const backups = readdirSync(backupDir)
41
- .filter(
42
- (f) =>
43
- (f.startsWith("pal-export-") || f.startsWith("pal-backup-")) &&
44
- f.endsWith(".zip")
45
- )
46
- .map((f) => ({ name: f, path: resolve(backupDir, f) }))
47
- .sort((a, b) => b.name.localeCompare(a.name));
48
- if (backups.length > 0) files.push(backups[0].name);
20
+ candidates.push(
21
+ ...readdirSync(root)
22
+ .filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
23
+ .map((f) => resolve(root, f))
24
+ );
49
25
  } catch {
50
- // No backups dir
26
+ /* empty */
51
27
  }
52
28
 
53
- if (files.length === 0) return null;
54
-
55
- // Find the most recent by mtime across both locations
56
- const candidates = [
57
- ...readdirSync(repoRoot)
58
- .filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
59
- .map((f) => resolve(repoRoot, f)),
60
- ];
61
29
  try {
62
- const backupDir = resolve(repoRoot, "backups");
30
+ const backupDir = resolve(root, "backups");
63
31
  candidates.push(
64
32
  ...readdirSync(backupDir)
65
33
  .filter(
@@ -70,54 +38,74 @@ function findLatestExport(): string | null {
70
38
  .map((f) => resolve(backupDir, f))
71
39
  );
72
40
  } catch {
73
- // No backups dir
41
+ /* empty */
74
42
  }
75
43
 
76
44
  if (candidates.length === 0) return null;
77
45
  return candidates.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
78
46
  }
79
47
 
80
- // Resolve zip path
81
- let zipPath: string;
48
+ export function importZip(zipPath: string, targetDir: string, dryRun: boolean): number {
49
+ const zip = new AdmZip(zipPath);
50
+ const entries = zip.getEntries();
82
51
 
83
- if (pathArg) {
84
- zipPath = resolve(pathArg);
85
- } else {
86
- const latest = findLatestExport();
87
- if (!latest) {
88
- console.error(
89
- "No export or backup files found. Provide a path: bun run tool:import <path-to-zip>"
90
- );
91
- process.exit(1);
52
+ if (entries.length === 0) {
53
+ console.log("Archive is empty — nothing to import.");
54
+ return 0;
92
55
  }
93
- console.log(`Found: ${latest}`);
94
- const zip = new AdmZip(latest);
95
- const entries = zip.getEntries();
96
- console.log(
97
- `Contains ${entries.length} files, created ${statSync(latest).mtime.toISOString().slice(0, 16).replace("T", " ")}`
98
- );
99
56
 
100
- if (!(await confirm("Import this file?"))) {
101
- console.log("Cancelled.");
102
- process.exit(0);
57
+ if (dryRun) {
58
+ console.log(`Would import ${entries.length} files → ${targetDir}\n`);
59
+ for (const e of entries) console.log(` ${e.entryName}`);
60
+ return entries.length;
103
61
  }
104
- zipPath = latest;
62
+
63
+ zip.extractAllTo(targetDir, true);
64
+ console.log(`Imported ${entries.length} files → ${targetDir}`);
65
+ console.log("\nRun 'bun run install:all' to re-create symlinks and hooks.");
66
+ return entries.length;
105
67
  }
106
68
 
107
- // Import
108
- const zip = new AdmZip(zipPath);
109
- const entries = zip.getEntries();
69
+ async function run() {
70
+ const repoRoot = palHome();
71
+ const args = process.argv.slice(2);
72
+ const dryRun = args.includes("--dry-run");
73
+ const pathArg = args.find((a) => a !== "--dry-run");
110
74
 
111
- if (entries.length === 0) {
112
- console.log("Archive is empty — nothing to import.");
113
- process.exit(0);
114
- }
75
+ let zipPath: string;
115
76
 
116
- if (dryRun) {
117
- console.log(`Would import ${entries.length} files → ${repoRoot}\n`);
118
- for (const e of entries) console.log(` ${e.entryName}`);
119
- } else {
120
- zip.extractAllTo(repoRoot, true);
121
- console.log(`Imported ${entries.length} files → ${repoRoot}`);
122
- console.log("\nRun 'bun run install:all' to re-create symlinks and hooks.");
77
+ if (pathArg) {
78
+ zipPath = resolve(pathArg);
79
+ } else {
80
+ const latest = findLatestExport(repoRoot);
81
+ if (!latest) {
82
+ console.error(
83
+ "No export or backup files found. Provide a path: bun run tool:import <path-to-zip>"
84
+ );
85
+ process.exit(1);
86
+ }
87
+ console.log(`Found: ${latest}`);
88
+ const zip = new AdmZip(latest);
89
+ const entries = zip.getEntries();
90
+ console.log(
91
+ `Contains ${entries.length} files, created ${statSync(latest).mtime.toISOString().slice(0, 16).replace("T", " ")}`
92
+ );
93
+
94
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
95
+ const answer = await new Promise<string>((res) => {
96
+ rl.question("Import this file? [y/N] ", (a) => {
97
+ rl.close();
98
+ res(a);
99
+ });
100
+ });
101
+ if (answer.trim().toLowerCase() !== "y") {
102
+ console.log("Cancelled.");
103
+ process.exit(0);
104
+ }
105
+ zipPath = latest;
106
+ }
107
+
108
+ importZip(zipPath, repoRoot, dryRun);
123
109
  }
110
+
111
+ if (import.meta.main) run();
@@ -27,12 +27,6 @@ import {
27
27
  import { palHome } from "../hooks/lib/paths";
28
28
  import { similarity } from "../hooks/lib/text-similarity";
29
29
 
30
- // ── Paths ──
31
-
32
- const RATINGS_FILE = resolve(palHome(), "memory", "signals", "ratings.jsonl");
33
- const RELATIONSHIP_DIR = resolve(palHome(), "memory", "relationship");
34
- const REFLECTION_DIR = resolve(palHome(), "memory", "relationship", "reflections");
35
-
36
30
  // ── Types ──
37
31
 
38
32
  interface Rating {
@@ -59,16 +53,17 @@ interface OpinionChange {
59
53
 
60
54
  // ── Note Parsing ──
61
55
 
62
- function loadNotes(daysBack: number): ParsedNote[] {
63
- if (!existsSync(RELATIONSHIP_DIR)) return [];
56
+ export function loadNotes(daysBack: number): ParsedNote[] {
57
+ const relationshipDir = resolve(palHome(), "memory", "relationship");
58
+ if (!existsSync(relationshipDir)) return [];
64
59
 
65
60
  const cutoff = new Date();
66
61
  cutoff.setDate(cutoff.getDate() - daysBack);
67
62
  const notes: ParsedNote[] = [];
68
63
 
69
- for (const monthDir of readdirSync(RELATIONSHIP_DIR).sort().reverse()) {
64
+ for (const monthDir of readdirSync(relationshipDir).sort().reverse()) {
70
65
  if (!/^\d{4}-\d{2}$/.test(monthDir)) continue;
71
- const monthPath = resolve(RELATIONSHIP_DIR, monthDir);
66
+ const monthPath = resolve(relationshipDir, monthDir);
72
67
 
73
68
  let files: string[];
74
69
  try {
@@ -130,13 +125,14 @@ function loadNotes(daysBack: number): ParsedNote[] {
130
125
 
131
126
  // ── Ratings ──
132
127
 
133
- function loadRatings(daysBack: number): Rating[] {
134
- if (!existsSync(RATINGS_FILE)) return [];
128
+ export function loadRatings(daysBack: number): Rating[] {
129
+ const ratingsFile = resolve(palHome(), "memory", "signals", "ratings.jsonl");
130
+ if (!existsSync(ratingsFile)) return [];
135
131
 
136
132
  const cutoff = new Date();
137
133
  cutoff.setDate(cutoff.getDate() - daysBack);
138
134
 
139
- return readFileSync(RATINGS_FILE, "utf-8")
135
+ return readFileSync(ratingsFile, "utf-8")
140
136
  .split("\n")
141
137
  .filter((l) => l.trim())
142
138
  .map((l) => {
@@ -153,11 +149,10 @@ function loadRatings(daysBack: number): Rating[] {
153
149
 
154
150
  // ── Opinion Promotion ──
155
151
 
156
- function promoteToOpinions(notes: ParsedNote[], dryRun: boolean): OpinionChange[] {
152
+ export function promoteToOpinions(notes: ParsedNote[], dryRun: boolean): OpinionChange[] {
157
153
  const changes: OpinionChange[] = [];
158
154
  const opinions = readOpinions();
159
155
 
160
- // All O notes in the window — deduplication happens at the evidence level
161
156
  const opinionNotes = notes.filter((n) => n.type === "O");
162
157
 
163
158
  // Group similar notes together
@@ -177,11 +172,9 @@ function promoteToOpinions(notes: ParsedNote[], dryRun: boolean): OpinionChange[
177
172
  }
178
173
 
179
174
  for (const [representative, group] of groups) {
180
- // Check against existing opinions
181
175
  const existing = findSimilarOpinion(representative, opinions);
182
176
 
183
177
  if (existing) {
184
- // Add supporting evidence for each new note
185
178
  let updated = existing;
186
179
  for (const note of group) {
187
180
  updated = addEvidence(updated, "supporting", note.text.slice(0, 120));
@@ -197,8 +190,6 @@ function promoteToOpinions(notes: ParsedNote[], dryRun: boolean): OpinionChange[
197
190
  if (!dryRun) saveOpinion(updated);
198
191
  }
199
192
  } else if (group.length >= 2) {
200
- // New opinion — requires at least 2 occurrences
201
- // Store individual note texts as evidence so re-runs deduplicate correctly
202
193
  let opinion = createOpinion(representative, group[0].text.slice(0, 120));
203
194
  for (const note of group.slice(1)) {
204
195
  opinion = addEvidence(opinion, "supporting", note.text.slice(0, 120));
@@ -224,7 +215,7 @@ interface OpinionSummary {
224
215
  dates: string[];
225
216
  }
226
217
 
227
- function groupNoteOccurrences(notes: ParsedNote[]): OpinionSummary[] {
218
+ export function groupNoteOccurrences(notes: ParsedNote[]): OpinionSummary[] {
228
219
  const opNotes = notes.filter((n) => n.type === "O");
229
220
  const groups = new Map<
230
221
  string,
@@ -297,7 +288,7 @@ function correlateRatings(ratings: Rating[]): string[] {
297
288
 
298
289
  // ── Report ──
299
290
 
300
- function formatReport(
291
+ export function formatReport(
301
292
  period: string,
302
293
  notes: ParsedNote[],
303
294
  ratings: Rating[],
@@ -360,8 +351,8 @@ function formatReport(
360
351
 
361
352
  if (ratingInsights.length > 0) {
362
353
  lines.push("## Rating Insights", "");
363
- for (const c of ratingInsights) {
364
- lines.push(`- ${c}`);
354
+ for (const insight of ratingInsights) {
355
+ lines.push(`- ${insight}`);
365
356
  }
366
357
  lines.push("");
367
358
  }
@@ -369,13 +360,14 @@ function formatReport(
369
360
  return lines.join("\n");
370
361
  }
371
362
 
372
- function writeReport(report: string, period: string): string {
373
- if (!existsSync(REFLECTION_DIR)) mkdirSync(REFLECTION_DIR, { recursive: true });
363
+ export function writeReport(report: string, period: string): string {
364
+ const reflectionDir = resolve(palHome(), "memory", "relationship", "reflections");
365
+ if (!existsSync(reflectionDir)) mkdirSync(reflectionDir, { recursive: true });
374
366
 
375
367
  const date = new Date().toISOString().slice(0, 10);
376
368
  const slug = period.toLowerCase().replace(/\s+/g, "-");
377
369
  const filename = `${date}_${slug}-reflection.md`;
378
- const filepath = resolve(REFLECTION_DIR, filename);
370
+ const filepath = resolve(reflectionDir, filename);
379
371
 
380
372
  writeFileSync(filepath, report, "utf-8");
381
373
  return filepath;
@@ -383,17 +375,18 @@ function writeReport(report: string, period: string): string {
383
375
 
384
376
  // ── CLI ──
385
377
 
386
- const { values } = parseArgs({
387
- args: Bun.argv.slice(2),
388
- options: {
389
- month: { type: "boolean" },
390
- "dry-run": { type: "boolean" },
391
- help: { type: "boolean", short: "h" },
392
- },
393
- });
394
-
395
- if (values.help) {
396
- console.log(`
378
+ function run() {
379
+ const { values } = parseArgs({
380
+ args: Bun.argv.slice(2),
381
+ options: {
382
+ month: { type: "boolean" },
383
+ "dry-run": { type: "boolean" },
384
+ help: { type: "boolean", short: "h" },
385
+ },
386
+ });
387
+
388
+ if (values.help) {
389
+ console.log(`
397
390
  RelationshipReflect — Periodic reflection + opinion promotion
398
391
 
399
392
  Reads recent relationship notes and ratings. Promotes recurring
@@ -408,65 +401,67 @@ Output:
408
401
  - Updates memory/relationship/opinions.json (confidence tracking)
409
402
  - Creates reflection report in memory/relationship/reflections/
410
403
  `);
411
- process.exit(0);
412
- }
404
+ process.exit(0);
405
+ }
413
406
 
414
- const daysBack = values.month ? 30 : 7;
415
- const period = values.month ? "Monthly" : "Weekly";
416
- const dryRun = values["dry-run"] ?? false;
407
+ const daysBack = values.month ? 30 : 7;
408
+ const period = values.month ? "Monthly" : "Weekly";
409
+ const dryRun = values["dry-run"] ?? false;
417
410
 
418
- const notes = loadNotes(daysBack);
419
- const ratings = loadRatings(daysBack);
411
+ const notes = loadNotes(daysBack);
412
+ const ratings = loadRatings(daysBack);
420
413
 
421
- console.log(`Loaded ${notes.length} notes from last ${daysBack} days`);
422
- console.log(`Loaded ${ratings.length} ratings`);
414
+ console.log(`Loaded ${notes.length} notes from last ${daysBack} days`);
415
+ console.log(`Loaded ${ratings.length} ratings`);
423
416
 
424
- if (notes.length === 0 && ratings.length === 0) {
425
- console.log("No data to analyze");
426
- process.exit(0);
427
- }
417
+ if (notes.length === 0 && ratings.length === 0) {
418
+ console.log("No data to analyze");
419
+ process.exit(0);
420
+ }
428
421
 
429
- // Promote notes to opinions
430
- const opinionChanges = promoteToOpinions(notes, dryRun);
422
+ const opinionChanges = promoteToOpinions(notes, dryRun);
431
423
 
432
- const avgRating =
433
- ratings.length > 0 ? ratings.reduce((s, r) => s + r.rating, 0) / ratings.length : 0;
434
- console.log(`\nAverage Rating: ${avgRating.toFixed(1)}/10`);
424
+ const avgRating =
425
+ ratings.length > 0 ? ratings.reduce((s, r) => s + r.rating, 0) / ratings.length : 0;
426
+ console.log(`\nAverage Rating: ${avgRating.toFixed(1)}/10`);
435
427
 
436
- const summaries = groupNoteOccurrences(notes);
437
- console.log(`Observations: ${summaries.length} unique`);
428
+ const summaries = groupNoteOccurrences(notes);
429
+ console.log(`Observations: ${summaries.length} unique`);
438
430
 
439
- if (opinionChanges.length > 0) {
440
- console.log(`\nOpinion changes:`);
441
- for (const change of opinionChanges) {
442
- if (change.action === "created") {
443
- console.log(
444
- ` + NEW (${Math.round(change.newConfidence * 100)}%) ${change.statement.slice(0, 80)}`
445
- );
446
- } else {
447
- console.log(
448
- ` ~ ${Math.round(change.oldConfidence ?? 0 * 100)}% → ${Math.round(change.newConfidence * 100)}% ${change.statement.slice(0, 80)}`
449
- );
431
+ if (opinionChanges.length > 0) {
432
+ console.log("\nOpinion changes:");
433
+ for (const change of opinionChanges) {
434
+ if (change.action === "created") {
435
+ console.log(
436
+ ` + NEW (${Math.round(change.newConfidence * 100)}%) ${change.statement.slice(0, 80)}`
437
+ );
438
+ } else {
439
+ console.log(
440
+ ` ~ ${Math.round(change.oldConfidence ?? 0 * 100)}% → ${Math.round(change.newConfidence * 100)}% ${change.statement.slice(0, 80)}`
441
+ );
442
+ }
450
443
  }
444
+ } else {
445
+ console.log("\nNo opinion changes");
451
446
  }
452
- } else {
453
- console.log("\nNo opinion changes");
454
- }
455
-
456
- if (dryRun) {
457
- console.log("\n[DRY RUN] Would write reflection report + update opinions");
458
- } else {
459
- const report = formatReport(period, notes, ratings, opinionChanges);
460
- const filepath = writeReport(report, period);
461
- setLastReflectDate(new Date().toISOString().slice(0, 10));
462
- console.log(`\nCreated reflection report: ${filepath}`);
463
447
 
464
- const opinions = readOpinions();
465
- const high = opinions.filter((o) => o.confidence >= 0.85);
466
- if (high.length > 0) {
467
- console.log(`\nHigh-confidence opinions (injected into context):`);
468
- for (const o of high) {
469
- console.log(` [${Math.round(o.confidence * 100)}%] ${o.statement.slice(0, 80)}`);
448
+ if (dryRun) {
449
+ console.log("\n[DRY RUN] Would write reflection report + update opinions");
450
+ } else {
451
+ const report = formatReport(period, notes, ratings, opinionChanges);
452
+ const filepath = writeReport(report, period);
453
+ setLastReflectDate(new Date().toISOString().slice(0, 10));
454
+ console.log(`\nCreated reflection report: ${filepath}`);
455
+
456
+ const opinions = readOpinions();
457
+ const high = opinions.filter((o) => o.confidence >= 0.85);
458
+ if (high.length > 0) {
459
+ console.log("\nHigh-confidence opinions (injected into context):");
460
+ for (const o of high) {
461
+ console.log(` [${Math.round(o.confidence * 100)}%] ${o.statement.slice(0, 80)}`);
462
+ }
470
463
  }
471
464
  }
472
465
  }
466
+
467
+ if (import.meta.main) run();
@@ -6,25 +6,30 @@
6
6
  */
7
7
 
8
8
  import { existsSync, readdirSync, readFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
9
10
  import { resolve } from "node:path";
10
11
  import { parseArgs } from "node:util";
11
12
  import { MODEL_PRICING } from "../hooks/lib/models";
12
13
 
13
- const { values: args } = parseArgs({
14
- options: { session: { type: "string" } },
15
- strict: false,
16
- });
17
-
18
- const sessionId = args.session;
19
- if (!sessionId) process.exit(0);
20
-
21
- import { homedir } from "node:os";
14
+ // ── Types ──
22
15
 
23
- const claudeDir = resolve(homedir(), ".claude", "projects");
16
+ interface Usage {
17
+ input: number;
18
+ output: number;
19
+ cacheWrite: number;
20
+ cacheRead: number;
21
+ cost: number;
22
+ calls: number;
23
+ models: Set<string>;
24
+ durationMs: number;
25
+ }
24
26
 
25
- // ── Find the JSONL file containing this session ──
27
+ // ── Core Functions ──
26
28
 
27
- function findSessionFile(): { filepath: string; project: string } | null {
29
+ export function findSessionFile(
30
+ sessionId: string,
31
+ claudeDir: string
32
+ ): { filepath: string; project: string } | null {
28
33
  if (!existsSync(claudeDir)) return null;
29
34
 
30
35
  const projectDirs = readdirSync(claudeDir, { withFileTypes: true }).filter((d) =>
@@ -35,7 +40,6 @@ function findSessionFile(): { filepath: string; project: string } | null {
35
40
  const projPath = resolve(claudeDir, dir.name);
36
41
  const projName = dir.name.split("-").pop() ?? dir.name;
37
42
 
38
- // Session ID is often the filename
39
43
  const directFile = resolve(projPath, `${sessionId}.jsonl`);
40
44
  if (existsSync(directFile)) {
41
45
  return { filepath: directFile, project: projName };
@@ -74,20 +78,7 @@ function findSessionFile(): { filepath: string; project: string } | null {
74
78
  return latest;
75
79
  }
76
80
 
77
- // ── Parse only messages belonging to this session ──
78
-
79
- interface Usage {
80
- input: number;
81
- output: number;
82
- cacheWrite: number;
83
- cacheRead: number;
84
- cost: number;
85
- calls: number;
86
- models: Set<string>;
87
- durationMs: number;
88
- }
89
-
90
- function parseSession(filepath: string): Usage {
81
+ export function parseSession(filepath: string, sessionId: string): Usage {
91
82
  const usage: Usage = {
92
83
  input: 0,
93
84
  output: 0,
@@ -122,7 +113,6 @@ function parseSession(filepath: string): Usage {
122
113
  };
123
114
  };
124
115
 
125
- // Only count messages from this session
126
116
  if (d.sessionId !== sessionId) continue;
127
117
 
128
118
  if (d.timestamp) {
@@ -187,21 +177,34 @@ function fmtDuration(ms: number): string {
187
177
  return rem > 0 ? `${hrs}h ${rem}m` : `${hrs}h`;
188
178
  }
189
179
 
190
- // ── Main ──
180
+ // ── CLI ──
181
+
182
+ function run() {
183
+ const { values: args } = parseArgs({
184
+ options: { session: { type: "string" } },
185
+ strict: false,
186
+ });
187
+
188
+ const sessionId = typeof args.session === "string" ? args.session : "";
189
+ if (!sessionId) process.exit(0);
191
190
 
192
- const file = findSessionFile();
193
- if (!file) process.exit(0);
191
+ const claudeDir = resolve(homedir(), ".claude", "projects");
192
+ const file = findSessionFile(sessionId, claudeDir);
193
+ if (!file) process.exit(0);
194
194
 
195
- const usage = parseSession(file.filepath);
196
- if (usage.calls === 0) process.exit(0);
195
+ const usage = parseSession(file.filepath, sessionId);
196
+ if (usage.calls === 0) process.exit(0);
197
197
 
198
- const totalTokens = usage.input + usage.output + usage.cacheWrite + usage.cacheRead;
199
- const model = [...usage.models].map((m) => m.replace("claude-", "")).join(", ");
198
+ const totalTokens = usage.input + usage.output + usage.cacheWrite + usage.cacheRead;
199
+ const model = [...usage.models].map((m) => m.replace("claude-", "")).join(", ");
200
200
 
201
- const dim = "\x1b[2m";
202
- const reset = "\x1b[0m";
203
- const cyan = "\x1b[36m";
201
+ const dim = "\x1b[2m";
202
+ const reset = "\x1b[0m";
203
+ const cyan = "\x1b[36m";
204
+
205
+ console.log(
206
+ `\n${dim}Session: ${file.project} · ${model} · ${fmtDuration(usage.durationMs)} · ${fmtTokens(totalTokens)} tokens · ${usage.calls} calls · ${cyan}${fmtCost(usage.cost)}${reset}`
207
+ );
208
+ }
204
209
 
205
- console.log(
206
- `\n${dim}Session: ${file.project} · ${model} · ${fmtDuration(usage.durationMs)} · ${fmtTokens(totalTokens)} tokens · ${usage.calls} calls · ${cyan}${fmtCost(usage.cost)}${reset}`
207
- );
210
+ if (import.meta.main) run();