@tekmidian/pai 0.5.1 → 0.5.3

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 (28) hide show
  1. package/ARCHITECTURE.md +84 -0
  2. package/README.md +94 -0
  3. package/dist/cli/index.mjs +72 -12
  4. package/dist/cli/index.mjs.map +1 -1
  5. package/dist/daemon/index.mjs +3 -3
  6. package/dist/{daemon-a1W4KgFq.mjs → daemon-D9evGlgR.mjs} +8 -8
  7. package/dist/{daemon-a1W4KgFq.mjs.map → daemon-D9evGlgR.mjs.map} +1 -1
  8. package/dist/{factory-CeXQzlwn.mjs → factory-Bzcy70G9.mjs} +3 -3
  9. package/dist/{factory-CeXQzlwn.mjs.map → factory-Bzcy70G9.mjs.map} +1 -1
  10. package/dist/hooks/context-compression-hook.mjs +333 -33
  11. package/dist/hooks/context-compression-hook.mjs.map +3 -3
  12. package/dist/hooks/post-compact-inject.mjs +51 -0
  13. package/dist/hooks/post-compact-inject.mjs.map +7 -0
  14. package/dist/index.d.mts.map +1 -1
  15. package/dist/index.mjs +1 -1
  16. package/dist/{indexer-CKQcgKsz.mjs → indexer-CMPOiY1r.mjs} +22 -1
  17. package/dist/{indexer-CKQcgKsz.mjs.map → indexer-CMPOiY1r.mjs.map} +1 -1
  18. package/dist/{indexer-backend-DQO-FqAI.mjs → indexer-backend-CIMXedqk.mjs} +26 -9
  19. package/dist/indexer-backend-CIMXedqk.mjs.map +1 -0
  20. package/dist/{postgres-CIxeqf_n.mjs → postgres-FXrHDPcE.mjs} +36 -13
  21. package/dist/postgres-FXrHDPcE.mjs.map +1 -0
  22. package/dist/{sqlite-CymLKiDE.mjs → sqlite-WWBq7_2C.mjs} +18 -1
  23. package/dist/{sqlite-CymLKiDE.mjs.map → sqlite-WWBq7_2C.mjs.map} +1 -1
  24. package/package.json +1 -1
  25. package/src/hooks/ts/pre-compact/context-compression-hook.ts +292 -70
  26. package/src/hooks/ts/session-start/post-compact-inject.ts +85 -0
  27. package/dist/indexer-backend-DQO-FqAI.mjs.map +0 -1
  28. package/dist/postgres-CIxeqf_n.mjs.map +0 -1
package/ARCHITECTURE.md CHANGED
@@ -500,6 +500,90 @@ PAI implements six Luhmann-inspired operations on the vault's dual representatio
500
500
 
501
501
  ---
502
502
 
