devsmind-mcp 4.1.1 → 4.3.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.
Files changed (62) hide show
  1. package/README.md +66 -17
  2. package/dist/cli/activity.js +1 -1
  3. package/dist/cli/activity.js.map +1 -1
  4. package/dist/cli/add-repo.d.ts +20 -0
  5. package/dist/cli/add-repo.js +171 -0
  6. package/dist/cli/add-repo.js.map +1 -0
  7. package/dist/cli/analyze.js +11 -1
  8. package/dist/cli/analyze.js.map +1 -1
  9. package/dist/cli/branch.d.ts +24 -0
  10. package/dist/cli/branch.js +148 -0
  11. package/dist/cli/branch.js.map +1 -0
  12. package/dist/cli/index.js +56 -4
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/init.js +27 -39
  15. package/dist/cli/init.js.map +1 -1
  16. package/dist/cli/integrations/memory-topics.js +1 -1
  17. package/dist/cli/integrations/memory-topics.js.map +1 -1
  18. package/dist/cli/integrations/prompt.d.ts +9 -0
  19. package/dist/cli/integrations/prompt.js +26 -1
  20. package/dist/cli/integrations/prompt.js.map +1 -1
  21. package/dist/cli/integrations/registry.d.ts +23 -7
  22. package/dist/cli/integrations/registry.js +69 -8
  23. package/dist/cli/integrations/registry.js.map +1 -1
  24. package/dist/cli/integrations/skill.d.ts +16 -10
  25. package/dist/cli/integrations/skill.js +108 -22
  26. package/dist/cli/integrations/skill.js.map +1 -1
  27. package/dist/cli/llm-client.js +15 -7
  28. package/dist/cli/llm-client.js.map +1 -1
  29. package/dist/cli/prune.js +3 -15
  30. package/dist/cli/prune.js.map +1 -1
  31. package/dist/cli/rule.js +2 -2
  32. package/dist/cli/rule.js.map +1 -1
  33. package/dist/cli/runner.d.ts +8 -0
  34. package/dist/cli/runner.js +12 -5
  35. package/dist/cli/runner.js.map +1 -1
  36. package/dist/cli/sync.d.ts +16 -0
  37. package/dist/cli/sync.js +80 -0
  38. package/dist/cli/sync.js.map +1 -1
  39. package/dist/cli/view.js +3 -19
  40. package/dist/cli/view.js.map +1 -1
  41. package/dist/db/analyze.d.ts +5 -0
  42. package/dist/db/analyze.js +23 -0
  43. package/dist/db/analyze.js.map +1 -1
  44. package/dist/db/database.d.ts +56 -0
  45. package/dist/db/database.js +124 -6
  46. package/dist/db/database.js.map +1 -1
  47. package/dist/db/grep.js +1 -1
  48. package/dist/db/grep.js.map +1 -1
  49. package/dist/db/indexer.d.ts +1 -1
  50. package/dist/db/indexer.js +3 -3
  51. package/dist/db/indexer.js.map +1 -1
  52. package/dist/mcp/server.js +359 -60
  53. package/dist/mcp/server.js.map +1 -1
  54. package/dist/utils/config.d.ts +76 -6
  55. package/dist/utils/config.js +110 -12
  56. package/dist/utils/config.js.map +1 -1
  57. package/dist/utils/devsmind-branch.d.ts +55 -0
  58. package/dist/utils/devsmind-branch.js +244 -0
  59. package/dist/utils/devsmind-branch.js.map +1 -0
  60. package/dist/utils/scanner.js +1 -1
  61. package/dist/utils/scanner.js.map +1 -1
  62. package/package.json +1 -1
@@ -2692,6 +2692,31 @@ class DevMindDatabase {
2692
2692
  this.writeWorkflowToDisk(workflowId);
2693
2693
  return { id, workflow_id: workflowId, step_id: opts.stepId || null, type: opts.type, source_name: opts.sourceName, file_path: filePath, created_at: now };
2694
2694
  }
