@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,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
|
+
});
|