@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 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
- console.log(brainExists ? 'Re-indexing codebase...' : 'Indexing codebase...');
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 out = safeRun(process.execPath, [indexer, cwd], {
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 token budget and model tiers`);
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
- // Update hot files (separate transaction)
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
- console.log(`Indexing ${cwd}${changedOnly ? ' (changed only)' : ''}...`);
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
- console.log(`Done in ${result.elapsed_ms}ms: ${result.indexed} files (${result.skipped} unchanged), ${result.nodes} symbols, ${result.edges} edges, ${result.statements} SQL statements`);
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: INIT BRAIN (0 tokens)
54
+ ## STEP 2: CONTEXT GATHERING (0 tokens)
55
55
 
56
- If `.shipfast/brain.db` does not exist:
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 exists, run incremental index on changed files only:
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
- This takes ~300ms and ensures the knowledge graph is current.
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 (0 tokens)
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
- Automatically update brain.db:
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
 
@@ -5,20 +5,24 @@ allowed-tools:
5
5
  - Bash
6
6
  ---
7
7
 
8
- Run this EXACT command. Do NOT modify it. Do NOT run any other commands. Do NOT add insights or commentary.
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
- Then output EXACTLY this format using the numbers from above. Nothing else:
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 Status
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. Just the status block.
28
+ STOP after printing this. No analysis. No suggestions. No insights.
@@ -2,68 +2,60 @@
2
2
  /**
3
3
  * ShipFast First-Run Hook — PreToolUse
4
4
  *
5
- * Detects when brain.db doesn't exist in the current repo.
6
- * Injects a message telling the agent to run the indexer first.
7
- * Only fires once per repo — after indexing, brain.db exists and this becomes a no-op.
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), 10000);
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
- // Only trigger for sf-* skill invocations
24
- const toolName = data.tool_name || '';
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
- // Check if brain.db exists
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
- // Check if this is a git repo
36
- if (!fs.existsSync(path.join(cwd, '.git'))) {
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
- // Find the indexer
41
- const possiblePaths = [
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
- const indexerPath = possiblePaths.find(p => fs.existsSync(p));
49
- if (!indexerPath) {
50
- process.exit(0); // can't find indexer
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
- // Inject training instruction
54
- const output = {
48
+ if (!indexer) process.exit(0);
49
+
50
+ process.stdout.write(JSON.stringify({
55
51
  hookSpecificOutput: {
56
52
  hookEventName: 'PreToolUse',
57
- additionalContext: 'SHIPFAST FIRST RUN: brain.db not found for this repo. ' +
58
- 'Before proceeding with the task, run the indexer to train the brain:\n' +
59
- '```bash\nnode "' + indexerPath + '" "' + cwd + '"\n```\n' +
60
- 'This indexes the codebase (~1 second) and creates .shipfast/brain.db. ' +
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.4.4",
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": [
@@ -2,8 +2,18 @@
2
2
 
3
3
  /**
4
4
  * Postinstall — auto-detect AI tools and install for all of them.
5
- * Runs automatically after `npm i -g @shipfast-ai/shipfast`
5
+ * Only runs when installed globally (npm i -g). Skips for local project deps.
6
6
  */
7
7
 
8
- // Just run the main CLI with no args — it auto-detects and installs
9
- require('../bin/install.js');
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
+ }