portable-agent-layer 0.25.0 → 0.26.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.
@@ -38,64 +38,69 @@ function isoDate(): string {
38
38
  return new Date().toISOString().replace("T", " ").slice(0, 19);
39
39
  }
40
40
 
41
- const args = process.argv.slice(2);
42
- const id = args[0];
43
- const row = args[1];
44
- const description = args[2];
45
-
46
- if (!id || !row || !description) {
47
- console.error('Usage: bun update-projects.ts <id> "<row>" "<description>"');
48
- console.error(
49
- '\nExample: bun update-projects.ts my-proj "| my-proj | My Project | In progress | High | Notes |" "Added My Project"'
50
- );
51
- process.exit(1);
41
+ export interface UpsertProjectResult {
42
+ file: string;
43
+ id: string;
44
+ mode: "replaced" | "appended";
45
+ backed_up: boolean;
46
+ logged: boolean;
47
+ description: string;
52
48
  }
53
49
 
54
- // Backup
55
- mkdirSync(BACKUPS_DIR, { recursive: true });
56
- if (existsSync(PROJECTS_FILE)) {
57
- const backupName = `PROJECTS-${timestamp()}.md`;
58
- copyFileSync(PROJECTS_FILE, resolve(BACKUPS_DIR, backupName));
59
- }
50
+ export function upsertProject(
51
+ id: string,
52
+ row: string,
53
+ description: string
54
+ ): UpsertProjectResult {
55
+ mkdirSync(BACKUPS_DIR, { recursive: true });
56
+ if (existsSync(PROJECTS_FILE)) {
57
+ const backupName = `PROJECTS-${timestamp()}.md`;
58
+ copyFileSync(PROJECTS_FILE, resolve(BACKUPS_DIR, backupName));
59
+ }
60
+
61
+ const existing = existsSync(PROJECTS_FILE) ? readFileSync(PROJECTS_FILE, "utf-8") : "";
62
+
63
+ const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
64
+ const idPattern = new RegExp(`^\\|\\s*${escapedId}\\s*\\|.*$`, "m");
65
+ let mode: "replaced" | "appended";
60
66
 
61
- const existing = existsSync(PROJECTS_FILE) ? readFileSync(PROJECTS_FILE, "utf-8") : "";
67
+ if (idPattern.test(existing)) {
68
+ writeFileSync(PROJECTS_FILE, existing.replace(idPattern, row.trim()), "utf-8");
69
+ mode = "replaced";
70
+ } else {
71
+ const separator = existing.trim() ? "\n" : "";
72
+ writeFileSync(
73
+ PROJECTS_FILE,
74
+ `${existing.trimEnd()}${separator}${row.trim()}\n`,
75
+ "utf-8"
76
+ );
77
+ mode = "appended";
78
+ }
62
79
 
63
- const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
64
- const idPattern = new RegExp(`^\\|\\s*${escapedId}\\s*\\|.*$`, "m");
65
- let mode: "replaced" | "appended";
80
+ const logEntry = `- **${isoDate()}** — \`PROJECTS.md\` [${id}]: ${description}`;
81
+ const existingLog = existsSync(UPDATES_LOG)
82
+ ? readFileSync(UPDATES_LOG, "utf-8")
83
+ : "# TELOS Updates\n";
84
+ writeFileSync(UPDATES_LOG, `${existingLog.trimEnd()}\n${logEntry}\n`, "utf-8");
66
85
 
67
- if (idPattern.test(existing)) {
68
- const updated = existing.replace(idPattern, row.trim());
69
- writeFileSync(PROJECTS_FILE, updated, "utf-8");
70
- mode = "replaced";
71
- } else {
72
- const separator = existing.trim() ? "\n" : "";
73
- writeFileSync(
74
- PROJECTS_FILE,
75
- `${existing.trimEnd()}${separator}${row.trim()}\n`,
76
- "utf-8"
77
- );
78
- mode = "appended";
86
+ return { file: "PROJECTS.md", id, mode, backed_up: true, logged: true, description };
79
87
  }
80
88
 
81
- // Log change
82
- const logEntry = `- **${isoDate()}** — \`PROJECTS.md\` [${id}]: ${description}`;
83
- const existingLog = existsSync(UPDATES_LOG)
84
- ? readFileSync(UPDATES_LOG, "utf-8")
85
- : "# TELOS Updates\n";
86
- writeFileSync(UPDATES_LOG, `${existingLog.trimEnd()}\n${logEntry}\n`, "utf-8");
89
+ function run() {
90
+ const args = process.argv.slice(2);
91
+ const id = args[0];
92
+ const row = args[1];
93
+ const description = args[2];
94
+
95
+ if (!id || !row || !description) {
96
+ console.error('Usage: bun update-projects.ts <id> "<row>" "<description>"');
97
+ console.error(
98
+ '\nExample: bun update-projects.ts my-proj "| my-proj | My Project | In progress | High | Notes |" "Added My Project"'
99
+ );
100
+ process.exit(1);
101
+ }
102
+
103
+ console.log(JSON.stringify(upsertProject(id, row, description), null, 2));
104
+ }
87
105
 
88
- console.log(
89
- JSON.stringify(
90
- {
91
- file: "PROJECTS.md",
92
- id,
93
- mode,
94
- backed_up: true,
95
- logged: true,
96
- description,
97
- },
98
- null,
99
- 2
100
- )
101
- );
106
+ if (import.meta.main) run();
@@ -58,7 +58,6 @@ Start your response with the following header in this mode:
58
58
 
59
59
  ---
60
60
 
61
- {{SETUP_PROMPT}}
62
61
  ## Context Routing
63
62
 
64
63
  When you need context about any of these topics, read `~/.pal/docs/CONTEXT_ROUTING.md` for the file path:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -22,6 +22,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node
22
22
  import { homedir } from "node:os";
23
23
  import { resolve } from "node:path";
24
24
  import { palHome, palPkg, platform } from "../hooks/lib/paths";
25
+ import { hasRealContent, SETUP_STEPS, STEP_ORDER } from "../hooks/lib/setup";
25
26
  import { log } from "../targets/lib";
26
27
 
27
28
  const allArgs = process.argv.slice(2);
@@ -460,19 +461,16 @@ function doctor(silent = false): DoctorResult {
460
461
  : fail("CLAUDE.md — missing (run 'pal cli install --claude')");
461
462
  }
462
463
 
463
- // Setup state
464
- const setupPath = resolve(home, "memory", "state", "setup.json");
465
- if (existsSync(setupPath)) {
466
- try {
467
- const setup = JSON.parse(readFileSync(setupPath, "utf-8"));
468
- setup?.completed
469
- ? ok("TELOS setup complete")
470
- : warn("TELOS setup incomplete — run 'pal cli install' or start a session");
471
- } catch {
472
- warn("TELOS setup — could not read setup.json");
473
- }
474
- } else {
475
- warn("TELOS setup — setup.json missing (run 'pal cli install')");
464
+ // Setup state — check file content directly (no setup.json dependency)
465
+ {
466
+ const missing = STEP_ORDER.filter(
467
+ (key) => !hasRealContent(resolve(home, SETUP_STEPS[key].file))
468
+ );
469
+ missing.length === 0
470
+ ? ok("TELOS setup complete")
471
+ : warn(
472
+ `TELOS setup incomplete — ${missing.join(", ")} missing (run 'pal cli install')`
473
+ );
476
474
  }
477
475
 
478
476
  // Skills (per installed agent)
@@ -574,7 +572,6 @@ function doctor(silent = false): DoctorResult {
574
572
  // ── Commands ──
575
573
 
576
574
  async function init(args: string[]) {
577
- const { ensureSetupState, isSetupComplete } = await import("../hooks/lib/setup");
578
575
  const { scaffoldTelos } = await import("../targets/lib");
579
576
 
580
577
  banner();
@@ -591,17 +588,10 @@ async function init(args: string[]) {
591
588
  mkdirSync(resolve(home, "memory"), { recursive: true });
592
589
 
593
590
  scaffoldTelos();
594
- ensureSetupState();
595
591
 
596
592
  // Auto-detect available targets
597
593
  const targets = resolveTargets(args, health);
598
594
  await install(targets);
599
-
600
- console.log("");
601
- const state = ensureSetupState();
602
- if (!isSetupComplete(state)) {
603
- log.info("Start a session — PAL will guide you through first-run setup");
604
- }
605
595
  }
606
596
 
607
597
  async function install(targets: Targets) {
@@ -620,9 +610,11 @@ async function install(targets: Targets) {
620
610
  // Scaffold TELOS + PAL settings, then prompt for missing identity
621
611
  const { scaffoldTelos, scaffoldPalSettings } = await import("../targets/lib");
622
612
  const { promptIdentity } = await import("./setup-identity");
613
+ const { promptTelos } = await import("./setup-telos");
623
614
  scaffoldTelos();
624
615
  scaffoldPalSettings();
625
616
  await promptIdentity();
617
+ await promptTelos();
626
618
 
627
619
  if (targets.claude) {
628
620
  console.log("━━━ Claude Code ━━━");
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Interactive TELOS setup — prompts for personal context during `pal install`.
3
+ * Skips any step whose TELOS file already has real content.
4
+ * Projects use the upsertProject tool directly with a structured add-another loop.
5
+ */
6
+
7
+ import { writeFileSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import * as clack from "@clack/prompts";
10
+ import { upsertProject } from "../../assets/skills/telos/tools/update-projects";
11
+ import { palHome } from "../hooks/lib/paths";
12
+ import { hasRealContent, SETUP_STEPS, STEP_ORDER } from "../hooks/lib/setup";
13
+
14
+ function toKebabCase(name: string): string {
15
+ return name
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, "-")
18
+ .replace(/^-|-$/g, "");
19
+ }
20
+
21
+ async function promptProjectsLoop(): Promise<void> {
22
+ const addFirst = await clack.confirm({
23
+ message: "Do you want to add any projects now?",
24
+ initialValue: true,
25
+ });
26
+ if (clack.isCancel(addFirst) || !addFirst) return;
27
+
28
+ let addMore = true;
29
+ while (addMore) {
30
+ const name = await clack.text({
31
+ message: "Project name?",
32
+ placeholder: "e.g. PAL, My SaaS, Work Dashboard",
33
+ });
34
+ if (clack.isCancel(name)) return;
35
+
36
+ const status = await clack.select({
37
+ message: "Status?",
38
+ options: [
39
+ { value: "Active", label: "Active" },
40
+ { value: "Planning", label: "Planning" },
41
+ { value: "Paused", label: "Paused" },
42
+ { value: "Complete", label: "Complete" },
43
+ ],
44
+ });
45
+ if (clack.isCancel(status)) return;
46
+
47
+ const priority = await clack.select({
48
+ message: "Priority?",
49
+ options: [
50
+ { value: "High", label: "High" },
51
+ { value: "Medium", label: "Medium" },
52
+ { value: "Low", label: "Low" },
53
+ ],
54
+ });
55
+ if (clack.isCancel(priority)) return;
56
+
57
+ const notes = await clack.text({
58
+ message: "Notes? (optional — leave blank to skip)",
59
+ placeholder: "e.g. Building the v2 API, blocked on design review",
60
+ });
61
+ if (clack.isCancel(notes)) return;
62
+
63
+ const id = toKebabCase(name as string);
64
+ const row = `| ${id} | ${name} | ${status} | ${priority} | ${notes || ""} |`;
65
+ upsertProject(id, row, `Added ${name} during PAL setup`);
66
+ clack.log.success(`Added: ${name}`);
67
+
68
+ const again = await clack.confirm({
69
+ message: "Add another project?",
70
+ initialValue: false,
71
+ });
72
+ if (clack.isCancel(again) || !again) addMore = false;
73
+ }
74
+ }
75
+
76
+ /** Prompt for missing TELOS context. Skips any step whose file already has real content. */
77
+ export async function promptTelos(): Promise<void> {
78
+ // Skip interactive prompts in non-TTY environments (tests, CI)
79
+ if (!process.stdin.isTTY) return;
80
+
81
+ const home = palHome();
82
+ const pending = STEP_ORDER.filter(
83
+ (key) => !hasRealContent(resolve(home, SETUP_STEPS[key].file))
84
+ );
85
+
86
+ if (pending.length === 0) {
87
+ clack.log.info("TELOS already configured");
88
+ return;
89
+ }
90
+
91
+ clack.intro("Personal Context Setup");
92
+ clack.note(
93
+ "Answer in a sentence or two — you can edit the files in ~/.pal/telos/ for more detail later.",
94
+ "Quick setup"
95
+ );
96
+
97
+ for (const key of pending) {
98
+ if (key === "projects") {
99
+ await promptProjectsLoop();
100
+ } else {
101
+ const step = SETUP_STEPS[key];
102
+ const title = key.charAt(0).toUpperCase() + key.slice(1);
103
+
104
+ const answer = await clack.text({
105
+ message: step.question,
106
+ placeholder: step.hint,
107
+ });
108
+
109
+ if (clack.isCancel(answer)) {
110
+ clack.cancel("Setup cancelled");
111
+ return;
112
+ }
113
+
114
+ const filePath = resolve(home, step.file);
115
+ writeFileSync(filePath, `# ${title}\n\n${answer}\n`, "utf-8");
116
+ }
117
+ }
118
+
119
+ clack.outro("Personal context saved ✓");
120
+ }
@@ -19,7 +19,6 @@ import {
19
19
  } from "node:fs";
20
20
  import { dirname, relative, resolve } from "node:path";
21
21
  import { assets, ensureDir, paths, platform } from "./paths";
22
- import { buildSetupPrompt, readSetupState } from "./setup";
23
22
 
24
23
  const TEMPLATE_PATH = assets.agentsMdTemplate();
25
24
 
@@ -114,14 +113,11 @@ import { identity } from "./settings";
114
113
  export function buildClaudeMd(): string {
115
114
  const template = existsSync(TEMPLATE_PATH)
116
115
  ? readFileSync(TEMPLATE_PATH, "utf-8")
117
- : "# PAL Context\n\n{{SETUP_PROMPT}}\n";
116
+ : "# PAL Context\n";
118
117
 
119
- const state = readSetupState();
120
- const setupPrompt = state ? buildSetupPrompt(state) : null;
121
118
  const id = identity();
122
119
 
123
120
  return template
124
- .replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "")
125
121
  .replaceAll("{{IDENTITY_NAME}}", id.ai.name)
126
122
  .replaceAll("{{IDENTITY_DISPLAY}}", id.ai.displayName)
127
123
  .replaceAll("{{IDENTITY_CATCHPHRASE}}", id.ai.catchphrase)
@@ -13,7 +13,7 @@ import { paths } from "./paths";
13
13
  import { loadRecentNotes } from "./relationship";
14
14
  import { readSessionNames } from "./session-names";
15
15
  import * as settings from "./settings";
16
- import { buildSetupPrompt, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
16
+ import { isSetupComplete, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
17
17
  import { computeSignalTrends, formatTrends } from "./signal-trends";
18
18
  import { readFramePrinciples } from "./wisdom";
19
19
  import { readProjectHistory, readSessions, recentSessions } from "./work-tracking";
@@ -141,12 +141,12 @@ export function buildGreeting(): string[] {
141
141
  const counts = loadCachedCounts();
142
142
  const work = loadActiveWork();
143
143
  const setupState = readSetupState();
144
- const setupPrompt = setupState ? buildSetupPrompt(setupState) : null;
144
+ const setupIncomplete = setupState && !isSetupComplete(setupState);
145
145
 
146
146
  const greeting: string[] = [];
147
147
 
148
- if (setupPrompt) {
149
- const done = STEP_ORDER.length - (setupState ? remainingSteps(setupState).length : 0);
148
+ if (setupIncomplete) {
149
+ const done = STEP_ORDER.length - remainingSteps(setupState).length;
150
150
  greeting.push(
151
151
  `🔧 PAL setup ${done}/${STEP_ORDER.length} | ${counts.signals} signals`
152
152
  );
@@ -23,31 +23,33 @@ export interface SetupState {
23
23
  }
24
24
 
25
25
  /** Ordered setup steps — defines the wizard flow */
26
- const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
26
+ export const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
27
27
  mission: {
28
28
  file: "telos/MISSION.md",
29
- question: "What do you do? What's your role and core purpose?",
30
- hint: "Write their role and core purpose to telos/MISSION.md",
29
+ question:
30
+ "What do you do? What's your role and core purpose? (~/.pal/telos/MISSION.md)",
31
+ hint: "e.g. Senior software engineer building developer tooling at Acme Corp",
31
32
  },
32
33
  goals: {
33
34
  file: "telos/GOALS.md",
34
- question: "What are your current goals? (short-term, medium-term, long-term)",
35
- hint: "Write goals organized by timeframe to telos/GOALS.md",
35
+ question:
36
+ "What are your current goals? (short-term, medium-term, long-term) (~/.pal/telos/GOALS.md)",
37
+ hint: "e.g. Ship v2 by Q3, learn Rust, get promoted to staff engineer",
36
38
  },
37
39
  projects: {
38
40
  file: "telos/PROJECTS.md",
39
- question: "What projects are you currently working on?",
40
- hint: "Write to telos/PROJECTS.md using table format: | Project | Status | Priority | Notes |",
41
+ question: "What projects are you currently working on? (~/.pal/telos/PROJECTS.md)",
42
+ hint: "e.g. PAL (active, high priority), personal blog (paused), side SaaS (early stage)",
41
43
  },
42
44
  beliefs: {
43
45
  file: "telos/BELIEFS.md",
44
- question: "What principles or values guide your work?",
45
- hint: "Write their values and principles to telos/BELIEFS.md",
46
+ question: "What principles or values guide your work? (~/.pal/telos/BELIEFS.md)",
47
+ hint: "e.g. Simple code > clever code, ship early and iterate, always write tests",
46
48
  },
47
49
  challenges: {
48
50
  file: "telos/CHALLENGES.md",
49
- question: "What are your biggest current challenges?",
50
- hint: "Write their challenges and obstacles to telos/CHALLENGES.md",
51
+ question: "What are your biggest current challenges? (~/.pal/telos/CHALLENGES.md)",
52
+ hint: "e.g. Context switching between projects, unclear requirements, work-life balance",
51
53
  },
52
54
  };
53
55
 
@@ -58,21 +60,17 @@ function setupPath(): string {
58
60
  }
59
61
 
60
62
  /** Check if a TELOS file has real content (not just template scaffolding) */
61
- function hasRealContent(filePath: string): boolean {
63
+ export function hasRealContent(filePath: string): boolean {
62
64
  if (!existsSync(filePath)) return false;
63
65
  try {
64
66
  const content = readFileSync(filePath, "utf-8").trim();
65
- return content
66
- .split("\n")
67
- .some(
68
- (l) =>
69
- !l.startsWith("#") &&
70
- !l.startsWith("<!--") &&
71
- !l.startsWith("-->") &&
72
- l.trim() &&
73
- !/^\s*-\s*$/.test(l) &&
74
- !/^\s*\|/.test(l)
75
- );
67
+ return content.split("\n").some((l) => {
68
+ if (!l.trim()) return false;
69
+ if (l.startsWith("#")) return false;
70
+ if (l.startsWith("<!--") || l.startsWith("-->")) return false;
71
+ if (/^\s*-\s*$/.test(l)) return false;
72
+ return true; // includes table rows (| ... |) — counts as real content
73
+ });
76
74
  } catch {
77
75
  return false;
78
76
  }
@@ -123,55 +121,3 @@ export function remainingSteps(state: SetupState): string[] {
123
121
  export function isSetupComplete(state: SetupState): boolean {
124
122
  return state.completed;
125
123
  }
126
-
127
- /**
128
- * Build the system-prompt instructions for the current setup state.
129
- * Returns null if setup is already complete.
130
- */
131
- export function buildSetupPrompt(state: SetupState): string | null {
132
- if (state.completed) return null;
133
-
134
- const remaining = remainingSteps(state);
135
- if (remaining.length === 0) return null;
136
-
137
- const completedSteps = STEP_ORDER.filter((k) => state.steps[k]?.done);
138
- const totalSteps = STEP_ORDER.length;
139
-
140
- const lines: string[] = [
141
- "## IMPORTANT: PAL First-Run Setup Required",
142
- "",
143
- "TELOS files are empty — the user's identity is already configured (via the installer),",
144
- "but personal context is still needed. You MUST start the setup process immediately.",
145
- "Greet them, explain that PAL needs to learn about them to personalize future sessions,",
146
- "and ask the first remaining question below. Do NOT wait for the user to ask about setup.",
147
- "",
148
- ];
149
-
150
- if (completedSteps.length > 0) {
151
- lines.push(
152
- `Setup in progress — ${completedSteps.length}/${totalSteps} steps complete. Continue from the next remaining step.`,
153
- ""
154
- );
155
- }
156
-
157
- lines.push("### Steps to complete (ask one at a time):", "");
158
-
159
- for (const key of remaining) {
160
- const step = state.steps[key];
161
- lines.push(`- **${key}** — Ask: "${step.question}" → ${step.hint}`);
162
- }
163
-
164
- lines.push(
165
- "",
166
- "### After each answer:",
167
- "1. Write the user's answer to the corresponding TELOS file.",
168
- `2. Read \`memory/state/setup.json\`, set \`steps.<key>.done = true\`, and write it back.`,
169
- "3. Ask the next remaining question.",
170
- "",
171
- `When all steps are done (or the user wants to skip), set \`completed: true\` in setup.json.`,
172
- "",
173
- "Keep it conversational and natural. If the user wants to skip a step, mark it done and move on."
174
- );
175
-
176
- return lines.join("\n");
177
- }
@@ -14,7 +14,8 @@ export type TokenCaller =
14
14
  | "work-learning"
15
15
  | "session-name"
16
16
  | "session-intelligence"
17
- | "relationship";
17
+ | "relationship"
18
+ | "self-model";
18
19
 
19
20
  interface TokenUsageEntry {
20
21
  ts: string;
@@ -21,6 +21,7 @@ import { parseArgs } from "node:util";
21
21
  import { inference } from "../hooks/lib/inference";
22
22
  import { SONNET_MODEL } from "../hooks/lib/models";
23
23
  import { ensureDir, paths } from "../hooks/lib/paths";
24
+ import { logTokenUsage } from "../hooks/lib/token-usage";
24
25
 
25
26
  // ── Config ──
26
27
 
@@ -537,6 +538,8 @@ export async function composeSelfModel(days: number): Promise<string> {
537
538
  timeout: 30000,
538
539
  });
539
540
 
541
+ if (result.usage) logTokenUsage("self-model", result.usage, SONNET_MODEL);
542
+
540
543
  if (result.success && result.output) {
541
544
  // Append meta line if inference didn't include it
542
545
  const output = result.output;
@@ -258,10 +258,11 @@ export function readClaudeCode(projectFilter?: string): {
258
258
  return { buckets, byModel, byProject };
259
259
  }
260
260
 
261
- // ── PAL Haiku inference ──
261
+ // ── PAL inference ──
262
262
 
263
263
  export function readPalInference(): {
264
264
  buckets: TimeBuckets;
265
+ byModel: Record<string, TimeBuckets>;
265
266
  byCaller: Record<string, Bucket>;
266
267
  } {
267
268
  const now = new Date();
@@ -270,13 +271,14 @@ export function readPalInference(): {
270
271
  const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
271
272
 
272
273
  const buckets = emptyTimeBuckets();
274
+ const byModel: Record<string, TimeBuckets> = {};
273
275
  const byCaller: Record<string, Bucket> = {};
274
276
 
275
277
  const filepath = resolve(palHome(), "memory", "signals", "token-usage.jsonl");
276
- if (!existsSync(filepath)) return { buckets, byCaller };
278
+ if (!existsSync(filepath)) return { buckets, byModel, byCaller };
277
279
 
278
280
  const content = readFileSync(filepath, "utf-8").trim();
279
- if (!content) return { buckets, byCaller };
281
+ if (!content) return { buckets, byModel, byCaller };
280
282
 
281
283
  for (const line of content.split("\n")) {
282
284
  try {
@@ -299,6 +301,19 @@ export function readPalInference(): {
299
301
  weekAgo,
300
302
  monthAgo
301
303
  );
304
+ if (!byModel[e.model]) byModel[e.model] = emptyTimeBuckets();
305
+ addToTimeBuckets(
306
+ byModel[e.model],
307
+ e.ts,
308
+ e.model,
309
+ e.inputTokens,
310
+ e.outputTokens,
311
+ 0,
312
+ 0,
313
+ todayPrefix,
314
+ weekAgo,
315
+ monthAgo
316
+ );
302
317
  if (!byCaller[e.caller]) byCaller[e.caller] = emptyBucket();
303
318
  addToBucket(byCaller[e.caller], e.model, e.inputTokens, e.outputTokens, 0, 0);
304
319
  } catch {
@@ -306,7 +321,7 @@ export function readPalInference(): {
306
321
  }
307
322
  }
308
323
 
309
- return { buckets, byCaller };
324
+ return { buckets, byModel, byCaller };
310
325
  }
311
326
 
312
327
  // ── CLI ──
@@ -352,12 +367,18 @@ function run() {
352
367
  }
353
368
  }
354
369
 
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);
370
+ for (const [model, tb] of Object.entries(pal.byModel)) {
371
+ if (tb.total.calls === 0) continue;
372
+ const label = model.includes("haiku")
373
+ ? "Haiku"
374
+ : model.includes("sonnet")
375
+ ? "Sonnet"
376
+ : model.replace("claude-", "");
377
+ console.log(`\n PAL Inference (${label})\n`);
378
+ printRow("Today", tb.today);
379
+ printRow("7d", tb.week);
380
+ printRow("30d", tb.month);
381
+ printRow("Total", tb.total);
361
382
  }
362
383
 
363
384
  const grand = emptyBucket();