portable-agent-layer 0.35.0 → 0.37.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.
Files changed (108) hide show
  1. package/README.md +2 -1
  2. package/assets/skills/analyze-pdf/tools/pdf-download.ts +1 -1
  3. package/assets/skills/analyze-youtube/tools/youtube-analyze.ts +1 -1
  4. package/assets/skills/consulting-report/tools/dev.ts +2 -2
  5. package/assets/skills/consulting-report/tools/generate-pdf.ts +9 -9
  6. package/assets/skills/consulting-report/tools/scaffold.ts +2 -2
  7. package/assets/skills/create-pdf/tools/md-to-html-pdf.ts +2 -2
  8. package/assets/skills/opinion/tools/opinion.ts +3 -2
  9. package/assets/skills/presentation/SKILL.md +1 -1
  10. package/assets/skills/presentation/tools/doctor.ts +2 -5
  11. package/assets/skills/presentation/tools/lib/inline.ts +6 -11
  12. package/assets/skills/presentation/tools/lib/lint-helpers.ts +2 -2
  13. package/assets/skills/presentation/tools/lib/lint-rules.ts +5 -2
  14. package/assets/skills/presentation/tools/setup-template.ts +10 -7
  15. package/assets/skills/projects/SKILL.md +44 -21
  16. package/assets/skills/research/tools/gemini-search.ts +2 -2
  17. package/assets/skills/research/tools/grok-search.ts +2 -2
  18. package/assets/skills/research/tools/perplexity-search.ts +2 -2
  19. package/assets/skills/telos/SKILL.md +7 -52
  20. package/assets/skills/telos/tools/update-telos.ts +0 -1
  21. package/assets/templates/PAL/ALGORITHM.md +54 -5
  22. package/assets/templates/PAL/PROJECT_LIFECYCLE.md +48 -0
  23. package/assets/templates/PAL/README.md +1 -1
  24. package/assets/templates/PAL/STEERING_RULES.md +4 -0
  25. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +32 -17
  26. package/assets/templates/PAL/WORK_TRACKING.md +1 -1
  27. package/assets/templates/hooks.codex.json +44 -0
  28. package/assets/templates/hooks.cursor.json +11 -5
  29. package/assets/templates/pal-settings.json +1 -3
  30. package/assets/templates/settings.claude.json +2 -1
  31. package/package.json +2 -1
  32. package/src/cli/index.ts +112 -14
  33. package/src/cli/migrate.ts +299 -0
  34. package/src/cli/setup-identity.ts +3 -3
  35. package/src/cli/setup-telos.ts +12 -80
  36. package/src/hooks/CompactRecover.ts +11 -5
  37. package/src/hooks/LoadContext.ts +35 -11
  38. package/src/hooks/PreCompactPersist.ts +26 -34
  39. package/src/hooks/SecurityValidator.ts +43 -21
  40. package/src/hooks/StopOrchestrator.ts +4 -1
  41. package/src/hooks/UserPromptOrchestrator.ts +4 -2
  42. package/src/hooks/handlers/auto-graduate.ts +2 -2
  43. package/src/hooks/handlers/backup.ts +3 -3
  44. package/src/hooks/handlers/context-digests.ts +74 -0
  45. package/src/hooks/handlers/failure.ts +5 -3
  46. package/src/hooks/handlers/inject-retrieval.ts +29 -6
  47. package/src/hooks/handlers/persist-last-exchange.ts +76 -0
  48. package/src/hooks/handlers/rating.ts +2 -1
  49. package/src/hooks/handlers/readme-sync.ts +3 -2
  50. package/src/hooks/handlers/session-intelligence.ts +17 -93
  51. package/src/hooks/handlers/session-name.ts +2 -2
  52. package/src/hooks/handlers/synthesis.ts +5 -2
  53. package/src/hooks/handlers/update-counts.ts +3 -2
  54. package/src/hooks/lib/agent.ts +20 -18
  55. package/src/hooks/lib/claude-md.ts +69 -14
  56. package/src/hooks/lib/context.ts +92 -246
  57. package/src/hooks/lib/entities.ts +7 -7
  58. package/src/hooks/lib/frontmatter.ts +4 -4
  59. package/src/hooks/lib/graduation.ts +7 -6
  60. package/src/hooks/lib/inference.ts +6 -2
  61. package/src/hooks/lib/learning-category.ts +1 -1
  62. package/src/hooks/lib/learning-store.ts +6 -1
  63. package/src/hooks/lib/notify.ts +2 -2
  64. package/src/hooks/lib/opinions.ts +3 -3
  65. package/src/hooks/lib/paths.ts +2 -0
  66. package/src/hooks/lib/projects.ts +142 -74
  67. package/src/hooks/lib/readme-sync.ts +1 -1
  68. package/src/hooks/lib/relationship.ts +4 -16
  69. package/src/hooks/lib/retrieval-index.ts +5 -3
  70. package/src/hooks/lib/retrieval.ts +11 -12
  71. package/src/hooks/lib/security.ts +24 -18
  72. package/src/hooks/lib/semi-static.ts +188 -0
  73. package/src/hooks/lib/session-names.ts +1 -1
  74. package/src/hooks/lib/settings.ts +1 -1
  75. package/src/hooks/lib/setup.ts +2 -65
  76. package/src/hooks/lib/signals.ts +2 -2
  77. package/src/hooks/lib/stdin.ts +1 -1
  78. package/src/hooks/lib/stop.ts +16 -6
  79. package/src/hooks/lib/token-usage.ts +1 -2
  80. package/src/hooks/lib/transcript.ts +1 -1
  81. package/src/hooks/lib/wisdom.ts +5 -5
  82. package/src/hooks/lib/work-tracking.ts +8 -14
  83. package/src/targets/claude/uninstall.ts +1 -1
  84. package/src/targets/codex/install.ts +95 -0
  85. package/src/targets/codex/uninstall.ts +70 -0
  86. package/src/targets/copilot/install.ts +39 -8
  87. package/src/targets/copilot/uninstall.ts +58 -17
  88. package/src/targets/cursor/install.ts +8 -0
  89. package/src/targets/cursor/uninstall.ts +18 -1
  90. package/src/targets/lib.ts +166 -14
  91. package/src/targets/opencode/install.ts +29 -1
  92. package/src/targets/opencode/plugin.ts +23 -12
  93. package/src/targets/opencode/uninstall.ts +30 -3
  94. package/src/tools/agent/algorithm-reflect.ts +1 -1
  95. package/src/tools/agent/analyze.ts +18 -18
  96. package/src/tools/agent/handoff-note.ts +116 -0
  97. package/src/tools/agent/project.ts +375 -75
  98. package/src/tools/agent/relationship-note.ts +51 -0
  99. package/src/tools/agent/synthesize.ts +6 -42
  100. package/src/tools/agent/thread.ts +15 -14
  101. package/src/tools/agent/wisdom-frame.ts +9 -3
  102. package/src/tools/import.ts +1 -1
  103. package/src/tools/relationship-reflect.ts +15 -13
  104. package/src/tools/self-model.ts +23 -19
  105. package/src/tools/session-summary.ts +3 -3
  106. package/src/tools/token-cost.ts +15 -16
  107. package/assets/skills/telos/tools/update-projects.ts +0 -106
  108. package/assets/templates/telos/PROJECTS.md +0 -7
