@tekmidian/pai 0.60.1 → 0.62.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 (93) hide show
  1. package/dist/{async-CLvuY7b-.mjs → async-DNVWtQl2.mjs} +2 -2
  2. package/dist/{async-CLvuY7b-.mjs.map → async-DNVWtQl2.mjs.map} +1 -1
  3. package/dist/{chain-CGyAIHo4.mjs → chain-CuaDawK9.mjs} +4 -4
  4. package/dist/{chain-CGyAIHo4.mjs.map → chain-CuaDawK9.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +13 -13
  6. package/dist/cli/program.mjs +13 -13
  7. package/dist/{context-handover-cache-BzM7Qudn.mjs → context-handover-cache-DFNnlWv2.mjs} +4 -4
  8. package/dist/{context-handover-cache-BzM7Qudn.mjs.map → context-handover-cache-DFNnlWv2.mjs.map} +1 -1
  9. package/dist/daemon/index.mjs +10 -10
  10. package/dist/{daemon-B_TodsIS.mjs → daemon-Bf7Eock4.mjs} +45 -24
  11. package/dist/{daemon-B_TodsIS.mjs.map → daemon-Bf7Eock4.mjs.map} +1 -1
  12. package/dist/daemon-bJi1gDkG.mjs +19 -0
  13. package/dist/daemon-mcp/index.mjs +8 -10
  14. package/dist/daemon-mcp/index.mjs.map +1 -1
  15. package/dist/{factory-EUUhSSoq.mjs → factory-BozZa81q.mjs} +3 -3
  16. package/dist/{factory-EUUhSSoq.mjs.map → factory-BozZa81q.mjs.map} +1 -1
  17. package/dist/{factory-DW7BCJJT.mjs → factory-UO9_DFMN.mjs} +1 -1
  18. package/dist/{fallback-CjdakQVu.mjs → fallback-F0TWGVM2.mjs} +11 -13
  19. package/dist/{fallback-CjdakQVu.mjs.map → fallback-F0TWGVM2.mjs.map} +1 -1
  20. package/dist/{helpers-D9WkIf9R.mjs → helpers-Dj-B314s.mjs} +3 -2
  21. package/dist/{helpers-D9WkIf9R.mjs.map → helpers-Dj-B314s.mjs.map} +1 -1
  22. package/dist/hooks/block-sleep-poll.mjs.map +2 -2
  23. package/dist/hooks/context-compression-hook.mjs.map +1 -1
  24. package/dist/hooks/load-project-context.mjs +8513 -133
  25. package/dist/hooks/load-project-context.mjs.map +4 -4
  26. package/dist/hooks/post-compact-inject.mjs.map +1 -1
  27. package/dist/hooks/route-agents-to-worker.mjs +3 -3
  28. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  29. package/dist/hooks/security-validator.mjs +58 -58
  30. package/dist/hooks/security-validator.mjs.map +3 -3
  31. package/dist/hooks/stop-hook.mjs +15 -15
  32. package/dist/hooks/stop-hook.mjs.map +3 -3
  33. package/dist/hooks/update-tab-on-action.mjs +14 -12
  34. package/dist/hooks/update-tab-on-action.mjs.map +3 -3
  35. package/dist/hooks/update-tab-titles.mjs +13 -9
  36. package/dist/hooks/update-tab-titles.mjs.map +3 -3
  37. package/dist/hooks/worker-guard.mjs +7859 -0
  38. package/dist/hooks/worker-guard.mjs.map +7 -0
  39. package/dist/hooks/worker-proxy.mjs +3 -3
  40. package/dist/hooks/worker-proxy.mjs.map +2 -2
  41. package/dist/hooks/worker-status-line.mjs +104 -11
  42. package/dist/hooks/worker-status-line.mjs.map +4 -4
  43. package/dist/hooks/worker-supervision.mjs +3 -3
  44. package/dist/hooks/worker-supervision.mjs.map +2 -2
  45. package/dist/index.mjs +1 -1
  46. package/dist/{indexer-backend-DcDtin1O.mjs → indexer-backend-BN9rpp9O.mjs} +2 -2
  47. package/dist/{main-resolver-BFQfU5xo.mjs → main-resolver-BIft_FVF.mjs} +4 -4
  48. package/dist/{main-resolver-BFQfU5xo.mjs.map → main-resolver-BIft_FVF.mjs.map} +1 -1
  49. package/dist/{main-resolver-VMBENifT.mjs → main-resolver-C9-poMdi.mjs} +2 -2
  50. package/dist/{planner-DN967Lvm.mjs → planner-BG9ivPj0.mjs} +6 -6
  51. package/dist/{planner-DN967Lvm.mjs.map → planner-BG9ivPj0.mjs.map} +1 -1
  52. package/dist/{postgres-nvU5pShF.mjs → postgres-CxD6_Dca.mjs} +3 -3
  53. package/dist/{postgres-nvU5pShF.mjs.map → postgres-CxD6_Dca.mjs.map} +1 -1
  54. package/dist/{program-BnsjB57Q.mjs → program-BRvCzhjx.mjs} +249 -82
  55. package/dist/program-BRvCzhjx.mjs.map +1 -0
  56. package/dist/{run-FeM1_UVa.mjs → run-BVQZI79D.mjs} +610 -253
  57. package/dist/run-BVQZI79D.mjs.map +1 -0
  58. package/dist/{run-env-CEHTeIUM.mjs → run-env-BE5xyCwy.mjs} +25 -6
  59. package/dist/run-env-BE5xyCwy.mjs.map +1 -0
  60. package/dist/{server-CekGOJcp.mjs → server-BY6AedmC.mjs} +2 -2
  61. package/dist/{server-CekGOJcp.mjs.map → server-BY6AedmC.mjs.map} +1 -1
  62. package/dist/{session-keepalive-Bc9aT678.mjs → session-keepalive-DJA97K2X.mjs} +4 -4
  63. package/dist/{session-keepalive-Bc9aT678.mjs.map → session-keepalive-DJA97K2X.mjs.map} +1 -1
  64. package/dist/{sqlite-D9p5a4qe.mjs → sqlite-CfKfuggn.mjs} +2 -2
  65. package/dist/{sqlite-D9p5a4qe.mjs.map → sqlite-CfKfuggn.mjs.map} +1 -1
  66. package/dist/{tools-r-7ZG8B5.mjs → tools-B52IvaU6.mjs} +3 -3
  67. package/dist/{tools-qXyHFwNj.mjs → tools-DFdPbX-X.mjs} +2 -2
  68. package/dist/{tools-qXyHFwNj.mjs.map → tools-DFdPbX-X.mjs.map} +1 -1
  69. package/dist/{vault-indexer-DHYBpRZ5.mjs → vault-indexer-mCFus_AM.mjs} +2 -2
  70. package/dist/{vault-indexer-DHYBpRZ5.mjs.map → vault-indexer-mCFus_AM.mjs.map} +1 -1
  71. package/dist/{wakeup-C48hcN6O.mjs → wakeup-DO1O4udw.mjs} +2 -2
  72. package/dist/{wakeup-C48hcN6O.mjs.map → wakeup-DO1O4udw.mjs.map} +1 -1
  73. package/dist/{work-queue-worker-BL4dq5P7.mjs → work-queue-worker-BbrC3I_R.mjs} +2 -2
  74. package/dist/{work-queue-worker-BL4dq5P7.mjs.map → work-queue-worker-BbrC3I_R.mjs.map} +1 -1
  75. package/dist/work-queue-worker-z-AATZ2t.mjs +11 -0
  76. package/docs/commands/README.md +2 -0
  77. package/docs/commands/worker.md +28 -0
  78. package/package.json +2 -2
  79. package/scripts/build-hooks.mjs +6 -5
  80. package/scripts/build-skill-stubs.mjs +6 -5
  81. package/src/hooks/ts/lib/tab-title.ts +18 -0
  82. package/src/hooks/ts/lib/worker-guard.test.ts +188 -0
  83. package/src/hooks/ts/lib/worker-guard.ts +315 -0
  84. package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +3 -20
  85. package/src/hooks/ts/pre-tool-use/worker-guard.ts +75 -0
  86. package/src/hooks/ts/session-start/load-project-context.ts +14 -0
  87. package/src/hooks/ts/stop/stop-hook.ts +4 -15
  88. package/src/hooks/ts/user-prompt/update-tab-titles.ts +2 -10
  89. package/dist/daemon-jviL3VVJ.mjs +0 -19
  90. package/dist/program-BnsjB57Q.mjs.map +0 -1
  91. package/dist/run-FeM1_UVa.mjs.map +0 -1
  92. package/dist/run-env-CEHTeIUM.mjs.map +0 -1
  93. package/dist/work-queue-worker-DVOVcEnO.mjs +0 -11
@@ -8,8 +8,19 @@ function isWorkerSession(env = process.env) {
8
8
  return env.PAI_WORKER === "1";
9
9
  }
10
10
 
11
+ // src/hooks/ts/lib/tab-title.ts
12
+ var CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
13
+ function setTabTitle(title) {
14
+ const sanitized = title.replace(CONTROL_CHARS_RE, "");
15
+ try {
16
+ process.stderr.write(`\x1B]0;${sanitized}\x07`);
17
+ process.stderr.write(`\x1B]2;${sanitized}\x07`);
18
+ process.stderr.write(`\x1B]30;${sanitized}\x07`);
19
+ } catch {
20
+ }
21
+ }
22
+
11
23
  // src/hooks/ts/post-tool-use/update-tab-on-action.ts
12
- import { execSync } from "child_process";
13
24
  var ACTION_TOOLS = /* @__PURE__ */ new Set([
14
25
  "Edit",
15
26
  "Write",
@@ -37,16 +48,6 @@ async function readStdinWithTimeout(timeout = 3e3) {
37
48
  });
38
49
  });
