@shipfast-ai/shipfast 0.4.4 → 0.6.1
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/bin/install.js +108 -4
- package/brain/indexer.cjs +50 -4
- package/commands/sf/do.md +18 -17
- package/commands/sf/status.md +8 -4
- package/hooks/sf-first-run.js +33 -41
- package/mcp/server.cjs +285 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +13 -3
package/bin/install.js
CHANGED
|
@@ -56,6 +56,10 @@ function main() {
|
|
|
56
56
|
case 'train': return cmdInit();
|
|
57
57
|
case 'update': return cmdUpdate();
|
|
58
58
|
case 'uninstall': return cmdUninstall();
|
|
59
|
+
case 'status': return cmdStatus();
|
|
60
|
+
case 'brain': return cmdBrain();
|
|
61
|
+
case 'learn': return cmdLearn();
|
|
62
|
+
case 'decide': return cmdDecide();
|
|
59
63
|
case 'help':
|
|
60
64
|
case 'h': return cmdHelp();
|
|
61
65
|
case 'version':
|
|
@@ -108,6 +112,9 @@ function installFor(key, runtime) {
|
|
|
108
112
|
const coreDir = path.join(sfDir, 'core');
|
|
109
113
|
for (const d of [sfDir, brainDir, coreDir]) fs.mkdirSync(d, { recursive: true });
|
|
110
114
|
|
|
115
|
+
// Write version file so /sf-status can read it
|
|
116
|
+
fs.writeFileSync(path.join(sfDir, 'version'), pkg.version);
|
|
117
|
+
|
|
111
118
|
for (const f of ['schema.sql', 'index.cjs', 'indexer.cjs'])
|
|
112
119
|
copy('brain/' + f, path.join(brainDir, f));
|
|
113
120
|
|
|
@@ -132,18 +139,25 @@ function installFor(key, runtime) {
|
|
|
132
139
|
for (const f of ['sf-context-monitor.js','sf-statusline.js','sf-first-run.js'])
|
|
133
140
|
copy('hooks/' + f, path.join(hooksDir, f));
|
|
134
141
|
|
|
142
|
+
// Copy MCP server
|
|
143
|
+
const mcpDir = path.join(sfDir, 'mcp');
|
|
144
|
+
fs.mkdirSync(mcpDir, { recursive: true });
|
|
145
|
+
copy('mcp/server.cjs', path.join(mcpDir, 'server.cjs'));
|
|
146
|
+
|
|
135
147
|
// Runtime-specific config
|
|
136
148
|
const claudeCompat = ['Claude Code', 'OpenCode', 'Kilo'];
|
|
137
149
|
const geminiCompat = ['Gemini CLI', 'Antigravity'];
|
|
138
150
|
|
|
139
151
|
if (claudeCompat.includes(runtime.name)) {
|
|
140
152
|
writeSettings(dir, hooksDir);
|
|
153
|
+
writeMcpConfig(dir);
|
|
141
154
|
writeInstruction(path.join(dir, 'CLAUDE.md'));
|
|
142
155
|
} else if (geminiCompat.includes(runtime.name)) {
|
|
143
156
|
writeInstruction(path.join(dir, 'AGENTS.md'));
|
|
144
157
|
} else if (runtime.name === 'Copilot') {
|
|
145
158
|
writeInstruction(path.join(dir, 'copilot-instructions.md'));
|
|
146
159
|
} else if (runtime.name === 'Cursor') {
|
|
160
|
+
writeMcpConfig(dir);
|
|
147
161
|
writeInstruction(path.join(dir, 'rules'));
|
|
148
162
|
} else {
|
|
149
163
|
writeInstruction(path.join(dir, 'AGENTS.md'));
|
|
@@ -168,6 +182,8 @@ function printDone(count) {
|
|
|
168
182
|
|
|
169
183
|
function cmdInit() {
|
|
170
184
|
const cwd = process.cwd();
|
|
185
|
+
const fresh = process.argv.includes('--fresh');
|
|
186
|
+
|
|
171
187
|
if (!fs.existsSync(path.join(cwd, '.git'))) {
|
|
172
188
|
console.log(`${red}Not a git repo.${reset} Run this inside a git repository.\n`);
|
|
173
189
|
return;
|
|
@@ -180,10 +196,19 @@ function cmdInit() {
|
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
const brainExists = fs.existsSync(path.join(cwd, '.shipfast', 'brain.db'));
|
|
183
|
-
|
|
199
|
+
|
|
200
|
+
// FIX #8: --fresh flag
|
|
201
|
+
if (fresh && brainExists) {
|
|
202
|
+
fs.unlinkSync(path.join(cwd, '.shipfast', 'brain.db'));
|
|
203
|
+
console.log('Cleared existing brain.db');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(brainExists && !fresh ? 'Re-indexing codebase...' : 'Indexing codebase...');
|
|
184
207
|
|
|
185
208
|
try {
|
|
186
|
-
const
|
|
209
|
+
const args = [indexer, cwd];
|
|
210
|
+
if (fresh) args.push('--fresh');
|
|
211
|
+
const out = safeRun(process.execPath, args, {
|
|
187
212
|
encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe']
|
|
188
213
|
});
|
|
189
214
|
console.log(`${green}${out.trim()}${reset}`);
|
|
@@ -258,8 +283,13 @@ function cmdUninstall() {
|
|
|
258
283
|
if (f.startsWith('sf-')) fs.unlinkSync(path.join(hd, f));
|
|
259
284
|
}
|
|
260
285
|
}
|
|
261
|
-
// Clean settings.json
|
|
286
|
+
// Clean settings.json hooks (#18: remove empty arrays too)
|
|
262
287
|
cleanSettings(dir);
|
|
288
|
+
// FIX #7: Clean instruction files (CLAUDE.md, AGENTS.md, etc.)
|
|
289
|
+
cleanInstructionFile(path.join(dir, 'CLAUDE.md'));
|
|
290
|
+
cleanInstructionFile(path.join(dir, 'AGENTS.md'));
|
|
291
|
+
cleanInstructionFile(path.join(dir, 'copilot-instructions.md'));
|
|
292
|
+
cleanInstructionFile(path.join(dir, 'rules'));
|
|
263
293
|
console.log(` ${green}${runtime.name}${reset} — removed`);
|
|
264
294
|
removed++;
|
|
265
295
|
}
|
|
@@ -309,6 +339,8 @@ function cleanSettings(dir) {
|
|
|
309
339
|
function cmdHelp() {
|
|
310
340
|
console.log(`${bold}Terminal commands:${reset}\n`);
|
|
311
341
|
console.log(` ${cyan}shipfast init${reset} Index current repo into .shipfast/brain.db`);
|
|
342
|
+
console.log(` ${cyan}shipfast init --fresh${reset} Full reindex (clears existing brain.db)`);
|
|
343
|
+
console.log(` ${cyan}shipfast status${reset} Show installed runtimes + brain status`);
|
|
312
344
|
console.log(` ${cyan}shipfast update${reset} Update to latest + re-detect runtimes`);
|
|
313
345
|
console.log(` ${cyan}shipfast uninstall${reset} Remove from all AI tools`);
|
|
314
346
|
console.log(` ${cyan}shipfast help${reset} Show this help\n`);
|
|
@@ -322,7 +354,7 @@ function cmdHelp() {
|
|
|
322
354
|
console.log(` ${cyan}/sf-undo${reset} Rollback a task`);
|
|
323
355
|
console.log(` ${cyan}/sf-brain${reset} <query> Query the knowledge graph`);
|
|
324
356
|
console.log(` ${cyan}/sf-learn${reset} <pattern> Teach a pattern or lesson`);
|
|
325
|
-
console.log(` ${cyan}/sf-config${reset} Set
|
|
357
|
+
console.log(` ${cyan}/sf-config${reset} Set model tiers and preferences`);
|
|
326
358
|
console.log(` ${cyan}/sf-milestone${reset} Complete or start a milestone`);
|
|
327
359
|
console.log(` ${cyan}/sf-help${reset} Show all commands\n`);
|
|
328
360
|
}
|
|
@@ -352,6 +384,23 @@ function writeSettings(dir, hooksDir) {
|
|
|
352
384
|
fs.writeFileSync(sp, JSON.stringify(s, null, 2));
|
|
353
385
|
}
|
|
354
386
|
|
|
387
|
+
function writeMcpConfig(dir) {
|
|
388
|
+
const serverPath = path.join(dir, 'shipfast', 'mcp', 'server.cjs');
|
|
389
|
+
const settingsPath = path.join(dir, 'settings.json');
|
|
390
|
+
let s = {};
|
|
391
|
+
if (fs.existsSync(settingsPath)) { try { s = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {} }
|
|
392
|
+
|
|
393
|
+
if (!s.mcpServers) s.mcpServers = {};
|
|
394
|
+
|
|
395
|
+
s.mcpServers['shipfast-brain'] = {
|
|
396
|
+
command: 'node',
|
|
397
|
+
args: [serverPath],
|
|
398
|
+
env: {}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
fs.writeFileSync(settingsPath, JSON.stringify(s, null, 2));
|
|
402
|
+
}
|
|
403
|
+
|
|
355
404
|
function writeInstruction(filePath) {
|
|
356
405
|
const marker = '<!-- ShipFast -->';
|
|
357
406
|
const close = '<!-- /ShipFast -->';
|
|
@@ -368,6 +417,61 @@ function writeInstruction(filePath) {
|
|
|
368
417
|
fs.writeFileSync(filePath, content);
|
|
369
418
|
}
|
|
370
419
|
|
|
420
|
+
// FIX #7: Remove ShipFast block from instruction files during uninstall
|
|
421
|
+
function cleanInstructionFile(filePath) {
|
|
422
|
+
if (!fs.existsSync(filePath)) return;
|
|
423
|
+
try {
|
|
424
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
425
|
+
const marker = '<!-- ShipFast -->';
|
|
426
|
+
const close = '<!-- /ShipFast -->';
|
|
427
|
+
const s = content.indexOf(marker);
|
|
428
|
+
const e = content.indexOf(close);
|
|
429
|
+
if (s !== -1 && e !== -1) {
|
|
430
|
+
content = content.slice(0, s) + content.slice(e + close.length);
|
|
431
|
+
content = content.trim() + '\n';
|
|
432
|
+
fs.writeFileSync(filePath, content);
|
|
433
|
+
}
|
|
434
|
+
} catch {}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// FIX #17: CLI status command — show installed runtimes + version + brain status
|
|
438
|
+
function cmdStatus() {
|
|
439
|
+
console.log(`${bold}Installed runtimes:${reset}\n`);
|
|
440
|
+
let count = 0;
|
|
441
|
+
for (const [key, runtime] of Object.entries(RUNTIMES)) {
|
|
442
|
+
const dir = path.join(os.homedir(), runtime.path);
|
|
443
|
+
if (fs.existsSync(path.join(dir, 'shipfast'))) {
|
|
444
|
+
console.log(` ${green}${runtime.name}${reset} ${dim}${dir}${reset}`);
|
|
445
|
+
count++;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (count === 0) {
|
|
449
|
+
console.log(` ${dim}(none)${reset}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Brain status for current directory
|
|
453
|
+
const cwd = process.cwd();
|
|
454
|
+
const brainPath = path.join(cwd, '.shipfast', 'brain.db');
|
|
455
|
+
console.log(`\n${bold}Current repo:${reset} ${cwd}\n`);
|
|
456
|
+
if (fs.existsSync(brainPath)) {
|
|
457
|
+
try {
|
|
458
|
+
const out = safeRun('sqlite3', [brainPath,
|
|
459
|
+
"SELECT 'nodes', COUNT(*) FROM nodes UNION ALL SELECT 'edges', COUNT(*) FROM edges UNION ALL SELECT 'decisions', COUNT(*) FROM decisions UNION ALL SELECT 'learnings', COUNT(*) FROM learnings;"
|
|
460
|
+
], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
461
|
+
console.log(` Brain: ${green}indexed${reset}`);
|
|
462
|
+
out.trim().split('\n').forEach(line => {
|
|
463
|
+
const [k, v] = line.split('|');
|
|
464
|
+
console.log(` ${dim}${k}: ${v}${reset}`);
|
|
465
|
+
});
|
|
466
|
+
} catch {
|
|
467
|
+
console.log(` Brain: ${green}exists${reset} (.shipfast/brain.db)`);
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
console.log(` Brain: ${yellow}not indexed${reset} — run ${cyan}shipfast init${reset}`);
|
|
471
|
+
}
|
|
472
|
+
console.log('');
|
|
473
|
+
}
|
|
474
|
+
|
|
371
475
|
function copy(rel, dest) {
|
|
372
476
|
const src = path.join(__dirname, '..', rel);
|
|
373
477
|
if (fs.existsSync(src)) fs.copyFileSync(src, dest);
|
package/brain/indexer.cjs
CHANGED
|
@@ -461,21 +461,67 @@ function indexCodebase(cwd, opts = {}) {
|
|
|
461
461
|
execFileSync('sqlite3', [dbPath], { input: sql, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
-
//
|
|
464
|
+
// FIX #1: Clean orphan nodes (deleted/renamed/moved files)
|
|
465
|
+
let cleaned = 0;
|
|
466
|
+
if (!changedOnly) {
|
|
467
|
+
const discoveredPaths = new Set(files.map(f => path.relative(cwd, f).replace(/\\/g, '/')));
|
|
468
|
+
const existingFiles = brain.query(cwd, "SELECT file_path FROM nodes WHERE kind = 'file'");
|
|
469
|
+
const orphans = existingFiles.filter(row => !discoveredPaths.has(row.file_path));
|
|
470
|
+
|
|
471
|
+
if (orphans.length > 0) {
|
|
472
|
+
const cleanSql = ['BEGIN TRANSACTION;'];
|
|
473
|
+
for (const orphan of orphans) {
|
|
474
|
+
const escaped = orphan.file_path.replace(/'/g, "''");
|
|
475
|
+
// Delete file node + all symbols from that file + all edges from/to that file
|
|
476
|
+
cleanSql.push(`DELETE FROM nodes WHERE file_path = '${escaped}';`);
|
|
477
|
+
cleanSql.push(`DELETE FROM edges WHERE source LIKE 'file:${escaped}%' OR target LIKE 'file:${escaped}%' OR source LIKE 'fn:${escaped}%' OR source LIKE 'type:${escaped}%' OR source LIKE 'class:${escaped}%' OR source LIKE 'component:${escaped}%';`);
|
|
478
|
+
}
|
|
479
|
+
cleanSql.push('COMMIT;');
|
|
480
|
+
const dbPath = brain.getBrainPath(cwd);
|
|
481
|
+
execFileSync('sqlite3', [dbPath], { input: cleanSql.join('\n'), stdio: ['pipe', 'pipe', 'pipe'] });
|
|
482
|
+
cleaned = orphans.length;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// FIX #6: Update hot files on every index
|
|
465
487
|
brain.updateHotFiles(cwd);
|
|
466
488
|
|
|
489
|
+
// FIX #11: Run co-change analysis
|
|
490
|
+
try {
|
|
491
|
+
const gitIntelPath = path.join(__dirname, '..', 'core', 'git-intel.cjs');
|
|
492
|
+
if (fs.existsSync(gitIntelPath)) {
|
|
493
|
+
const gitIntel = require(gitIntelPath);
|
|
494
|
+
gitIntel.analyzeCoChanges(cwd, 200);
|
|
495
|
+
}
|
|
496
|
+
} catch { /* git-intel optional */ }
|
|
497
|
+
|
|
467
498
|
const elapsed = Date.now() - startTime;
|
|
468
|
-
return { files: files.length, indexed, skipped, nodes: totalNodes, edges: totalEdges, statements: batch.count, elapsed_ms: elapsed };
|
|
499
|
+
return { files: files.length, indexed, skipped, cleaned, nodes: totalNodes, edges: totalEdges, statements: batch.count, elapsed_ms: elapsed };
|
|
469
500
|
}
|
|
470
501
|
|
|
471
502
|
// CLI mode
|
|
472
503
|
if (require.main === module) {
|
|
473
504
|
const args = process.argv.slice(2);
|
|
474
505
|
const changedOnly = args.includes('--changed-only');
|
|
506
|
+
const fresh = args.includes('--fresh');
|
|
475
507
|
const cwd = args.find(a => !a.startsWith('-')) || process.cwd();
|
|
476
|
-
|
|
508
|
+
|
|
509
|
+
// FIX #8: --fresh flag deletes brain.db first
|
|
510
|
+
if (fresh) {
|
|
511
|
+
const dbPath = path.join(cwd, '.shipfast', 'brain.db');
|
|
512
|
+
if (fs.existsSync(dbPath)) {
|
|
513
|
+
fs.unlinkSync(dbPath);
|
|
514
|
+
console.log('Cleared existing brain.db');
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
console.log(`Indexing ${cwd}...`);
|
|
477
519
|
const result = indexCodebase(cwd, { verbose: true, changedOnly });
|
|
478
|
-
|
|
520
|
+
const parts = [`Done in ${result.elapsed_ms}ms: ${result.indexed} files indexed`];
|
|
521
|
+
if (result.skipped) parts.push(`${result.skipped} unchanged`);
|
|
522
|
+
if (result.cleaned) parts.push(`${result.cleaned} deleted files cleaned`);
|
|
523
|
+
parts.push(`${result.nodes} symbols, ${result.edges} edges`);
|
|
524
|
+
console.log(parts.join(', '));
|
|
479
525
|
}
|
|
480
526
|
|
|
481
527
|
module.exports = { indexCodebase, discoverFiles, discoverChangedFiles };
|
package/commands/sf/do.md
CHANGED
|
@@ -51,19 +51,13 @@ Classify the user's input using these heuristics:
|
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
54
|
-
## STEP 2:
|
|
54
|
+
## STEP 2: CONTEXT GATHERING (0 tokens)
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
node [shipfast-path]/brain/indexer.cjs [cwd]
|
|
59
|
-
```
|
|
56
|
+
**FIX #5: Git diff awareness** — Run `git diff --name-only HEAD` to see what files changed since last commit. Pass this list to Scout so it focuses on recent changes instead of searching blindly.
|
|
60
57
|
|
|
61
|
-
If brain.db
|
|
62
|
-
```bash
|
|
63
|
-
node [shipfast-path]/brain/indexer.cjs [cwd] --changed-only
|
|
64
|
-
```
|
|
58
|
+
If `.shipfast/brain.db` does not exist, tell user to run `shipfast init` first.
|
|
65
59
|
|
|
66
|
-
|
|
60
|
+
**Store the changed files list** — use it in Step 4 (Scout) and Step 6 (Builder) for targeted context.
|
|
67
61
|
|
|
68
62
|
---
|
|
69
63
|
|
|
@@ -208,14 +202,21 @@ If brain.db has requirements for this phase:
|
|
|
208
202
|
|
|
209
203
|
---
|
|
210
204
|
|
|
211
|
-
## STEP 8: LEARN
|
|
205
|
+
## STEP 8: LEARN
|
|
206
|
+
|
|
207
|
+
**FIX #9/#10: Explicitly record decisions and learnings using these exact commands:**
|
|
208
|
+
|
|
209
|
+
If you made any architectural decisions during this task, record each one:
|
|
210
|
+
```bash
|
|
211
|
+
sqlite3 .shipfast/brain.db "INSERT INTO decisions (question, decision, reasoning, phase) VALUES ('[what was decided]', '[the choice]', '[why]', '[current task]');"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
If you encountered and fixed any errors, record the pattern:
|
|
215
|
+
```bash
|
|
216
|
+
sqlite3 .shipfast/brain.db "INSERT INTO learnings (pattern, problem, solution, domain, source, confidence) VALUES ('[short pattern name]', '[what went wrong]', '[what fixed it]', '[domain]', 'auto', 0.5);"
|
|
217
|
+
```
|
|
212
218
|
|
|
213
|
-
|
|
214
|
-
- **Decisions**: Extract "chose X", "decided on Y" patterns from the session
|
|
215
|
-
- **Learnings**: Record any error→fix patterns for future sessions
|
|
216
|
-
- **Conventions**: If Builder matched a pattern not yet in brain.db, store it
|
|
217
|
-
- **Boost**: Increase confidence of learnings that helped this session
|
|
218
|
-
- **Prune**: Remove old low-confidence learnings that haven't been used
|
|
219
|
+
**These are not optional.** If decisions were made or errors were fixed, you MUST record them. This is how ShipFast gets smarter over time.
|
|
219
220
|
|
|
220
221
|
---
|
|
221
222
|
|
package/commands/sf/status.md
CHANGED
|
@@ -5,20 +5,24 @@ allowed-tools:
|
|
|
5
5
|
- Bash
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Run
|
|
8
|
+
Run these EXACT commands. Do NOT modify them. Do NOT add insights or commentary.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
sqlite3 .shipfast/brain.db "SELECT 'nodes' as k, COUNT(*) as v FROM nodes UNION ALL SELECT 'edges', COUNT(*) FROM edges UNION ALL SELECT 'decisions', COUNT(*) FROM decisions UNION ALL SELECT 'learnings', COUNT(*) FROM learnings UNION ALL SELECT 'tasks', COUNT(*) FROM tasks UNION ALL SELECT 'checkpoints', COUNT(*) FROM checkpoints UNION ALL SELECT 'hot_files', COUNT(*) FROM hot_files UNION ALL SELECT 'active', (SELECT COUNT(*) FROM tasks WHERE status IN ('running','pending')) UNION ALL SELECT 'passed', (SELECT COUNT(*) FROM tasks WHERE status='passed');" 2>/dev/null || echo "No brain.db found. Run: shipfast init"
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
```bash
|
|
15
|
+
cat ~/.claude/shipfast/version 2>/dev/null || cat ~/.cursor/shipfast/version 2>/dev/null || cat ~/.gemini/shipfast/version 2>/dev/null || echo "unknown"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then output EXACTLY this format. Nothing else:
|
|
15
19
|
|
|
16
20
|
```
|
|
17
|
-
ShipFast
|
|
21
|
+
ShipFast v[version]
|
|
18
22
|
===============
|
|
19
23
|
Brain: [nodes] nodes | [edges] edges | [decisions] decisions | [learnings] learnings | [hot_files] hot files
|
|
20
24
|
Tasks: [active] active | [passed] completed
|
|
21
25
|
Checkpoints: [checkpoints] available
|
|
22
26
|
```
|
|
23
27
|
|
|
24
|
-
STOP after printing this. No analysis. No suggestions. No insights.
|
|
28
|
+
STOP after printing this. No analysis. No suggestions. No insights.
|
package/hooks/sf-first-run.js
CHANGED
|
@@ -2,68 +2,60 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* ShipFast First-Run Hook — PreToolUse
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* FIX #3: Exits immediately for non-Skill tools (near-zero overhead).
|
|
6
|
+
* Only injects brain indexing instruction when:
|
|
7
|
+
* - Tool is Skill (sf-* command)
|
|
8
|
+
* - brain.db doesn't exist for this repo
|
|
9
|
+
* - This is a git repo
|
|
10
|
+
*
|
|
11
|
+
* After user runs `shipfast init`, brain.db exists and this becomes a no-op.
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
const fs = require('fs');
|
|
11
15
|
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
12
17
|
|
|
13
18
|
let input = '';
|
|
14
|
-
const stdinTimeout = setTimeout(() => process.exit(0),
|
|
19
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 5000);
|
|
15
20
|
process.stdin.setEncoding('utf8');
|
|
16
21
|
process.stdin.on('data', chunk => input += chunk);
|
|
17
22
|
process.stdin.on('end', () => {
|
|
18
23
|
clearTimeout(stdinTimeout);
|
|
19
24
|
try {
|
|
20
25
|
const data = JSON.parse(input);
|
|
21
|
-
const cwd = data.cwd || process.cwd();
|
|
22
26
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
if (toolName !== 'Skill') {
|
|
26
|
-
process.exit(0);
|
|
27
|
-
}
|
|
27
|
+
// FIX #3: Fast exit for non-Skill tools (99% of calls)
|
|
28
|
+
if ((data.tool_name || '') !== 'Skill') process.exit(0);
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
const brainPath = path.join(cwd, '.shipfast', 'brain.db');
|
|
31
|
-
if (fs.existsSync(brainPath)) {
|
|
32
|
-
process.exit(0); // already trained, no-op
|
|
33
|
-
}
|
|
30
|
+
const cwd = data.cwd || process.cwd();
|
|
34
31
|
|
|
35
|
-
//
|
|
36
|
-
if (
|
|
37
|
-
process.exit(0); // not a repo, skip
|
|
38
|
-
}
|
|
32
|
+
// Already indexed — no-op
|
|
33
|
+
if (fs.existsSync(path.join(cwd, '.shipfast', 'brain.db'))) process.exit(0);
|
|
39
34
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
path.join(cwd, '.claude', 'shipfast', 'brain', 'indexer.cjs'),
|
|
43
|
-
path.join(cwd, '.cursor', 'shipfast', 'brain', 'indexer.cjs'),
|
|
44
|
-
path.join(cwd, '.gemini', 'shipfast', 'brain', 'indexer.cjs'),
|
|
45
|
-
path.join(require('os').homedir(), '.claude', 'shipfast', 'brain', 'indexer.cjs'),
|
|
46
|
-
];
|
|
35
|
+
// Not a git repo — skip
|
|
36
|
+
if (!fs.existsSync(path.join(cwd, '.git'))) process.exit(0);
|
|
47
37
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
// Find indexer in global install locations
|
|
39
|
+
const home = os.homedir();
|
|
40
|
+
const indexer = [
|
|
41
|
+
path.join(home, '.claude', 'shipfast', 'brain', 'indexer.cjs'),
|
|
42
|
+
path.join(home, '.cursor', 'shipfast', 'brain', 'indexer.cjs'),
|
|
43
|
+
path.join(home, '.gemini', 'shipfast', 'brain', 'indexer.cjs'),
|
|
44
|
+
path.join(home, '.codex', 'shipfast', 'brain', 'indexer.cjs'),
|
|
45
|
+
path.join(home, '.config', 'opencode', 'shipfast', 'brain', 'indexer.cjs'),
|
|
46
|
+
].find(p => fs.existsSync(p));
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
if (!indexer) process.exit(0);
|
|
49
|
+
|
|
50
|
+
process.stdout.write(JSON.stringify({
|
|
55
51
|
hookSpecificOutput: {
|
|
56
52
|
hookEventName: 'PreToolUse',
|
|
57
|
-
additionalContext: 'SHIPFAST
|
|
58
|
-
'
|
|
59
|
-
'```bash\nnode "' +
|
|
60
|
-
'
|
|
61
|
-
'Also add ".shipfast/" to .gitignore if not already there. ' +
|
|
62
|
-
'After indexing, proceed with the user\'s command normally.'
|
|
53
|
+
additionalContext: 'SHIPFAST: brain.db not found for this repo. ' +
|
|
54
|
+
'Run this first to index the codebase:\n' +
|
|
55
|
+
'```bash\nnode "' + indexer + '" "' + cwd + '"\n```\n' +
|
|
56
|
+
'Then proceed with the command.'
|
|
63
57
|
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
process.stdout.write(JSON.stringify(output));
|
|
58
|
+
}));
|
|
67
59
|
} catch {
|
|
68
60
|
process.exit(0);
|
|
69
61
|
}
|
package/mcp/server.cjs
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ShipFast MCP Server — stdio transport
|
|
5
|
+
*
|
|
6
|
+
* Exposes brain.db as structured MCP tools.
|
|
7
|
+
* Works with: Claude Code, Cursor, Windsurf, Codex, OpenCode, Gemini, etc.
|
|
8
|
+
*
|
|
9
|
+
* Tools:
|
|
10
|
+
* brain_stats — node/edge/decision/learning counts
|
|
11
|
+
* brain_search — search files/functions by name
|
|
12
|
+
* brain_files — list indexed files
|
|
13
|
+
* brain_decisions — list or add decisions
|
|
14
|
+
* brain_learnings — list or add learnings
|
|
15
|
+
* brain_hot_files — show most changed files
|
|
16
|
+
* brain_status — full status summary
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { execFileSync: safeRun } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const CWD = process.env.SHIPFAST_CWD || process.cwd();
|
|
26
|
+
const DB_PATH = path.join(CWD, '.shipfast', 'brain.db');
|
|
27
|
+
|
|
28
|
+
// ============================================================
|
|
29
|
+
// SQLite query helper
|
|
30
|
+
// ============================================================
|
|
31
|
+
|
|
32
|
+
function query(sql) {
|
|
33
|
+
if (!fs.existsSync(DB_PATH)) return [];
|
|
34
|
+
try {
|
|
35
|
+
const result = safeRun('sqlite3', ['-json', DB_PATH, sql], {
|
|
36
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
|
|
37
|
+
});
|
|
38
|
+
return result.trim() ? JSON.parse(result) : [];
|
|
39
|
+
} catch { return []; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function run(sql) {
|
|
43
|
+
if (!fs.existsSync(DB_PATH)) return false;
|
|
44
|
+
try {
|
|
45
|
+
safeRun('sqlite3', [DB_PATH, sql], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
46
|
+
return true;
|
|
47
|
+
} catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function esc(s) {
|
|
51
|
+
return s == null ? '' : String(s).replace(/'/g, "''");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================
|
|
55
|
+
// Tool implementations
|
|
56
|
+
// ============================================================
|
|
57
|
+
|
|
58
|
+
const TOOLS = {
|
|
59
|
+
brain_stats: {
|
|
60
|
+
description: 'Get brain.db statistics: node count, edge count, decisions, learnings, hot files, tasks.',
|
|
61
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
62
|
+
handler() {
|
|
63
|
+
if (!fs.existsSync(DB_PATH)) return { error: 'brain.db not found. Run: shipfast init' };
|
|
64
|
+
const rows = query(
|
|
65
|
+
"SELECT 'nodes' as metric, COUNT(*) as count FROM nodes " +
|
|
66
|
+
"UNION ALL SELECT 'edges', COUNT(*) FROM edges " +
|
|
67
|
+
"UNION ALL SELECT 'decisions', COUNT(*) FROM decisions " +
|
|
68
|
+
"UNION ALL SELECT 'learnings', COUNT(*) FROM learnings " +
|
|
69
|
+
"UNION ALL SELECT 'tasks', COUNT(*) FROM tasks " +
|
|
70
|
+
"UNION ALL SELECT 'checkpoints', COUNT(*) FROM checkpoints " +
|
|
71
|
+
"UNION ALL SELECT 'hot_files', COUNT(*) FROM hot_files"
|
|
72
|
+
);
|
|
73
|
+
const stats = {};
|
|
74
|
+
rows.forEach(r => stats[r.metric] = r.count);
|
|
75
|
+
return stats;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
brain_search: {
|
|
80
|
+
description: 'Search the codebase knowledge graph for files, functions, types, or components by name.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
query: { type: 'string', description: 'Search term (file name, function name, type name)' },
|
|
85
|
+
kind: { type: 'string', description: 'Filter by kind: file, function, type, class, component. Optional.', enum: ['file', 'function', 'type', 'class', 'component', ''] }
|
|
86
|
+
},
|
|
87
|
+
required: ['query']
|
|
88
|
+
},
|
|
89
|
+
handler({ query: q, kind }) {
|
|
90
|
+
const kindFilter = kind ? `AND kind = '${esc(kind)}'` : '';
|
|
91
|
+
return query(
|
|
92
|
+
`SELECT kind, name, file_path, signature, line_start FROM nodes ` +
|
|
93
|
+
`WHERE (name LIKE '%${esc(q)}%' OR file_path LIKE '%${esc(q)}%') ${kindFilter} ` +
|
|
94
|
+
`ORDER BY kind, name LIMIT 30`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
brain_files: {
|
|
100
|
+
description: 'List indexed files in brain.db. Optionally filter by path pattern.',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
pattern: { type: 'string', description: 'Filter file paths containing this string. Optional.' }
|
|
105
|
+
},
|
|
106
|
+
required: []
|
|
107
|
+
},
|
|
108
|
+
handler({ pattern }) {
|
|
109
|
+
const where = pattern ? `AND file_path LIKE '%${esc(pattern)}%'` : '';
|
|
110
|
+
return query(
|
|
111
|
+
`SELECT file_path, hash FROM nodes WHERE kind = 'file' ${where} ORDER BY file_path LIMIT 50`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
brain_decisions: {
|
|
117
|
+
description: 'List all decisions, or add a new decision. Decisions persist across sessions.',
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
action: { type: 'string', description: 'list or add', enum: ['list', 'add'] },
|
|
122
|
+
question: { type: 'string', description: 'What was decided? (required for add)' },
|
|
123
|
+
decision: { type: 'string', description: 'The choice made. (required for add)' },
|
|
124
|
+
reasoning: { type: 'string', description: 'Why this choice? (optional)' },
|
|
125
|
+
phase: { type: 'string', description: 'Which phase/task. (optional)' }
|
|
126
|
+
},
|
|
127
|
+
required: ['action']
|
|
128
|
+
},
|
|
129
|
+
handler({ action, question, decision, reasoning, phase }) {
|
|
130
|
+
if (action === 'add') {
|
|
131
|
+
if (!question || !decision) return { error: 'question and decision are required' };
|
|
132
|
+
const ok = run(
|
|
133
|
+
`INSERT INTO decisions (question, decision, reasoning, phase) ` +
|
|
134
|
+
`VALUES ('${esc(question)}', '${esc(decision)}', '${esc(reasoning || '')}', '${esc(phase || '')}')`
|
|
135
|
+
);
|
|
136
|
+
return ok ? { status: 'recorded', question, decision } : { error: 'failed to insert' };
|
|
137
|
+
}
|
|
138
|
+
return query("SELECT id, question, decision, reasoning, phase, created_at FROM decisions ORDER BY created_at DESC LIMIT 20");
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
brain_learnings: {
|
|
143
|
+
description: 'List all learnings, or add a new learning. Learnings help ShipFast avoid past mistakes.',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
action: { type: 'string', description: 'list or add', enum: ['list', 'add'] },
|
|
148
|
+
pattern: { type: 'string', description: 'Short identifier e.g. "react-19-refs". (required for add)' },
|
|
149
|
+
problem: { type: 'string', description: 'What went wrong. (required for add)' },
|
|
150
|
+
solution: { type: 'string', description: 'What fixed it. (optional)' },
|
|
151
|
+
domain: { type: 'string', description: 'Area: frontend, backend, database, auth, etc. (optional)' }
|
|
152
|
+
},
|
|
153
|
+
required: ['action']
|
|
154
|
+
},
|
|
155
|
+
handler({ action, pattern, problem, solution, domain }) {
|
|
156
|
+
if (action === 'add') {
|
|
157
|
+
if (!pattern || !problem) return { error: 'pattern and problem are required' };
|
|
158
|
+
const ok = run(
|
|
159
|
+
`INSERT INTO learnings (pattern, problem, solution, domain, source, confidence) ` +
|
|
160
|
+
`VALUES ('${esc(pattern)}', '${esc(problem)}', '${esc(solution || '')}', '${esc(domain || '')}', 'user', 0.8)`
|
|
161
|
+
);
|
|
162
|
+
return ok ? { status: 'recorded', pattern, problem, solution } : { error: 'failed to insert' };
|
|
163
|
+
}
|
|
164
|
+
return query("SELECT id, pattern, problem, solution, domain, confidence, times_used FROM learnings ORDER BY confidence DESC LIMIT 20");
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
brain_hot_files: {
|
|
169
|
+
description: 'Show most frequently changed files based on git history.',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
limit: { type: 'number', description: 'How many files to show. Default: 15.' }
|
|
174
|
+
},
|
|
175
|
+
required: []
|
|
176
|
+
},
|
|
177
|
+
handler({ limit }) {
|
|
178
|
+
return query(`SELECT file_path, change_count FROM hot_files ORDER BY change_count DESC LIMIT ${parseInt(limit) || 15}`);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
brain_status: {
|
|
183
|
+
description: 'Full ShipFast status: brain stats, active tasks, recent tasks, checkpoints.',
|
|
184
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
185
|
+
handler() {
|
|
186
|
+
if (!fs.existsSync(DB_PATH)) return { error: 'brain.db not found. Run: shipfast init' };
|
|
187
|
+
|
|
188
|
+
const stats = TOOLS.brain_stats.handler();
|
|
189
|
+
const active = query("SELECT id, description, status FROM tasks WHERE status IN ('running','pending') ORDER BY created_at DESC LIMIT 5");
|
|
190
|
+
const recent = query("SELECT id, description, status, commit_sha FROM tasks WHERE status = 'passed' ORDER BY finished_at DESC LIMIT 5");
|
|
191
|
+
const checkpoints = query("SELECT id, description FROM checkpoints ORDER BY created_at DESC LIMIT 5");
|
|
192
|
+
|
|
193
|
+
return { stats, activeTasks: active, recentTasks: recent, checkpoints };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// ============================================================
|
|
199
|
+
// MCP Protocol (JSON-RPC over stdio)
|
|
200
|
+
// ============================================================
|
|
201
|
+
|
|
202
|
+
let buffer = '';
|
|
203
|
+
|
|
204
|
+
process.stdin.setEncoding('utf8');
|
|
205
|
+
process.stdin.on('data', chunk => {
|
|
206
|
+
buffer += chunk;
|
|
207
|
+
|
|
208
|
+
// MCP uses Content-Length header framing
|
|
209
|
+
while (true) {
|
|
210
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
211
|
+
if (headerEnd === -1) break;
|
|
212
|
+
|
|
213
|
+
const header = buffer.slice(0, headerEnd);
|
|
214
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
215
|
+
if (!match) { buffer = buffer.slice(headerEnd + 4); continue; }
|
|
216
|
+
|
|
217
|
+
const contentLength = parseInt(match[1]);
|
|
218
|
+
const bodyStart = headerEnd + 4;
|
|
219
|
+
if (buffer.length < bodyStart + contentLength) break;
|
|
220
|
+
|
|
221
|
+
const body = buffer.slice(bodyStart, bodyStart + contentLength);
|
|
222
|
+
buffer = buffer.slice(bodyStart + contentLength);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const msg = JSON.parse(body);
|
|
226
|
+
handleMessage(msg);
|
|
227
|
+
} catch {}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
function send(msg) {
|
|
232
|
+
const body = JSON.stringify(msg);
|
|
233
|
+
const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
|
|
234
|
+
process.stdout.write(header + body);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function handleMessage(msg) {
|
|
238
|
+
const { id, method, params } = msg;
|
|
239
|
+
|
|
240
|
+
if (method === 'initialize') {
|
|
241
|
+
return send({
|
|
242
|
+
jsonrpc: '2.0', id,
|
|
243
|
+
result: {
|
|
244
|
+
protocolVersion: '2024-11-05',
|
|
245
|
+
capabilities: { tools: {} },
|
|
246
|
+
serverInfo: { name: 'shipfast-brain', version: '0.5.0' }
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (method === 'notifications/initialized') return;
|
|
252
|
+
|
|
253
|
+
if (method === 'tools/list') {
|
|
254
|
+
const tools = Object.entries(TOOLS).map(([name, t]) => ({
|
|
255
|
+
name,
|
|
256
|
+
description: t.description,
|
|
257
|
+
inputSchema: t.inputSchema
|
|
258
|
+
}));
|
|
259
|
+
return send({ jsonrpc: '2.0', id, result: { tools } });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (method === 'tools/call') {
|
|
263
|
+
const toolName = params.name;
|
|
264
|
+
const tool = TOOLS[toolName];
|
|
265
|
+
if (!tool) {
|
|
266
|
+
return send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: 'Unknown tool: ' + toolName }], isError: true } });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const result = tool.handler(params.arguments || {});
|
|
271
|
+
return send({
|
|
272
|
+
jsonrpc: '2.0', id,
|
|
273
|
+
result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
274
|
+
});
|
|
275
|
+
} catch (err) {
|
|
276
|
+
return send({
|
|
277
|
+
jsonrpc: '2.0', id,
|
|
278
|
+
result: { content: [{ type: 'text', text: 'Error: ' + err.message }], isError: true }
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Unknown method
|
|
284
|
+
send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found: ' + method } });
|
|
285
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfast-ai/shipfast",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Autonomous context-engineered development system. 5 agents, 12 commands, SQLite brain. 70-90% less tokens than alternatives.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"shipfast": "bin/install.js"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"core",
|
|
13
13
|
"agents",
|
|
14
14
|
"hooks",
|
|
15
|
+
"mcp",
|
|
15
16
|
"scripts"
|
|
16
17
|
],
|
|
17
18
|
"keywords": [
|
package/scripts/postinstall.js
CHANGED
|
@@ -2,8 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Postinstall — auto-detect AI tools and install for all of them.
|
|
5
|
-
*
|
|
5
|
+
* Only runs when installed globally (npm i -g). Skips for local project deps.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
|
|
8
|
+
// FIX #4: Only run auto-install when installed globally
|
|
9
|
+
const isGlobal = process.env.npm_config_global === 'true' ||
|
|
10
|
+
(process.env.npm_lifecycle_event === 'postinstall' && !process.env.INIT_CWD);
|
|
11
|
+
|
|
12
|
+
if (isGlobal) {
|
|
13
|
+
require('../bin/install.js');
|
|
14
|
+
} else {
|
|
15
|
+
// Local install — just show a message
|
|
16
|
+
const cyan = '\x1b[36m';
|
|
17
|
+
const reset = '\x1b[0m';
|
|
18
|
+
console.log(`\nShipFast installed locally. For global install: ${cyan}npm i -g @shipfast-ai/shipfast${reset}\n`);
|
|
19
|
+
}
|