@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.
Files changed (48) hide show
  1. package/README.md +42 -0
  2. package/dist/cli.js +390 -0
  3. package/dist/lib/interfaces/IDockerClient.js +2 -0
  4. package/dist/lib/interfaces/IMcpClient.js +2 -0
  5. package/dist/lib/interfaces/IServices.js +2 -0
  6. package/dist/lib/interfaces/ITemplateCopier.js +2 -0
  7. package/dist/lib/mcp.js +35 -0
  8. package/dist/lib/services.js +57 -0
  9. package/dist/package.json +36 -0
  10. package/dist/templates/agents/.sample.env +12 -0
  11. package/dist/templates/agents/docs/STORAGE_SETUP.md +161 -0
  12. package/dist/templates/agents/skills/common/group-id.js +193 -0
  13. package/dist/templates/agents/skills/init-review/SKILL.md +119 -0
  14. package/dist/templates/agents/skills/init-review/scripts/ai-enrich.js +258 -0
  15. package/dist/templates/agents/skills/init-review/scripts/init-review.js +769 -0
  16. package/dist/templates/agents/skills/lisa/SKILL.md +92 -0
  17. package/dist/templates/agents/skills/lisa/cache/.gitkeep +0 -0
  18. package/dist/templates/agents/skills/lisa/scripts/storage.js +374 -0
  19. package/dist/templates/agents/skills/memory/SKILL.md +31 -0
  20. package/dist/templates/agents/skills/memory/scripts/memory.js +533 -0
  21. package/dist/templates/agents/skills/prompt/SKILL.md +19 -0
  22. package/dist/templates/agents/skills/prompt/scripts/prompt.js +184 -0
  23. package/dist/templates/agents/skills/tasks/SKILL.md +31 -0
  24. package/dist/templates/agents/skills/tasks/scripts/tasks.js +489 -0
  25. package/dist/templates/claude/config.js +40 -0
  26. package/dist/templates/claude/hooks/README.md +158 -0
  27. package/dist/templates/claude/hooks/common/complexity-rater.js +290 -0
  28. package/dist/templates/claude/hooks/common/context.js +263 -0
  29. package/dist/templates/claude/hooks/common/group-id.js +188 -0
  30. package/dist/templates/claude/hooks/common/mcp-client.js +131 -0
  31. package/dist/templates/claude/hooks/common/transcript-parser.js +256 -0
  32. package/dist/templates/claude/hooks/common/zep-client.js +175 -0
  33. package/dist/templates/claude/hooks/session-start.js +401 -0
  34. package/dist/templates/claude/hooks/session-stop-worker.js +341 -0
  35. package/dist/templates/claude/hooks/session-stop.js +122 -0
  36. package/dist/templates/claude/hooks/user-prompt-submit.js +256 -0
  37. package/dist/templates/claude/settings.json +46 -0
  38. package/dist/templates/docker/.env.lisa.example +17 -0
  39. package/dist/templates/docker/docker-compose.graphiti.yml +45 -0
  40. package/dist/templates/rules/shared/clean-architecture.md +333 -0
  41. package/dist/templates/rules/shared/code-quality-rules.md +469 -0
  42. package/dist/templates/rules/shared/git-rules.md +64 -0
  43. package/dist/templates/rules/shared/testing-principles.md +469 -0
  44. package/dist/templates/rules/typescript/coding-standards.md +751 -0
  45. package/dist/templates/rules/typescript/testing.md +629 -0
  46. package/dist/templates/rules/typescript/typescript-config-guide.md +465 -0
  47. package/package.json +64 -0
  48. 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
+ });