39
50
  }
40
- function setTabTitle(title) {
41
- try {
42
- const truncated = title.length > 50 ? title.slice(0, 47) + "..." : title;
43
- const escaped = truncated.replace(/'/g, "'\\''");
44
- execSync(`printf '\\033]0;${escaped}\\007' >&2`, { stdio: ["pipe", "pipe", "inherit"] });
45
- execSync(`printf '\\033]2;${escaped}\\007' >&2`, { stdio: ["pipe", "pipe", "inherit"] });
46
- execSync(`printf '\\033]30;${escaped}\\007' >&2`, { stdio: ["pipe", "pipe", "inherit"] });
47
- } catch {
48
- }
49
- }
50
51
  function generateTitle(toolName, input) {
51
52
  if (input.description) {
52
53
  return input.description;
@@ -89,7 +90,8 @@ async function main() {
89
90
  process.exit(0);
90
91
  }
91
92
  const title = generateTitle(data.tool_name, data.tool_input);
92
- setTabTitle(title);
93
+ const truncated = title.length > 50 ? title.slice(0, 47) + "..." : title;
94
+ setTabTitle(truncated);
93
95
  process.exit(0);
94
96
  } catch {
95
97
  process.exit(0);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/post-tool-use/update-tab-on-action.ts"],
4
- "sourcesContent": ["/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n", "#!/usr/bin/env node\n/**\n * Tab Title Update on Action Tools\n * Updates Kitty tab title when action tools complete (Edit, Write, Bash, Task)\n * Uses the tool's description directly - no AI call needed\n *\n * PAI-safe: No sensitive data, just tool descriptions\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\nimport { execSync } from 'child_process';\n\n// Action tools that warrant a tab title update\nconst ACTION_TOOLS = new Set([\n 'Edit',\n 'Write',\n 'Bash',\n 'Task',\n 'NotebookEdit',\n 'MultiEdit'\n]);\n\ninterface HookInput {\n session_id: string;\n tool_name: string;\n tool_input: {\n description?: string;\n command?: string;\n file_path?: string;\n prompt?: string;\n subagent_type?: string;\n };\n tool_response?: unknown;\n hook_event_name: string;\n}\n\n/**\n * Read stdin with timeout\n */\nasync function readStdinWithTimeout(timeout: number = 3000): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n const timer = setTimeout(() => {\n reject(new Error('Timeout'));\n }, timeout);\n\n process.stdin.on('data', (chunk) => {\n data += chunk.toString();\n });\n\n process.stdin.on('end', () => {\n clearTimeout(timer);\n resolve(data);\n });\n\n process.stdin.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n}\n\n/**\n * Update Kitty tab title using escape codes\n */\nfunction setTabTitle(title: string): void {\n try {\n // Truncate to reasonable length\n const truncated = title.length > 50 ? title.slice(0, 47) + '...' : title;\n const escaped = truncated.replace(/'/g, \"'\\\\''\");\n\n // Multiple escape sequences for compatibility\n execSync(`printf '\\\\033]0;${escaped}\\\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] });\n execSync(`printf '\\\\033]2;${escaped}\\\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] });\n execSync(`printf '\\\\033]30;${escaped}\\\\007' >&2`, { stdio: ['pipe', 'pipe', 'inherit'] });\n } catch {\n // Silently fail - don't interrupt Claude's work\n }\n}\n\n/**\n * Generate a human-readable title from tool input\n */\nfunction generateTitle(toolName: string, input: HookInput['tool_input']): string {\n // Use explicit description if provided\n if (input.description) {\n return input.description;\n }\n\n // Generate based on tool type\n switch (toolName) {\n case 'Edit':\n case 'Write':\n case 'MultiEdit':\n case 'NotebookEdit':\n if (input.file_path) {\n const filename = input.file_path.split('/').pop() || input.file_path;\n return `Editing ${filename}`;\n }\n return `Editing file`;\n\n case 'Bash':\n if (input.command) {\n // Extract first command word\n const cmd = input.command.split(/\\s+/)[0].split('/').pop() || 'command';\n return `Running ${cmd}`;\n }\n return 'Running command';\n\n case 'Task':\n if (input.subagent_type) {\n return `Agent: ${input.subagent_type}`;\n }\n if (input.prompt) {\n const words = input.prompt.slice(0, 30).split(/\\s+/).slice(0, 3).join(' ');\n return `Task: ${words}...`;\n }\n return 'Spawning agent';\n\n default:\n return `${toolName}...`;\n }\n}\n\nasync function main() {\n if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n try {\n const input = await readStdinWithTimeout();\n const data: HookInput = JSON.parse(input);\n\n // Only process action tools\n if (!ACTION_TOOLS.has(data.tool_name)) {\n process.exit(0);\n }\n\n // Generate and set title\n const title = generateTitle(data.tool_name, data.tool_input);\n setTabTitle(title);\n\n process.exit(0);\n } catch {\n // Silently exit - don't interrupt Claude's flow\n process.exit(0);\n }\n}\n\nmain();\n"],
5
- "mappings": ";;;;;;AAoBO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;ACZA,SAAS,gBAAgB;AAGzB,IAAM,eAAe,oBAAI,IAAI;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAmBD,eAAe,qBAAqB,UAAkB,KAAuB;AAC3E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AACX,UAAM,QAAQ,WAAW,MAAM;AAC7B,aAAO,IAAI,MAAM,SAAS,CAAC;AAAA,IAC7B,GAAG,OAAO;AAEV,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AAClC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AAED,YAAQ,MAAM,GAAG,OAAO,MAAM;AAC5B,mBAAa,KAAK;AAClB,cAAQ,IAAI;AAAA,IACd,CAAC;AAED,YAAQ,MAAM,GAAG,SAAS,CAAC,QAAQ;AACjC,mBAAa,KAAK;AAClB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;AAKA,SAAS,YAAY,OAAqB;AACxC,MAAI;AAEF,UAAM,YAAY,MAAM,SAAS,KAAK,MAAM,MAAM,GAAG,EAAE,IAAI,QAAQ;AACnE,UAAM,UAAU,UAAU,QAAQ,MAAM,OAAO;AAG/C,aAAS,mBAAmB,OAAO,cAAc,EAAE,OAAO,CAAC,QAAQ,QAAQ,SAAS,EAAE,CAAC;AACvF,aAAS,mBAAmB,OAAO,cAAc,EAAE,OAAO,CAAC,QAAQ,QAAQ,SAAS,EAAE,CAAC;AACvF,aAAS,oBAAoB,OAAO,cAAc,EAAE,OAAO,CAAC,QAAQ,QAAQ,SAAS,EAAE,CAAC;AAAA,EAC1F,QAAQ;AAAA,EAER;AACF;AAKA,SAAS,cAAc,UAAkB,OAAwC;AAE/E,MAAI,MAAM,aAAa;AACrB,WAAO,MAAM;AAAA,EACf;AAGA,UAAQ,UAAU;AAAA,IAChB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,UAAI,MAAM,WAAW;AACnB,cAAM,WAAW,MAAM,UAAU,MAAM,GAAG,EAAE,IAAI,KAAK,MAAM;AAC3D,eAAO,WAAW,QAAQ;AAAA,MAC5B;AACA,aAAO;AAAA,IAET,KAAK;AACH,UAAI,MAAM,SAAS;AAEjB,cAAM,MAAM,MAAM,QAAQ,MAAM,KAAK,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK;AAC9D,eAAO,WAAW,GAAG;AAAA,MACvB;AACA,aAAO;AAAA,IAET,KAAK;AACH,UAAI,MAAM,eAAe;AACvB,eAAO,UAAU,MAAM,aAAa;AAAA,MACtC;AACA,UAAI,MAAM,QAAQ;AAChB,cAAM,QAAQ,MAAM,OAAO,MAAM,GAAG,EAAE,EAAE,MAAM,KAAK,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AACzE,eAAO,SAAS,KAAK;AAAA,MACvB;AACA,aAAO;AAAA,IAET;AACE,aAAO,GAAG,QAAQ;AAAA,EACtB;AACF;AAEA,eAAe,OAAO;AACpB,MAAI,gBAAgB,EAAG;AACvB,MAAI;AACF,UAAM,QAAQ,MAAM,qBAAqB;AACzC,UAAM,OAAkB,KAAK,MAAM,KAAK;AAGxC,QAAI,CAAC,aAAa,IAAI,KAAK,SAAS,GAAG;AACrC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAGA,UAAM,QAAQ,cAAc,KAAK,WAAW,KAAK,UAAU;AAC3D,gBAAY,KAAK;AAEjB,YAAQ,KAAK,CAAC;AAAA,EAChB,QAAQ;AAEN,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;",
3
+ "sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/lib/tab-title.ts", "../../../src/hooks/ts/post-tool-use/update-tab-on-action.ts"],
4
+ "sourcesContent": ["/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n", "/**\n * Terminal tab title escape sequences (OSC 0/2/30), written directly to\n * stderr. No shell involved, so untrusted title text (derived from user\n * prompts) can't reach a shell for interpolation.\n */\n\nconst CONTROL_CHARS_RE = /[\\x00-\\x1f\\x7f]/g;\n\nexport function setTabTitle(title: string): void {\n const sanitized = title.replace(CONTROL_CHARS_RE, '');\n try {\n process.stderr.write(`\\x1b]0;${sanitized}\\x07`);\n process.stderr.write(`\\x1b]2;${sanitized}\\x07`);\n process.stderr.write(`\\x1b]30;${sanitized}\\x07`);\n } catch {\n // Silently fail - don't interrupt Claude's work\n }\n}\n", "#!/usr/bin/env node\n/**\n * Tab Title Update on Action Tools\n * Updates Kitty tab title when action tools complete (Edit, Write, Bash, Task)\n * Uses the tool's description directly - no AI call needed\n *\n * PAI-safe: No sensitive data, just tool descriptions\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\nimport { setTabTitle } from \"../lib/tab-title.js\";\n\n// Action tools that warrant a tab title update\nconst ACTION_TOOLS = new Set([\n 'Edit',\n 'Write',\n 'Bash',\n 'Task',\n 'NotebookEdit',\n 'MultiEdit'\n]);\n\ninterface HookInput {\n session_id: string;\n tool_name: string;\n tool_input: {\n description?: string;\n command?: string;\n file_path?: string;\n prompt?: string;\n subagent_type?: string;\n };\n tool_response?: unknown;\n hook_event_name: string;\n}\n\n/**\n * Read stdin with timeout\n */\nasync function readStdinWithTimeout(timeout: number = 3000): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n const timer = setTimeout(() => {\n reject(new Error('Timeout'));\n }, timeout);\n\n process.stdin.on('data', (chunk) => {\n data += chunk.toString();\n });\n\n process.stdin.on('end', () => {\n clearTimeout(timer);\n resolve(data);\n });\n\n process.stdin.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n}\n\n/**\n * Generate a human-readable title from tool input\n */\nfunction generateTitle(toolName: string, input: HookInput['tool_input']): string {\n // Use explicit description if provided\n if (input.description) {\n return input.description;\n }\n\n // Generate based on tool type\n switch (toolName) {\n case 'Edit':\n case 'Write':\n case 'MultiEdit':\n case 'NotebookEdit':\n if (input.file_path) {\n const filename = input.file_path.split('/').pop() || input.file_path;\n return `Editing ${filename}`;\n }\n return `Editing file`;\n\n case 'Bash':\n if (input.command) {\n // Extract first command word\n const cmd = input.command.split(/\\s+/)[0].split('/').pop() || 'command';\n return `Running ${cmd}`;\n }\n return 'Running command';\n\n case 'Task':\n if (input.subagent_type) {\n return `Agent: ${input.subagent_type}`;\n }\n if (input.prompt) {\n const words = input.prompt.slice(0, 30).split(/\\s+/).slice(0, 3).join(' ');\n return `Task: ${words}...`;\n }\n return 'Spawning agent';\n\n default:\n return `${toolName}...`;\n }\n}\n\nasync function main() {\n if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n try {\n const input = await readStdinWithTimeout();\n const data: HookInput = JSON.parse(input);\n\n // Only process action tools\n if (!ACTION_TOOLS.has(data.tool_name)) {\n process.exit(0);\n }\n\n // Generate and set title\n const title = generateTitle(data.tool_name, data.tool_input);\n const truncated = title.length > 50 ? title.slice(0, 47) + '...' : title;\n setTabTitle(truncated);\n\n process.exit(0);\n } catch {\n // Silently exit - don't interrupt Claude's flow\n process.exit(0);\n }\n}\n\nmain();\n"],
5
+ "mappings": ";;;;;;AAoBO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;AChBA,IAAM,mBAAmB;AAElB,SAAS,YAAY,OAAqB;AAC/C,QAAM,YAAY,MAAM,QAAQ,kBAAkB,EAAE;AACpD,MAAI;AACF,YAAQ,OAAO,MAAM,UAAU,SAAS,MAAM;AAC9C,YAAQ,OAAO,MAAM,UAAU,SAAS,MAAM;AAC9C,YAAQ,OAAO,MAAM,WAAW,SAAS,MAAM;AAAA,EACjD,QAAQ;AAAA,EAER;AACF;;;ACJA,IAAM,eAAe,oBAAI,IAAI;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAmBD,eAAe,qBAAqB,UAAkB,KAAuB;AAC3E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AACX,UAAM,QAAQ,WAAW,MAAM;AAC7B,aAAO,IAAI,MAAM,SAAS,CAAC;AAAA,IAC7B,GAAG,OAAO;AAEV,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AAClC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AAED,YAAQ,MAAM,GAAG,OAAO,MAAM;AAC5B,mBAAa,KAAK;AAClB,cAAQ,IAAI;AAAA,IACd,CAAC;AAED,YAAQ,MAAM,GAAG,SAAS,CAAC,QAAQ;AACjC,mBAAa,KAAK;AAClB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;AAKA,SAAS,cAAc,UAAkB,OAAwC;AAE/E,MAAI,MAAM,aAAa;AACrB,WAAO,MAAM;AAAA,EACf;AAGA,UAAQ,UAAU;AAAA,IAChB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,UAAI,MAAM,WAAW;AACnB,cAAM,WAAW,MAAM,UAAU,MAAM,GAAG,EAAE,IAAI,KAAK,MAAM;AAC3D,eAAO,WAAW,QAAQ;AAAA,MAC5B;AACA,aAAO;AAAA,IAET,KAAK;AACH,UAAI,MAAM,SAAS;AAEjB,cAAM,MAAM,MAAM,QAAQ,MAAM,KAAK,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK;AAC9D,eAAO,WAAW,GAAG;AAAA,MACvB;AACA,aAAO;AAAA,IAET,KAAK;AACH,UAAI,MAAM,eAAe;AACvB,eAAO,UAAU,MAAM,aAAa;AAAA,MACtC;AACA,UAAI,MAAM,QAAQ;AAChB,cAAM,QAAQ,MAAM,OAAO,MAAM,GAAG,EAAE,EAAE,MAAM,KAAK,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AACzE,eAAO,SAAS,KAAK;AAAA,MACvB;AACA,aAAO;AAAA,IAET;AACE,aAAO,GAAG,QAAQ;AAAA,EACtB;AACF;AAEA,eAAe,OAAO;AACpB,MAAI,gBAAgB,EAAG;AACvB,MAAI;AACF,UAAM,QAAQ,MAAM,qBAAqB;AACzC,UAAM,OAAkB,KAAK,MAAM,KAAK;AAGxC,QAAI,CAAC,aAAa,IAAI,KAAK,SAAS,GAAG;AACrC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAGA,UAAM,QAAQ,cAAc,KAAK,WAAW,KAAK,UAAU;AAC3D,UAAM,YAAY,MAAM,SAAS,KAAK,MAAM,MAAM,GAAG,EAAE,IAAI,QAAQ;AACnE,gBAAY,SAAS;AAErB,YAAQ,KAAK,CAAC;AAAA,EAChB,QAAQ;AAEN,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;",
6
6
  "names": []
7
7
  }
@@ -8,8 +8,19 @@ function isWorkerSession(env = process.env) {
8
8
  return env.PAI_WORKER === "1";
9
9
  }
10
10
 
11
+ // src/hooks/ts/lib/tab-title.ts
12
+ var CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
13
+ function setTabTitle(title) {
14
+ const sanitized = title.replace(CONTROL_CHARS_RE, "");
15
+ try {
16
+ process.stderr.write(`\x1B]0;${sanitized}\x07`);
17
+ process.stderr.write(`\x1B]2;${sanitized}\x07`);
18
+ process.stderr.write(`\x1B]30;${sanitized}\x07`);
19
+ } catch {
20
+ }
21
+ }
22
+
11
23
  // src/hooks/ts/user-prompt/update-tab-titles.ts
12
- import { execSync } from "child_process";
13
24
  async function readStdinWithTimeout(timeout = 5e3) {
14
25
  return new Promise((resolve, reject) => {
15
26
  let data = "";
@@ -46,14 +57,7 @@ async function main() {
46
57
  tabTitle += "...";
47
58
  }
48
59
  }
49
- try {
50
- const titleWithEmoji = "\u267B\uFE0F " + tabTitle;
51
- const escapedTitle = titleWithEmoji.replace(/'/g, "'\\''");
52
- execSync(`printf '\\033]0;${escapedTitle}\\007' >&2`);
53
- execSync(`printf '\\033]2;${escapedTitle}\\007' >&2`);
54
- execSync(`printf '\\033]30;${escapedTitle}\\007' >&2`);
55
- } catch (e) {
56
- }
60
+ setTabTitle("\u267B\uFE0F " + tabTitle);
57
61
  process.exit(0);
58
62
  } catch (error) {
59
63
  console.error("Tab title update error:", error);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/user-prompt/update-tab-titles.ts"],
4
- "sourcesContent": ["/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n", "#!/usr/bin/env node\n/**\n * Tab Title Update Hook\n * Updates tab title based on user prompt\n *\n * Note: All context loading handled by PAI skill system (core identity in skill description, full context in SKILL.md)\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\nimport { execSync } from 'child_process';\n\ninterface HookInput {\n session_id: string;\n prompt: string;\n transcript_path: string;\n hook_event_name: string;\n}\n\n/**\n * Read stdin with timeout\n */\nasync function readStdinWithTimeout(timeout: number = 5000): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n const timer = setTimeout(() => {\n reject(new Error('Timeout reading from stdin'));\n }, timeout);\n\n process.stdin.on('data', (chunk) => {\n data += chunk.toString();\n });\n\n process.stdin.on('end', () => {\n clearTimeout(timer);\n resolve(data);\n });\n\n process.stdin.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n}\n\nasync function main() {\n if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n try {\n // Read the hook input from stdin\n const input = await readStdinWithTimeout();\n const data: HookInput = JSON.parse(input);\n\n const prompt = data.prompt || '';\n\n // UPDATE TAB TITLE\n // Generate quick fallback tab title\n let tabTitle = 'Processing request...';\n if (prompt) {\n const words = prompt.replace(/[^\\w\\s]/g, ' ').trim().split(/\\s+/)\n .filter(w => w.length > 2 && !['the', 'and', 'but', 'for', 'are', 'with', 'you', 'can'].includes(w.toLowerCase()))\n .slice(0, 3);\n\n if (words.length > 0) {\n tabTitle = words[0].charAt(0).toUpperCase() + words[0].slice(1).toLowerCase();\n if (words.length > 1) {\n tabTitle += ' ' + words.slice(1).map(w => w.toLowerCase()).join(' ');\n }\n tabTitle += '...';\n }\n }\n\n // Set initial tab title with recycle emoji\n try {\n const titleWithEmoji = '\u267B\uFE0F ' + tabTitle;\n const escapedTitle = titleWithEmoji.replace(/'/g, \"'\\\\''\");\n execSync(`printf '\\\\033]0;${escapedTitle}\\\\007' >&2`);\n execSync(`printf '\\\\033]2;${escapedTitle}\\\\007' >&2`);\n execSync(`printf '\\\\033]30;${escapedTitle}\\\\007' >&2`);\n } catch (e) {\n // Silently fail\n }\n\n process.exit(0);\n } catch (error) {\n // Silently fail to not interrupt Claude's flow\n console.error('Tab title update error:', error);\n process.exit(0);\n }\n}\n\nmain();\n"],
5
- "mappings": ";;;;;;AAoBO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;ACbA,SAAS,gBAAgB;AAYzB,eAAe,qBAAqB,UAAkB,KAAuB;AAC3E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AACX,UAAM,QAAQ,WAAW,MAAM;AAC7B,aAAO,IAAI,MAAM,4BAA4B,CAAC;AAAA,IAChD,GAAG,OAAO;AAEV,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AAClC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AAED,YAAQ,MAAM,GAAG,OAAO,MAAM;AAC5B,mBAAa,KAAK;AAClB,cAAQ,IAAI;AAAA,IACd,CAAC;AAED,YAAQ,MAAM,GAAG,SAAS,CAAC,QAAQ;AACjC,mBAAa,KAAK;AAClB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAe,OAAO;AACpB,MAAI,gBAAgB,EAAG;AACvB,MAAI;AAEF,UAAM,QAAQ,MAAM,qBAAqB;AACzC,UAAM,OAAkB,KAAK,MAAM,KAAK;AAExC,UAAM,SAAS,KAAK,UAAU;AAI9B,QAAI,WAAW;AACf,QAAI,QAAQ;AACV,YAAM,QAAQ,OAAO,QAAQ,YAAY,GAAG,EAAE,KAAK,EAAE,MAAM,KAAK,EAC7D,OAAO,OAAK,EAAE,SAAS,KAAK,CAAC,CAAC,OAAO,OAAO,OAAO,OAAO,OAAO,QAAQ,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,EAChH,MAAM,GAAG,CAAC;AAEb,UAAI,MAAM,SAAS,GAAG;AACpB,mBAAW,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,YAAY;AAC5E,YAAI,MAAM,SAAS,GAAG;AACpB,sBAAY,MAAM,MAAM,MAAM,CAAC,EAAE,IAAI,OAAK,EAAE,YAAY,CAAC,EAAE,KAAK,GAAG;AAAA,QACrE;AACA,oBAAY;AAAA,MACd;AAAA,IACF;AAGA,QAAI;AACF,YAAM,iBAAiB,kBAAQ;AAC/B,YAAM,eAAe,eAAe,QAAQ,MAAM,OAAO;AACzD,eAAS,mBAAmB,YAAY,YAAY;AACpD,eAAS,mBAAmB,YAAY,YAAY;AACpD,eAAS,oBAAoB,YAAY,YAAY;AAAA,IACvD,SAAS,GAAG;AAAA,IAEZ;AAEA,YAAQ,KAAK,CAAC;AAAA,EAChB,SAAS,OAAO;AAEd,YAAQ,MAAM,2BAA2B,KAAK;AAC9C,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;",
3
+ "sources": ["../../../src/hooks/ts/lib/worker-session.ts", "../../../src/hooks/ts/lib/tab-title.ts", "../../../src/hooks/ts/user-prompt/update-tab-titles.ts"],
4
+ "sourcesContent": ["/**\n * Worker-session detection.\n *\n * A disposable headless worker (a `claude -p` run started by an orchestrating\n * session, possibly against a different model provider with a different\n * context window) shares the project directory and the hook configuration\n * with the real session that spawned it. Left alone, the hooks treat it as a\n * session in its own right: they inject project context into it, create and\n * rename a numbered session note for it, autosave it, and enqueue a\n * model-written handover for it. Its compactions also land in the project's\n * transcript folder, where they are indistinguishable from a real session's\n * and drag the measured compaction trigger down (observed 2026-09-17: two\n * workers compacting at ~151k tokens pulled a project's trigger from ~784k\n * to ~151k, and the real session's handover fired at ~50k tokens).\n *\n * The launcher marks such sessions with `PAI_WORKER=1`. Every hook that does\n * per-session bookkeeping returns immediately when this predicate is true.\n * Deliberately NOT guarded: the security validator (a worker's shell\n * commands must still be checked) and observability capture.\n */\nexport function isWorkerSession(env: NodeJS.ProcessEnv = process.env): boolean {\n return env.PAI_WORKER === \"1\";\n}\n", "/**\n * Terminal tab title escape sequences (OSC 0/2/30), written directly to\n * stderr. No shell involved, so untrusted title text (derived from user\n * prompts) can't reach a shell for interpolation.\n */\n\nconst CONTROL_CHARS_RE = /[\\x00-\\x1f\\x7f]/g;\n\nexport function setTabTitle(title: string): void {\n const sanitized = title.replace(CONTROL_CHARS_RE, '');\n try {\n process.stderr.write(`\\x1b]0;${sanitized}\\x07`);\n process.stderr.write(`\\x1b]2;${sanitized}\\x07`);\n process.stderr.write(`\\x1b]30;${sanitized}\\x07`);\n } catch {\n // Silently fail - don't interrupt Claude's work\n }\n}\n", "#!/usr/bin/env node\n/**\n * Tab Title Update Hook\n * Updates tab title based on user prompt\n *\n * Note: All context loading handled by PAI skill system (core identity in skill description, full context in SKILL.md)\n */\n\nimport { isWorkerSession } from \"../lib/worker-session.js\";\nimport { setTabTitle } from \"../lib/tab-title.js\";\n\ninterface HookInput {\n session_id: string;\n prompt: string;\n transcript_path: string;\n hook_event_name: string;\n}\n\n/**\n * Read stdin with timeout\n */\nasync function readStdinWithTimeout(timeout: number = 5000): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n const timer = setTimeout(() => {\n reject(new Error('Timeout reading from stdin'));\n }, timeout);\n\n process.stdin.on('data', (chunk) => {\n data += chunk.toString();\n });\n\n process.stdin.on('end', () => {\n clearTimeout(timer);\n resolve(data);\n });\n\n process.stdin.on('error', (err) => {\n clearTimeout(timer);\n reject(err);\n });\n });\n}\n\nasync function main() {\n if (isWorkerSession()) return; // disposable worker: no per-session bookkeeping\n try {\n // Read the hook input from stdin\n const input = await readStdinWithTimeout();\n const data: HookInput = JSON.parse(input);\n\n const prompt = data.prompt || '';\n\n // UPDATE TAB TITLE\n // Generate quick fallback tab title\n let tabTitle = 'Processing request...';\n if (prompt) {\n const words = prompt.replace(/[^\\w\\s]/g, ' ').trim().split(/\\s+/)\n .filter(w => w.length > 2 && !['the', 'and', 'but', 'for', 'are', 'with', 'you', 'can'].includes(w.toLowerCase()))\n .slice(0, 3);\n\n if (words.length > 0) {\n tabTitle = words[0].charAt(0).toUpperCase() + words[0].slice(1).toLowerCase();\n if (words.length > 1) {\n tabTitle += ' ' + words.slice(1).map(w => w.toLowerCase()).join(' ');\n }\n tabTitle += '...';\n }\n }\n\n // Set initial tab title with recycle emoji\n setTabTitle('\u267B\uFE0F ' + tabTitle);\n\n process.exit(0);\n } catch (error) {\n // Silently fail to not interrupt Claude's flow\n console.error('Tab title update error:', error);\n process.exit(0);\n }\n}\n\nmain();\n"],
5
+ "mappings": ";;;;;;AAoBO,SAAS,gBAAgB,MAAyB,QAAQ,KAAc;AAC7E,SAAO,IAAI,eAAe;AAC5B;;;AChBA,IAAM,mBAAmB;AAElB,SAAS,YAAY,OAAqB;AAC/C,QAAM,YAAY,MAAM,QAAQ,kBAAkB,EAAE;AACpD,MAAI;AACF,YAAQ,OAAO,MAAM,UAAU,SAAS,MAAM;AAC9C,YAAQ,OAAO,MAAM,UAAU,SAAS,MAAM;AAC9C,YAAQ,OAAO,MAAM,WAAW,SAAS,MAAM;AAAA,EACjD,QAAQ;AAAA,EAER;AACF;;;ACIA,eAAe,qBAAqB,UAAkB,KAAuB;AAC3E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AACX,UAAM,QAAQ,WAAW,MAAM;AAC7B,aAAO,IAAI,MAAM,4BAA4B,CAAC;AAAA,IAChD,GAAG,OAAO;AAEV,YAAQ,MAAM,GAAG,QAAQ,CAAC,UAAU;AAClC,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AAED,YAAQ,MAAM,GAAG,OAAO,MAAM;AAC5B,mBAAa,KAAK;AAClB,cAAQ,IAAI;AAAA,IACd,CAAC;AAED,YAAQ,MAAM,GAAG,SAAS,CAAC,QAAQ;AACjC,mBAAa,KAAK;AAClB,aAAO,GAAG;AAAA,IACZ,CAAC;AAAA,EACH,CAAC;AACH;AAEA,eAAe,OAAO;AACpB,MAAI,gBAAgB,EAAG;AACvB,MAAI;AAEF,UAAM,QAAQ,MAAM,qBAAqB;AACzC,UAAM,OAAkB,KAAK,MAAM,KAAK;AAExC,UAAM,SAAS,KAAK,UAAU;AAI9B,QAAI,WAAW;AACf,QAAI,QAAQ;AACV,YAAM,QAAQ,OAAO,QAAQ,YAAY,GAAG,EAAE,KAAK,EAAE,MAAM,KAAK,EAC7D,OAAO,OAAK,EAAE,SAAS,KAAK,CAAC,CAAC,OAAO,OAAO,OAAO,OAAO,OAAO,QAAQ,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC,EAChH,MAAM,GAAG,CAAC;AAEb,UAAI,MAAM,SAAS,GAAG;AACpB,mBAAW,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,YAAY;AAC5E,YAAI,MAAM,SAAS,GAAG;AACpB,sBAAY,MAAM,MAAM,MAAM,CAAC,EAAE,IAAI,OAAK,EAAE,YAAY,CAAC,EAAE,KAAK,GAAG;AAAA,QACrE;AACA,oBAAY;AAAA,MACd;AAAA,IACF;AAGA,gBAAY,kBAAQ,QAAQ;AAE5B,YAAQ,KAAK,CAAC;AAAA,EAChB,SAAS,OAAO;AAEd,YAAQ,MAAM,2BAA2B,KAAK;AAC9C,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,KAAK;",
6
6
  "names": []
7
7
  }