@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,769 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * init-review.ts - Codebase analysis for Lisa
5
+ *
6
+ * Commands:
7
+ * node init-review.js run [--force] - Run static analysis + queue AI enrichment
8
+ * node init-review.js show - Show current init review from memory
9
+ * node init-review.js status - Check if init review is done
10
+ *
11
+ * This runs automatically during npm install via postinstall.js
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { spawn } = require('child_process');
17
+ // ============================================================================
18
+ // Group ID Utilities (inline to avoid import complexity)
19
+ // ============================================================================
20
+ const MAX_GROUP_ID_LENGTH = 128;
21
+ function normalizePathToGroupId(absolutePath) {
22
+ let normalized = absolutePath
23
+ .toLowerCase()
24
+ .replace(/^[a-z]:/i, (match) => match.charAt(0))
25
+ .replace(/^\//, '')
26
+ .replace(/\\/g, '-')
27
+ .replace(/\//g, '-')
28
+ .replace(/\./g, '_')
29
+ .replace(/^-+/, '')
30
+ .replace(/-+/g, '-');
31
+ if (normalized.length > MAX_GROUP_ID_LENGTH) {
32
+ normalized = normalized.slice(-MAX_GROUP_ID_LENGTH);
33
+ }
34
+ return normalized;
35
+ }
36
+ function getCurrentGroupId(cwd = process.cwd()) {
37
+ return normalizePathToGroupId(cwd);
38
+ }
39
+ // ============================================================================
40
+ // Detection Utilities
41
+ // ============================================================================
42
+ const PROJECT_FILES = {
43
+ // High confidence - definite codebase
44
+ high: [
45
+ 'package.json',
46
+ 'pyproject.toml',
47
+ 'setup.py',
48
+ 'requirements.txt',
49
+ 'Cargo.toml',
50
+ 'go.mod',
51
+ 'pom.xml',
52
+ 'build.gradle',
53
+ 'Gemfile',
54
+ 'composer.json',
55
+ 'Makefile',
56
+ 'CMakeLists.txt',
57
+ ],
58
+ // Medium confidence
59
+ medium: [
60
+ '.git',
61
+ 'src',
62
+ 'lib',
63
+ 'app',
64
+ 'README.md',
65
+ ],
66
+ };
67
+ const LANGUAGE_EXTENSIONS = {
68
+ '.ts': 'TypeScript',
69
+ '.tsx': 'TypeScript',
70
+ '.js': 'JavaScript',
71
+ '.jsx': 'JavaScript',
72
+ '.py': 'Python',
73
+ '.go': 'Go',
74
+ '.rs': 'Rust',
75
+ '.java': 'Java',
76
+ '.kt': 'Kotlin',
77
+ '.rb': 'Ruby',
78
+ '.php': 'PHP',
79
+ '.cs': 'C#',
80
+ '.cpp': 'C++',
81
+ '.c': 'C',
82
+ '.swift': 'Swift',
83
+ };
84
+ const FRAMEWORK_INDICATORS = {
85
+ 'React': { files: [], deps: ['react', 'react-dom'] },
86
+ 'Next.js': { files: ['next.config.js', 'next.config.mjs'], deps: ['next'] },
87
+ 'Vue': { files: ['vue.config.js'], deps: ['vue'] },
88
+ 'Angular': { files: ['angular.json'], deps: ['@angular/core'] },
89
+ 'Express': { files: [], deps: ['express'] },
90
+ 'NestJS': { files: ['nest-cli.json'], deps: ['@nestjs/core'] },
91
+ 'FastAPI': { files: [], deps: ['fastapi'] },
92
+ 'Django': { files: ['manage.py'], deps: ['django'] },
93
+ 'Flask': { files: [], deps: ['flask'] },
94
+ 'Rails': { files: ['Gemfile'], deps: ['rails'] },
95
+ 'Spring': { files: [], deps: ['spring-boot'] },
96
+ };
97
+ const ARCHITECTURE_INDICATORS = {
98
+ 'clean-architecture': ['domain', 'application', 'infrastructure'],
99
+ 'mvc': ['models', 'views', 'controllers'],
100
+ 'hexagonal': ['adapters', 'ports', 'domain'],
101
+ 'monorepo': ['packages', 'apps', 'libs'],
102
+ 'microservices': ['services', 'gateway', 'shared'],
103
+ };
104
+ const TEST_DIRS = ['tests', 'test', '__tests__', 'spec', 'specs'];
105
+ const ENTRY_PATTERNS = ['index.ts', 'index.js', 'main.ts', 'main.js', 'app.ts', 'app.js', 'cli.ts', 'cli.js', 'main.py', 'app.py', '__main__.py'];
106
+ // ============================================================================
107
+ // Analysis Functions
108
+ // ============================================================================
109
+ function isCodebase(projectRoot) {
110
+ // Check high confidence files
111
+ for (const file of PROJECT_FILES.high) {
112
+ const filePath = path.join(projectRoot, file);
113
+ if (fs.existsSync(filePath)) {
114
+ return { isCodebase: true, confidence: 'high', reason: `Found ${file}` };
115
+ }
116
+ }
117
+ // Check medium confidence
118
+ let mediumCount = 0;
119
+ for (const file of PROJECT_FILES.medium) {
120
+ const filePath = path.join(projectRoot, file);
121
+ if (fs.existsSync(filePath)) {
122
+ mediumCount++;
123
+ }
124
+ }
125
+ if (mediumCount >= 2) {
126
+ return { isCodebase: true, confidence: 'medium', reason: `Found ${mediumCount} indicators` };
127
+ }
128
+ // Check for code files
129
+ try {
130
+ const files = fs.readdirSync(projectRoot);
131
+ const codeFiles = files.filter((f) => {
132
+ const ext = path.extname(f);
133
+ return LANGUAGE_EXTENSIONS[ext];
134
+ });
135
+ if (codeFiles.length >= 3) {
136
+ return { isCodebase: true, confidence: 'medium', reason: `Found ${codeFiles.length} code files` };
137
+ }
138
+ }
139
+ catch (_) {
140
+ // ignore
141
+ }
142
+ return { isCodebase: false, confidence: 'low', reason: 'No codebase indicators found' };
143
+ }
144
+ function getProjectName(projectRoot) {
145
+ // Try package.json
146
+ const pkgPath = path.join(projectRoot, 'package.json');
147
+ if (fs.existsSync(pkgPath)) {
148
+ try {
149
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
150
+ if (pkg.name) {
151
+ return pkg.name.replace(/^@[^/]+\//, '');
152
+ }
153
+ }
154
+ catch (_) {
155
+ // Ignore errors
156
+ }
157
+ }
158
+ // Try pyproject.toml
159
+ const pyprojectPath = path.join(projectRoot, 'pyproject.toml');
160
+ if (fs.existsSync(pyprojectPath)) {
161
+ try {
162
+ const content = fs.readFileSync(pyprojectPath, 'utf8');
163
+ const match = content.match(/name\s*=\s*"([^"]+)"/);
164
+ if (match)
165
+ return match[1];
166
+ }
167
+ catch (_) {
168
+ // Ignore errors
169
+ }
170
+ }
171
+ // Fall back to directory name
172
+ return path.basename(projectRoot);
173
+ }
174
+ function scanDirectory(dir, maxDepth = 4, currentDepth = 0) {
175
+ const files = [];
176
+ const dirs = [];
177
+ if (currentDepth >= maxDepth)
178
+ return { files, dirs };
179
+ try {
180
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
181
+ for (const entry of entries) {
182
+ // Skip common non-source directories and lisa scaffolding
183
+ if (['node_modules', '.git', 'dist', 'build', '__pycache__', '.venv', 'venv', 'target', '.next', '.agents', '.claude', '.dev'].includes(entry.name)) {
184
+ continue;
185
+ }
186
+ const fullPath = path.join(dir, entry.name);
187
+ const relativePath = path.relative(process.cwd(), fullPath);
188
+ if (entry.isDirectory()) {
189
+ dirs.push(relativePath);
190
+ const sub = scanDirectory(fullPath, maxDepth, currentDepth + 1);
191
+ files.push(...sub.files);
192
+ dirs.push(...sub.dirs);
193
+ }
194
+ else if (entry.isFile()) {
195
+ files.push(relativePath);
196
+ }
197
+ }
198
+ }
199
+ catch (_) {
200
+ // ignore permission errors
201
+ }
202
+ return { files, dirs };
203
+ }
204
+ function detectLanguages(files) {
205
+ const langCounts = {};
206
+ for (const file of files) {
207
+ const ext = path.extname(file);
208
+ const lang = LANGUAGE_EXTENSIONS[ext];
209
+ if (lang) {
210
+ langCounts[lang] = (langCounts[lang] || 0) + 1;
211
+ }
212
+ }
213
+ // Sort by count
214
+ return Object.entries(langCounts)
215
+ .sort((a, b) => b[1] - a[1])
216
+ .map(([lang]) => lang);
217
+ }
218
+ function detectFrameworks(projectRoot, deps) {
219
+ const frameworks = [];
220
+ for (const [framework, indicators] of Object.entries(FRAMEWORK_INDICATORS)) {
221
+ // Check files
222
+ for (const file of indicators.files) {
223
+ if (fs.existsSync(path.join(projectRoot, file))) {
224
+ frameworks.push(framework);
225
+ break;
226
+ }
227
+ }
228
+ // Check dependencies
229
+ if (!frameworks.includes(framework)) {
230
+ for (const dep of indicators.deps) {
231
+ if (deps.includes(dep)) {
232
+ frameworks.push(framework);
233
+ break;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ return frameworks;
239
+ }
240
+ function detectArchitecture(dirs) {
241
+ const dirNames = dirs.map(d => path.basename(d).toLowerCase());
242
+ for (const [arch, indicators] of Object.entries(ARCHITECTURE_INDICATORS)) {
243
+ const matches = indicators.filter(ind => dirNames.includes(ind));
244
+ if (matches.length >= 2) {
245
+ return arch;
246
+ }
247
+ }
248
+ return null;
249
+ }
250
+ function detectBuildTools(projectRoot) {
251
+ const tools = [];
252
+ if (fs.existsSync(path.join(projectRoot, 'package.json')))
253
+ tools.push('npm');
254
+ if (fs.existsSync(path.join(projectRoot, 'yarn.lock')))
255
+ tools.push('yarn');
256
+ if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml')))
257
+ tools.push('pnpm');
258
+ if (fs.existsSync(path.join(projectRoot, 'tsconfig.json')))
259
+ tools.push('tsc');
260
+ if (fs.existsSync(path.join(projectRoot, 'webpack.config.js')))
261
+ tools.push('webpack');
262
+ if (fs.existsSync(path.join(projectRoot, 'vite.config.js')) || fs.existsSync(path.join(projectRoot, 'vite.config.ts')))
263
+ tools.push('vite');
264
+ if (fs.existsSync(path.join(projectRoot, 'rollup.config.js')))
265
+ tools.push('rollup');
266
+ if (fs.existsSync(path.join(projectRoot, 'Makefile')))
267
+ tools.push('make');
268
+ if (fs.existsSync(path.join(projectRoot, 'Cargo.toml')))
269
+ tools.push('cargo');
270
+ if (fs.existsSync(path.join(projectRoot, 'go.mod')))
271
+ tools.push('go');
272
+ if (fs.existsSync(path.join(projectRoot, 'pyproject.toml')))
273
+ tools.push('poetry');
274
+ if (fs.existsSync(path.join(projectRoot, 'requirements.txt')))
275
+ tools.push('pip');
276
+ return tools;
277
+ }
278
+ function getDependencies(projectRoot) {
279
+ const production = [];
280
+ const dev = [];
281
+ // Try package.json
282
+ const pkgPath = path.join(projectRoot, 'package.json');
283
+ if (fs.existsSync(pkgPath)) {
284
+ try {
285
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
286
+ if (pkg.dependencies) {
287
+ production.push(...Object.keys(pkg.dependencies));
288
+ }
289
+ if (pkg.devDependencies) {
290
+ dev.push(...Object.keys(pkg.devDependencies));
291
+ }
292
+ }
293
+ catch (_) {
294
+ // Ignore errors
295
+ }
296
+ }
297
+ // Try requirements.txt
298
+ const reqPath = path.join(projectRoot, 'requirements.txt');
299
+ if (fs.existsSync(reqPath)) {
300
+ try {
301
+ const content = fs.readFileSync(reqPath, 'utf8');
302
+ const deps = content.split('\n')
303
+ .map((l) => l.split('==')[0].split('>=')[0].trim())
304
+ .filter((l) => l && !l.startsWith('#'));
305
+ production.push(...deps);
306
+ }
307
+ catch (_) {
308
+ // Ignore errors
309
+ }
310
+ }
311
+ return { production, dev, all: [...production, ...dev] };
312
+ }
313
+ function findEntryPoints(projectRoot, _files) {
314
+ const entryPoints = [];
315
+ // Check common entry point patterns
316
+ for (const pattern of ENTRY_PATTERNS) {
317
+ // Check root
318
+ if (fs.existsSync(path.join(projectRoot, pattern))) {
319
+ entryPoints.push(pattern);
320
+ }
321
+ // Check src/
322
+ if (fs.existsSync(path.join(projectRoot, 'src', pattern))) {
323
+ entryPoints.push(`src/${pattern}`);
324
+ }
325
+ }
326
+ // Check package.json main/bin
327
+ const pkgPath = path.join(projectRoot, 'package.json');
328
+ if (fs.existsSync(pkgPath)) {
329
+ try {
330
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
331
+ if (pkg.main && !entryPoints.includes(pkg.main)) {
332
+ entryPoints.push(pkg.main);
333
+ }
334
+ if (pkg.bin) {
335
+ const bins = typeof pkg.bin === 'string' ? [pkg.bin] : Object.values(pkg.bin);
336
+ for (const bin of bins) {
337
+ if (!entryPoints.includes(bin)) {
338
+ entryPoints.push(bin);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ catch (_) {
344
+ // Ignore errors
345
+ }
346
+ }
347
+ return entryPoints.slice(0, 5); // Limit to 5
348
+ }
349
+ function findMainModules(dirs) {
350
+ const mainDirs = ['src', 'lib', 'app', 'domain', 'application', 'infrastructure', 'services', 'components', 'utils', 'core'];
351
+ return dirs
352
+ .filter(d => mainDirs.includes(path.basename(d)))
353
+ .slice(0, 10);
354
+ }
355
+ function findTestDirs(dirs) {
356
+ return dirs
357
+ .filter(d => TEST_DIRS.includes(path.basename(d)))
358
+ .slice(0, 5);
359
+ }
360
+ function findConfigFiles(projectRoot) {
361
+ const configs = [
362
+ 'tsconfig.json', '.eslintrc.js', '.eslintrc.json', '.prettierrc',
363
+ 'jest.config.js', 'vitest.config.ts', '.env.example',
364
+ 'docker-compose.yml', 'Dockerfile', '.github/workflows'
365
+ ];
366
+ return configs.filter(c => fs.existsSync(path.join(projectRoot, c)));
367
+ }
368
+ function detectTesting(projectRoot, deps) {
369
+ if (deps.includes('jest'))
370
+ return 'jest';
371
+ if (deps.includes('vitest'))
372
+ return 'vitest';
373
+ if (deps.includes('mocha'))
374
+ return 'mocha';
375
+ if (deps.includes('pytest'))
376
+ return 'pytest';
377
+ if (fs.existsSync(path.join(projectRoot, 'jest.config.js')))
378
+ return 'jest';
379
+ if (fs.existsSync(path.join(projectRoot, 'vitest.config.ts')))
380
+ return 'vitest';
381
+ return null;
382
+ }
383
+ function detectFormatting(deps) {
384
+ if (deps.includes('prettier'))
385
+ return 'prettier';
386
+ if (deps.includes('eslint'))
387
+ return 'eslint';
388
+ if (deps.includes('black'))
389
+ return 'black';
390
+ return null;
391
+ }
392
+ function detectCI(projectRoot) {
393
+ if (fs.existsSync(path.join(projectRoot, '.github', 'workflows')))
394
+ return 'github-actions';
395
+ if (fs.existsSync(path.join(projectRoot, '.gitlab-ci.yml')))
396
+ return 'gitlab-ci';
397
+ if (fs.existsSync(path.join(projectRoot, 'Jenkinsfile')))
398
+ return 'jenkins';
399
+ if (fs.existsSync(path.join(projectRoot, '.circleci')))
400
+ return 'circleci';
401
+ return null;
402
+ }
403
+ function getNoteworthy(deps) {
404
+ const noteworthy = [
405
+ // Databases
406
+ 'pg', 'mysql', 'mongodb', 'redis', 'neo4j-driver', 'prisma', 'typeorm', 'sequelize', 'mongoose',
407
+ // Frameworks
408
+ 'express', 'fastify', 'koa', 'nestjs', 'next', 'react', 'vue', 'angular', 'svelte',
409
+ // AI/ML
410
+ 'openai', '@anthropic-ai', 'langchain', 'tensorflow', 'pytorch',
411
+ // Auth
412
+ 'passport', 'jsonwebtoken', 'bcrypt',
413
+ // Testing
414
+ 'jest', 'vitest', 'playwright', 'cypress',
415
+ // Build
416
+ 'webpack', 'vite', 'rollup', 'esbuild',
417
+ // GraphQL
418
+ 'graphql', 'apollo', '@apollo/client',
419
+ ];
420
+ return deps.filter(d => noteworthy.some(n => d.includes(n))).slice(0, 10);
421
+ }
422
+ // ============================================================================
423
+ // Main Analysis
424
+ // ============================================================================
425
+ function runAnalysis(projectRoot) {
426
+ const { files, dirs } = scanDirectory(projectRoot);
427
+ const deps = getDependencies(projectRoot);
428
+ const languages = detectLanguages(files);
429
+ const frameworks = detectFrameworks(projectRoot, deps.all);
430
+ const testDirs = findTestDirs(dirs);
431
+ return {
432
+ version: '1.0',
433
+ timestamp: new Date().toISOString(),
434
+ project: {
435
+ name: getProjectName(projectRoot),
436
+ path: projectRoot,
437
+ groupId: getCurrentGroupId(projectRoot),
438
+ },
439
+ codebase: {
440
+ language: languages[0] || 'Unknown',
441
+ languages,
442
+ framework: frameworks[0] || null,
443
+ frameworks,
444
+ buildTools: detectBuildTools(projectRoot),
445
+ },
446
+ structure: {
447
+ entryPoints: findEntryPoints(projectRoot, files),
448
+ mainModules: findMainModules(dirs),
449
+ testDirs,
450
+ configFiles: findConfigFiles(projectRoot),
451
+ },
452
+ dependencies: {
453
+ count: deps.all.length,
454
+ production: deps.production.slice(0, 10),
455
+ dev: deps.dev.slice(0, 5),
456
+ noteworthy: getNoteworthy(deps.all),
457
+ },
458
+ patterns: {
459
+ architecture: detectArchitecture(dirs),
460
+ testing: detectTesting(projectRoot, deps.all),
461
+ formatting: detectFormatting(deps.all),
462
+ ci: detectCI(projectRoot),
463
+ },
464
+ metrics: {
465
+ fileCount: files.length,
466
+ dirCount: dirs.length,
467
+ hasTests: testDirs.length > 0,
468
+ hasDocumentation: fs.existsSync(path.join(projectRoot, 'README.md')),
469
+ },
470
+ };
471
+ }
472
+ function generateSummary(result) {
473
+ const parts = [];
474
+ // Language and framework
475
+ const framework = result.codebase.framework ? ` with ${result.codebase.framework}` : '';
476
+ parts.push(`${result.codebase.language} project${framework}`);
477
+ // Architecture
478
+ if (result.patterns.architecture) {
479
+ parts.push(`using ${result.patterns.architecture} pattern`);
480
+ }
481
+ // Build tools
482
+ if (result.codebase.buildTools.length > 0) {
483
+ parts.push(`Build: ${result.codebase.buildTools.join(', ')}`);
484
+ }
485
+ // Entry points
486
+ if (result.structure.entryPoints.length > 0) {
487
+ parts.push(`Entry: ${result.structure.entryPoints.slice(0, 3).join(', ')}`);
488
+ }
489
+ // Main modules
490
+ if (result.structure.mainModules.length > 0) {
491
+ parts.push(`Modules: ${result.structure.mainModules.slice(0, 4).join(', ')}`);
492
+ }
493
+ // Dependencies
494
+ if (result.dependencies.noteworthy.length > 0) {
495
+ parts.push(`Key deps: ${result.dependencies.noteworthy.slice(0, 5).join(', ')}`);
496
+ }
497
+ // Testing
498
+ if (result.patterns.testing) {
499
+ parts.push(`Testing: ${result.patterns.testing}`);
500
+ }
501
+ // Metrics
502
+ parts.push(`${result.metrics.fileCount} files, ${result.metrics.dirCount} directories`);
503
+ return parts.join('. ') + '.';
504
+ }
505
+ // ============================================================================
506
+ // Memory Storage
507
+ // ============================================================================
508
+ async function storeToMemory(summary, projectRoot) {
509
+ const memoryScript = path.join(__dirname, '..', '..', 'memory', 'scripts', 'memory.js');
510
+ if (!fs.existsSync(memoryScript)) {
511
+ throw new Error('Memory script not found');
512
+ }
513
+ return new Promise((resolve, reject) => {
514
+ const child = spawn('node', [
515
+ memoryScript,
516
+ 'add',
517
+ summary,
518
+ '--type', 'init-review',
519
+ '--tag', 'scope:codebase',
520
+ '--cache',
521
+ ], {
522
+ cwd: projectRoot,
523
+ stdio: ['ignore', 'pipe', 'pipe'],
524
+ });
525
+ let stdout = '';
526
+ let stderr = '';
527
+ child.stdout.on('data', (data) => { stdout += data.toString(); });
528
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
529
+ child.on('close', (code) => {
530
+ if (code === 0) {
531
+ resolve();
532
+ }
533
+ else {
534
+ reject(new Error(`Memory storage failed: ${stderr || stdout}`));
535
+ }
536
+ });
537
+ });
538
+ }
539
+ async function loadFromMemory(projectRoot) {
540
+ const memoryScript = path.join(__dirname, '..', '..', 'memory', 'scripts', 'memory.js');
541
+ if (!fs.existsSync(memoryScript)) {
542
+ return null;
543
+ }
544
+ return new Promise((resolve) => {
545
+ const child = spawn('node', [
546
+ memoryScript,
547
+ 'load',
548
+ '--query', 'init-review',
549
+ '--limit', '1',
550
+ '--cache',
551
+ ], {
552
+ cwd: projectRoot,
553
+ stdio: ['ignore', 'pipe', 'pipe'],
554
+ });
555
+ let stdout = '';
556
+ child.stdout.on('data', (data) => { stdout += data.toString(); });
557
+ child.on('close', (code) => {
558
+ if (code === 0) {
559
+ try {
560
+ const result = JSON.parse(stdout);
561
+ const facts = result.facts || [];
562
+ if (facts.length > 0) {
563
+ resolve(facts[0].fact || facts[0].name || null);
564
+ }
565
+ else {
566
+ resolve(null);
567
+ }
568
+ }
569
+ catch (_) {
570
+ resolve(null);
571
+ }
572
+ }
573
+ else {
574
+ resolve(null);
575
+ }
576
+ });
577
+ });
578
+ }
579
+ // ============================================================================
580
+ // Marker File
581
+ // ============================================================================
582
+ function getMarkerPath(projectRoot) {
583
+ return path.join(projectRoot, '.agents', '.init-review-done');
584
+ }
585
+ function readMarker(projectRoot) {
586
+ const markerPath = getMarkerPath(projectRoot);
587
+ if (!fs.existsSync(markerPath)) {
588
+ return { done: false, enriched: false, timestamp: null };
589
+ }
590
+ try {
591
+ const content = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
592
+ return {
593
+ done: true,
594
+ enriched: content.enriched || false,
595
+ timestamp: content.timestamp || null,
596
+ };
597
+ }
598
+ catch (_) {
599
+ return { done: true, enriched: false, timestamp: null };
600
+ }
601
+ }
602
+ function writeMarker(projectRoot, enriched = false) {
603
+ const markerPath = getMarkerPath(projectRoot);
604
+ const content = {
605
+ version: '1.0',
606
+ timestamp: new Date().toISOString(),
607
+ groupId: getCurrentGroupId(projectRoot),
608
+ enriched,
609
+ };
610
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
611
+ fs.writeFileSync(markerPath, JSON.stringify(content, null, 2));
612
+ }
613
+ function deleteMarker(projectRoot) {
614
+ const markerPath = getMarkerPath(projectRoot);
615
+ if (fs.existsSync(markerPath)) {
616
+ fs.unlinkSync(markerPath);
617
+ }
618
+ }
619
+ // ============================================================================
620
+ // CLI Commands
621
+ // ============================================================================
622
+ async function commandRun(projectRoot, force) {
623
+ // Check marker
624
+ const marker = readMarker(projectRoot);
625
+ if (marker.done && !force) {
626
+ console.log(JSON.stringify({
627
+ status: 'skipped',
628
+ action: 'run',
629
+ reason: 'Init review already done. Use --force to re-run.',
630
+ timestamp: marker.timestamp,
631
+ }, null, 2));
632
+ return;
633
+ }
634
+ // Delete marker if forcing
635
+ if (force) {
636
+ deleteMarker(projectRoot);
637
+ }
638
+ // Check if codebase
639
+ const codebaseInfo = isCodebase(projectRoot);
640
+ if (!codebaseInfo.isCodebase) {
641
+ console.log(JSON.stringify({
642
+ status: 'skipped',
643
+ action: 'run',
644
+ reason: codebaseInfo.reason,
645
+ }, null, 2));
646
+ return;
647
+ }
648
+ // Run analysis
649
+ const result = runAnalysis(projectRoot);
650
+ const summary = generateSummary(result);
651
+ // Store to memory
652
+ try {
653
+ await storeToMemory(summary, projectRoot);
654
+ }
655
+ catch (err) {
656
+ // Log but don't fail
657
+ console.error(`Warning: Could not store to memory: ${err instanceof Error ? err.message : err}`);
658
+ }
659
+ // Write marker (not enriched yet)
660
+ writeMarker(projectRoot, false);
661
+ // Save static analysis for AI enrichment worker
662
+ const agentsDir = path.join(projectRoot, '.agents');
663
+ const staticFile = path.join(agentsDir, '.init-review-static.json');
664
+ try {
665
+ fs.writeFileSync(staticFile, JSON.stringify({ summary, result }, null, 2));
666
+ }
667
+ catch (err) {
668
+ console.error(`Warning: Could not save static analysis: ${err instanceof Error ? err.message : err}`);
669
+ }
670
+ // Spawn AI enrichment worker in background
671
+ const enrichWorker = path.join(__dirname, 'ai-enrich.js');
672
+ if (fs.existsSync(enrichWorker)) {
673
+ try {
674
+ // Use stdio: 'ignore' for cross-platform compatibility
675
+ // The worker handles its own logging via fs.appendFileSync
676
+ const child = spawn('node', [enrichWorker, projectRoot, agentsDir], {
677
+ cwd: projectRoot,
678
+ stdio: 'ignore',
679
+ detached: true,
680
+ windowsHide: true,
681
+ });
682
+ child.unref();
683
+ }
684
+ catch (err) {
685
+ // Log but don't fail
686
+ console.error(`Warning: Could not start AI enrichment: ${err instanceof Error ? err.message : err}`);
687
+ }
688
+ }
689
+ // Output result
690
+ console.log(JSON.stringify({
691
+ status: 'ok',
692
+ action: 'run',
693
+ result,
694
+ summary,
695
+ enrichmentQueued: fs.existsSync(path.join(__dirname, 'ai-enrich.js')),
696
+ }, null, 2));
697
+ }
698
+ async function commandShow(projectRoot) {
699
+ const marker = readMarker(projectRoot);
700
+ if (!marker.done) {
701
+ console.log(JSON.stringify({
702
+ status: 'not_found',
703
+ action: 'show',
704
+ message: 'No init review found. Run with: node init-review.js run',
705
+ }, null, 2));
706
+ return;
707
+ }
708
+ // Try to load from memory
709
+ const review = await loadFromMemory(projectRoot);
710
+ console.log(JSON.stringify({
711
+ status: 'ok',
712
+ action: 'show',
713
+ review: review || 'Init review stored but not found in memory',
714
+ enriched: marker.enriched,
715
+ timestamp: marker.timestamp,
716
+ }, null, 2));
717
+ }
718
+ async function commandStatus(projectRoot) {
719
+ const marker = readMarker(projectRoot);
720
+ const codebaseInfo = isCodebase(projectRoot);
721
+ console.log(JSON.stringify({
722
+ status: 'ok',
723
+ action: 'status',
724
+ isCodebase: codebaseInfo.isCodebase,
725
+ confidence: codebaseInfo.confidence,
726
+ done: marker.done,
727
+ enriched: marker.enriched,
728
+ timestamp: marker.timestamp,
729
+ groupId: getCurrentGroupId(projectRoot),
730
+ }, null, 2));
731
+ }
732
+ // ============================================================================
733
+ // Main
734
+ // ============================================================================
735
+ async function main() {
736
+ const args = process.argv.slice(2);
737
+ const command = args[0] || 'status';
738
+ const force = args.includes('--force');
739
+ const projectRoot = process.cwd();
740
+ try {
741
+ switch (command) {
742
+ case 'run':
743
+ await commandRun(projectRoot, force);
744
+ break;
745
+ case 'show':
746
+ await commandShow(projectRoot);
747
+ break;
748
+ case 'status':
749
+ await commandStatus(projectRoot);
750
+ break;
751
+ default:
752
+ console.log(JSON.stringify({
753
+ status: 'error',
754
+ error: `Unknown command: ${command}. Use run|show|status`,
755
+ }, null, 2));
756
+ process.exit(1);
757
+ }
758
+ }
759
+ catch (err) {
760
+ console.log(JSON.stringify({
761
+ status: 'error',
762
+ error: err instanceof Error ? err.message : String(err),
763
+ }, null, 2));
764
+ process.exit(1);
765
+ }
766
+ }
767
+ // Export for use by postinstall
768
+ module.exports = { isCodebase, runAnalysis, generateSummary, writeMarker };
769
+ main();