503
+ ## Hook System
504
+
505
+ PAI ships a comprehensive set of lifecycle hooks that integrate with Claude Code's hook events. Hooks are TypeScript source files (`src/hooks/ts/`) compiled to `.mjs` modules and deployed to `~/.claude/Hooks/`.
506
+
507
+ ### Hook Architecture
508
+
509
+ ```
510
+ Claude Code Event
511
+
512
+ ├── stdin: JSON { session_id, transcript_path, cwd, hook_event_name }
513
+
514
+ ├── Hook Process (.mjs)
515
+ │ ├── Reads stdin for context
516
+ │ ├── Performs side effects (file writes, notifications)
517
+ │ └── Writes stdout (injected as <system-reminder> into conversation)
518
+
519
+ └── stderr: diagnostic logs (visible in Claude Code's hook output)
520
+ ```
521
+
522
+ **Key constraint:** Not all hook events support stdout injection. `SessionStart` does. `PreCompact` does not. This matters for context preservation.
523
+
524
+ ### Hook Inventory
525
+
526
+ | Hook | Event | Purpose |
527
+ |------|-------|---------|
528
+ | `load-core-context.mjs` | SessionStart | Loads PAI skill system and core configuration |
529
+ | `load-project-context.mjs` | SessionStart | Detects project, loads notes dir, TODO, session note |
530
+ | `initialize-session.mjs` | SessionStart | Creates numbered session note, registers in PAI registry |
531
+ | `post-compact-inject.mjs` | SessionStart (compact) | Reads saved state and injects into post-compaction context |
532
+ | `security-validator.mjs` | PreToolUse (Bash) | Validates shell commands against security rules |
533
+ | `capture-all-events.mjs` | All events | Observability — logs every hook event to session timeline |
534
+ | `context-compression-hook.mjs` | PreCompact | Extracts session state, saves checkpoint, writes temp file for relay |
535
+ | `capture-tool-output.mjs` | PostToolUse | Records tool inputs/outputs for observability dashboard |
536
+ | `update-tab-on-action.mjs` | PostToolUse | Updates terminal tab title based on current activity |
537
+ | `sync-todo-to-md.mjs` | PostToolUse (TodoWrite) | Syncs Claude's internal TODO list to `Notes/TODO.md` |
538
+ | `cleanup-session-files.mjs` | UserPromptSubmit | Cleans up stale temp files between prompts |
539
+ | `update-tab-titles.mjs` | UserPromptSubmit | Sets terminal tab title from session context |
540
+ | `stop-hook.mjs` | Stop | Writes work items to session note, sends notification |
541
+ | `capture-session-summary.mjs` | SessionEnd | Final session summary written to session note |
542
+ | `subagent-stop-hook.mjs` | SubagentStop | Captures sub-agent completion for observability |
543
+
544
+ ### Context Preservation Relay
545
+
546
+ The most critical hook interaction is the PreCompact → SessionStart relay that preserves context across compaction:
547
+
548
+ ```
549
+ PreCompact fires (context-compression-hook.mjs)
550
+
551
+ ├── Reads transcript JSONL from stdin { transcript_path }
552
+ ├── Extracts: recent user messages, work summaries, files modified, captures
553
+ ├── Writes checkpoint to session note (persistent)
554
+ ├── Writes injection payload to /tmp/pai-compact-state-{session_id}.txt
555
+ └── Sends notification (WhatsApp or ntfy.sh)
556
+
557
+ ⬇ Claude Code runs compaction (conversation is summarized)
558
+
559
+ SessionStart(compact) fires (post-compact-inject.mjs)
560
+
561
+ ├── Reads /tmp/pai-compact-state-{session_id}.txt
562
+ ├── Outputs content to stdout → injected into post-compaction context
563
+ └── Deletes temp file (one-shot relay)
564
+ ```
565
+
566
+ **Why the relay?** PreCompact hooks cannot inject into the conversation (stdout is ignored by Claude Code for this event). SessionStart hooks can. The temp file bridges the gap.
567
+
568
+ ### Building Hooks
569
+
570
+ ```bash
571
+ bun run build # Builds everything including hooks
572
+ node scripts/build-hooks.mjs # Build hooks only
573
+ ```
574
+
575
+ The build script compiles each `.ts` hook to a self-contained `.mjs` module using `tsx` bundling, then copies them to the configured hooks directory (`~/.claude/Hooks/` by default).
576
+
577
+ ### Adding a New Hook
578
+
579
+ 1. Create the TypeScript source in the appropriate `src/hooks/ts/<event>/` directory
580
+ 2. Read stdin for `HookInput` JSON (session_id, transcript_path, cwd, hook_event_name)
581
+ 3. Use stderr for diagnostics, stdout only if the event supports injection
582
+ 4. Register the hook in `~/.claude/settings.json` under the appropriate event with the correct matcher
583
+ 5. Run `bun run build` to compile and deploy
584
+
585
+ ---
586
+
503
587
  ## Templates
504
588
 
505
589
  PAI ships three templates used during setup and customizable for your workflow.
package/README.md CHANGED
@@ -60,6 +60,44 @@ Claude finds the setup skill, checks your system, runs the interactive wizard, a
60
60
 
61
61
  ---
62
62
 
