@tonycasey/lisa 0.5.13
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.
- package/README.md +42 -0
- package/dist/cli.js +390 -0
- package/dist/lib/interfaces/IDockerClient.js +2 -0
- package/dist/lib/interfaces/IMcpClient.js +2 -0
- package/dist/lib/interfaces/IServices.js +2 -0
- package/dist/lib/interfaces/ITemplateCopier.js +2 -0
- package/dist/lib/mcp.js +35 -0
- package/dist/lib/services.js +57 -0
- package/dist/package.json +36 -0
- package/dist/templates/agents/.sample.env +12 -0
- package/dist/templates/agents/docs/STORAGE_SETUP.md +161 -0
- package/dist/templates/agents/skills/common/group-id.js +193 -0
- package/dist/templates/agents/skills/init-review/SKILL.md +119 -0
- package/dist/templates/agents/skills/init-review/scripts/ai-enrich.js +258 -0
- package/dist/templates/agents/skills/init-review/scripts/init-review.js +769 -0
- package/dist/templates/agents/skills/lisa/SKILL.md +92 -0
- package/dist/templates/agents/skills/lisa/cache/.gitkeep +0 -0
- package/dist/templates/agents/skills/lisa/scripts/storage.js +374 -0
- package/dist/templates/agents/skills/memory/SKILL.md +31 -0
- package/dist/templates/agents/skills/memory/scripts/memory.js +533 -0
- package/dist/templates/agents/skills/prompt/SKILL.md +19 -0
- package/dist/templates/agents/skills/prompt/scripts/prompt.js +184 -0
- package/dist/templates/agents/skills/tasks/SKILL.md +31 -0
- package/dist/templates/agents/skills/tasks/scripts/tasks.js +489 -0
- package/dist/templates/claude/config.js +40 -0
- package/dist/templates/claude/hooks/README.md +158 -0
- package/dist/templates/claude/hooks/common/complexity-rater.js +290 -0
- package/dist/templates/claude/hooks/common/context.js +263 -0
- package/dist/templates/claude/hooks/common/group-id.js +188 -0
- package/dist/templates/claude/hooks/common/mcp-client.js +131 -0
- package/dist/templates/claude/hooks/common/transcript-parser.js +256 -0
- package/dist/templates/claude/hooks/common/zep-client.js +175 -0
- package/dist/templates/claude/hooks/session-start.js +401 -0
- package/dist/templates/claude/hooks/session-stop-worker.js +341 -0
- package/dist/templates/claude/hooks/session-stop.js +122 -0
- package/dist/templates/claude/hooks/user-prompt-submit.js +256 -0
- package/dist/templates/claude/settings.json +46 -0
- package/dist/templates/docker/.env.lisa.example +17 -0
- package/dist/templates/docker/docker-compose.graphiti.yml +45 -0
- package/dist/templates/rules/shared/clean-architecture.md +333 -0
- package/dist/templates/rules/shared/code-quality-rules.md +469 -0
- package/dist/templates/rules/shared/git-rules.md +64 -0
- package/dist/templates/rules/shared/testing-principles.md +469 -0
- package/dist/templates/rules/typescript/coding-standards.md +751 -0
- package/dist/templates/rules/typescript/testing.md +629 -0
- package/dist/templates/rules/typescript/typescript-config-guide.md +465 -0
- package/package.json +64 -0
- package/scripts/postinstall.js +710 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code - Session Stop Worker (Background)
|
|
6
|
+
*
|
|
7
|
+
* ASYNC BACKGROUND WORKER - Runs detached from main hook
|
|
8
|
+
*
|
|
9
|
+
* This worker:
|
|
10
|
+
* 1. Parses the session transcript
|
|
11
|
+
* 2. Rates work complexity (1-5)
|
|
12
|
+
* 3. Routes to Graphiti (3+) or local logs (1-2)
|
|
13
|
+
*
|
|
14
|
+
* Called by session-stop.ts with input as CLI argument
|
|
15
|
+
*/
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { spawn } = require('child_process');
|
|
19
|
+
// Import common modules
|
|
20
|
+
const { parseTranscript, findMostRecentTranscript, formatDuration } = require('./common/transcript-parser');
|
|
21
|
+
const { rateComplexity, getRatingLabel } = require('./common/complexity-rater');
|
|
22
|
+
const { detectRepo, detectBranch } = require('./common/context');
|
|
23
|
+
const LOGS_DIR = '.logs';
|
|
24
|
+
const WORK_SESSIONS_FILE = 'work-sessions.jsonl';
|
|
25
|
+
const ERROR_LOG_FILE = 'stop-hook-errors.log';
|
|
26
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
27
|
+
const MEMORY_SKILL_TIMEOUT_MS = 10000; // 10 seconds
|
|
28
|
+
/**
|
|
29
|
+
* Main worker entry point
|
|
30
|
+
*/
|
|
31
|
+
async function main() {
|
|
32
|
+
try {
|
|
33
|
+
// 1. Parse input from CLI argument
|
|
34
|
+
const inputArg = process.argv[2];
|
|
35
|
+
if (!inputArg) {
|
|
36
|
+
logError('No input provided to worker');
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
const input = JSON.parse(inputArg);
|
|
40
|
+
// 2. Change to working directory
|
|
41
|
+
if (input.cwd && fs.existsSync(input.cwd)) {
|
|
42
|
+
process.chdir(input.cwd);
|
|
43
|
+
}
|
|
44
|
+
// 3. Find transcript file
|
|
45
|
+
const transcriptPath = findTranscript(input.transcript_path);
|
|
46
|
+
if (!transcriptPath) {
|
|
47
|
+
logError('Transcript not found', input.transcript_path);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
// 4. Parse transcript
|
|
51
|
+
const work = parseTranscript(transcriptPath);
|
|
52
|
+
// 5. Check if there was any meaningful work
|
|
53
|
+
if (work.filesModified.size === 0 && work.filesCreated.size === 0 && work.commandsRun.length === 0) {
|
|
54
|
+
// No work to capture - exit silently
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
// 6. Rate complexity
|
|
58
|
+
const rating = rateComplexity(work);
|
|
59
|
+
// 7. Route based on complexity
|
|
60
|
+
if (rating.rating >= 3) {
|
|
61
|
+
// Significant work -> Graphiti
|
|
62
|
+
await saveToGraphiti(work, rating, input);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Minor work -> Local logs
|
|
66
|
+
await saveToLocalLogs(work, rating, input);
|
|
67
|
+
}
|
|
68
|
+
// 8. Exit successfully
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const error = err;
|
|
73
|
+
logError('Worker error', error.message);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Find transcript file, handling known bugs with stale paths
|
|
79
|
+
*/
|
|
80
|
+
function findTranscript(providedPath) {
|
|
81
|
+
// Try provided path first
|
|
82
|
+
if (providedPath && fs.existsSync(providedPath)) {
|
|
83
|
+
return providedPath;
|
|
84
|
+
}
|
|
85
|
+
// Fall back to finding most recent transcript in the session directory
|
|
86
|
+
if (providedPath) {
|
|
87
|
+
const dir = path.dirname(providedPath);
|
|
88
|
+
const found = findMostRecentTranscript(dir);
|
|
89
|
+
if (found)
|
|
90
|
+
return found;
|
|
91
|
+
}
|
|
92
|
+
// Try common transcript locations
|
|
93
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
94
|
+
const sessionDirs = [
|
|
95
|
+
path.join(homeDir, '.claude', 'projects'),
|
|
96
|
+
path.join(homeDir, '.claude-code', 'sessions'),
|
|
97
|
+
];
|
|
98
|
+
for (const baseDir of sessionDirs) {
|
|
99
|
+
if (fs.existsSync(baseDir)) {
|
|
100
|
+
const found = findMostRecentTranscript(baseDir);
|
|
101
|
+
if (found)
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Save significant work to Graphiti via memory skill
|
|
109
|
+
*/
|
|
110
|
+
async function saveToGraphiti(work, rating, input) {
|
|
111
|
+
const repo = detectRepo();
|
|
112
|
+
const branch = detectBranch();
|
|
113
|
+
// Build summary text for Graphiti
|
|
114
|
+
const summary = buildGraphitiSummary(work, rating, repo, branch, input.session_id);
|
|
115
|
+
// Find memory skill script
|
|
116
|
+
const memoryScript = path.join(process.cwd(), '.agents/skills/memory/scripts/memory.js');
|
|
117
|
+
if (!fs.existsSync(memoryScript)) {
|
|
118
|
+
// Memory skill not available - fall back to local logs
|
|
119
|
+
logError('Memory skill not found, saving locally');
|
|
120
|
+
await saveToLocalLogs(work, rating, input);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Call memory skill to save
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
let resolved = false; // Guard against multiple resolves
|
|
126
|
+
const safeResolve = () => {
|
|
127
|
+
if (resolved)
|
|
128
|
+
return;
|
|
129
|
+
resolved = true;
|
|
130
|
+
resolve();
|
|
131
|
+
};
|
|
132
|
+
const fallbackAndResolve = async () => {
|
|
133
|
+
if (resolved)
|
|
134
|
+
return;
|
|
135
|
+
resolved = true;
|
|
136
|
+
await saveToLocalLogs(work, rating, input);
|
|
137
|
+
resolve();
|
|
138
|
+
};
|
|
139
|
+
const args = [
|
|
140
|
+
memoryScript,
|
|
141
|
+
'add',
|
|
142
|
+
summary,
|
|
143
|
+
'--group',
|
|
144
|
+
repo || 'agent-memories',
|
|
145
|
+
'--tag',
|
|
146
|
+
'automated',
|
|
147
|
+
'--tag',
|
|
148
|
+
`complexity:${rating.rating}`,
|
|
149
|
+
'--tag',
|
|
150
|
+
'milestone',
|
|
151
|
+
'--tag',
|
|
152
|
+
`session:${input.session_id}`,
|
|
153
|
+
'--source',
|
|
154
|
+
'session-stop',
|
|
155
|
+
'--cache',
|
|
156
|
+
];
|
|
157
|
+
if (repo)
|
|
158
|
+
args.push('--tag', `repo:${repo}`);
|
|
159
|
+
if (branch)
|
|
160
|
+
args.push('--tag', `branch:${branch}`);
|
|
161
|
+
const child = spawn('node', args, {
|
|
162
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
163
|
+
cwd: process.cwd(),
|
|
164
|
+
});
|
|
165
|
+
let stderr = '';
|
|
166
|
+
child.stderr?.on('data', (data) => {
|
|
167
|
+
stderr += data.toString();
|
|
168
|
+
});
|
|
169
|
+
child.on('close', (code) => {
|
|
170
|
+
if (code !== 0 && stderr) {
|
|
171
|
+
logError('Memory skill failed', stderr);
|
|
172
|
+
// Fall back to local logs on Graphiti failure
|
|
173
|
+
fallbackAndResolve();
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
safeResolve();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
child.on('error', (err) => {
|
|
180
|
+
logError('Memory skill spawn error', err.message);
|
|
181
|
+
// Fall back to local logs
|
|
182
|
+
fallbackAndResolve();
|
|
183
|
+
});
|
|
184
|
+
// Timeout handling
|
|
185
|
+
const timeoutId = setTimeout(() => {
|
|
186
|
+
if (resolved)
|
|
187
|
+
return;
|
|
188
|
+
try {
|
|
189
|
+
child.kill();
|
|
190
|
+
}
|
|
191
|
+
catch (_e) {
|
|
192
|
+
// Ignore kill errors
|
|
193
|
+
}
|
|
194
|
+
logError('Memory skill timeout');
|
|
195
|
+
fallbackAndResolve();
|
|
196
|
+
}, MEMORY_SKILL_TIMEOUT_MS);
|
|
197
|
+
// Clear timeout if resolved normally
|
|
198
|
+
child.on('close', () => {
|
|
199
|
+
clearTimeout(timeoutId);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Build summary text for Graphiti storage
|
|
205
|
+
*/
|
|
206
|
+
function buildGraphitiSummary(work, rating, repo, branch, sessionId) {
|
|
207
|
+
const lines = [];
|
|
208
|
+
// Header with complexity rating
|
|
209
|
+
lines.push(`MILESTONE [complexity:${rating.rating}]: ${rating.summary}`);
|
|
210
|
+
lines.push('');
|
|
211
|
+
// Session info
|
|
212
|
+
lines.push(`Session: ${sessionId}`);
|
|
213
|
+
if (repo)
|
|
214
|
+
lines.push(`Repository: ${repo}${branch ? ` (${branch})` : ''}`);
|
|
215
|
+
lines.push(`Duration: ${formatDuration(work.durationMs)}`);
|
|
216
|
+
lines.push(`Timestamp: ${work.timestamp}`);
|
|
217
|
+
lines.push('');
|
|
218
|
+
// Work summary
|
|
219
|
+
lines.push(`Files modified: ${work.filesModified.size}`);
|
|
220
|
+
lines.push(`Files created: ${work.filesCreated.size}`);
|
|
221
|
+
if (work.commandsRun.length > 0) {
|
|
222
|
+
lines.push(`Commands run: ${work.commandsRun.length}`);
|
|
223
|
+
}
|
|
224
|
+
lines.push('');
|
|
225
|
+
// Key changes
|
|
226
|
+
if (work.filesCreated.size > 0 || work.filesModified.size > 0) {
|
|
227
|
+
lines.push('Key changes:');
|
|
228
|
+
const allFiles = [...work.filesCreated].map((f) => `- Created ${f}`);
|
|
229
|
+
allFiles.push(...[...work.filesModified].map((f) => `- Modified ${f}`));
|
|
230
|
+
// Limit to 10 files
|
|
231
|
+
lines.push(...allFiles.slice(0, 10));
|
|
232
|
+
if (allFiles.length > 10) {
|
|
233
|
+
lines.push(`- ... and ${allFiles.length - 10} more`);
|
|
234
|
+
}
|
|
235
|
+
lines.push('');
|
|
236
|
+
}
|
|
237
|
+
// Tools used
|
|
238
|
+
const toolsSummary = Array.from(work.toolsUsed.entries())
|
|
239
|
+
.map(([tool, count]) => `${tool} (${count}x)`)
|
|
240
|
+
.join(', ');
|
|
241
|
+
if (toolsSummary) {
|
|
242
|
+
lines.push(`Tools used: ${toolsSummary}`);
|
|
243
|
+
lines.push('');
|
|
244
|
+
}
|
|
245
|
+
// Complexity signals
|
|
246
|
+
if (rating.signals.length > 0) {
|
|
247
|
+
lines.push(`Signals: ${rating.signals.join(', ')}`);
|
|
248
|
+
lines.push('');
|
|
249
|
+
}
|
|
250
|
+
// Assistant summary (truncated)
|
|
251
|
+
if (work.assistantSummary) {
|
|
252
|
+
lines.push('Summary:');
|
|
253
|
+
lines.push(work.assistantSummary);
|
|
254
|
+
}
|
|
255
|
+
return lines.join('\n');
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Save minor work to local JSONL logs
|
|
259
|
+
*/
|
|
260
|
+
async function saveToLocalLogs(work, rating, input) {
|
|
261
|
+
const logsDir = path.join(process.cwd(), LOGS_DIR);
|
|
262
|
+
const logsFile = path.join(logsDir, WORK_SESSIONS_FILE);
|
|
263
|
+
// Ensure directory exists
|
|
264
|
+
if (!fs.existsSync(logsDir)) {
|
|
265
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
// Rotate logs if needed
|
|
268
|
+
rotateLogsIfNeeded(logsFile);
|
|
269
|
+
// Build log entry
|
|
270
|
+
const entry = {
|
|
271
|
+
timestamp: new Date().toISOString(),
|
|
272
|
+
sessionId: input.session_id,
|
|
273
|
+
complexity: rating.rating,
|
|
274
|
+
rawScore: rating.rawScore,
|
|
275
|
+
signals: rating.signals,
|
|
276
|
+
summary: rating.summary,
|
|
277
|
+
repo: detectRepo(),
|
|
278
|
+
branch: detectBranch(),
|
|
279
|
+
duration: formatDuration(work.durationMs),
|
|
280
|
+
durationMs: work.durationMs,
|
|
281
|
+
filesModified: Array.from(work.filesModified),
|
|
282
|
+
filesCreated: Array.from(work.filesCreated),
|
|
283
|
+
commandCount: work.commandsRun.length,
|
|
284
|
+
toolsUsed: Object.fromEntries(work.toolsUsed),
|
|
285
|
+
assistantSummary: work.assistantSummary.substring(0, 200),
|
|
286
|
+
};
|
|
287
|
+
// Append to JSONL file
|
|
288
|
+
fs.appendFileSync(logsFile, JSON.stringify(entry) + '\n');
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Rotate log file if it exceeds max size
|
|
292
|
+
*/
|
|
293
|
+
function rotateLogsIfNeeded(logsFile) {
|
|
294
|
+
if (!fs.existsSync(logsFile))
|
|
295
|
+
return;
|
|
296
|
+
try {
|
|
297
|
+
const stats = fs.statSync(logsFile);
|
|
298
|
+
if (stats.size > MAX_LOG_SIZE) {
|
|
299
|
+
const archiveDir = path.join(path.dirname(logsFile), 'archive');
|
|
300
|
+
if (!fs.existsSync(archiveDir)) {
|
|
301
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
302
|
+
}
|
|
303
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
304
|
+
const archivePath = path.join(archiveDir, `work-sessions-${timestamp}.jsonl`);
|
|
305
|
+
fs.renameSync(logsFile, archivePath);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (_err) {
|
|
309
|
+
// Ignore rotation errors
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Log error to error log file
|
|
314
|
+
*/
|
|
315
|
+
function logError(message, details) {
|
|
316
|
+
const logsDir = path.join(process.cwd(), LOGS_DIR);
|
|
317
|
+
const errorLog = path.join(logsDir, ERROR_LOG_FILE);
|
|
318
|
+
try {
|
|
319
|
+
if (!fs.existsSync(logsDir)) {
|
|
320
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
321
|
+
}
|
|
322
|
+
const timestamp = new Date().toISOString();
|
|
323
|
+
const entry = `[${timestamp}] ${message}${details ? `: ${details}` : ''}\n`;
|
|
324
|
+
fs.appendFileSync(errorLog, entry);
|
|
325
|
+
}
|
|
326
|
+
catch (_err) {
|
|
327
|
+
// Ignore log errors - don't fail silently
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Global error handlers
|
|
331
|
+
process.on('uncaughtException', (err) => {
|
|
332
|
+
logError('Uncaught exception', err.stack || err.message);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
});
|
|
335
|
+
process.on('unhandledRejection', (reason) => {
|
|
336
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
337
|
+
logError('Unhandled rejection', message);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
});
|
|
340
|
+
// Run main
|
|
341
|
+
main();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code - Session Stop Hook (Main)
|
|
6
|
+
*
|
|
7
|
+
* FAST EXIT HOOK - Must exit in < 50ms
|
|
8
|
+
*
|
|
9
|
+
* This hook spawns a detached background worker to analyze the session
|
|
10
|
+
* and capture work to Graphiti or local logs. The main hook exits
|
|
11
|
+
* immediately to avoid blocking the CLI.
|
|
12
|
+
*
|
|
13
|
+
* Architecture:
|
|
14
|
+
* 1. Main Hook (this file) - Spawns worker, exits fast
|
|
15
|
+
* 2. Background Worker (session-stop-worker.js) - Does heavy lifting async
|
|
16
|
+
*
|
|
17
|
+
* Configuration: .claude/settings.json -> hooks.Stop
|
|
18
|
+
*/
|
|
19
|
+
const { spawn } = require('child_process');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const STDIN_TIMEOUT_MS = 100; // Max time to wait for stdin
|
|
23
|
+
/**
|
|
24
|
+
* Read JSON input from stdin
|
|
25
|
+
*/
|
|
26
|
+
async function readStdin() {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
let input = '';
|
|
29
|
+
// Set a short timeout for reading stdin
|
|
30
|
+
const timeout = setTimeout(() => {
|
|
31
|
+
resolve({});
|
|
32
|
+
}, STDIN_TIMEOUT_MS);
|
|
33
|
+
process.stdin.setEncoding('utf8');
|
|
34
|
+
process.stdin.on('data', (chunk) => {
|
|
35
|
+
input += chunk;
|
|
36
|
+
});
|
|
37
|
+
process.stdin.on('end', () => {
|
|
38
|
+
clearTimeout(timeout);
|
|
39
|
+
try {
|
|
40
|
+
resolve(JSON.parse(input));
|
|
41
|
+
}
|
|
42
|
+
catch (_err) {
|
|
43
|
+
resolve({});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// Handle case where stdin is already closed
|
|
47
|
+
if (process.stdin.readableEnded) {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
resolve({});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Main hook entry point
|
|
55
|
+
* MUST exit within 50ms to avoid blocking CLI
|
|
56
|
+
*/
|
|
57
|
+
async function main() {
|
|
58
|
+
// 1. Read input from stdin
|
|
59
|
+
const input = await readStdin();
|
|
60
|
+
// 2. Safety: prevent infinite loops
|
|
61
|
+
if (input.stop_hook_active) {
|
|
62
|
+
console.log(JSON.stringify({ continue: false }));
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
// 3. Determine working directory
|
|
66
|
+
const cwd = input.cwd || process.cwd();
|
|
67
|
+
// 4. Find the worker script
|
|
68
|
+
const workerPath = path.join(__dirname, 'session-stop-worker.js');
|
|
69
|
+
// Check if worker exists
|
|
70
|
+
if (!fs.existsSync(workerPath)) {
|
|
71
|
+
// Worker not deployed yet - exit silently
|
|
72
|
+
console.log(JSON.stringify({
|
|
73
|
+
continue: false,
|
|
74
|
+
stopReason: 'Worker not found',
|
|
75
|
+
}));
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
// 5. Prepare input for worker
|
|
79
|
+
const workerInput = JSON.stringify({
|
|
80
|
+
session_id: input.session_id || 'unknown',
|
|
81
|
+
transcript_path: input.transcript_path || '',
|
|
82
|
+
cwd: cwd,
|
|
83
|
+
});
|
|
84
|
+
// 6. Spawn detached background worker
|
|
85
|
+
try {
|
|
86
|
+
const worker = spawn('node', [workerPath, workerInput], {
|
|
87
|
+
detached: true, // Detach from parent process
|
|
88
|
+
stdio: 'ignore', // Don't wait for I/O
|
|
89
|
+
cwd: cwd, // Worker runs in project directory
|
|
90
|
+
env: {
|
|
91
|
+
...process.env,
|
|
92
|
+
STOP_HOOK_WORKER: 'true', // Mark as worker process
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
// Unref to allow parent to exit independently
|
|
96
|
+
worker.unref();
|
|
97
|
+
}
|
|
98
|
+
catch (_err) {
|
|
99
|
+
// Spawn failed - exit silently, don't block CLI
|
|
100
|
+
}
|
|
101
|
+
// 7. Exit immediately
|
|
102
|
+
console.log(JSON.stringify({
|
|
103
|
+
continue: false,
|
|
104
|
+
stopReason: 'Work capture queued in background',
|
|
105
|
+
}));
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
// Run main with error handling
|
|
109
|
+
// CRITICAL: Never block - exit cleanly even on error
|
|
110
|
+
main().catch((err) => {
|
|
111
|
+
// Log error for debugging but don't block
|
|
112
|
+
const errorLog = '/tmp/claude-stop-hook-error.log';
|
|
113
|
+
try {
|
|
114
|
+
fs.appendFileSync(errorLog, `[${new Date().toISOString()}] ${err.message}\n`);
|
|
115
|
+
}
|
|
116
|
+
catch (_writeErr) {
|
|
117
|
+
// Ignore write errors
|
|
118
|
+
}
|
|
119
|
+
// Always exit cleanly
|
|
120
|
+
console.log(JSON.stringify({ continue: false }));
|
|
121
|
+
process.exit(0);
|
|
122
|
+
});
|