@tekmidian/pai 0.3.2 → 0.5.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 (101) hide show
  1. package/ARCHITECTURE.md +16 -10
  2. package/README.md +46 -6
  3. package/dist/{auto-route-JjW3f7pV.mjs → auto-route-B5MSUJZK.mjs} +3 -3
  4. package/dist/{auto-route-JjW3f7pV.mjs.map → auto-route-B5MSUJZK.mjs.map} +1 -1
  5. package/dist/cli/index.mjs +313 -43
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/{config-DELNqq3Z.mjs → config-B4brrHHE.mjs} +1 -1
  8. package/dist/{config-DELNqq3Z.mjs.map → config-B4brrHHE.mjs.map} +1 -1
  9. package/dist/daemon/index.mjs +7 -7
  10. package/dist/daemon-mcp/index.mjs +11 -4
  11. package/dist/daemon-mcp/index.mjs.map +1 -1
  12. package/dist/{daemon-CeTX4NpF.mjs → daemon-s868Paua.mjs} +12 -12
  13. package/dist/{daemon-CeTX4NpF.mjs.map → daemon-s868Paua.mjs.map} +1 -1
  14. package/dist/{detect-D7gPV3fQ.mjs → detect-CdaA48EI.mjs} +1 -1
  15. package/dist/{detect-D7gPV3fQ.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
  16. package/dist/{detector-cYYhK2Mi.mjs → detector-Bp-2SM3x.mjs} +2 -2
  17. package/dist/{detector-cYYhK2Mi.mjs.map → detector-Bp-2SM3x.mjs.map} +1 -1
  18. package/dist/{factory-DZLvRf4m.mjs → factory-CeXQzlwn.mjs} +3 -3
  19. package/dist/{factory-DZLvRf4m.mjs.map → factory-CeXQzlwn.mjs.map} +1 -1
  20. package/dist/hooks/capture-all-events.mjs +238 -0
  21. package/dist/hooks/capture-all-events.mjs.map +7 -0
  22. package/dist/hooks/capture-session-summary.mjs +198 -0
  23. package/dist/hooks/capture-session-summary.mjs.map +7 -0
  24. package/dist/hooks/capture-tool-output.mjs +105 -0
  25. package/dist/hooks/capture-tool-output.mjs.map +7 -0
  26. package/dist/hooks/cleanup-session-files.mjs +129 -0
  27. package/dist/hooks/cleanup-session-files.mjs.map +7 -0
  28. package/dist/hooks/context-compression-hook.mjs +283 -0
  29. package/dist/hooks/context-compression-hook.mjs.map +7 -0
  30. package/dist/hooks/initialize-session.mjs +206 -0
  31. package/dist/hooks/initialize-session.mjs.map +7 -0
  32. package/dist/hooks/load-core-context.mjs +110 -0
  33. package/dist/hooks/load-core-context.mjs.map +7 -0
  34. package/dist/hooks/load-project-context.mjs +548 -0
  35. package/dist/hooks/load-project-context.mjs.map +7 -0
  36. package/dist/hooks/security-validator.mjs +159 -0
  37. package/dist/hooks/security-validator.mjs.map +7 -0
  38. package/dist/hooks/stop-hook.mjs +625 -0
  39. package/dist/hooks/stop-hook.mjs.map +7 -0
  40. package/dist/hooks/subagent-stop-hook.mjs +152 -0
  41. package/dist/hooks/subagent-stop-hook.mjs.map +7 -0
  42. package/dist/hooks/sync-todo-to-md.mjs +322 -0
  43. package/dist/hooks/sync-todo-to-md.mjs.map +7 -0
  44. package/dist/hooks/update-tab-on-action.mjs +90 -0
  45. package/dist/hooks/update-tab-on-action.mjs.map +7 -0
  46. package/dist/hooks/update-tab-titles.mjs +55 -0
  47. package/dist/hooks/update-tab-titles.mjs.map +7 -0
  48. package/dist/index.d.mts +29 -1
  49. package/dist/index.d.mts.map +1 -1
  50. package/dist/index.mjs +4 -3
  51. package/dist/{indexer-backend-BHztlJJg.mjs → indexer-backend-DQO-FqAI.mjs} +1 -1
  52. package/dist/{indexer-backend-BHztlJJg.mjs.map → indexer-backend-DQO-FqAI.mjs.map} +1 -1
  53. package/dist/{ipc-client-CLt2fNlC.mjs → ipc-client-CgSpwHDC.mjs} +1 -1
  54. package/dist/{ipc-client-CLt2fNlC.mjs.map → ipc-client-CgSpwHDC.mjs.map} +1 -1
  55. package/dist/mcp/index.mjs +15 -5
  56. package/dist/mcp/index.mjs.map +1 -1
  57. package/dist/{postgres-CRBe30Ag.mjs → postgres-CIxeqf_n.mjs} +1 -1
  58. package/dist/{postgres-CRBe30Ag.mjs.map → postgres-CIxeqf_n.mjs.map} +1 -1
  59. package/dist/reranker-D7bRAHi6.mjs +71 -0
  60. package/dist/reranker-D7bRAHi6.mjs.map +1 -0
  61. package/dist/{schemas-BY3Pjvje.mjs → schemas-BFIgGntb.mjs} +1 -1
  62. package/dist/{schemas-BY3Pjvje.mjs.map → schemas-BFIgGntb.mjs.map} +1 -1
  63. package/dist/{search-GK0ibTJy.mjs → search-_oHfguA5.mjs} +47 -4
  64. package/dist/search-_oHfguA5.mjs.map +1 -0
  65. package/dist/{sqlite-RyR8Up1v.mjs → sqlite-CymLKiDE.mjs} +2 -2
  66. package/dist/{sqlite-RyR8Up1v.mjs.map → sqlite-CymLKiDE.mjs.map} +1 -1
  67. package/dist/{tools-CUg0Lyg-.mjs → tools-Dx7GjOHd.mjs} +23 -14
  68. package/dist/tools-Dx7GjOHd.mjs.map +1 -0
  69. package/dist/{vault-indexer-Bo2aPSzP.mjs → vault-indexer-DXWs9pDn.mjs} +1 -1
  70. package/dist/{vault-indexer-Bo2aPSzP.mjs.map → vault-indexer-DXWs9pDn.mjs.map} +1 -1
  71. package/dist/{zettelkasten-Co-w0XSZ.mjs → zettelkasten-e-a4rW_6.mjs} +2 -2
  72. package/dist/{zettelkasten-Co-w0XSZ.mjs.map → zettelkasten-e-a4rW_6.mjs.map} +1 -1
  73. package/package.json +4 -2
  74. package/scripts/build-hooks.mjs +51 -0
  75. package/src/hooks/ts/capture-all-events.ts +179 -0
  76. package/src/hooks/ts/lib/detect-environment.ts +53 -0
  77. package/src/hooks/ts/lib/metadata-extraction.ts +144 -0
  78. package/src/hooks/ts/lib/pai-paths.ts +124 -0
  79. package/src/hooks/ts/lib/project-utils.ts +914 -0
  80. package/src/hooks/ts/post-tool-use/capture-tool-output.ts +78 -0
  81. package/src/hooks/ts/post-tool-use/sync-todo-to-md.ts +230 -0
  82. package/src/hooks/ts/post-tool-use/update-tab-on-action.ts +145 -0
  83. package/src/hooks/ts/pre-compact/context-compression-hook.ts +155 -0
  84. package/src/hooks/ts/pre-tool-use/security-validator.ts +258 -0
  85. package/src/hooks/ts/session-end/capture-session-summary.ts +185 -0
  86. package/src/hooks/ts/session-start/initialize-session.ts +155 -0
  87. package/src/hooks/ts/session-start/load-core-context.ts +104 -0
  88. package/src/hooks/ts/session-start/load-project-context.ts +394 -0
  89. package/src/hooks/ts/stop/stop-hook.ts +407 -0
  90. package/src/hooks/ts/subagent-stop/subagent-stop-hook.ts +212 -0
  91. package/src/hooks/ts/user-prompt/cleanup-session-files.ts +45 -0
  92. package/src/hooks/ts/user-prompt/update-tab-titles.ts +88 -0
  93. package/tab-color-command.sh +24 -0
  94. package/templates/skills/createskill-skill.template.md +78 -0
  95. package/templates/skills/history-system.template.md +371 -0
  96. package/templates/skills/hook-system.template.md +913 -0
  97. package/templates/skills/sessions-skill.template.md +102 -0
  98. package/templates/skills/skill-system.template.md +214 -0
  99. package/templates/skills/terminal-tabs.template.md +120 -0
  100. package/dist/search-GK0ibTJy.mjs.map +0 -1
  101. package/dist/tools-CUg0Lyg-.mjs.map +0 -1
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/ts/capture-all-events.ts
4
+ import { readFileSync as readFileSync2, appendFileSync, mkdirSync, existsSync as existsSync2, writeFileSync } from "fs";
5
+ import { join as join2 } from "path";
6
+
7
+ // src/hooks/ts/lib/pai-paths.ts
8
+ import { homedir } from "os";
9
+ import { resolve, join } from "path";
10
+ import { existsSync, readFileSync } from "fs";
11
+ function loadEnvFile() {
12
+ const possiblePaths = [
13
+ resolve(process.env.PAI_DIR || "", ".env"),
14
+ resolve(homedir(), ".claude", ".env")
15
+ ];
16
+ for (const envPath of possiblePaths) {
17
+ if (existsSync(envPath)) {
18
+ try {
19
+ const content = readFileSync(envPath, "utf-8");
20
+ for (const line of content.split("\n")) {
21
+ const trimmed = line.trim();
22
+ if (!trimmed || trimmed.startsWith("#")) continue;
23
+ const eqIndex = trimmed.indexOf("=");
24
+ if (eqIndex > 0) {
25
+ const key = trimmed.substring(0, eqIndex).trim();
26
+ let value = trimmed.substring(eqIndex + 1).trim();
27
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
28
+ value = value.slice(1, -1);
29
+ }
30
+ value = value.replace(/\$HOME/g, homedir());
31
+ value = value.replace(/^~(?=\/|$)/, homedir());
32
+ if (process.env[key] === void 0) {
33
+ process.env[key] = value;
34
+ }
35
+ }
36
+ }
37
+ break;
38
+ } catch {
39
+ }
40
+ }
41
+ }
42
+ }
43
+ loadEnvFile();
44
+ var PAI_DIR = process.env.PAI_DIR ? resolve(process.env.PAI_DIR) : resolve(homedir(), ".claude");
45
+ var HOOKS_DIR = join(PAI_DIR, "Hooks");
46
+ var SKILLS_DIR = join(PAI_DIR, "Skills");
47
+ var AGENTS_DIR = join(PAI_DIR, "Agents");
48
+ var HISTORY_DIR = join(PAI_DIR, "History");
49
+ var COMMANDS_DIR = join(PAI_DIR, "Commands");
50
+ function validatePAIStructure() {
51
+ if (!existsSync(PAI_DIR)) {
52
+ console.error(`PAI_DIR does not exist: ${PAI_DIR}`);
53
+ console.error(` Expected ~/.claude or set PAI_DIR environment variable`);
54
+ process.exit(1);
55
+ }
56
+ if (!existsSync(HOOKS_DIR)) {
57
+ console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);
58
+ console.error(` Your PAI_DIR may be misconfigured`);
59
+ console.error(` Current PAI_DIR: ${PAI_DIR}`);
60
+ process.exit(1);
61
+ }
62
+ }
63
+ validatePAIStructure();
64
+
65
+ // src/hooks/ts/lib/metadata-extraction.ts
66
+ function extractAgentInstanceId(toolInput, description) {
67
+ const result = {};
68
+ if (description) {
69
+ const descMatch = description.match(/\[([a-z-]+-researcher)-(\d+)\]/);
70
+ if (descMatch) {
71
+ result.agent_type = descMatch[1];
72
+ result.instance_number = parseInt(descMatch[2], 10);
73
+ result.agent_instance_id = `${result.agent_type}-${result.instance_number}`;
74
+ }
75
+ }
76
+ if (!result.agent_instance_id && toolInput?.prompt && typeof toolInput.prompt === "string") {
77
+ const promptMatch = toolInput.prompt.match(/\[AGENT_INSTANCE:\s*([^\]]+)\]/);
78
+ if (promptMatch) {
79
+ result.agent_instance_id = promptMatch[1].trim();
80
+ const parts = result.agent_instance_id.match(/^([a-z-]+)-(\d+)$/);
81
+ if (parts) {
82
+ result.agent_type = parts[1];
83
+ result.instance_number = parseInt(parts[2], 10);
84
+ }
85
+ }
86
+ }
87
+ if (toolInput?.prompt && typeof toolInput.prompt === "string") {
88
+ const parentSessionMatch = toolInput.prompt.match(/\[PARENT_SESSION:\s*([^\]]+)\]/);
89
+ if (parentSessionMatch) {
90
+ result.parent_session_id = parentSessionMatch[1].trim();
91
+ }
92
+ const parentTaskMatch = toolInput.prompt.match(/\[PARENT_TASK:\s*([^\]]+)\]/);
93
+ if (parentTaskMatch) {
94
+ result.parent_task_id = parentTaskMatch[1].trim();
95
+ }
96
+ }
97
+ if (!result.agent_type && toolInput?.subagent_type) {
98
+ result.agent_type = toolInput.subagent_type;
99
+ }
100
+ return result;
101
+ }
102
+ function enrichEventWithAgentMetadata(event, toolInput, description) {
103
+ const metadata = extractAgentInstanceId(toolInput, description);
104
+ const enrichedEvent = { ...event };
105
+ if (metadata.agent_instance_id) {
106
+ enrichedEvent.agent_instance_id = metadata.agent_instance_id;
107
+ }
108
+ if (metadata.agent_type) {
109
+ enrichedEvent.agent_type = metadata.agent_type;
110
+ }
111
+ if (metadata.instance_number !== void 0) {
112
+ enrichedEvent.instance_number = metadata.instance_number;
113
+ }
114
+ if (metadata.parent_session_id) {
115
+ enrichedEvent.parent_session_id = metadata.parent_session_id;
116
+ }
117
+ if (metadata.parent_task_id) {
118
+ enrichedEvent.parent_task_id = metadata.parent_task_id;
119
+ }
120
+ return enrichedEvent;
121
+ }
122
+ function isAgentSpawningCall(toolName, toolInput) {
123
+ return toolName === "Task" && toolInput?.subagent_type !== void 0;
124
+ }
125
+
126
+ // src/hooks/ts/capture-all-events.ts
127
+ function getLocalTimestamp() {
128
+ const date = /* @__PURE__ */ new Date();
129
+ const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;
130
+ const localDate = new Date(date.toLocaleString("en-US", { timeZone: tz }));
131
+ const year = localDate.getFullYear();
132
+ const month = String(localDate.getMonth() + 1).padStart(2, "0");
133
+ const day = String(localDate.getDate()).padStart(2, "0");
134
+ const hours = String(localDate.getHours()).padStart(2, "0");
135
+ const minutes = String(localDate.getMinutes()).padStart(2, "0");
136
+ const seconds = String(localDate.getSeconds()).padStart(2, "0");
137
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${tz}`;
138
+ }
139
+ function getEventsFilePath() {
140
+ const now = /* @__PURE__ */ new Date();
141
+ const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;
142
+ const localDate = new Date(now.toLocaleString("en-US", { timeZone: tz }));
143
+ const year = localDate.getFullYear();
144
+ const month = String(localDate.getMonth() + 1).padStart(2, "0");
145
+ const day = String(localDate.getDate()).padStart(2, "0");
146
+ const monthDir = join2(HISTORY_DIR, "raw-outputs", `${year}-${month}`);
147
+ if (!existsSync2(monthDir)) {
148
+ mkdirSync(monthDir, { recursive: true });
149
+ }
150
+ return join2(monthDir, `${year}-${month}-${day}_all-events.jsonl`);
151
+ }
152
+ function getSessionMappingFile() {
153
+ return join2(PAI_DIR, "agent-sessions.json");
154
+ }
155
+ function getAgentForSession(sessionId) {
156
+ try {
157
+ const mappingFile = getSessionMappingFile();
158
+ if (existsSync2(mappingFile)) {
159
+ const mappings = JSON.parse(readFileSync2(mappingFile, "utf-8"));
160
+ return mappings[sessionId] || "pai";
161
+ }
162
+ } catch (error) {
163
+ }
164
+ return "pai";
165
+ }
166
+ function setAgentForSession(sessionId, agentName) {
167
+ try {
168
+ const mappingFile = getSessionMappingFile();
169
+ let mappings = {};
170
+ if (existsSync2(mappingFile)) {
171
+ mappings = JSON.parse(readFileSync2(mappingFile, "utf-8"));
172
+ }
173
+ mappings[sessionId] = agentName;
174
+ writeFileSync(mappingFile, JSON.stringify(mappings, null, 2), "utf-8");
175
+ } catch (error) {
176
+ }
177
+ }
178
+ async function main() {
179
+ try {
180
+ const args = process.argv.slice(2);
181
+ const eventTypeIndex = args.indexOf("--event-type");
182
+ if (eventTypeIndex === -1) {
183
+ console.error("Missing --event-type argument");
184
+ process.exit(0);
185
+ }
186
+ const eventType = args[eventTypeIndex + 1];
187
+ const chunks = [];
188
+ for await (const chunk of process.stdin) {
189
+ chunks.push(chunk);
190
+ }
191
+ const stdinData = Buffer.concat(chunks).toString("utf-8");
192
+ const hookData = JSON.parse(stdinData);
193
+ const sessionId = hookData.session_id || "main";
194
+ let agentName = getAgentForSession(sessionId);
195
+ if (hookData.tool_name === "Task" && hookData.tool_input?.subagent_type) {
196
+ agentName = hookData.tool_input.subagent_type;
197
+ setAgentForSession(sessionId, agentName);
198
+ } else if (eventType === "SubagentStop" || eventType === "Stop") {
199
+ agentName = "pai";
200
+ setAgentForSession(sessionId, "pai");
201
+ } else if (process.env.CLAUDE_CODE_AGENT) {
202
+ agentName = process.env.CLAUDE_CODE_AGENT;
203
+ setAgentForSession(sessionId, agentName);
204
+ } else if (hookData.agent_type) {
205
+ agentName = hookData.agent_type;
206
+ setAgentForSession(sessionId, agentName);
207
+ } else if (hookData.cwd && hookData.cwd.includes("/agents/")) {
208
+ const agentMatch = hookData.cwd.match(/\/agents\/([^\/]+)/);
209
+ if (agentMatch) {
210
+ agentName = agentMatch[1];
211
+ setAgentForSession(sessionId, agentName);
212
+ }
213
+ }
214
+ let event = {
215
+ source_app: agentName,
216
+ session_id: hookData.session_id || "main",
217
+ hook_event_type: eventType,
218
+ payload: hookData,
219
+ timestamp: Date.now(),
220
+ timestamp_local: getLocalTimestamp()
221
+ };
222
+ if (isAgentSpawningCall(hookData.tool_name, hookData.tool_input)) {
223
+ event = enrichEventWithAgentMetadata(
224
+ event,
225
+ hookData.tool_input,
226
+ hookData.description
227
+ );
228
+ }
229
+ const eventsFile = getEventsFilePath();
230
+ const jsonLine = JSON.stringify(event) + "\n";
231
+ appendFileSync(eventsFile, jsonLine, "utf-8");
232
+ } catch (error) {
233
+ console.error("Event capture error:", error);
234
+ }
235
+ process.exit(0);
236
+ }
237
+ main();
238
+ //# sourceMappingURL=capture-all-events.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/hooks/ts/capture-all-events.ts", "../../src/hooks/ts/lib/pai-paths.ts", "../../src/hooks/ts/lib/metadata-extraction.ts"],
4
+ "sourcesContent": ["#!/usr/bin/env node\n/**\n * Capture All Events Hook\n * Captures ALL Claude Code hook events (not just tools) to JSONL\n * This replaces the Python send_event.py hook\n * Enhanced with agent instance metadata extraction\n */\n\nimport { readFileSync, appendFileSync, mkdirSync, existsSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { PAI_DIR, HISTORY_DIR } from './lib/pai-paths';\nimport { enrichEventWithAgentMetadata, isAgentSpawningCall } from './lib/metadata-extraction';\n\ninterface HookEvent {\n source_app: string;\n session_id: string;\n hook_event_type: string;\n payload: Record<string, any>;\n timestamp: number;\n timestamp_local: string;\n}\n\n// Get local timestamp using system timezone\nfunction getLocalTimestamp(): string {\n const date = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(date.toLocaleString('en-US', { timeZone: tz }));\n\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n const day = String(localDate.getDate()).padStart(2, '0');\n const hours = String(localDate.getHours()).padStart(2, '0');\n const minutes = String(localDate.getMinutes()).padStart(2, '0');\n const seconds = String(localDate.getSeconds()).padStart(2, '0');\n\n return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${tz}`;\n}\n\n// Get current events file path\nfunction getEventsFilePath(): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n const day = String(localDate.getDate()).padStart(2, '0');\n\n const monthDir = join(HISTORY_DIR, 'raw-outputs', `${year}-${month}`);\n\n // Ensure directory exists\n if (!existsSync(monthDir)) {\n mkdirSync(monthDir, { recursive: true });\n }\n\n return join(monthDir, `${year}-${month}-${day}_all-events.jsonl`);\n}\n\n// Session-to-agent mapping functions\nfunction getSessionMappingFile(): string {\n return join(PAI_DIR, 'agent-sessions.json');\n}\n\nfunction getAgentForSession(sessionId: string): string {\n try {\n const mappingFile = getSessionMappingFile();\n if (existsSync(mappingFile)) {\n const mappings = JSON.parse(readFileSync(mappingFile, 'utf-8'));\n return mappings[sessionId] || 'pai';\n }\n } catch (error) {\n // Ignore errors, default to pai\n }\n return 'pai';\n}\n\nfunction setAgentForSession(sessionId: string, agentName: string): void {\n try {\n const mappingFile = getSessionMappingFile();\n let mappings: Record<string, string> = {};\n\n if (existsSync(mappingFile)) {\n mappings = JSON.parse(readFileSync(mappingFile, 'utf-8'));\n }\n\n mappings[sessionId] = agentName;\n writeFileSync(mappingFile, JSON.stringify(mappings, null, 2), 'utf-8');\n } catch (error) {\n // Silently fail - don't block\n }\n}\n\nasync function main() {\n try {\n // Get event type from command line args\n const args = process.argv.slice(2);\n const eventTypeIndex = args.indexOf('--event-type');\n\n if (eventTypeIndex === -1) {\n console.error('Missing --event-type argument');\n process.exit(0); // Don't block Claude Code\n }\n\n const eventType = args[eventTypeIndex + 1];\n\n // Read hook data from stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const stdinData = Buffer.concat(chunks).toString('utf-8');\n const hookData = JSON.parse(stdinData);\n\n // Detect agent type from session mapping or payload\n const sessionId = hookData.session_id || 'main';\n let agentName = getAgentForSession(sessionId);\n\n // If this is a Task tool launching a subagent, update the session mapping\n if (hookData.tool_name === 'Task' && hookData.tool_input?.subagent_type) {\n agentName = hookData.tool_input.subagent_type;\n setAgentForSession(sessionId, agentName);\n }\n // If this is a SubagentStop or Stop event, reset to pai\n else if (eventType === 'SubagentStop' || eventType === 'Stop') {\n agentName = 'pai';\n setAgentForSession(sessionId, 'pai');\n }\n // Check if CLAUDE_CODE_AGENT env variable is set (for subagents)\n else if (process.env.CLAUDE_CODE_AGENT) {\n agentName = process.env.CLAUDE_CODE_AGENT;\n setAgentForSession(sessionId, agentName);\n }\n // Check if agent type is in the payload (alternative detection method)\n else if (hookData.agent_type) {\n agentName = hookData.agent_type;\n setAgentForSession(sessionId, agentName);\n }\n // Check if this is from a subagent based on cwd containing 'agent'\n else if (hookData.cwd && hookData.cwd.includes('/agents/')) {\n // Extract agent name from path like \"/agents/designer/\"\n const agentMatch = hookData.cwd.match(/\\/agents\\/([^\\/]+)/);\n if (agentMatch) {\n agentName = agentMatch[1];\n setAgentForSession(sessionId, agentName);\n }\n }\n\n // Create base event object\n let event: HookEvent = {\n source_app: agentName,\n session_id: hookData.session_id || 'main',\n hook_event_type: eventType,\n payload: hookData,\n timestamp: Date.now(),\n timestamp_local: getLocalTimestamp()\n };\n\n // Enrich with agent instance metadata if this is a Task tool call\n if (isAgentSpawningCall(hookData.tool_name, hookData.tool_input)) {\n event = enrichEventWithAgentMetadata(\n event,\n hookData.tool_input,\n hookData.description\n );\n }\n\n // Append to events file\n const eventsFile = getEventsFilePath();\n const jsonLine = JSON.stringify(event) + '\\n';\n appendFileSync(eventsFile, jsonLine, 'utf-8');\n\n } catch (error) {\n // Silently fail - don't block Claude Code\n console.error('Event capture error:', error);\n }\n\n process.exit(0);\n}\n\nmain();\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n", "/**\n * Metadata Extraction Library for UOCS Enhancement\n *\n * Extracts agent instance IDs, parent-child relationships, and session info\n * from Task tool calls and other tool inputs.\n *\n * Design Philosophy: Optional extraction with graceful fallbacks\n * - If instance IDs are present in descriptions/prompts, extract them\n * - If not present, fall back to agent type only\n * - Never fail - always return usable metadata\n */\n\nexport interface AgentInstanceMetadata {\n agent_instance_id?: string; // \"perplexity-researcher-1\" (full ID)\n agent_type?: string; // \"perplexity-researcher\" (base type)\n instance_number?: number; // 1 (sequence number)\n parent_session_id?: string; // Session that spawned this agent\n parent_task_id?: string; // Task ID that spawned this agent\n}\n\n/**\n * Extract agent instance ID from Task tool input\n *\n * Looks for patterns in priority order:\n * 1. [agent-type-N] in description (e.g., \"Research topic [perplexity-researcher-1]\")\n * 2. [AGENT_INSTANCE: agent-type-N] in prompt\n * 3. subagent_type field (fallback to just type, no instance number)\n *\n * @param toolInput The tool input object from PreToolUse/PostToolUse hooks\n * @param description Optional description field from tool input\n * @returns Metadata object with extracted information\n */\nexport function extractAgentInstanceId(\n toolInput: any,\n description?: string\n): AgentInstanceMetadata {\n const result: AgentInstanceMetadata = {};\n\n // Strategy 1: Extract from description [agent-type-N]\n // Example: \"Research consumer complaints [perplexity-researcher-1]\"\n if (description) {\n const descMatch = description.match(/\\[([a-z-]+-researcher)-(\\d+)\\]/);\n if (descMatch) {\n result.agent_type = descMatch[1];\n result.instance_number = parseInt(descMatch[2], 10);\n result.agent_instance_id = `${result.agent_type}-${result.instance_number}`;\n }\n }\n\n // Strategy 2: Extract from prompt [AGENT_INSTANCE: ...]\n // Example: \"[AGENT_INSTANCE: perplexity-researcher-1]\"\n if (!result.agent_instance_id && toolInput?.prompt && typeof toolInput.prompt === 'string') {\n const promptMatch = toolInput.prompt.match(/\\[AGENT_INSTANCE:\\s*([^\\]]+)\\]/);\n if (promptMatch) {\n result.agent_instance_id = promptMatch[1].trim();\n\n // Parse agent type and instance number from ID\n const parts = result.agent_instance_id.match(/^([a-z-]+)-(\\d+)$/);\n if (parts) {\n result.agent_type = parts[1];\n result.instance_number = parseInt(parts[2], 10);\n }\n }\n }\n\n // Strategy 3: Extract parent session from prompt\n // Example: \"[PARENT_SESSION: b7062b5a-03d3-4168-9555-a748e0b2efa3]\"\n if (toolInput?.prompt && typeof toolInput.prompt === 'string') {\n const parentSessionMatch = toolInput.prompt.match(/\\[PARENT_SESSION:\\s*([^\\]]+)\\]/);\n if (parentSessionMatch) {\n result.parent_session_id = parentSessionMatch[1].trim();\n }\n\n // Extract parent task from prompt\n // Example: \"[PARENT_TASK: research_1731445892345]\"\n const parentTaskMatch = toolInput.prompt.match(/\\[PARENT_TASK:\\s*([^\\]]+)\\]/);\n if (parentTaskMatch) {\n result.parent_task_id = parentTaskMatch[1].trim();\n }\n }\n\n // Strategy 4: Fallback to subagent_type if available (no instance number)\n // This ensures we at least capture the agent type even without instance IDs\n if (!result.agent_type && toolInput?.subagent_type) {\n result.agent_type = toolInput.subagent_type;\n }\n\n return result;\n}\n\n/**\n * Enrich event with agent metadata\n *\n * Takes a base event object and adds agent instance metadata to it.\n * Returns a new object with merged metadata.\n *\n * @param event Base event object (from PreToolUse/PostToolUse)\n * @param toolInput Tool input object\n * @param description Optional description field\n * @returns Enriched event with agent metadata\n */\nexport function enrichEventWithAgentMetadata(\n event: any,\n toolInput: any,\n description?: string\n): any {\n const metadata = extractAgentInstanceId(toolInput, description);\n\n // Only add fields that have values (keep events clean)\n const enrichedEvent = { ...event };\n\n if (metadata.agent_instance_id) {\n enrichedEvent.agent_instance_id = metadata.agent_instance_id;\n }\n\n if (metadata.agent_type) {\n enrichedEvent.agent_type = metadata.agent_type;\n }\n\n if (metadata.instance_number !== undefined) {\n enrichedEvent.instance_number = metadata.instance_number;\n }\n\n if (metadata.parent_session_id) {\n enrichedEvent.parent_session_id = metadata.parent_session_id;\n }\n\n if (metadata.parent_task_id) {\n enrichedEvent.parent_task_id = metadata.parent_task_id;\n }\n\n return enrichedEvent;\n}\n\n/**\n * Check if a tool call is spawning a subagent\n *\n * @param toolName Name of the tool being called\n * @param toolInput Tool input object\n * @returns true if this is a Task tool call spawning an agent\n */\nexport function isAgentSpawningCall(toolName: string, toolInput: any): boolean {\n return toolName === 'Task' && toolInput?.subagent_type !== undefined;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["readFileSync", "existsSync", "join", "join", "existsSync", "readFileSync"]
7
+ }
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/ts/session-end/capture-session-summary.ts
4
+ import { writeFileSync, mkdirSync, existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
5
+ import { join as join2 } from "path";
6
+
7
+ // src/hooks/ts/lib/pai-paths.ts
8
+ import { homedir } from "os";
9
+ import { resolve, join } from "path";
10
+ import { existsSync, readFileSync } from "fs";
11
+ function loadEnvFile() {
12
+ const possiblePaths = [
13
+ resolve(process.env.PAI_DIR || "", ".env"),
14
+ resolve(homedir(), ".claude", ".env")
15
+ ];
16
+ for (const envPath of possiblePaths) {
17
+ if (existsSync(envPath)) {
18
+ try {
19
+ const content = readFileSync(envPath, "utf-8");
20
+ for (const line of content.split("\n")) {
21
+ const trimmed = line.trim();
22
+ if (!trimmed || trimmed.startsWith("#")) continue;
23
+ const eqIndex = trimmed.indexOf("=");
24
+ if (eqIndex > 0) {
25
+ const key = trimmed.substring(0, eqIndex).trim();
26
+ let value = trimmed.substring(eqIndex + 1).trim();
27
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
28
+ value = value.slice(1, -1);
29
+ }
30
+ value = value.replace(/\$HOME/g, homedir());
31
+ value = value.replace(/^~(?=\/|$)/, homedir());
32
+ if (process.env[key] === void 0) {
33
+ process.env[key] = value;
34
+ }
35
+ }
36
+ }
37
+ break;
38
+ } catch {
39
+ }
40
+ }
41
+ }
42
+ }
43
+ loadEnvFile();
44
+ var PAI_DIR = process.env.PAI_DIR ? resolve(process.env.PAI_DIR) : resolve(homedir(), ".claude");
45
+ var HOOKS_DIR = join(PAI_DIR, "Hooks");
46
+ var SKILLS_DIR = join(PAI_DIR, "Skills");
47
+ var AGENTS_DIR = join(PAI_DIR, "Agents");
48
+ var HISTORY_DIR = join(PAI_DIR, "History");
49
+ var COMMANDS_DIR = join(PAI_DIR, "Commands");
50
+ function validatePAIStructure() {
51
+ if (!existsSync(PAI_DIR)) {
52
+ console.error(`PAI_DIR does not exist: ${PAI_DIR}`);
53
+ console.error(` Expected ~/.claude or set PAI_DIR environment variable`);
54
+ process.exit(1);
55
+ }
56
+ if (!existsSync(HOOKS_DIR)) {
57
+ console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);
58
+ console.error(` Your PAI_DIR may be misconfigured`);
59
+ console.error(` Current PAI_DIR: ${PAI_DIR}`);
60
+ process.exit(1);
61
+ }
62
+ }
63
+ validatePAIStructure();
64
+
65
+ // src/hooks/ts/session-end/capture-session-summary.ts
66
+ async function main() {
67
+ try {
68
+ const chunks = [];
69
+ for await (const chunk of process.stdin) {
70
+ chunks.push(chunk);
71
+ }
72
+ const input = Buffer.concat(chunks).toString("utf-8");
73
+ if (!input || input.trim() === "") {
74
+ process.exit(0);
75
+ }
76
+ const data = JSON.parse(input);
77
+ const now = /* @__PURE__ */ new Date();
78
+ const timestamp = now.toISOString().replace(/:/g, "").replace(/\..+/, "").replace("T", "-");
79
+ const yearMonth = timestamp.substring(0, 7);
80
+ const sessionInfo = await analyzeSession(data.conversation_id, yearMonth);
81
+ const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;
82
+ const sessionDir = join2(HISTORY_DIR, "sessions", yearMonth);
83
+ if (!existsSync2(sessionDir)) {
84
+ mkdirSync(sessionDir, { recursive: true });
85
+ }
86
+ const sessionDoc = formatSessionDocument(timestamp, data, sessionInfo);
87
+ writeFileSync(join2(sessionDir, filename), sessionDoc);
88
+ process.exit(0);
89
+ } catch (error) {
90
+ console.error(`[UOCS] SessionEnd hook error: ${error}`);
91
+ process.exit(0);
92
+ }
93
+ }
94
+ async function analyzeSession(conversationId, yearMonth) {
95
+ const rawOutputsDir = join2(HISTORY_DIR, "raw-outputs", yearMonth);
96
+ let filesChanged = [];
97
+ let commandsExecuted = [];
98
+ let toolsUsed = /* @__PURE__ */ new Set();
99
+ try {
100
+ if (existsSync2(rawOutputsDir)) {
101
+ const files = readdirSync(rawOutputsDir).filter((f) => f.endsWith(".jsonl"));
102
+ for (const file of files) {
103
+ const filePath = join2(rawOutputsDir, file);
104
+ const content = readFileSync2(filePath, "utf-8");
105
+ const lines = content.split("\n").filter((l) => l.trim());
106
+ for (const line of lines) {
107
+ try {
108
+ const entry = JSON.parse(line);
109
+ if (entry.session === conversationId) {
110
+ toolsUsed.add(entry.tool);
111
+ if (entry.tool === "Edit" || entry.tool === "Write") {
112
+ if (entry.input?.file_path) {
113
+ filesChanged.push(entry.input.file_path);
114
+ }
115
+ }
116
+ if (entry.tool === "Bash" && entry.input?.command) {
117
+ commandsExecuted.push(entry.input.command);
118
+ }
119
+ }
120
+ } catch (e) {
121
+ }
122
+ }
123
+ }
124
+ }
125
+ } catch (error) {
126
+ }
127
+ return {
128
+ focus: "general-work",
129
+ filesChanged: [...new Set(filesChanged)].slice(0, 10),
130
+ // Unique, max 10
131
+ commandsExecuted: commandsExecuted.slice(0, 10),
132
+ // Max 10
133
+ toolsUsed: Array.from(toolsUsed),
134
+ duration: 0
135
+ // Unknown
136
+ };
137
+ }
138
+ function formatSessionDocument(timestamp, data, info) {
139
+ const date = timestamp.substring(0, 10);
140
+ const time = timestamp.substring(11).replace(/-/g, ":");
141
+ const da = process.env.DA || "PAI";
142
+ return `---
143
+ capture_type: SESSION
144
+ timestamp: ${(/* @__PURE__ */ new Date()).toISOString()}
145
+ session_id: ${data.conversation_id}
146
+ duration_minutes: ${info.duration}
147
+ executor: ${da}
148
+ ---
149
+
150
+ # Session: ${info.focus}
151
+
152
+ **Date:** ${date}
153
+ **Time:** ${time}
154
+ **Session ID:** ${data.conversation_id}
155
+
156
+ ---
157
+
158
+ ## Session Overview
159
+
160
+ **Focus:** General development work
161
+ **Duration:** ${info.duration > 0 ? `${info.duration} minutes` : "Unknown"}
162
+
163
+ ---
164
+
165
+ ## Tools Used
166
+
167
+ ${info.toolsUsed.length > 0 ? info.toolsUsed.map((t) => `- ${t}`).join("\n") : "- None recorded"}
168
+
169
+ ---
170
+
171
+ ## Files Modified
172
+
173
+ ${info.filesChanged.length > 0 ? info.filesChanged.map((f) => `- \`${f}\``).join("\n") : "- None recorded"}
174
+
175
+ **Total Files Changed:** ${info.filesChanged.length}
176
+
177
+ ---
178
+
179
+ ## Commands Executed
180
+
181
+ ${info.commandsExecuted.length > 0 ? "```bash\n" + info.commandsExecuted.join("\n") + "\n```" : "None recorded"}
182
+
183
+ ---
184
+
185
+ ## Notes
186
+
187
+ This session summary was automatically generated by the UOCS SessionEnd hook.
188
+
189
+ For detailed tool outputs, see: \`\${PAI_DIR}/History/raw-outputs/${timestamp.substring(0, 7)}/\`
190
+
191
+ ---
192
+
193
+ **Session Outcome:** Completed
194
+ **Generated:** ${(/* @__PURE__ */ new Date()).toISOString()}
195
+ `;
196
+ }
197
+ main();
198
+ //# sourceMappingURL=capture-session-summary.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/hooks/ts/session-end/capture-session-summary.ts", "../../src/hooks/ts/lib/pai-paths.ts"],
4
+ "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * SessionEnd Hook - Captures session summary for UOCS\n *\n * Generates a session summary document when a Claude Code session ends,\n * documenting what was accomplished during the session.\n */\n\nimport { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { PAI_DIR, HISTORY_DIR } from '../lib/pai-paths';\n\ninterface SessionData {\n conversation_id: string;\n timestamp: string;\n [key: string]: any;\n}\n\nasync function main() {\n try {\n // Read input from stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const input = Buffer.concat(chunks).toString('utf-8');\n if (!input || input.trim() === '') {\n process.exit(0);\n }\n\n const data: SessionData = JSON.parse(input);\n\n // Generate timestamp for filename\n const now = new Date();\n const timestamp = now.toISOString()\n .replace(/:/g, '')\n .replace(/\\..+/, '')\n .replace('T', '-'); // YYYY-MM-DD-HHMMSS\n\n const yearMonth = timestamp.substring(0, 7); // YYYY-MM\n\n // Try to extract session info from raw outputs\n const sessionInfo = await analyzeSession(data.conversation_id, yearMonth);\n\n // Generate filename\n const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;\n\n // Ensure directory exists\n const sessionDir = join(HISTORY_DIR, 'sessions', yearMonth);\n if (!existsSync(sessionDir)) {\n mkdirSync(sessionDir, { recursive: true });\n }\n\n // Generate session document\n const sessionDoc = formatSessionDocument(timestamp, data, sessionInfo);\n\n // Write session file\n writeFileSync(join(sessionDir, filename), sessionDoc);\n\n // Exit successfully\n process.exit(0);\n } catch (error) {\n // Silent failure - don't disrupt workflow\n console.error(`[UOCS] SessionEnd hook error: ${error}`);\n process.exit(0);\n }\n}\n\nasync function analyzeSession(conversationId: string, yearMonth: string): Promise<any> {\n // Try to read raw outputs for this session\n const rawOutputsDir = join(HISTORY_DIR, 'raw-outputs', yearMonth);\n\n let filesChanged: string[] = [];\n let commandsExecuted: string[] = [];\n let toolsUsed: Set<string> = new Set();\n\n try {\n if (existsSync(rawOutputsDir)) {\n const files = readdirSync(rawOutputsDir).filter(f => f.endsWith('.jsonl'));\n\n for (const file of files) {\n const filePath = join(rawOutputsDir, file);\n const content = readFileSync(filePath, 'utf-8');\n const lines = content.split('\\n').filter(l => l.trim());\n\n for (const line of lines) {\n try {\n const entry = JSON.parse(line);\n if (entry.session === conversationId) {\n toolsUsed.add(entry.tool);\n\n // Extract file changes\n if (entry.tool === 'Edit' || entry.tool === 'Write') {\n if (entry.input?.file_path) {\n filesChanged.push(entry.input.file_path);\n }\n }\n\n // Extract bash commands\n if (entry.tool === 'Bash' && entry.input?.command) {\n commandsExecuted.push(entry.input.command);\n }\n }\n } catch (e) {\n // Skip invalid JSON lines\n }\n }\n }\n }\n } catch (error) {\n // Silent failure\n }\n\n return {\n focus: 'general-work',\n filesChanged: [...new Set(filesChanged)].slice(0, 10), // Unique, max 10\n commandsExecuted: commandsExecuted.slice(0, 10), // Max 10\n toolsUsed: Array.from(toolsUsed),\n duration: 0 // Unknown\n };\n}\n\nfunction formatSessionDocument(timestamp: string, data: SessionData, info: any): string {\n const date = timestamp.substring(0, 10); // YYYY-MM-DD\n const time = timestamp.substring(11).replace(/-/g, ':'); // HH:MM:SS\n const da = process.env.DA || 'PAI';\n\n return `---\ncapture_type: SESSION\ntimestamp: ${new Date().toISOString()}\nsession_id: ${data.conversation_id}\nduration_minutes: ${info.duration}\nexecutor: ${da}\n---\n\n# Session: ${info.focus}\n\n**Date:** ${date}\n**Time:** ${time}\n**Session ID:** ${data.conversation_id}\n\n---\n\n## Session Overview\n\n**Focus:** General development work\n**Duration:** ${info.duration > 0 ? `${info.duration} minutes` : 'Unknown'}\n\n---\n\n## Tools Used\n\n${info.toolsUsed.length > 0 ? info.toolsUsed.map((t: string) => `- ${t}`).join('\\n') : '- None recorded'}\n\n---\n\n## Files Modified\n\n${info.filesChanged.length > 0 ? info.filesChanged.map((f: string) => `- \\`${f}\\``).join('\\n') : '- None recorded'}\n\n**Total Files Changed:** ${info.filesChanged.length}\n\n---\n\n## Commands Executed\n\n${info.commandsExecuted.length > 0 ? '```bash\\n' + info.commandsExecuted.join('\\n') + '\\n```' : 'None recorded'}\n\n---\n\n## Notes\n\nThis session summary was automatically generated by the UOCS SessionEnd hook.\n\nFor detailed tool outputs, see: \\`\\${PAI_DIR}/History/raw-outputs/${timestamp.substring(0, 7)}/\\`\n\n---\n\n**Session Outcome:** Completed\n**Generated:** ${new Date().toISOString()}\n`;\n}\n\nmain();\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["existsSync", "readFileSync", "join", "join", "existsSync", "readFileSync"]
7
+ }
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/hooks/ts/post-tool-use/capture-tool-output.ts
4
+ import { appendFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
5
+ import { join as join2 } from "path";
6
+
7
+ // src/hooks/ts/lib/pai-paths.ts
8
+ import { homedir } from "os";
9
+ import { resolve, join } from "path";
10
+ import { existsSync, readFileSync } from "fs";
11
+ function loadEnvFile() {
12
+ const possiblePaths = [
13
+ resolve(process.env.PAI_DIR || "", ".env"),
14
+ resolve(homedir(), ".claude", ".env")
15
+ ];
16
+ for (const envPath of possiblePaths) {
17
+ if (existsSync(envPath)) {
18
+ try {
19
+ const content = readFileSync(envPath, "utf-8");
20
+ for (const line of content.split("\n")) {
21
+ const trimmed = line.trim();
22
+ if (!trimmed || trimmed.startsWith("#")) continue;
23
+ const eqIndex = trimmed.indexOf("=");
24
+ if (eqIndex > 0) {
25
+ const key = trimmed.substring(0, eqIndex).trim();
26
+ let value = trimmed.substring(eqIndex + 1).trim();
27
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
28
+ value = value.slice(1, -1);
29
+ }
30
+ value = value.replace(/\$HOME/g, homedir());
31
+ value = value.replace(/^~(?=\/|$)/, homedir());
32
+ if (process.env[key] === void 0) {
33
+ process.env[key] = value;
34
+ }
35
+ }
36
+ }
37
+ break;
38
+ } catch {
39
+ }
40
+ }
41
+ }
42
+ }
43
+ loadEnvFile();
44
+ var PAI_DIR = process.env.PAI_DIR ? resolve(process.env.PAI_DIR) : resolve(homedir(), ".claude");
45
+ var HOOKS_DIR = join(PAI_DIR, "Hooks");
46
+ var SKILLS_DIR = join(PAI_DIR, "Skills");
47
+ var AGENTS_DIR = join(PAI_DIR, "Agents");
48
+ var HISTORY_DIR = join(PAI_DIR, "History");
49
+ var COMMANDS_DIR = join(PAI_DIR, "Commands");
50
+ function validatePAIStructure() {
51
+ if (!existsSync(PAI_DIR)) {
52
+ console.error(`PAI_DIR does not exist: ${PAI_DIR}`);
53
+ console.error(` Expected ~/.claude or set PAI_DIR environment variable`);
54
+ process.exit(1);
55
+ }
56
+ if (!existsSync(HOOKS_DIR)) {
57
+ console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);
58
+ console.error(` Your PAI_DIR may be misconfigured`);
59
+ console.error(` Current PAI_DIR: ${PAI_DIR}`);
60
+ process.exit(1);
61
+ }
62
+ }
63
+ validatePAIStructure();
64
+
65
+ // src/hooks/ts/post-tool-use/capture-tool-output.ts
66
+ var CAPTURE_DIR = join2(HISTORY_DIR, "raw-outputs");
67
+ var INTERESTING_TOOLS = ["Bash", "Edit", "Write", "Read", "Task", "NotebookEdit"];
68
+ async function main() {
69
+ try {
70
+ const chunks = [];
71
+ for await (const chunk of process.stdin) {
72
+ chunks.push(chunk);
73
+ }
74
+ const input = Buffer.concat(chunks).toString("utf-8");
75
+ if (!input || input.trim() === "") {
76
+ process.exit(0);
77
+ }
78
+ const data = JSON.parse(input);
79
+ if (!INTERESTING_TOOLS.includes(data.tool_name)) {
80
+ process.exit(0);
81
+ }
82
+ const now = /* @__PURE__ */ new Date();
83
+ const today = now.toISOString().split("T")[0];
84
+ const yearMonth = today.substring(0, 7);
85
+ const dateDir = join2(CAPTURE_DIR, yearMonth);
86
+ if (!existsSync2(dateDir)) {
87
+ mkdirSync(dateDir, { recursive: true });
88
+ }
89
+ const captureFile = join2(dateDir, `${today}_tool-outputs.jsonl`);
90
+ const captureEntry = JSON.stringify({
91
+ timestamp: data.timestamp || now.toISOString(),
92
+ tool: data.tool_name,
93
+ input: data.tool_input,
94
+ output: data.tool_response,
95
+ session: data.conversation_id
96
+ }) + "\n";
97
+ appendFileSync(captureFile, captureEntry);
98
+ process.exit(0);
99
+ } catch (error) {
100
+ console.error(`[UOCS] PostToolUse hook error: ${error}`);
101
+ process.exit(0);
102
+ }
103
+ }
104
+ main();
105
+ //# sourceMappingURL=capture-tool-output.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/hooks/ts/post-tool-use/capture-tool-output.ts", "../../src/hooks/ts/lib/pai-paths.ts"],
4
+ "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * PostToolUse Hook - Captures tool outputs for UOCS\n *\n * Automatically logs all tool executions to daily JSONL files\n * for later processing and analysis.\n */\n\nimport { appendFileSync, mkdirSync, existsSync } from 'fs';\nimport { join } from 'path';\nimport { PAI_DIR, HISTORY_DIR } from '../lib/pai-paths';\n\ninterface ToolUseData {\n tool_name: string;\n tool_input: Record<string, any>;\n tool_response: Record<string, any>;\n conversation_id: string;\n timestamp: string;\n}\n\n// Configuration\nconst CAPTURE_DIR = join(HISTORY_DIR, 'raw-outputs');\nconst INTERESTING_TOOLS = ['Bash', 'Edit', 'Write', 'Read', 'Task', 'NotebookEdit'];\n\nasync function main() {\n try {\n // Read input from stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n const input = Buffer.concat(chunks).toString('utf-8');\n if (!input || input.trim() === '') {\n process.exit(0);\n }\n\n const data: ToolUseData = JSON.parse(input);\n\n // Only capture interesting tools\n if (!INTERESTING_TOOLS.includes(data.tool_name)) {\n process.exit(0);\n }\n\n // Get today's date for organization\n const now = new Date();\n const today = now.toISOString().split('T')[0]; // YYYY-MM-DD\n const yearMonth = today.substring(0, 7); // YYYY-MM\n\n // Ensure capture directory exists\n const dateDir = join(CAPTURE_DIR, yearMonth);\n if (!existsSync(dateDir)) {\n mkdirSync(dateDir, { recursive: true });\n }\n\n // Format output as JSONL (one JSON object per line)\n const captureFile = join(dateDir, `${today}_tool-outputs.jsonl`);\n const captureEntry = JSON.stringify({\n timestamp: data.timestamp || now.toISOString(),\n tool: data.tool_name,\n input: data.tool_input,\n output: data.tool_response,\n session: data.conversation_id\n }) + '\\n';\n\n // Append to daily log\n appendFileSync(captureFile, captureEntry);\n\n // Exit successfully (code 0 = continue normally)\n process.exit(0);\n } catch (error) {\n // Silent failure - don't disrupt workflow\n console.error(`[UOCS] PostToolUse hook error: ${error}`);\n process.exit(0);\n }\n}\n\nmain();\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n * It handles PAI_DIR detection whether set explicitly or defaulting to ~/.claude\n *\n * ALSO loads .env file from PAI_DIR so all hooks get environment variables\n * without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n * import { PAI_DIR, HOOKS_DIR, SKILLS_DIR } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE PAI_DIR resolution so .env can set PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n // Check common locations for .env\n const possiblePaths = [\n resolve(process.env.PAI_DIR || '', '.env'),\n resolve(homedir(), '.claude', '.env'),\n ];\n\n for (const envPath of possiblePaths) {\n if (existsSync(envPath)) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (!trimmed || trimmed.startsWith('#')) continue;\n\n const eqIndex = trimmed.indexOf('=');\n if (eqIndex > 0) {\n const key = trimmed.substring(0, eqIndex).trim();\n let value = trimmed.substring(eqIndex + 1).trim();\n\n // Remove surrounding quotes if present\n if ((value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n value = value.slice(1, -1);\n }\n\n // Expand $HOME and ~ in values\n value = value.replace(/\\$HOME/g, homedir());\n value = value.replace(/^~(?=\\/|$)/, homedir());\n\n // Only set if not already defined (env vars take precedence)\n if (process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n }\n // Found and loaded, don't check other paths\n break;\n } catch {\n // Silently continue if .env can't be read\n }\n }\n }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\n/**\n * Smart PAI_DIR detection with fallback\n * Priority:\n * 1. PAI_DIR environment variable (if set)\n * 2. ~/.claude (standard location)\n */\nexport const PAI_DIR = process.env.PAI_DIR\n ? resolve(process.env.PAI_DIR)\n : resolve(homedir(), '.claude');\n\n/**\n * Common PAI directories\n */\nexport const HOOKS_DIR = join(PAI_DIR, 'Hooks');\nexport const SKILLS_DIR = join(PAI_DIR, 'Skills');\nexport const AGENTS_DIR = join(PAI_DIR, 'Agents');\nexport const HISTORY_DIR = join(PAI_DIR, 'History');\nexport const COMMANDS_DIR = join(PAI_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n if (!existsSync(PAI_DIR)) {\n console.error(`PAI_DIR does not exist: ${PAI_DIR}`);\n console.error(` Expected ~/.claude or set PAI_DIR environment variable`);\n process.exit(1);\n }\n\n if (!existsSync(HOOKS_DIR)) {\n console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n console.error(` Your PAI_DIR may be misconfigured`);\n console.error(` Current PAI_DIR: ${PAI_DIR}`);\n process.exit(1);\n }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n const now = new Date();\n const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n const year = localDate.getFullYear();\n const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n return join(HISTORY_DIR, subdir, `${year}-${month}`, filename);\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["existsSync", "join", "join", "existsSync"]
7
+ }