63
+ ## Context Preservation
64
+
65
+ When Claude's context window fills up, it compresses the conversation. Without PAI, everything from before that point is lost — Claude forgets what it was working on, what files it changed, and what you asked for.
66
+
67
+ PAI intercepts this compression with a two-stage relay:
68
+
69
+ 1. **Before compression** — PAI extracts session state from the conversation transcript: your recent requests, work summaries, files modified, and current task context. This gets saved to a checkpoint.
70
+
71
+ 2. **After compression** — PAI reads that checkpoint and injects it back into Claude's fresh context. Claude picks up exactly where it left off.
72
+
73
+ This happens automatically. You don't need to do anything — just keep working, and PAI handles the continuity.
74
+
75
+ ### What Gets Preserved
76
+
77
+ - Your last 3 requests (so Claude knows what you were asking)
78
+ - Work summaries and captured context
79
+ - Files modified during the session
80
+ - Current working directory and task state
81
+ - Session note checkpoints (persistent — survive even full restarts)
82
+
83
+ ### Session Lifecycle Hooks
84
+
85
+ PAI runs hooks at every stage of a Claude Code session:
86
+
87
+ | Event | What PAI Does |
88
+ |-------|--------------|
89
+ | **Session Start** | Loads project context, detects which project you're in, creates a session note |
90
+ | **User Prompt** | Cleans up temp files, updates terminal tab titles |
91
+ | **Pre-Compact** | Saves session state checkpoint, sends notification |
92
+ | **Post-Compact** | Injects preserved state back into Claude's context |
93
+ | **Tool Use** | Captures tool outputs for observability |
94
+ | **Session End** | Summarizes work done, finalizes session note |
95
+ | **Stop** | Writes work items to session note, sends notification |
96
+
97
+ All hooks are TypeScript compiled to `.mjs` modules. They run as separate processes, communicate via stdin (JSON input from Claude Code) and stdout (context injection back into the conversation).
98
+
99
+ ---
100
+
63
101
  ## Auto-Compact Context Window
64
102
 
65
103
  Claude Code can automatically compact your context window when it fills up, preventing session interruptions mid-task. PAI's statusline shows you at a glance whether auto-compact is active.
@@ -195,6 +233,62 @@ pai memory settings rerank false
195
233
 
196
234
  Settings live in the `search` section of `~/.config/pai/config.json`. Per-call parameters (CLI flags or MCP tool arguments) always override config defaults.
197
235
 
236
+ ### Using Search from Within Claude
237
+
238
+ When PAI is configured as an MCP server, Claude uses the `memory_search` tool automatically. You don't need to call it yourself — just ask Claude naturally and it searches your memory behind the scenes.
239
+
240
+ **Example prompts you can give Claude:**
241
+
242
+ ```
243
+ "Search your memory for authentication"
244
+ "What do you know about the database migration?"
245
+ "Find where we discussed the notification system"
246
+ ```
247
+
248
+ Claude calls `memory_search` with the right parameters based on your config defaults. Reranking and recency boost are both active by default — you don't need to configure anything for good results.
249
+
250
+ **Overriding defaults for a specific search:**
251
+
252
+ You can ask Claude to adjust search behavior per-query:
253
+
254
+ ```
255
+ "Search for authentication using semantic mode"
256
+ → Claude passes mode: "semantic"
257
+
258
+ "Search for the old logging discussion without recency boost"
259
+ → Claude passes recency_boost: 0
260
+
261
+ "Search for database schema across all projects with no reranking"
262
+ → Claude passes all_projects: true, rerank: false
263
+ ```
264
+
265
+ **The `memory_search` MCP tool accepts these parameters:**
266
+
267
+ | Parameter | Type | Description |
268
+ |-----------|------|-------------|
269
+ | `query` | string | Free-text search query (required) |
270
+ | `project` | string | Scope to one project by slug |
271
+ | `all_projects` | boolean | Explicitly search all projects |
272
+ | `sources` | array | Restrict to `"memory"` or `"notes"` |
273
+ | `limit` | integer | Max results (1–100, default from config) |
274
+ | `mode` | string | `"keyword"`, `"semantic"`, or `"hybrid"` |
275
+ | `rerank` | boolean | Cross-encoder reranking (default: true from config) |
276
+ | `recency_boost` | integer | Recency half-life in days (0 = off, default from config) |
277
+
278
+ All parameters except `query` are optional. Omitted values fall back to your `~/.config/pai/config.json` defaults.
279
+
280
+ **Changing defaults permanently:**
281
+
282
+ Tell Claude to change your search settings:
283
+
284
+ ```
285
+ "Set my default search mode to hybrid"
286
+ "Turn off reranking by default"
287
+ "Change the recency boost to 60 days"
288
+ ```
289
+
290
+ Claude runs `pai memory settings <key> <value>` to update `~/.config/pai/config.json`. Changes take effect on the next search — no restart needed.
291
+
198
292
  ---
