@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,710 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * postinstall.js
4
+ *
5
+ * Automatically scaffolds .agents/ and .claude/ folders when the package is installed.
6
+ * This enables plug-and-play memory and rules for Claude Code.
7
+ *
8
+ * Interactive prompts default to "yes" for seamless installation.
9
+ */
10
+ const path = require('path');
11
+ const fs = require('fs-extra');
12
+ const { execSync, spawn } = require('child_process');
13
+ const readline = require('readline');
14
+ const net = require('net');
15
+
16
+ // When installed as a dependency, the project root is four levels up from node_modules/@tonycasey/lisa/scripts
17
+ // When running locally (development), use the current working directory
18
+ const isInstalledAsDependency = __dirname.includes('node_modules');
19
+ const projectRoot = isInstalledAsDependency
20
+ ? path.resolve(__dirname, '..', '..', '..', '..')
21
+ : process.cwd();
22
+
23
+ const templateRoot = path.resolve(__dirname, '..', 'dist', 'templates');
24
+
25
+ const DEFAULT_ENDPOINT = 'http://localhost:8010/mcp/';
26
+
27
+ /**
28
+ * Get project name from package.json or directory name
29
+ */
30
+ function getProjectName() {
31
+ try {
32
+ const pkgPath = path.join(projectRoot, 'package.json');
33
+ if (fs.existsSync(pkgPath)) {
34
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
35
+ if (pkg.name) {
36
+ // Remove scope prefix if present (e.g., @tonycasey/lisa -> lisa)
37
+ return pkg.name.replace(/^@[^/]+\//, '');
38
+ }
39
+ }
40
+ } catch (_) {
41
+ // Ignore errors reading package.json
42
+ }
43
+ // Fall back to directory name
44
+ return path.basename(projectRoot);
45
+ }
46
+
47
+ const DEFAULT_GROUP = getProjectName();
48
+
49
+ // Files that indicate this is primarily a non-npm project
50
+ const NON_NPM_PROJECT_FILES = [
51
+ 'requirements.txt', // Python
52
+ 'pyproject.toml', // Python
53
+ 'setup.py', // Python
54
+ 'go.mod', // Go
55
+ 'Cargo.toml', // Rust
56
+ 'pom.xml', // Java Maven
57
+ 'build.gradle', // Java Gradle
58
+ 'Gemfile', // Ruby
59
+ 'composer.json', // PHP
60
+ ];
61
+
62
+ /**
63
+ * Check if this is primarily a non-npm project (Python, Go, Rust, etc.)
64
+ * Returns true if project has non-npm project files and package.json only has lisa
65
+ */
66
+ async function isNonNpmProject() {
67
+ // Check for non-npm project files
68
+ for (const file of NON_NPM_PROJECT_FILES) {
69
+ const filePath = path.join(projectRoot, file);
70
+ if (await fs.pathExists(filePath)) {
71
+ return true;
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+
77
+ /**
78
+ * Set up isolated mode for non-npm projects
79
+ * Creates .claude/lib/ structure to keep project root clean
80
+ */
81
+ async function setupIsolatedMode(claudeDir) {
82
+ const libDir = path.join(claudeDir, 'lib');
83
+ await fs.ensureDir(libDir);
84
+
85
+ // Create minimal package.json in .claude/lib
86
+ const libPackageJson = {
87
+ name: 'claude-lib',
88
+ version: '1.0.0',
89
+ private: true,
90
+ description: 'Lisa support files for Claude Code',
91
+ };
92
+
93
+ const libPackagePath = path.join(libDir, 'package.json');
94
+ if (!(await fs.pathExists(libPackagePath))) {
95
+ await fs.writeJson(libPackagePath, libPackageJson, { spaces: 2 });
96
+ }
97
+
98
+ // Move node_modules to .claude/lib/ if it exists in project root
99
+ const rootNodeModules = path.join(projectRoot, 'node_modules');
100
+ const libNodeModules = path.join(libDir, 'node_modules');
101
+
102
+ if (await fs.pathExists(rootNodeModules) && !(await fs.pathExists(libNodeModules))) {
103
+ await fs.move(rootNodeModules, libNodeModules);
104
+ console.log(' ✓ Moved node_modules to .claude/lib/');
105
+ }
106
+
107
+ // Move package.json and package-lock.json to .claude/lib/ if they're minimal (only lisa)
108
+ const rootPackageJson = path.join(projectRoot, 'package.json');
109
+ if (await fs.pathExists(rootPackageJson)) {
110
+ try {
111
+ const pkg = await fs.readJson(rootPackageJson);
112
+ const deps = Object.keys(pkg.dependencies || {});
113
+ // If package.json only has lisa as a dependency, move it to lib
114
+ if (deps.length === 1 && deps[0] === '@tonycasey/lisa') {
115
+ const libPkgPath = path.join(libDir, 'package.json');
116
+ await fs.move(rootPackageJson, libPkgPath, { overwrite: true });
117
+ console.log(' ✓ Moved package.json to .claude/lib/');
118
+
119
+ // Also move package-lock.json if exists
120
+ const rootLockFile = path.join(projectRoot, 'package-lock.json');
121
+ if (await fs.pathExists(rootLockFile)) {
122
+ await fs.move(rootLockFile, path.join(libDir, 'package-lock.json'), { overwrite: true });
123
+ console.log(' ✓ Moved package-lock.json to .claude/lib/');
124
+ }
125
+ }
126
+ } catch (e) {
127
+ // Ignore errors reading package.json
128
+ }
129
+ }
130
+
131
+ // Add .claude/lib to .gitignore
132
+ const gitignorePath = path.join(projectRoot, '.gitignore');
133
+ if (await fs.pathExists(gitignorePath)) {
134
+ let gitignore = await fs.readFile(gitignorePath, 'utf8');
135
+ if (!gitignore.includes('.claude/lib/node_modules')) {
136
+ gitignore += '\n# Lisa support files\n.claude/lib/node_modules/\n';
137
+ await fs.writeFile(gitignorePath, gitignore);
138
+ console.log(' ✓ Added .claude/lib/node_modules to .gitignore');
139
+ }
140
+ }
141
+
142
+ return true;
143
+ }
144
+
145
+ /**
146
+ * Ask a yes/no question with default "yes"
147
+ */
148
+ function askYesNo(question) {
149
+ return new Promise((resolve) => {
150
+ // If not interactive (CI, piped input), default to yes
151
+ if (!process.stdin.isTTY) {
152
+ resolve(true);
153
+ return;
154
+ }
155
+
156
+ const rl = readline.createInterface({
157
+ input: process.stdin,
158
+ output: process.stdout
159
+ });
160
+
161
+ rl.question(`${question} [Y/n] `, (answer) => {
162
+ rl.close();
163
+ const normalized = answer.trim().toLowerCase();
164
+ // Default to yes (empty or 'y' or 'yes')
165
+ resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
166
+ });
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Check if Docker is available and running
172
+ */
173
+ function isDockerAvailable() {
174
+ try {
175
+ execSync('docker info', { stdio: 'ignore' });
176
+ return true;
177
+ } catch {
178
+ return false;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Check if docker compose is available
184
+ */
185
+ function isDockerComposeAvailable() {
186
+ try {
187
+ execSync('docker compose version', { stdio: 'ignore' });
188
+ return true;
189
+ } catch {
190
+ try {
191
+ execSync('docker-compose version', { stdio: 'ignore' });
192
+ return true;
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Check if lisa docker containers are already running
201
+ */
202
+ function isLisaContainerRunning() {
203
+ try {
204
+ const output = execSync('docker ps --format "{{.Names}}"', { encoding: 'utf8' });
205
+ // Check for lisa-graphiti-mcp container
206
+ return output.includes('lisa-graphiti-mcp') || output.includes('lisa_graphiti-mcp');
207
+ } catch {
208
+ return false;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Check if a port is available
214
+ */
215
+ function isPortAvailable(port) {
216
+ return new Promise((resolve) => {
217
+ const server = net.createServer();
218
+ server.once('error', () => resolve(false));
219
+ server.once('listening', () => {
220
+ server.close();
221
+ resolve(true);
222
+ });
223
+ server.listen(port, '0.0.0.0');
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Find an available port starting from the given port
229
+ */
230
+ async function findAvailablePort(startPort, maxAttempts = 10) {
231
+ for (let i = 0; i < maxAttempts; i++) {
232
+ const port = startPort + i;
233
+ if (await isPortAvailable(port)) {
234
+ return port;
235
+ }
236
+ }
237
+ return null;
238
+ }
239
+
240
+ /**
241
+ * Update docker-compose file with new port mappings
242
+ */
243
+ async function updateDockerComposePorts(composeFile, neo4jBrowserPort, neo4jBoltPort, mcpPort) {
244
+ let content = await fs.readFile(composeFile, 'utf8');
245
+
246
+ // Update Neo4j browser port (7474)
247
+ content = content.replace(
248
+ /- "7474:7474"/g,
249
+ `- "${neo4jBrowserPort}:7474"`
250
+ );
251
+
252
+ // Update Neo4j bolt port (7687) - host mapping only, internal stays the same
253
+ content = content.replace(
254
+ /- "7687:7687"/g,
255
+ `- "${neo4jBoltPort}:7687"`
256
+ );
257
+
258
+ // Update MCP port
259
+ content = content.replace(
260
+ /- "8010:8000"/g,
261
+ `- "${mcpPort}:8000"`
262
+ );
263
+
264
+ await fs.writeFile(composeFile, content);
265
+
266
+ return { neo4jBrowserPort, neo4jBoltPort, mcpPort };
267
+ }
268
+
269
+ /**
270
+ * Run docker compose command
271
+ */
272
+ function runDockerCompose(composeFile, args) {
273
+ return new Promise((resolve, reject) => {
274
+ // Try 'docker compose' first, fall back to 'docker-compose'
275
+ let cmd = 'docker';
276
+ let cmdArgs = ['compose', '-f', composeFile, ...args];
277
+
278
+ const child = spawn(cmd, cmdArgs, {
279
+ cwd: projectRoot,
280
+ stdio: 'inherit'
281
+ });
282
+
283
+ child.on('close', (code) => {
284
+ if (code === 0) {
285
+ resolve();
286
+ } else {
287
+ reject(new Error(`Docker compose exited with code ${code}`));
288
+ }
289
+ });
290
+
291
+ child.on('error', (err) => {
292
+ reject(err);
293
+ });
294
+ });
295
+ }
296
+
297
+ /**
298
+ * Run docker compose in the background without waiting for completion.
299
+ * Output is redirected to a log file.
300
+ */
301
+ function runDockerComposeBackground(composeFile, args) {
302
+ const logFile = path.join(path.dirname(composeFile), '.docker-setup.log');
303
+ const logStream = require('fs').createWriteStream(logFile);
304
+
305
+ const cmd = 'docker';
306
+ const cmdArgs = ['compose', '-f', composeFile, ...args];
307
+
308
+ const child = spawn(cmd, cmdArgs, {
309
+ cwd: projectRoot,
310
+ stdio: ['ignore', logStream, logStream],
311
+ detached: true
312
+ });
313
+
314
+ // Unref so parent process can exit
315
+ child.unref();
316
+
317
+ return { pid: child.pid, logFile };
318
+ }
319
+
320
+ async function copyTemplates(src, dest, force = false) {
321
+ if (!force && (await fs.pathExists(dest))) {
322
+ return false;
323
+ }
324
+ await fs.ensureDir(path.dirname(dest));
325
+ await fs.copy(src, dest, { overwrite: force });
326
+ return true;
327
+ }
328
+
329
+ async function createSymlink(target, linkPath) {
330
+ try {
331
+ if (await fs.pathExists(linkPath)) {
332
+ const stat = await fs.lstat(linkPath);
333
+ if (stat.isSymbolicLink()) {
334
+ await fs.remove(linkPath);
335
+ } else {
336
+ return false;
337
+ }
338
+ }
339
+ await fs.ensureDir(path.dirname(linkPath));
340
+ await fs.symlink(target, linkPath, 'dir');
341
+ return true;
342
+ } catch {
343
+ return false;
344
+ }
345
+ }
346
+
347
+ async function writeEnvFile(dest, endpoint, group) {
348
+ if (await fs.pathExists(dest)) {
349
+ return false;
350
+ }
351
+ await fs.ensureDir(path.dirname(dest));
352
+ const content = `GRAPHITI_ENDPOINT=${endpoint}\nGRAPHITI_GROUP_ID=${group}\n`;
353
+ await fs.writeFile(dest, content, 'utf8');
354
+ return true;
355
+ }
356
+
357
+ async function copyDockerFiles(agentsDir) {
358
+ const dockerSrc = path.join(templateRoot, 'docker');
359
+ if (!(await fs.pathExists(dockerSrc))) {
360
+ return false;
361
+ }
362
+
363
+ // Copy docker-compose file to .agents/
364
+ const composeSrc = path.join(dockerSrc, 'docker-compose.graphiti.yml');
365
+ const composeDest = path.join(agentsDir, 'docker-compose.graphiti.yml');
366
+ if (await fs.pathExists(composeSrc)) {
367
+ await fs.copy(composeSrc, composeDest, { overwrite: false });
368
+ }
369
+
370
+ // Copy .env.lisa.example to project root (if no .env exists)
371
+ const envSrc = path.join(dockerSrc, '.env.lisa.example');
372
+ const rootEnv = path.join(path.dirname(agentsDir), '.env');
373
+ const envExampleDest = path.join(path.dirname(agentsDir), '.env.lisa.example');
374
+ if (await fs.pathExists(envSrc)) {
375
+ // Always copy the example file for reference
376
+ await fs.copy(envSrc, envExampleDest, { overwrite: false });
377
+ // If no .env exists, create one from the example
378
+ if (!(await fs.pathExists(rootEnv))) {
379
+ await fs.copy(envSrc, rootEnv);
380
+ console.log(' Created .env from .env.lisa.example');
381
+ }
382
+ }
383
+
384
+ return true;
385
+ }
386
+
387
+ // ============================================================================
388
+ // Init Review - Automatic Codebase Analysis
389
+ // ============================================================================
390
+
391
+ const CODEBASE_INDICATORS = [
392
+ 'package.json', 'pyproject.toml', 'setup.py', 'requirements.txt',
393
+ 'Cargo.toml', 'go.mod', 'pom.xml', 'build.gradle', 'Gemfile',
394
+ 'composer.json', 'Makefile', 'CMakeLists.txt'
395
+ ];
396
+
397
+ /**
398
+ * Check if the project root is a codebase (not just an empty folder)
399
+ */
400
+ async function isCodebase() {
401
+ for (const file of CODEBASE_INDICATORS) {
402
+ const filePath = path.join(projectRoot, file);
403
+ if (await fs.pathExists(filePath)) {
404
+ return true;
405
+ }
406
+ }
407
+ // Check for .git or src directory
408
+ if (await fs.pathExists(path.join(projectRoot, '.git'))) return true;
409
+ if (await fs.pathExists(path.join(projectRoot, 'src'))) return true;
410
+ return false;
411
+ }
412
+
413
+ /**
414
+ * Run init-review in the background (doesn't block postinstall)
415
+ */
416
+ async function runInitReview(agentsDir) {
417
+ // Skip if user explicitly disabled init-review (useful for CI or large repos)
418
+ if (process.env.LISA_SKIP_INIT_REVIEW === '1' || process.env.LISA_SKIP_INIT_REVIEW === 'true') {
419
+ return;
420
+ }
421
+
422
+ const markerPath = path.join(agentsDir, '.init-review-done');
423
+
424
+ // Skip if already done
425
+ if (await fs.pathExists(markerPath)) {
426
+ return;
427
+ }
428
+
429
+ // Skip if not a codebase
430
+ if (!(await isCodebase())) {
431
+ return;
432
+ }
433
+
434
+ const initReviewScript = path.join(agentsDir, 'skills', 'init-review', 'scripts', 'init-review.js');
435
+
436
+ // Skip if script doesn't exist (first install, templates not yet copied)
437
+ if (!(await fs.pathExists(initReviewScript))) {
438
+ return;
439
+ }
440
+
441
+ console.log('');
442
+ console.log(' Running codebase analysis in background...');
443
+
444
+ try {
445
+ // Run init-review in detached background process
446
+ const logFile = path.join(agentsDir, '.init-review.log');
447
+ const logStream = require('fs').createWriteStream(logFile);
448
+
449
+ const child = spawn('node', [initReviewScript, 'run'], {
450
+ cwd: projectRoot,
451
+ stdio: ['ignore', logStream, logStream],
452
+ detached: true
453
+ });
454
+
455
+ child.unref();
456
+
457
+ console.log(' ✓ Init review queued (logs: .agents/.init-review.log)');
458
+ } catch (err) {
459
+ console.log(` Init review failed: ${err.message}`);
460
+ }
461
+ }
462
+
463
+ async function setupDocker(agentsDir) {
464
+ console.log('');
465
+
466
+ // Check if Docker is available
467
+ if (!isDockerAvailable()) {
468
+ console.log(' Docker is not running or not installed.');
469
+ console.log(' To enable memory persistence, install Docker and run:');
470
+ console.log(' docker compose -f .agents/docker-compose.graphiti.yml up -d');
471
+ return;
472
+ }
473
+
474
+ if (!isDockerComposeAvailable()) {
475
+ console.log(' Docker Compose is not available.');
476
+ return;
477
+ }
478
+
479
+ // Check if lisa container is already running - skip to avoid wiping env vars
480
+ if (isLisaContainerRunning()) {
481
+ console.log(' ✓ Lisa memory stack is already running (container detected)');
482
+ console.log(' Skipping docker compose to preserve existing configuration.');
483
+ return;
484
+ }
485
+
486
+ // Ask user if they want to start Docker
487
+ const shouldStart = await askYesNo('Start Graphiti memory stack (requires Docker)?');
488
+
489
+ if (!shouldStart) {
490
+ console.log(' Skipping Docker setup. Run later with:');
491
+ console.log(' docker compose -f .agents/docker-compose.graphiti.yml up -d');
492
+ return;
493
+ }
494
+
495
+ // Copy Docker files to .agents/
496
+ console.log(' Copying Docker configuration...');
497
+ await copyDockerFiles(agentsDir);
498
+
499
+ // Check if .env exists in project root, if not copy from example
500
+ const projectRoot = path.dirname(agentsDir);
501
+ const envFile = path.join(projectRoot, '.env');
502
+ const envExample = path.join(projectRoot, '.env.lisa.example');
503
+ if (!(await fs.pathExists(envFile)) && (await fs.pathExists(envExample))) {
504
+ await fs.copy(envExample, envFile);
505
+ console.log(' Created .env from .env.lisa.example');
506
+ console.log(' IMPORTANT: Edit .env and add your OPENAI_API_KEY');
507
+ }
508
+
509
+ // Check for required API key
510
+ const envContent = await fs.pathExists(envFile) ? await fs.readFile(envFile, 'utf8') : '';
511
+ if (!envContent.includes('OPENAI_API_KEY=') || envContent.includes('OPENAI_API_KEY=sk-...')) {
512
+ console.log('');
513
+ console.log(' OPENAI_API_KEY not configured in .env');
514
+ console.log(' Graphiti requires an OpenAI API key for LLM-powered entity extraction.');
515
+ console.log(' Please edit .env and add your key, then run:');
516
+ console.log(' docker compose -f .agents/docker-compose.graphiti.yml up -d');
517
+ return;
518
+ }
519
+
520
+ // Check for available ports and update compose file if needed
521
+ const composeFile = path.join(agentsDir, 'docker-compose.graphiti.yml');
522
+
523
+ console.log(' Checking port availability...');
524
+
525
+ let neo4jBrowserPort = 7474;
526
+ let neo4jBoltPort = 7687;
527
+ let mcpPort = 8010;
528
+ let portsChanged = false;
529
+
530
+ // Check Neo4j browser port (7474)
531
+ if (!(await isPortAvailable(7474))) {
532
+ neo4jBrowserPort = await findAvailablePort(7475);
533
+ if (!neo4jBrowserPort) {
534
+ console.log(' Could not find available port for Neo4j browser (tried 7474-7484)');
535
+ console.log(' Please free up a port and try again.');
536
+ return;
537
+ }
538
+ portsChanged = true;
539
+ console.log(` Port 7474 in use, using ${neo4jBrowserPort} for Neo4j browser`);
540
+ }
541
+
542
+ // Check Neo4j bolt port (7687)
543
+ if (!(await isPortAvailable(7687))) {
544
+ neo4jBoltPort = await findAvailablePort(7688);
545
+ if (!neo4jBoltPort) {
546
+ console.log(' Could not find available port for Neo4j bolt (tried 7687-7697)');
547
+ console.log(' Please free up a port and try again.');
548
+ return;
549
+ }
550
+ portsChanged = true;
551
+ console.log(` Port 7687 in use, using ${neo4jBoltPort} for Neo4j bolt`);
552
+ }
553
+
554
+ // Check MCP port (8010)
555
+ if (!(await isPortAvailable(8010))) {
556
+ mcpPort = await findAvailablePort(8011);
557
+ if (!mcpPort) {
558
+ console.log(' Could not find available port for Graphiti MCP (tried 8010-8020)');
559
+ console.log(' Please free up a port and try again.');
560
+ return;
561
+ }
562
+ portsChanged = true;
563
+ console.log(` Port 8010 in use, using ${mcpPort} for Graphiti MCP`);
564
+ }
565
+
566
+ // Update compose file if ports changed
567
+ if (portsChanged) {
568
+ await updateDockerComposePorts(composeFile, neo4jBrowserPort, neo4jBoltPort, mcpPort);
569
+
570
+ // Also update .env with the new endpoint
571
+ const envFilePath = path.join(projectRoot, '.env');
572
+ if (await fs.pathExists(envFilePath)) {
573
+ let envContent = await fs.readFile(envFilePath, 'utf8');
574
+ envContent = envContent.replace(
575
+ /GRAPHITI_ENDPOINT=http:\/\/localhost:\d+\/mcp\//,
576
+ `GRAPHITI_ENDPOINT=http://localhost:${mcpPort}/mcp/`
577
+ );
578
+ // If no endpoint exists, add it
579
+ if (!envContent.includes('GRAPHITI_ENDPOINT=')) {
580
+ envContent += `\nGRAPHITI_ENDPOINT=http://localhost:${mcpPort}/mcp/\n`;
581
+ }
582
+ await fs.writeFile(envFilePath, envContent);
583
+ }
584
+
585
+ // Also update .agents/skills/.env (where skills actually read config from)
586
+ const skillsEnvPath = path.join(projectRoot, '.agents', 'skills', '.env');
587
+ if (await fs.pathExists(skillsEnvPath)) {
588
+ let skillsEnv = await fs.readFile(skillsEnvPath, 'utf8');
589
+ skillsEnv = skillsEnv.replace(
590
+ /GRAPHITI_ENDPOINT=http:\/\/localhost:\d+\/mcp\//,
591
+ `GRAPHITI_ENDPOINT=http://localhost:${mcpPort}/mcp/`
592
+ );
593
+ // If no endpoint exists, add it
594
+ if (!skillsEnv.includes('GRAPHITI_ENDPOINT=')) {
595
+ skillsEnv += `\nGRAPHITI_ENDPOINT=http://localhost:${mcpPort}/mcp/\n`;
596
+ }
597
+ await fs.writeFile(skillsEnvPath, skillsEnv);
598
+ console.log(` Updated .agents/skills/.env with port ${mcpPort}`);
599
+ }
600
+ }
601
+
602
+ // Start Docker stack in the background
603
+ try {
604
+ runDockerComposeBackground(composeFile, ['up', '-d']);
605
+ console.log('');
606
+ console.log(' Starting Graphiti memory stack in the background...');
607
+ console.log(' (First run may take a few minutes to download images)');
608
+ console.log('');
609
+ console.log(' Check status: docker ps --filter "name=lisa"');
610
+ console.log(' View logs: cat .agents/.docker-setup.log');
611
+ console.log('');
612
+ console.log(' Once running:');
613
+ console.log(` Neo4j Browser: http://localhost:${neo4jBrowserPort}`);
614
+ console.log(` Neo4j Bolt: bolt://localhost:${neo4jBoltPort}`);
615
+ console.log(` Graphiti MCP: http://localhost:${mcpPort}`);
616
+ } catch (err) {
617
+ console.log(` Docker startup failed: ${err.message}`);
618
+ console.log(' You can try manually with:');
619
+ console.log(' docker compose -f .agents/docker-compose.graphiti.yml up -d');
620
+ }
621
+ }
622
+
623
+ async function main() {
624
+ // Skip postinstall in development mode (when running npm ci/install in the source repo)
625
+ // Only run when installed as a dependency in another project
626
+ if (!isInstalledAsDependency) {
627
+ // Silent exit in dev mode - this is expected behavior
628
+ return;
629
+ }
630
+
631
+ console.log('');
632
+ console.log('lisa: Setting up Claude Code memory and rules...');
633
+ console.log('');
634
+
635
+ // Check if templates exist
636
+ if (!(await fs.pathExists(templateRoot))) {
637
+ console.error(' Templates not found. Package may not be built correctly.');
638
+ process.exit(1);
639
+ }
640
+
641
+ const agentsDir = path.join(projectRoot, '.agents');
642
+ const claudeDir = path.join(projectRoot, '.claude');
643
+
644
+ // Copy .agents templates
645
+ const agentsSrc = path.join(templateRoot, 'agents');
646
+ const rulesSrc = path.join(templateRoot, 'rules');
647
+
648
+ if (await fs.pathExists(agentsSrc)) {
649
+ const skillsSrc = path.join(agentsSrc, 'skills');
650
+ if (await fs.pathExists(skillsSrc)) {
651
+ await copyTemplates(skillsSrc, path.join(agentsDir, 'skills'));
652
+ console.log(' ✓ Copied .agents/skills/');
653
+ }
654
+ }
655
+
656
+ if (await fs.pathExists(rulesSrc)) {
657
+ await copyTemplates(rulesSrc, path.join(agentsDir, 'rules'));
658
+ console.log(' ✓ Copied .agents/rules/');
659
+ }
660
+
661
+ // Copy .claude templates
662
+ const claudeSrc = path.join(templateRoot, 'claude');
663
+ if (await fs.pathExists(claudeSrc)) {
664
+ await copyTemplates(path.join(claudeSrc, 'settings.json'), path.join(claudeDir, 'settings.json'));
665
+ await copyTemplates(path.join(claudeSrc, 'config.js'), path.join(claudeDir, 'config.js'));
666
+ await copyTemplates(path.join(claudeSrc, 'hooks'), path.join(claudeDir, 'hooks'));
667
+ console.log(' ✓ Copied .claude/ hooks and settings');
668
+ }
669
+
670
+ // Create symlinks
671
+ if (await createSymlink('../.agents/rules', path.join(claudeDir, 'rules'))) {
672
+ console.log(' ✓ Created symlink .claude/rules');
673
+ }
674
+ if (await createSymlink('../.agents/skills', path.join(claudeDir, 'skills'))) {
675
+ console.log(' ✓ Created symlink .claude/skills');
676
+ }
677
+
678
+ // Create .env with defaults
679
+ const envPath = path.join(agentsDir, 'skills', '.env');
680
+ const endpoint = process.env.GRAPHITI_ENDPOINT || DEFAULT_ENDPOINT;
681
+ const group = process.env.GRAPHITI_GROUP_ID || DEFAULT_GROUP;
682
+ await writeEnvFile(envPath, endpoint, group);
683
+
684
+ // Check if this is a non-npm project (Python, Go, Rust, etc.)
685
+ // If so, move node_modules and package.json to .claude/lib/ to keep project clean
686
+ if (await isNonNpmProject()) {
687
+ console.log('');
688
+ console.log(' Detected non-npm project (Python, Go, Rust, etc.)');
689
+ console.log(' Setting up isolated mode to keep your project clean...');
690
+ await setupIsolatedMode(claudeDir);
691
+ }
692
+
693
+ // Init review - automatic codebase analysis (runs in background)
694
+ await runInitReview(agentsDir);
695
+
696
+ // Docker setup with interactive prompt
697
+ await setupDocker(agentsDir);
698
+
699
+ console.log('');
700
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
701
+ console.log(' lisa: Setup complete!');
702
+ console.log(' Claude Code now has automatic memory and coding rules.');
703
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
704
+ console.log('');
705
+ }
706
+
707
+ main().catch((err) => {
708
+ console.error('lisa postinstall failed:', err.message);
709
+ // Don't exit with error - allow npm install to complete
710
+ });