2695
+ /**
2696
+ * Copies an EXISTING file's bytes into `.devmind/workflows/<workflowId>/<artifactId>_<name>` and
2697
+ * records the DB row — same storage as addWorkflowArtifact, but the source is a file already on
2698
+ * disk rather than a string already in memory, so this is copyFileSync (binary-safe: PDFs,
2699
+ * docx, images) instead of writeFileSync(..., 'utf-8').
2700
+ */
2701
+ addWorkflowArtifactFromFile(workflowId, opts) {
2702
+ if (!this.getWorkflow(workflowId))
2703
+ throw new Error(`Workflow not found: ${workflowId}`);
2704
+ const id = crypto.randomUUID();
2705
+ const now = new Date().toISOString();
2706
+ const sourceName = path.basename(opts.sourcePath);
2707
+ const safeName = sourceName.replace(/[^a-zA-Z0-9._-]/g, '_') || 'artifact';
2708
+ const dir = path.join(this.workflowsDir(), workflowId);
2709
+ fs.mkdirSync(dir, { recursive: true });
2710
+ const filePath = path.join(dir, `${id}_${safeName}`);
2711
+ fs.copyFileSync(opts.sourcePath, filePath);
2712
+ this.db.prepare(`
2713
+ INSERT INTO workflow_artifacts (id, workflow_id, step_id, type, source_name, file_path, created_at)
2714
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2715
+ `).run(id, workflowId, opts.stepId || null, opts.type, sourceName, filePath, now);
2716
+ this.db.prepare(`UPDATE workflows SET updated_at = ? WHERE id = ?`).run(now, workflowId);
2717
+ this.writeWorkflowToDisk(workflowId);
2718
+ return { id, workflow_id: workflowId, step_id: opts.stepId || null, type: opts.type, source_name: sourceName, file_path: filePath, created_at: now };
2719
+ }
2695
2720
  /**
2696
2721
  * The workflow's story: its steps in order, plus the docs attached to it.
2697
2722
  *
@@ -2822,6 +2847,58 @@ class DevMindDatabase {
2822
2847
  }
2823
2848
  return { spurious, missingFile };
2824
2849
  }
2850
+ /**
2851
+ * Finds `.devmind/graph`/`.devmind/vectors` JSON output paths that a live node's file_path
2852
+ * still maps to, but that are occupied by a stray DIRECTORY instead of a file — the case
2853
+ * `isDegenerateDiskJsonPath`'s existsSync+isDirectory branch refuses to write over. Left
2854
+ * behind by some earlier node whose file_path once collapsed onto that exact path (see that
2855
+ * method's comment for how); once the directory exists, `writeGraphToDisk`/`writeVectorsToDisk`
2856
+ * can only skip-and-warn forever, since neither one ever deletes anything. This is what
2857
+ * `devsmind analyze --fix` uses to actually remove the blocker and re-trigger the write, so a
2858
+ * perfectly healthy node's file stops being warned about on every future sync.
2859
+ *
2860
+ * Deliberately mirrors ONLY the third of `isDegenerateDiskJsonPath`'s three checks (never the
2861
+ * first two — an empty/trailing-slash/`..`-collapsing diskRelPath means the node's OWN
2862
+ * file_path is degenerate, already covered by `missingFile` above and fixed by deprecating the
2863
+ * node, not by deleting anything on disk). Skipping those here is required for safety: a
2864
+ * collapsed diskRelPath would make `targetPath` resolve to `graphDir`/`vectorsDir` itself (or
2865
+ * an ancestor of it), and blindly reporting that as "stray" would recommend deleting the whole
2866
+ * output tree.
2867
+ */
2868
+ findStrayOutputDirs() {
2869
+ const rows = this.db.prepare('SELECT DISTINCT file_path FROM nodes').all();
2870
+ const filePaths = new Set();
2871
+ for (const row of rows) {
2872
+ if (row.file_path) {
2873
+ for (const p of row.file_path.split(',').map(s => s.trim()).filter(Boolean)) {
2874
+ filePaths.add(p);
2875
+ }
2876
+ }
2877
+ }
2878
+ const workspaceRoot = (0, config_1.canonicalizePath)(path.dirname(this.dbPath));
2879
+ const dirsByKind = [
2880
+ { dir: path.join(workspaceRoot, 'graph'), kind: 'graph' },
2881
+ { dir: path.join(workspaceRoot, 'vectors'), kind: 'vectors' },
2882
+ ];
2883
+ const results = [];
2884
+ for (const filePath of filePaths) {
2885
+ const absPath = (0, config_1.canonicalizePath)(filePath);
2886
+ const repoRelPath = this.toRepoRelativePath(absPath);
2887
+ const diskRelPath = repoRelPath.replace(/^\{([^}]+)\}/, '$1').replace(/\.[^/.]+$/, '.json');
2888
+ if (!diskRelPath || diskRelPath.endsWith('/') || diskRelPath.endsWith('\\'))
2889
+ continue;
2890
+ for (const { dir, kind } of dirsByKind) {
2891
+ const targetPath = path.join(dir, diskRelPath);
2892
+ const relToContainer = path.relative(dir, targetPath);
2893
+ if (relToContainer === '' || relToContainer.startsWith('..'))
2894
+ continue;
2895
+ if (fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()) {
2896
+ results.push({ file_path: filePath, target: targetPath, kind });
2897
+ }
2898
+ }
2899
+ }
2900
+ return results;
2901
+ }
2825
2902
  pruneSpuriousNodes(workspaceRoot) {
2826
2903
  const { spurious, missingFile } = this.findSpuriousAndMissingFileNodes(workspaceRoot);
2827
2904
  const candidates = [...spurious, ...missingFile];
@@ -3393,6 +3470,28 @@ class DevMindDatabase {
3393
3470
  return true;
3394
3471
  return fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory();
3395
3472
  }
3473
+ /**
3474
+ * True when `filePath` ITSELF can never resolve to a valid `graph/`/`vectors/` JSON target —
3475
+ * i.e. `isDegenerateDiskJsonPath`'s first two checks (empty/trailing-slash/`..`-collapsing
3476
+ * `diskRelPath`), never its third (existsSync+isDirectory, which is about a stray directory
3477
+ * left on disk, not about the file_path being bad — that case is exactly what
3478
+ * `findStrayOutputDirs` fixes, and can legitimately happen for a perfectly healthy file_path,
3479
+ * so it must NOT be treated as permanent here). Deliberately does no filesystem I/O — pure
3480
+ * string logic, cheap enough to call for every distinct file_path a full `syncToDisk` touches.
3481
+ * Used to recognize a file_path that's structurally unfixable no matter how many times a write
3482
+ * is retried, as opposed to one that's merely blocked by removable disk cruft.
3483
+ */
3484
+ isFilePathStructurallyDegenerate(filePath) {
3485
+ const workspaceRoot = (0, config_1.canonicalizePath)(path.dirname(this.dbPath));
3486
+ const absPath = (0, config_1.canonicalizePath)(filePath);
3487
+ const repoRelPath = this.toRepoRelativePath(absPath);
3488
+ const diskRelPath = repoRelPath.replace(/^\{([^}]+)\}/, '$1').replace(/\.[^/.]+$/, '.json');
3489
+ if (!diskRelPath || diskRelPath.endsWith('/') || diskRelPath.endsWith('\\'))
3490
+ return true;
3491
+ const graphDir = path.join(workspaceRoot, 'graph');
3492
+ const relToContainer = path.relative(graphDir, path.join(graphDir, diskRelPath));
3493
+ return relToContainer === '' || relToContainer.startsWith('..');
3494
+ }
3396
3495
  writeGraphToDisk(filePath) {
3397
3496
  try {
3398
3497
  if (!filePath)
@@ -3538,16 +3637,35 @@ class DevMindDatabase {
3538
3637
  * the DB, or after a past write silently failed (e.g. the directory-typed file_path bug
3539
3638
  * `isDegenerateDiskJsonPath` now catches) — so leaving vectors out of the force-resync
3540
3639
  * defeated half the point of running it.
3640
+ *
3641
+ * A file_path shared by at least one ACTIVE node is always re-attempted, deprecated or not —
3642
+ * a deprecated node sharing a real file with a live one still needs its `deprecated:1` flag
3643
+ * kept current on disk. A file_path held ONLY by deprecated nodes is skipped when it's also
3644
+ * `isFilePathStructurallyDegenerate` (a directory rather than a file — `deprecateNode` already
3645
+ * wrote it once, at the moment it was deprecated, so nothing new would come of retrying):
3646
+ * without this, a node deprecated for exactly this reason produced the identical "Skipped
3647
+ * writing graph JSON" warning on every single future `sync`, forever, since deprecating a node
3648
+ * never clears its (already-garbage) file_path — indistinguishable from the bug never having
3649
+ * been fixed at all.
3541
3650
  */
3542
3651
  syncToDisk() {
3543
3652
  try {
3544
- const rows = this.db.prepare('SELECT DISTINCT file_path FROM nodes').all();
3545
- const filePaths = new Set();
3653
+ const rows = this.db.prepare('SELECT DISTINCT file_path, deprecated FROM nodes').all();
3654
+ const activeFilePaths = new Set();
3655
+ const allFilePaths = new Set();
3546
3656
  for (const row of rows) {
3547
- if (row.file_path) {
3548
- for (const p of row.file_path.split(',').map(s => s.trim()).filter(Boolean)) {
3549
- filePaths.add(p);
3550
- }
3657
+ if (!row.file_path)
3658
+ continue;
3659
+ for (const p of row.file_path.split(',').map(s => s.trim()).filter(Boolean)) {
3660
+ allFilePaths.add(p);
3661
+ if (!row.deprecated)
3662
+ activeFilePaths.add(p);
3663
+ }
3664
+ }
3665
+ const filePaths = new Set();
3666
+ for (const p of allFilePaths) {
3667
+ if (activeFilePaths.has(p) || !this.isFilePathStructurallyDegenerate(p)) {
3668
+ filePaths.add(p);
3551
3669
  }
3552
3670
  }
3553
3671
  for (const filePath of filePaths) {