199
293
 
200
294
  ## Zettelkasten Intelligence
@@ -4,13 +4,13 @@ import { _ as warn, a as fmtDate, c as ok, d as scaffoldProjectDirs, f as shorte
4
4
  import { a as slugify$1, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "../migrate-jokLenje.mjs";
5
5
  import { n as ensurePaiMarker, t as discoverPaiMarkers } from "../pai-marker-CXQPX2P6.mjs";
6
6
  import { n as openFederation } from "../db-Dp8VXIMR.mjs";
7
- import { a as indexProject, n as embedChunks, r as indexAll } from "../indexer-CKQcgKsz.mjs";
7
+ import { a as indexProject, n as embedChunks, r as indexAll } from "../indexer-CMPOiY1r.mjs";
8
8
  import "../embeddings-DGRAPAYb.mjs";
9
9
  import { n as populateSlugs, r as searchMemory } from "../search-_oHfguA5.mjs";
10
10
  import { a as expandHome, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig$1, t as CONFIG_DIR } from "../config-Cf92lGX_.mjs";
11
11
  import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "../detect-BU3Nx_2L.mjs";
12
12
  import { t as PaiClient } from "../ipc-client-Bjg_a1dc.mjs";
13
- import { t as createStorageBackend } from "../factory-CeXQzlwn.mjs";
13
+ import { t as createStorageBackend } from "../factory-Bzcy70G9.mjs";
14
14
  import { appendFileSync, chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
15
15
  import { homedir, tmpdir } from "node:os";
16
16
  import { basename, dirname, join, relative, resolve } from "node:path";
@@ -1811,7 +1811,7 @@ function cmdActive(db, opts) {
1811
1811
  async function cmdAutoRoute(opts) {
1812
1812
  const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-BG6I_4B1.mjs");
1813
1813
  const { openRegistry } = await import("../db-4lSqLFb8.mjs").then((n) => n.t);
1814
- const { createStorageBackend } = await import("../factory-CeXQzlwn.mjs").then((n) => n.n);
1814
+ const { createStorageBackend } = await import("../factory-Bzcy70G9.mjs").then((n) => n.n);
1815
1815
  const { loadConfig } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
1816
1816
  const config = loadConfig();
1817
1817
  const registryDb = openRegistry();
@@ -2341,7 +2341,7 @@ async function countVectorDbPaths(oldPaths) {
2341
2341
  if (oldPaths.length === 0) return 0;
2342
2342
  try {
2343
2343
  const { loadConfig } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
2344
- const { PostgresBackend } = await import("../postgres-CIxeqf_n.mjs");
2344
+ const { PostgresBackend } = await import("../postgres-FXrHDPcE.mjs");
2345
2345
  const config = loadConfig();
2346
2346
  if (config.storageBackend !== "postgres") return 0;
2347
2347
  const pgBackend = new PostgresBackend(config.postgres ?? {});
@@ -2368,7 +2368,7 @@ async function updateVectorDbPaths(moves) {
2368
2368
  if (moves.length === 0) return 0;
2369
2369
  try {
2370
2370
  const { loadConfig } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
2371
- const { PostgresBackend } = await import("../postgres-CIxeqf_n.mjs");
2371
+ const { PostgresBackend } = await import("../postgres-FXrHDPcE.mjs");
2372
2372
  const config = loadConfig();
2373
2373
  if (config.storageBackend !== "postgres") return 0;
2374
2374
  const pgBackend = new PostgresBackend(config.postgres ?? {});
@@ -3737,7 +3737,7 @@ function cmdLogs(opts) {
3737
3737
  }
3738
3738
  function registerDaemonCommands(daemonCmd) {
3739
3739
  daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
3740
- const { serve } = await import("../daemon-a1W4KgFq.mjs").then((n) => n.t);
3740
+ const { serve } = await import("../daemon-D9evGlgR.mjs").then((n) => n.t);
3741
3741
  const { loadConfig: lc, ensureConfigDir } = await import("../config-Cf92lGX_.mjs").then((n) => n.r);
3742
3742
  ensureConfigDir();
3743
3743
  await serve(lc());
@@ -4142,9 +4142,18 @@ function mergeEnv(settings, incoming, report) {
4142
4142
  return changed;
4143
4143
  }
4144
4144
  /**
4145
+ * Strip file extension from a command basename for extension-agnostic dedup.
4146
+ * This ensures that e.g. "context-compression-hook.ts" and
4147
+ * "context-compression-hook.mjs" are treated as the same hook.
4148
+ */
4149
+ function commandStem(cmd) {
4150
+ return (cmd.split("/").pop() ?? cmd).replace(/\.(mjs|ts|js|sh)$/, "");
4151
+ }
4152
+ /**
4145
4153
  * Collect every command string already registered for a given hookType.
4146
- * Stores both the full command and the basename for flexible matching
4147
- * (handles ${PAI_DIR}/Hooks/foo.sh vs /Users/.../Hooks/foo.sh).
4154
+ * Stores full command, basename, AND extension-stripped stem for flexible
4155
+ * matching (handles ${PAI_DIR}/Hooks/foo.sh vs /Users/.../Hooks/foo.sh,
4156
+ * and .ts → .mjs migrations).
4148
4157
  */
4149
4158
  function existingCommandsForHookType(rules) {
4150
4159
  const cmds = /* @__PURE__ */ new Set();
@@ -4152,11 +4161,30 @@ function existingCommandsForHookType(rules) {
4152
4161
  cmds.add(entry.command);
4153
4162
  const base = entry.command.split("/").pop();
4154
4163
  if (base) cmds.add(base);
4164
+ cmds.add(commandStem(entry.command));
4155
4165
  }
4156
4166
  return cmds;
4157
4167
  }
4158
4168
  /**
4169
+ * Find and remove an existing rule whose command has the same stem
4170
+ * (extension-agnostic) as the incoming command. Returns true if a
4171
+ * replacement was made. This handles .ts → .mjs migrations cleanly.
4172
+ */
4173
+ function replaceStaleHook(existingRules, incomingStem, incomingCommand, incomingMatcher) {
4174
+ for (let i = 0; i < existingRules.length; i++) {
4175
+ const rule = existingRules[i];
4176
+ for (let j = 0; j < rule.hooks.length; j++) if (commandStem(rule.hooks[j].command) === incomingStem && rule.hooks[j].command !== incomingCommand) {
4177
+ rule.hooks[j].command = incomingCommand;
4178
+ if (incomingMatcher !== void 0) rule.matcher = incomingMatcher;
4179
+ return true;
4180
+ }
4181
+ }
4182
+ return false;
4183
+ }
4184
+ /**
4159
4185
  * Merge hooks — append entries, deduplicating by command string.
4186
+ * Extension-agnostic: a .mjs hook replaces an existing .ts hook with the
4187
+ * same stem, ensuring clean .ts → .mjs migrations without duplicates.
4160
4188
  */
4161
4189
  function mergeHooks(settings, incoming, report) {
4162
4190
  let changed = false;
@@ -4164,12 +4192,21 @@ function mergeHooks(settings, incoming, report) {
4164
4192
  for (const entry of incoming) {
4165
4193
  const { hookType, matcher, command } = entry;
4166
4194
  const existingRules = Array.isArray(hooksSection[hookType]) ? hooksSection[hookType] : [];
4167
- const existingCmds = existingCommandsForHookType(existingRules);
4168
4195
  const basename = command.split("/").pop() ?? command;
4196
+ const stem = commandStem(command);
4197
+ const existingCmds = existingCommandsForHookType(existingRules);
4169
4198
  if (existingCmds.has(command) || existingCmds.has(basename)) {
4170
4199
  report.push(chalk.dim(` Skipped: hook ${hookType} → ${basename} already registered`));
4171
4200
  continue;
4172
4201
  }
4202
+ if (existingCmds.has(stem)) {
4203
+ if (replaceStaleHook(existingRules, stem, command, matcher)) {
4204
+ hooksSection[hookType] = existingRules;
4205
+ report.push(chalk.yellow(` Upgraded: hook ${hookType} → ${basename} (replaced stale extension)`));
4206
+ changed = true;
4207
+ continue;
4208
+ }
4209
+ }
4173
4210
  const newRule = { hooks: [{
4174
4211
  type: "command",
4175
4212
  command
@@ -4874,6 +4911,7 @@ async function stepTsHooks(rl) {
4874
4911
  line$1();
4875
4912
  let copiedCount = 0;
4876
4913
  let skippedCount = 0;
4914
+ let cleanedCount = 0;
4877
4915
  for (const filename of allFiles) {
4878
4916
  const src = join(distHooksDir, filename);
4879
4917
  const dest = join(hooksTarget, filename);
@@ -4882,6 +4920,12 @@ async function stepTsHooks(rl) {
4882
4920
  if (srcContent === readFileSync(dest, "utf-8")) {
4883
4921
  console.log(c.dim(` Unchanged: ${filename}`));
4884
4922
  skippedCount++;
4923
+ const staleTsPath = join(hooksTarget, filename.replace(/\.mjs$/, ".ts"));
4924
+ if (existsSync(staleTsPath)) {
4925
+ unlinkSync(staleTsPath);
4926
+ console.log(c.ok(`Cleaned up stale: ${filename.replace(/\.mjs$/, ".ts")}`));
4927
+ cleanedCount++;
4928
+ }
4885
4929
  continue;
4886
4930
  }
4887
4931
  }
@@ -4889,11 +4933,22 @@ async function stepTsHooks(rl) {
4889
4933
  chmodSync(dest, 493);
4890
4934
  console.log(c.ok(`Installed: ${filename}`));
4891
4935
  copiedCount++;
4936
+ const staleTsPath = join(hooksTarget, filename.replace(/\.mjs$/, ".ts"));
4937
+ if (existsSync(staleTsPath)) {
4938
+ unlinkSync(staleTsPath);
4939
+ console.log(c.ok(`Cleaned up stale: ${filename.replace(/\.mjs$/, ".ts")}`));
4940
+ cleanedCount++;
4941
+ }
4892
4942
  }
4893
4943
  line$1();
4894
- if (copiedCount > 0) console.log(c.ok(`${copiedCount} hook(s) installed, ${skippedCount} unchanged.`));
4895
- else console.log(c.dim(` All ${skippedCount} hook(s) already up-to-date.`));
4896
- return copiedCount > 0;
4944
+ if (copiedCount > 0 || cleanedCount > 0) {
4945
+ const parts = [];
4946
+ if (copiedCount > 0) parts.push(`${copiedCount} hook(s) installed`);
4947
+ if (skippedCount > 0) parts.push(`${skippedCount} unchanged`);
4948
+ if (cleanedCount > 0) parts.push(`${cleanedCount} stale .ts file(s) cleaned up`);
4949
+ console.log(c.ok(parts.join(", ") + "."));
4950
+ } else console.log(c.dim(` All ${skippedCount} hook(s) already up-to-date.`));
4951
+ return copiedCount > 0 || cleanedCount > 0;
4897
4952
  }
4898
4953
  /**
4899
4954
  * Step 8b: Prompt for the DA (Digital Assistant) name
@@ -4952,6 +5007,11 @@ async function stepSettings(rl, daName) {
4952
5007
  hookType: "SessionStart",
4953
5008
  command: "${PAI_DIR}/Hooks/capture-all-events.mjs --event-type SessionStart"
4954
5009
  },
5010
+ {
5011
+ hookType: "SessionStart",
5012
+ matcher: "compact",
5013
+ command: "${PAI_DIR}/Hooks/post-compact-inject.mjs"
5014
+ },
4955
5015
  {
4956
5016
  hookType: "UserPromptSubmit",
4957
5017
  command: "${PAI_DIR}/Hooks/cleanup-session-files.mjs"