@shipfast-ai/shipfast 0.4.4 → 0.6.0
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 +105 -4
- package/brain/indexer.cjs +50 -4
- package/commands/sf/do.md +18 -17
- package/commands/sf/status.md +6 -1
- 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':
|
|
@@ -132,18 +136,25 @@ function installFor(key, runtime) {
|
|
|
132
136
|
for (const f of ['sf-context-monitor.js','sf-statusline.js','sf-first-run.js'])
|
|
133
137
|
copy('hooks/' + f, path.join(hooksDir, f));
|
|
134
138
|
|
|
139
|
+
// Copy MCP server
|
|
140
|
+
const mcpDir = path.join(sfDir, 'mcp');
|
|
141
|
+
fs.mkdirSync(mcpDir, { recursive: true });
|
|
142
|
+
copy('mcp/server.cjs', path.join(mcpDir, 'server.cjs'));
|
|
143
|
+
|
|
135
144
|
// Runtime-specific config
|
|
136
145
|
const claudeCompat = ['Claude Code', 'OpenCode', 'Kilo'];
|
|
137
146
|
const geminiCompat = ['Gemini CLI', 'Antigravity'];
|
|
138
147
|
|
|
139
148
|
if (claudeCompat.includes(runtime.name)) {
|
|
140
149
|
writeSettings(dir, hooksDir);
|
|
150
|
+
writeMcpConfig(dir);
|
|
141
151
|
writeInstruction(path.join(dir, 'CLAUDE.md'));
|
|
142
152
|
} else if (geminiCompat.includes(runtime.name)) {
|
|
143
153
|
writeInstruction(path.join(dir, 'AGENTS.md'));
|
|
144
154
|
} else if (runtime.name === 'Copilot') {
|
|
145
155
|
writeInstruction(path.join(dir, 'copilot-instructions.md'));
|
|
146
156
|
} else if (runtime.name === 'Cursor') {
|
|
157
|
+
writeMcpConfig(dir);
|
|
147
158
|
writeInstruction(path.join(dir, 'rules'));
|
|
148
159
|
} else {
|
|
149
160
|
writeInstruction(path.join(dir, 'AGENTS.md'));
|
|
@@ -168,6 +179,8 @@ function printDone(count) {
|
|
|
168
179
|
|
|
169
180
|
function cmdInit() {
|
|
170
181
|
const cwd = process.cwd();
|
|
182
|
+
const fresh = process.argv.includes('--fresh');
|
|
183
|
+
|
|
171
184
|
if (!fs.existsSync(path.join(cwd, '.git'))) {
|
|
172
185
|
console.log(`${red}Not a git repo.${reset} Run this inside a git repository.\n`);
|
|
173
186
|
return;
|
|
@@ -180,10 +193,19 @@ function cmdInit() {
|
|
|
180
193
|
}
|
|
181
194
|
|
|
182
195
|
const brainExists = fs.existsSync(path.join(cwd, '.shipfast', 'brain.db'));
|
|
183
|
-
|
|
196
|
+
|
|
197
|
+
// FIX #8: --fresh flag
|
|
198
|
+
if (fresh && brainExists) {
|
|
199
|
+
fs.unlinkSync(path.join(cwd, '.shipfast', 'brain.db'));
|
|
200
|
+
console.log('Cleared existing brain.db');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(brainExists && !fresh ? 'Re-indexing codebase...' : 'Indexing codebase...');
|
|
184
204
|
|
|
185
205
|
try {
|
|
186
|
-
const
|
|
206
|
+
const args = [indexer, cwd];
|
|
207
|
+
if (fresh) args.push('--fresh');
|
|
208
|
+
const out = safeRun(process.execPath, args, {
|
|
187
209
|
encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe']
|
|
188
210
|
});
|
|
189
211
|
console.log(`${green}${out.trim()}${reset}`);
|
|
@@ -258,8 +280,13 @@ function cmdUninstall() {
|
|
|
258
280
|
if (f.startsWith('sf-')) fs.unlinkSync(path.join(hd, f));
|
|
259
281
|
}
|
|
260
282
|
}
|
|
261
|
-
// Clean settings.json
|
|
283
|
+
// Clean settings.json hooks (#18: remove empty arrays too)
|
|
262
284
|
cleanSettings(dir);
|
|
285
|
+
// FIX #7: Clean instruction files (CLAUDE.md, AGENTS.md, etc.)
|
|
286
|
+
cleanInstructionFile(path.join(dir, 'CLAUDE.md'));
|
|
287
|
+
cleanInstructionFile(path.join(dir, 'AGENTS.md'));
|
|
288
|
+
cleanInstructionFile(path.join(dir, 'copilot-instructions.md'));
|
|
289
|
+
cleanInstructionFile(path.join(dir, 'rules'));
|
|
263
290
|
console.log(` ${green}${runtime.name}${reset} — removed`);
|
|
264
291
|
removed++;
|
|
265
292
|
}
|
|
@@ -309,6 +336,8 @@ function cleanSettings(dir) {
|
|
|
309
336
|
function cmdHelp() {
|
|
310
337
|
console.log(`${bold}Terminal commands:${reset}\n`);
|
|
311
338
|
console.log(` ${cyan}shipfast init${reset} Index current repo into .shipfast/brain.db`);
|
|
339
|
+
console.log(` ${cyan}shipfast init --fresh${reset} Full reindex (clears existing brain.db)`);
|
|
340
|
+
console.log(` ${cyan}shipfast status${reset} Show installed runtimes + brain status`);
|
|
312
341
|
console.log(` ${cyan}shipfast update${reset} Update to latest + re-detect runtimes`);
|
|
313
342
|
console.log(` ${cyan}shipfast uninstall${reset} Remove from all AI tools`);
|
|
314
343
|
console.log(` ${cyan}shipfast help${reset} Show this help\n`);
|
|
@@ -322,7 +351,7 @@ function cmdHelp() {
|
|
|
322
351
|
console.log(` ${cyan}/sf-undo${reset} Rollback a task`);
|
|
323
352
|
console.log(` ${cyan}/sf-brain${reset} <query> Query the knowledge graph`);
|
|
324
353
|
console.log(` ${cyan}/sf-learn${reset} <pattern> Teach a pattern or lesson`);
|
|
325
|
-
console.log(` ${cyan}/sf-config${reset} Set
|
|
354
|
+
console.log(` ${cyan}/sf-config${reset} Set model tiers and preferences`);
|
|
326
355
|
console.log(` ${cyan}/sf-milestone${reset} Complete or start a milestone`);
|
|
327
356
|
console.log(` ${cyan}/sf-help${reset} Show all commands\n`);
|
|
328
357
|
}
|
|
@@ -352,6 +381,23 @@ function writeSettings(dir, hooksDir) {
|
|
|
352
381
|
fs.writeFileSync(sp, JSON.stringify(s, null, 2));
|
|
353
382
|
}
|
|
354
383
|
|
|
384
|
+
function writeMcpConfig(dir) {
|
|
385
|
+
const serverPath = path.join(dir, 'shipfast', 'mcp', 'server.cjs');
|
|
386
|
+
const settingsPath = path.join(dir, 'settings.json');
|
|
387
|
+
let s = {};
|
|
388
|
+
if (fs.existsSync(settingsPath)) { try { s = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {} }
|
|
389
|
+
|
|
390
|
+
if (!s.mcpServers) s.mcpServers = {};
|
|
391
|
+
|
|
392
|
+
s.mcpServers['shipfast-brain'] = {
|
|
393
|
+
command: 'node',
|
|
394
|
+
args: [serverPath],
|
|
395
|
+
env: {}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
fs.writeFileSync(settingsPath, JSON.stringify(s, null, 2));
|
|
399
|
+
}
|
|
400
|
+
|
|
355
401
|
function writeInstruction(filePath) {
|
|
356
402
|
const marker = '<!-- ShipFast -->';
|
|
357
403
|
const close = '<!-- /ShipFast -->';
|
|
@@ -368,6 +414,61 @@ function writeInstruction(filePath) {
|
|
|
368
414
|
fs.writeFileSync(filePath, content);
|
|
369
415
|
}
|
|
370
416
|
|
|
417
|
+
// FIX #7: Remove ShipFast block from instruction files during uninstall
|
|
418
|
+
function cleanInstructionFile(filePath) {
|
|
419
|
+
if (!fs.existsSync(filePath)) return;
|
|
420
|
+
try {
|
|
421
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
422
|
+
const marker = '<!-- ShipFast -->';
|
|
423
|
+
const close = '<!-- /ShipFast -->';
|
|
424
|
+
const s = content.indexOf(marker);
|
|
425
|
+
const e = content.indexOf(close);
|
|
426
|
+
if (s !== -1 && e !== -1) {
|
|
427
|
+
content = content.slice(0, s) + content.slice(e + close.length);
|
|
428
|
+
content = content.trim() + '\n';
|
|
429
|
+
fs.writeFileSync(filePath, content);
|
|
430
|
+
}
|
|
431
|
+
} catch {}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// FIX #17: CLI status command — show installed runtimes + version + brain status
|
|
435
|
+
function cmdStatus() {
|
|
436
|
+
console.log(`${bold}Installed runtimes:${reset}\n`);
|
|
437
|
+
let count = 0;
|
|
438
|
+
for (const [key, runtime] of Object.entries(RUNTIMES)) {
|
|
439
|
+
const dir = path.join(os.homedir(), runtime.path);
|
|
440
|
+
if (fs.existsSync(path.join(dir, 'shipfast'))) {
|
|
441
|
+
console.log(` ${green}${runtime.name}${reset} ${dim}${dir}${reset}`);
|
|
442
|
+
count++;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (count === 0) {
|
|
446
|
+
console.log(` ${dim}(none)${reset}`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Brain status for current directory
|
|
450
|
+
const cwd = process.cwd();
|
|
451
|
+
const brainPath = path.join(cwd, '.shipfast', 'brain.db');
|
|
452
|
+
console.log(`\n${bold}Current repo:${reset} ${cwd}\n`);
|
|
453
|
+
if (fs.existsSync(brainPath)) {
|
|
454
|
+
try {
|
|
455
|
+
const out = safeRun('sqlite3', [brainPath,
|
|
456
|
+
"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;"
|
|
457
|
+
], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
458
|
+
console.log(` Brain: ${green}indexed${reset}`);
|
|
459
|
+
out.trim().split('\n').forEach(line => {
|
|
460
|
+
const [k, v] = line.split('|');
|
|
461
|
+
console.log(` ${dim}${k}: ${v}${reset}`);
|
|
462
|
+
});
|
|
463
|
+
} catch {
|
|
464
|
+
console.log(` Brain: ${green}exists${reset} (.shipfast/brain.db)`);
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
console.log(` Brain: ${yellow}not indexed${reset} — run ${cyan}shipfast init${reset}`);
|
|
468
|
+
}
|
|
469
|
+
console.log('');
|
|
470
|
+
}
|
|
471
|
+
|
|
371
472
|
function copy(rel, dest) {
|
|
372
473
|
const src = path.join(__dirname, '..', rel);
|
|
373
474
|
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
|
@@ -11,10 +11,15 @@ Run this EXACT command. Do NOT modify it. Do NOT run any other commands. Do NOT
|
|
|
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
|
+
Also get the version:
|
|
15
|
+
```bash
|
|
16
|
+
cat $(find ~/.claude/shipfast ~/.cursor/shipfast ~/.gemini/shipfast -name "package.json" 2>/dev/null | head -1) 2>/dev/null | grep version || echo "version unknown"
|
|
17
|
+
```
|
|
18
|
+
|
|
14
19
|
Then output EXACTLY this format using the numbers from above. Nothing else:
|
|
15
20
|
|
|
16
21
|
```
|
|
17
|
-
ShipFast
|
|
22
|
+
ShipFast [version]
|
|
18
23
|
===============
|
|
19
24
|
Brain: [nodes] nodes | [edges] edges | [decisions] decisions | [learnings] learnings | [hot_files] hot files
|
|
20
25
|
Tasks: [active] active | [passed] completed
|
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.0",
|
|
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
|
+
}
|