@@ -17,6 +17,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { resolve } from "node:path";
18
18
  import { parseArgs } from "node:util";
19
19
  import { ensureDir, paths } from "../../hooks/lib/paths";
20
+ import { readJsonl } from "../self-model";
20
21
 
21
22
  // ── Config ──
22
23
 
@@ -27,7 +28,6 @@ const SYNTHESIS_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
27
28
  interface SynthesisState {
28
29
  timestamp: string;
29
30
  days: number;
30
- threads: { id: string; cwd?: string; title: string; context: string; opened: string }[];
31
31
  sessions: { date: string; titles: string[] }[];
32
32
  sessionCount: number;
33
33
  ratings: {
@@ -74,14 +74,6 @@ function shouldRun(force: boolean): boolean {
74
74
  }
75
75
  }
76
76
 
77
- function readJsonl<T>(path: string): T[] {
78
- if (!existsSync(path)) return [];
79
- return readFileSync(path, "utf-8")
80
- .split("\n")
81
- .filter((l) => l.trim())
82
- .map((l) => JSON.parse(l) as T);
83
- }
84
-
85
77
  function daysAgo(days: number): Date {
86
78
  return new Date(Date.now() - days * 24 * 60 * 60 * 1000);
87
79
  }
@@ -100,28 +92,6 @@ function safeReaddir(dir: string): string[] {
100
92
 
101
93
  // ── Data readers ──
102
94
 
103
- interface Thread {
104
- id: string;
105
- cwd?: string;
106
- title: string;
107
- context: string;
108
- status: string;
109
- created: string;
110
- }
111
-
112
- function getOpenThreads(): SynthesisState["threads"] {
113
- const threads = readJsonl<Thread>(resolve(stateDir(), "threads.jsonl"));
114
- return threads
115
- .filter((t) => t.status === "open")
116
- .map((t) => ({
117
- id: t.id,
118
- cwd: t.cwd,
119
- title: t.title,
120
- context: t.context,
121
- opened: formatDate(t.created),
122
- }));
123
- }
124
-
125
95
  interface Reflection {
126
96
  timestamp: string;
127
97
  cwd?: string;
@@ -210,12 +180,9 @@ function getRatingStats(since: Date): SynthesisState["ratings"] {
210
180
  const secondHalf = ratings.slice(mid);
211
181
  const firstAvg = firstHalf.reduce((s, r) => s + r.rating, 0) / firstHalf.length;
212
182
  const secondAvg = secondHalf.reduce((s, r) => s + r.rating, 0) / secondHalf.length;
213
- const trend =
214
- secondAvg - firstAvg > 0.5
215
- ? "improving"
216
- : secondAvg - firstAvg < -0.5
217
- ? "declining"
218
- : "stable";
183
+ const diff = secondAvg - firstAvg;
184
+ const decliningOrStable = diff < -0.5 ? "declining" : "stable";
185
+ const trend = diff > 0.5 ? "improving" : decliningOrStable;
219
186
 
220
187
  return {
221
188
  count: ratings.length,
@@ -251,8 +218,8 @@ function getRecentSessions(since: Date): {
251
218
 
252
219
  const content = readFileSync(resolve(monthDir, file), "utf-8");
253
220
  const titleMatch =
254
- content.match(/^title:\s*"?(.+?)"?\s*$/m) ??
255
- content.match(/^\*\*Title:\*\*\s*(.+?)\s*$/m);
221
+ new RegExp(/^title:\s*"?(.+?)"?\s*$/m).exec(content) ??
222
+ new RegExp(/^\*\*Title:\*\*\s*(.+?)\s*$/m).exec(content);
256
223
  const title = titleMatch?.[1] ?? file.replace(/\.md$/, "");
257
224
 
258
225
  const existing = byDate.get(isoDate) ?? [];
@@ -280,7 +247,6 @@ export function writeSynthesis(state: SynthesisState): string {
280
247
 
281
248
  export function synthesize(days: number): SynthesisState {
282
249
  const since = daysAgo(days);
283
- const threads = getOpenThreads();
284
250
  const { sessions, count: sessionCount } = getRecentSessions(since);
285
251
  const ratings = getRatingStats(since);
286
252
  const algorithm = getAlgorithmStats(since);
@@ -288,7 +254,6 @@ export function synthesize(days: number): SynthesisState {
288
254
  return {
289
255
  timestamp: new Date().toISOString(),
290
256
  days,
291
- threads,
292
257
  sessions,
293
258
  sessionCount,
294
259
  ratings,
@@ -346,7 +311,6 @@ Output: ~/.pal/memory/state/synthesis.json
346
311
  {
347
312
  success: true,
348
313
  path: sp,
349
- threads: state.threads.length,
350
314
  sessions: state.sessionCount,
351
315
  ratings: state.ratings.count,
352
316
  reflections: state.algorithm.reflectionCount,
@@ -18,7 +18,7 @@ import { ensureDir, paths } from "../../hooks/lib/paths";
18
18
 
19
19
  // ── Types ──
20
20
 
21
- interface Thread {
21
+ export interface Thread {
22
22
  id: string;
23
23
  cwd: string;
24
24
  title: string;
@@ -38,16 +38,20 @@ function generateId(): string {
38
38
  return Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
39
39
  }
40
40
 
41
- function readThreads(): Thread[] {
41
+ export function readThreads(): Thread[] {
42
42
  const p = threadsPath();
43
43
  if (!existsSync(p)) return [];
44
- return readFileSync(p, "utf-8")
45
- .split("\n")
46
- .filter((l) => l.trim())
47
- .map((l) => JSON.parse(l) as Thread);
44
+ try {
45
+ return readFileSync(p, "utf-8")
46
+ .split("\n")
47
+ .filter((l) => l.trim())
48
+ .map((l) => JSON.parse(l) as Thread);
49
+ } catch {
50
+ return [];
51
+ }
48
52
  }
49
53
 
50
- function writeThreads(threads: Thread[]): void {
54
+ export function writeThreads(threads: Thread[]): void {
51
55
  writeFileSync(
52
56
  threadsPath(),
53
57
  `${threads.map((t) => JSON.stringify(t)).join("\n")}\n`,
@@ -110,13 +114,10 @@ function run() {
110
114
  },
111
115
  });
112
116
 
113
- const cmd = values.add
114
- ? "add"
115
- : values.resolve
116
- ? "resolve"
117
- : values.list
118
- ? "list"
119
- : null;
117
+ let cmd: string | null = null;
118
+ if (values.add) cmd = "add";
119
+ else if (values.resolve) cmd = "resolve";
120
+ else if (values.list) cmd = "list";
120
121
 
121
122
  if (values.help || !cmd) {
122
123
  console.log(`
@@ -39,7 +39,7 @@ function date(): string {
39
39
  }
40
40
 
41
41
  function parseObservationCount(content: string): number {
42
- const match = content.match(/\*\*Observation Count:\*\*\s*(\d+)/);
42
+ const match = new RegExp(/\*\*Observation Count:\*\*\s*(\d+)/).exec(content);
43
43
  return match ? parseInt(match[1], 10) : 0;
44
44
  }
45
45
 
@@ -90,6 +90,12 @@ export function updateFrame(
90
90
  if (!existsSync(framePath)) {
91
91
  mkdirSync(framesDir, { recursive: true });
92
92
 
93
+ const contextualRuleEntry =
94
+ type === "contextual-rule" ? `- ${observation} (${date()})` : "*None yet.*";
95
+ const antiPatternEntry =
96
+ type === "anti-pattern"
97
+ ? `### ${observation}\n- **Severity:** Medium\n- **Frequency:** Observed`
98
+ : "*None yet.*";
93
99
  const content = `# Frame: ${domain.charAt(0).toUpperCase() + domain.slice(1)}
94
100
 
95
101
  ## Meta
@@ -107,13 +113,13 @@ export function updateFrame(
107
113
 
108
114
  ## Contextual Rules
109
115
 
110
- ${type === "contextual-rule" ? `- ${observation} (${date()})` : "*None yet.*"}
116
+ ${contextualRuleEntry}
111
117
 
112
118
  ---
113
119
 
114
120
  ## Anti-Patterns
115
121
 
116
- ${type === "anti-pattern" ? `### ${observation}\n- **Severity:** Medium\n- **Frequency:** Observed` : "*None yet.*"}
122
+ ${antiPatternEntry}
117
123
 
118
124
  ---
119
125
 
@@ -108,4 +108,4 @@ async function run() {
108
108
  importZip(zipPath, repoRoot, dryRun);
109
109
  }
110
110
 
111
- if (import.meta.main) run();
111
+ if (import.meta.main) await run();
@@ -37,7 +37,7 @@ interface Rating {
37
37
  }
38
38
 
39
39
  interface ParsedNote {
40
- type: "W" | "O" | "B";
40
+ type: "W" | "O" | "Session";
41
41
  text: string;
42
42
  confidence?: number;
43
43
  date: string;
@@ -84,17 +84,17 @@ export function loadNotes(daysBack: number): ParsedNote[] {
84
84
  let currentTime = "";
85
85
 
86
86
  for (const line of content.split("\n")) {
87
- const timeMatch = line.match(/^## (\d{2}:\d{2})/);
87
+ const timeMatch = new RegExp(/^## (\d{2}:\d{2})/).exec(line);
88
88
  if (timeMatch) {
89
89
  currentTime = timeMatch[1];
90
90
  continue;
91
91
  }
92
92
 
93
93
  // O(c=0.85): text or B(c=0.85): text
94
- const obMatch = line.match(/^- ([OB])\(c=([\d.]+)\):\s*(.+)$/);
94
+ const obMatch = new RegExp(/^- ([OB])\(c=([\d.]+)\):\s*(.+)$/).exec(line);
95
95
  if (obMatch) {
96
96
  notes.push({
97
- type: obMatch[1] as "O" | "B",
97
+ type: obMatch[1] as "O" | "Session",
98
98
  confidence: Number.parseFloat(obMatch[2]),
99
99
  text: obMatch[3],
100
100
  date: dateStr,
@@ -104,7 +104,7 @@ export function loadNotes(daysBack: number): ParsedNote[] {
104
104
  }
105
105
 
106
106
  // W: text
107
- const worldMatch = line.match(/^- W:\s*(.+)$/);
107
+ const worldMatch = new RegExp(/^- W:\s*(.+)$/).exec(line);
108
108
  if (worldMatch) {
109
109
  notes.push({
110
110
  type: "W",
@@ -261,19 +261,21 @@ function correlateRatings(ratings: Rating[]): string[] {
261
261
  const highRatings = ratings.filter((r) => r.rating >= 7);
262
262
 
263
263
  if (lowRatings.length > 0) {
264
+ const lowContexts = lowRatings
265
+ .slice(0, 3)
266
+ .map((r) => `"${r.context.slice(0, 60)}"`)
267
+ .join(", ");
264
268
  correlations.push(
265
- `${lowRatings.length} low ratings (<=4) — common contexts: ${lowRatings
266
- .slice(0, 3)
267
- .map((r) => `"${r.context.slice(0, 60)}"`)
268
- .join(", ")}`
269
+ `${lowRatings.length} low ratings (<=4) — common contexts: ${lowContexts}`
269
270
  );
270
271
  }
271
272
  if (highRatings.length > 0) {
273
+ const highContexts = highRatings
274
+ .slice(0, 3)
275
+ .map((r) => `"${r.context.slice(0, 60)}"`)
276
+ .join(", ");
272
277
  correlations.push(
273
- `${highRatings.length} high ratings (>=7) — common contexts: ${highRatings
274
- .slice(0, 3)
275
- .map((r) => `"${r.context.slice(0, 60)}"`)
276
- .join(", ")}`
278
+ `${highRatings.length} high ratings (>=7) — common contexts: ${highContexts}`
277
279
  );
278
280
  }
279
281
 
@@ -71,7 +71,7 @@ interface AlgorithmReflection {
71
71
  }
72
72
 
73
73
  interface RelationshipNote {
74
- type: "O" | "W" | "B";
74
+ type: "O" | "W" | "Session";
75
75
  content: string;
76
76
  confidence?: number;
77
77
  date: string;
@@ -107,12 +107,16 @@ function shouldRun(force: boolean): boolean {
107
107
  }
108
108
  }
109
109
 
110
- function readJsonl<T>(path: string): T[] {
110
+ export function readJsonl<T>(path: string): T[] {
111
111
  if (!existsSync(path)) return [];
112
- return readFileSync(path, "utf-8")
113
- .split("\n")
114
- .filter((l) => l.trim())
115
- .map((l) => JSON.parse(l) as T);
112
+ try {
113
+ return readFileSync(path, "utf-8")
114
+ .split("\n")
115
+ .filter((l) => l.trim())
116
+ .map((l) => JSON.parse(l) as T);
117
+ } catch {
118
+ return [];
119
+ }
116
120
  }
117
121
 
118
122
  function safeReadJson<T>(path: string, fallback: T): T {
@@ -261,7 +265,9 @@ function readRelationshipNotes(since: Date): RelationshipNote[] {
261
265
  const notes: RelationshipNote[] = [];
262
266
  const sinceStr = formatDate(since.toISOString());
263
267
 
264
- for (const monthDir of safeReaddir(baseDir).filter((d) => d.match(/^\d{4}-\d{2}$/))) {
268
+ for (const monthDir of safeReaddir(baseDir).filter((d) =>
269
+ new RegExp(/^\d{4}-\d{2}$/).exec(d)
270
+ )) {
265
271
  const fullMonthDir = resolve(baseDir, monthDir);
266
272
  for (const file of safeReaddir(fullMonthDir).filter((f) => f.endsWith(".md"))) {
267
273
  const dateStr = file.replace(/\.md$/, "");
@@ -275,7 +281,7 @@ function readRelationshipNotes(since: Date): RelationshipNote[] {
275
281
  const noteContent = trimmed.substring(2);
276
282
 
277
283
  // Parse O(c=X.XX): ..., W: ..., B: ...
278
- const opinionMatch = noteContent.match(/^O\(c=([\d.]+)\):\s*(.+)$/);
284
+ const opinionMatch = new RegExp(/^O\(c=([\d.]+)\):\s*(.+)$/).exec(noteContent);
279
285
  if (opinionMatch) {
280
286
  notes.push({
281
287
  type: "O",
@@ -286,15 +292,15 @@ function readRelationshipNotes(since: Date): RelationshipNote[] {
286
292
  continue;
287
293
  }
288
294
 
289
- const wisdomMatch = noteContent.match(/^W:\s*(.+)$/);
295
+ const wisdomMatch = new RegExp(/^W:\s*(.+)$/).exec(noteContent);
290
296
  if (wisdomMatch) {
291
297
  notes.push({ type: "W", content: wisdomMatch[1], date: dateStr });
292
298
  continue;
293
299
  }
294
300
 
295
- const behaviorMatch = noteContent.match(/^B:\s*(.+)$/);
301
+ const behaviorMatch = new RegExp(/^Session:\s*(.+)$/).exec(noteContent);
296
302
  if (behaviorMatch) {
297
- notes.push({ type: "B", content: behaviorMatch[1], date: dateStr });
303
+ notes.push({ type: "Session", content: behaviorMatch[1], date: dateStr });
298
304
  }
299
305
  }
300
306
  }
@@ -376,7 +382,7 @@ function gatherData(days: number): SelfModelData {
376
382
  wisdomFrames,
377
383
  graduated,
378
384
  reflections,
379
- behaviorNotes: relNotes.filter((n) => n.type === "B").map((n) => n.content),
385
+ behaviorNotes: relNotes.filter((n) => n.type === "Session").map((n) => n.content),
380
386
  wisdomNotes: relNotes.filter((n) => n.type === "W").map((n) => n.content),
381
387
  selfObservations: reflections.map((r) => r.q1).filter(Boolean),
382
388
  algorithmObservations: reflections.map((r) => r.q2).filter(Boolean),
@@ -388,12 +394,10 @@ function gatherData(days: number): SelfModelData {
388
394
  function formatDataForInference(data: SelfModelData): string {
389
395
  const sections: string[] = [];
390
396
 
391
- sections.push(`## Raw Data — ${data.days}-day window, ${data.now}`);
392
- sections.push(`Sessions: ${data.sessionCount}`);
393
- sections.push(
394
- `Ratings: ${data.ratings.count} total, ${data.ratings.avg}/10 avg, recent ${data.ratings.recentAvg}/10, trend ${data.ratings.trend}`
395
- );
396
397
  sections.push(
398
+ `## Raw Data — ${data.days}-day window, ${data.now}`,
399
+ `Sessions: ${data.sessionCount}`,
400
+ `Ratings: ${data.ratings.count} total, ${data.ratings.avg}/10 avg, recent ${data.ratings.recentAvg}/10, trend ${data.ratings.trend}`,
397
401
  `${data.ratings.highCount} high (8+), ${data.ratings.lowCount} low (<=3)`
398
402
  );
399
403
 
@@ -508,7 +512,7 @@ Where are you heading? Improving, declining, stagnating? What's the single most
508
512
 
509
513
  // ── Narrative Composer ──
510
514
 
511
- export async function composeSelfModel(days: number): Promise<string> {
515
+ async function composeSelfModel(days: number): Promise<string> {
512
516
  const data = gatherData(days);
513
517
  const rawData = formatDataForInference(data);
514
518
  const id = loadSettingsIdentity();
@@ -668,4 +672,4 @@ Output: ~/.pal/memory/self-model.md (synthesized by Sonnet)
668
672
  );
669
673
  }
670
674
 
671
- if (import.meta.main) run();
675
+ if (import.meta.main) await run();
@@ -27,7 +27,7 @@ interface Usage {
27
27
 
28
28
  // ── Core Functions ──
29
29
 
30
- export function findSessionFile(
30
+ function findSessionFile(
31
31
  sessionId: string,
32
32
  claudeDir: string
33
33
  ): { filepath: string; project: string } | null {
@@ -79,7 +79,7 @@ export function findSessionFile(
79
79
  return latest;
80
80
  }
81
81
 
82
- export function parseSession(filepath: string, sessionId: string): Usage {
82
+ function parseSession(filepath: string, sessionId: string): Usage {
83
83
  const usage: Usage = {
84
84
  input: 0,
85
85
  output: 0,
@@ -122,7 +122,7 @@ export function parseSession(filepath: string, sessionId: string): Usage {
122
122
  if (d.sessionId !== sessionId) continue;
123
123
 
124
124
  if (d.timestamp) {
125
- if (!firstTs) firstTs = d.timestamp;
125
+ firstTs ??= d.timestamp;
126
126
  lastTs = d.timestamp;
127
127
  }
128
128
 
@@ -17,7 +17,7 @@ import { palHome } from "../hooks/lib/paths";
17
17
 
18
18
  // ── Types ──
19
19
 
20
- export interface Bucket {
20
+ interface Bucket {
21
21
  input: number;
22
22
  output: number;
23
23
  cacheWrite5m: number;
@@ -27,7 +27,7 @@ export interface Bucket {
27
27
  calls: number;
28
28
  }
29
29
 
30
- export function emptyBucket(): Bucket {
30
+ function emptyBucket(): Bucket {
31
31
  return {
32
32
  input: 0,
33
33
  output: 0,
@@ -39,14 +39,14 @@ export function emptyBucket(): Bucket {
39
39
  };
40
40
  }
41
41
 
42
- export interface TimeBuckets {
42
+ interface TimeBuckets {
43
43
  today: Bucket;
44
44
  week: Bucket;
45
45
  month: Bucket;
46
46
  total: Bucket;
47
47
  }
48
48
 
49
- export function emptyTimeBuckets(): TimeBuckets {
49
+ function emptyTimeBuckets(): TimeBuckets {
50
50
  return {
51
51
  today: emptyBucket(),
52
52
  week: emptyBucket(),
@@ -85,7 +85,7 @@ function costForUsage(
85
85
  );
86
86
  }
87
87
 
88
- export function addToBucket(
88
+ function addToBucket(
89
89
  bucket: Bucket,
90
90
  model: string,
91
91
  input: number,
@@ -160,7 +160,7 @@ function addToTimeBuckets(
160
160
  addToBucket(tb.today, model, input, output, cacheWrite5m, cacheWrite1h, cacheRead);
161
161
  }
162
162
 
163
- export function readClaudeCode(projectFilter?: string): {
163
+ function readClaudeCode(projectFilter?: string): {
164
164
  buckets: TimeBuckets;
165
165
  byModel: Record<string, Bucket>;
166
166
  byProject: Record<string, TimeBuckets>;
@@ -267,7 +267,7 @@ export function readClaudeCode(projectFilter?: string): {
267
267
  monthAgo
268
268
  );
269
269
 
270
- if (!byModel[model]) byModel[model] = emptyBucket();
270
+ byModel[model] ??= emptyBucket();
271
271
  addToBucket(
272
272
  byModel[model],
273
273
  model,
@@ -278,7 +278,7 @@ export function readClaudeCode(projectFilter?: string): {
278
278
  cr
279
279
  );
280
280
 
281
- if (!byProject[projName]) byProject[projName] = emptyTimeBuckets();
281
+ byProject[projName] ??= emptyTimeBuckets();
282
282
  addToTimeBuckets(
283
283
  byProject[projName],
284
284
  ts,
@@ -304,7 +304,7 @@ export function readClaudeCode(projectFilter?: string): {
304
304
 
305
305
  // ── PAL inference ──
306
306
 
307
- export function readPalInference(): {
307
+ function readPalInference(): {
308
308
  buckets: TimeBuckets;
309
309
  byModel: Record<string, TimeBuckets>;
310
310
  byCaller: Record<string, Bucket>;
@@ -346,7 +346,7 @@ export function readPalInference(): {
346
346
  weekAgo,
347
347
  monthAgo
348
348
  );
349
- if (!byModel[e.model]) byModel[e.model] = emptyTimeBuckets();
349
+ byModel[e.model] ??= emptyTimeBuckets();
350
350
  addToTimeBuckets(
351
351
  byModel[e.model],
352
352
  e.ts,
@@ -360,7 +360,7 @@ export function readPalInference(): {
360
360
  weekAgo,
361
361
  monthAgo
362
362
  );
363
- if (!byCaller[e.caller]) byCaller[e.caller] = emptyBucket();
363
+ byCaller[e.caller] ??= emptyBucket();
364
364
  addToBucket(byCaller[e.caller], e.model, e.inputTokens, e.outputTokens, 0, 0, 0);
365
365
  } catch {
366
366
  /* skip */
@@ -415,11 +415,10 @@ export function usage() {
415
415
 
416
416
  for (const [model, tb] of Object.entries(pal.byModel)) {
417
417
  if (tb.total.calls === 0) continue;
418
- const label = model.includes("haiku")
419
- ? "Haiku"
420
- : model.includes("sonnet")
421
- ? "Sonnet"
422
- : model.replace("claude-", "");
418
+ let label: string;
419
+ if (model.includes("haiku")) label = "Haiku";
420
+ else if (model.includes("sonnet")) label = "Sonnet";
421
+ else label = model.replace("claude-", "");
423
422
  console.log(`\n PAL Inference (${label})\n`);
424
423
  printRow("Today", tb.today);
425
424
  printRow("7d", tb.week);
@@ -1,106 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * UpdateProjects — Upsert a project row in PROJECTS.md by ID.
4
- *
5
- * Usage:
6
- * bun update-projects.ts <id> "<row>" "<description>"
7
- *
8
- * - <id> is the value in the first column (e.g., "my-proj", "side-gig")
9
- * - <row> is the full table row including the ID column (e.g., "| my-proj | My Project | Done | High | ... |")
10
- * - If a row with that ID exists, it is replaced
11
- * - If no row with that ID exists, it is appended
12
- * - Creates a timestamped backup before modifying
13
- * - Logs the change to updates.md
14
- */
15
-
16
- import {
17
- copyFileSync,
18
- existsSync,
19
- mkdirSync,
20
- readFileSync,
21
- writeFileSync,
22
- } from "node:fs";
23
- import { resolve } from "node:path";
24
- import { palHome } from "../../../../src/hooks/lib/paths";
25
-
26
- const TELOS_DIR = resolve(palHome(), "telos");
27
- const BACKUPS_DIR = resolve(TELOS_DIR, "backups");
28
- const UPDATES_LOG = resolve(TELOS_DIR, "updates.md");
29
- const PROJECTS_FILE = resolve(TELOS_DIR, "PROJECTS.md");
30
-
31
- function timestamp(): string {
32
- const now = new Date();
33
- const pad = (n: number) => n.toString().padStart(2, "0");
34
- return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
35
- }
36
-
37
- function isoDate(): string {
38
- return new Date().toISOString().replace("T", " ").slice(0, 19);
39
- }
40
-
41
- export interface UpsertProjectResult {
42
- file: string;
43
- id: string;
44
- mode: "replaced" | "appended";
45
- backed_up: boolean;
46
- logged: boolean;
47
- description: string;
48
- }
49
-
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";
66
-
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
- }
79
-
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");
85
-
86
- return { file: "PROJECTS.md", id, mode, backed_up: true, logged: true, description };
87
- }
88
-
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
- }
105
-
106
- if (import.meta.main) run();
@@ -1,7 +0,0 @@
1
- # Projects
2
-
3
- <!-- Current projects, their status, and how they relate to each other. -->
4
-
5
- | Project | Status | Priority | Notes |
6
- |---------|--------|----------|-------|
7
- | | | | |