devsmind-mcp 4.1.0 → 4.2.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/README.md +39 -15
- package/dist/cli/activity.js +1 -1
- package/dist/cli/activity.js.map +1 -1
- package/dist/cli/analyze.js +11 -1
- package/dist/cli/analyze.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +21 -11
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/integrations/prompt.js +5 -1
- package/dist/cli/integrations/prompt.js.map +1 -1
- package/dist/cli/integrations/registry.d.ts +1 -1
- package/dist/cli/integrations/registry.js +1 -1
- package/dist/cli/prune.js +3 -15
- package/dist/cli/prune.js.map +1 -1
- package/dist/cli/sync.d.ts +7 -2
- package/dist/cli/sync.js +15 -4
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/view.js +3 -19
- package/dist/cli/view.js.map +1 -1
- package/dist/db/analyze.d.ts +5 -0
- package/dist/db/analyze.js +23 -0
- package/dist/db/analyze.js.map +1 -1
- package/dist/db/database.d.ts +79 -1
- package/dist/db/database.js +169 -11
- package/dist/db/database.js.map +1 -1
- package/dist/db/grep.js +1 -1
- package/dist/db/grep.js.map +1 -1
- package/dist/mcp/server.js +19 -29
- package/dist/mcp/server.js.map +1 -1
- package/dist/utils/config.d.ts +50 -6
- package/dist/utils/config.js +68 -12
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/scanner.js +1 -1
- package/dist/utils/scanner.js.map +1 -1
- package/package.json +1 -1
package/dist/db/database.js
CHANGED
|
@@ -381,7 +381,7 @@ class DevMindDatabase {
|
|
|
381
381
|
/* istanbul ignore next -- every call site here is a `SELECT COUNT(*) AS c FROM ...`,
|
|
382
382
|
which always returns exactly one row; `row` can only be undefined if this helper is
|
|
383
383
|
ever repurposed for a query that can return zero rows. Kept as a real guard, not
|
|
384
|
-
because today's
|
|
384
|
+
because today's call sites can hit it. */
|
|
385
385
|
return row ? row.c : 0;
|
|
386
386
|
}
|
|
387
387
|
catch {
|
|
@@ -392,6 +392,8 @@ class DevMindDatabase {
|
|
|
392
392
|
nodes: one('SELECT COUNT(*) AS c FROM nodes WHERE deprecated = 0'),
|
|
393
393
|
connections: one('SELECT COUNT(*) AS c FROM node_connections'),
|
|
394
394
|
history: one('SELECT COUNT(*) AS c FROM history'),
|
|
395
|
+
vectors: one('SELECT COUNT(*) AS c FROM node_vectors'),
|
|
396
|
+
workflows: one('SELECT COUNT(*) AS c FROM workflows'),
|
|
395
397
|
};
|
|
396
398
|
}
|
|
397
399
|
vacuum() {
|
|
@@ -2797,9 +2799,21 @@ class DevMindDatabase {
|
|
|
2797
2799
|
if (node.file_path) {
|
|
2798
2800
|
const paths = node.file_path.split(',').map(p => p.trim()).filter(Boolean);
|
|
2799
2801
|
if (paths.length > 0) {
|
|
2802
|
+
// "Missing" also covers a file_path that resolves to a DIRECTORY rather than a real
|
|
2803
|
+
// file — plain fs.existsSync() alone says a directory "exists", so a node corrupted
|
|
2804
|
+
// this way (its file_path IS a repo or workspace root, from some earlier bug) sailed
|
|
2805
|
+
// through this check indefinitely; it only ever surfaced as an EISDIR crash when
|
|
2806
|
+
// writeGraphToDisk/writeVectorsToDisk later tried to write a JSON file at that same
|
|
2807
|
+
// path. statSync().isFile() catches both "doesn't exist" and "exists but isn't a file"
|
|
2808
|
+
// in one check — a node deprecated for either reason has no real source to hold anyway.
|
|
2800
2809
|
const allMissing = paths.every(p => {
|
|
2801
2810
|
const resolvedPath = path.isAbsolute(p) ? p : path.resolve(workspaceRoot, p);
|
|
2802
|
-
|
|
2811
|
+
try {
|
|
2812
|
+
return !fs.statSync(resolvedPath).isFile();
|
|
2813
|
+
}
|
|
2814
|
+
catch {
|
|
2815
|
+
return true;
|
|
2816
|
+
}
|
|
2803
2817
|
});
|
|
2804
2818
|
if (allMissing)
|
|
2805
2819
|
missingFile.push(node);
|
|
@@ -2808,6 +2822,58 @@ class DevMindDatabase {
|
|
|
2808
2822
|
}
|
|
2809
2823
|
return { spurious, missingFile };
|
|
2810
2824
|
}
|
|
2825
|
+
/**
|
|
2826
|
+
* Finds `.devmind/graph`/`.devmind/vectors` JSON output paths that a live node's file_path
|
|
2827
|
+
* still maps to, but that are occupied by a stray DIRECTORY instead of a file — the case
|
|
2828
|
+
* `isDegenerateDiskJsonPath`'s existsSync+isDirectory branch refuses to write over. Left
|
|
2829
|
+
* behind by some earlier node whose file_path once collapsed onto that exact path (see that
|
|
2830
|
+
* method's comment for how); once the directory exists, `writeGraphToDisk`/`writeVectorsToDisk`
|
|
2831
|
+
* can only skip-and-warn forever, since neither one ever deletes anything. This is what
|
|
2832
|
+
* `devsmind analyze --fix` uses to actually remove the blocker and re-trigger the write, so a
|
|
2833
|
+
* perfectly healthy node's file stops being warned about on every future sync.
|
|
2834
|
+
*
|
|
2835
|
+
* Deliberately mirrors ONLY the third of `isDegenerateDiskJsonPath`'s three checks (never the
|
|
2836
|
+
* first two — an empty/trailing-slash/`..`-collapsing diskRelPath means the node's OWN
|
|
2837
|
+
* file_path is degenerate, already covered by `missingFile` above and fixed by deprecating the
|
|
2838
|
+
* node, not by deleting anything on disk). Skipping those here is required for safety: a
|
|
2839
|
+
* collapsed diskRelPath would make `targetPath` resolve to `graphDir`/`vectorsDir` itself (or
|
|
2840
|
+
* an ancestor of it), and blindly reporting that as "stray" would recommend deleting the whole
|
|
2841
|
+
* output tree.
|
|
2842
|
+
*/
|
|
2843
|
+
findStrayOutputDirs() {
|
|
2844
|
+
const rows = this.db.prepare('SELECT DISTINCT file_path FROM nodes').all();
|
|
2845
|
+
const filePaths = new Set();
|
|
2846
|
+
for (const row of rows) {
|
|
2847
|
+
if (row.file_path) {
|
|
2848
|
+
for (const p of row.file_path.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
2849
|
+
filePaths.add(p);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
const workspaceRoot = (0, config_1.canonicalizePath)(path.dirname(this.dbPath));
|
|
2854
|
+
const dirsByKind = [
|
|
2855
|
+
{ dir: path.join(workspaceRoot, 'graph'), kind: 'graph' },
|
|
2856
|
+
{ dir: path.join(workspaceRoot, 'vectors'), kind: 'vectors' },
|
|
2857
|
+
];
|
|
2858
|
+
const results = [];
|
|
2859
|
+
for (const filePath of filePaths) {
|
|
2860
|
+
const absPath = (0, config_1.canonicalizePath)(filePath);
|
|
2861
|
+
const repoRelPath = this.toRepoRelativePath(absPath);
|
|
2862
|
+
const diskRelPath = repoRelPath.replace(/^\{([^}]+)\}/, '$1').replace(/\.[^/.]+$/, '.json');
|
|
2863
|
+
if (!diskRelPath || diskRelPath.endsWith('/') || diskRelPath.endsWith('\\'))
|
|
2864
|
+
continue;
|
|
2865
|
+
for (const { dir, kind } of dirsByKind) {
|
|
2866
|
+
const targetPath = path.join(dir, diskRelPath);
|
|
2867
|
+
const relToContainer = path.relative(dir, targetPath);
|
|
2868
|
+
if (relToContainer === '' || relToContainer.startsWith('..'))
|
|
2869
|
+
continue;
|
|
2870
|
+
if (fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()) {
|
|
2871
|
+
results.push({ file_path: filePath, target: targetPath, kind });
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
return results;
|
|
2876
|
+
}
|
|
2811
2877
|
pruneSpuriousNodes(workspaceRoot) {
|
|
2812
2878
|
const { spurious, missingFile } = this.findSpuriousAndMissingFileNodes(workspaceRoot);
|
|
2813
2879
|
const candidates = [...spurious, ...missingFile];
|
|
@@ -3351,6 +3417,56 @@ class DevMindDatabase {
|
|
|
3351
3417
|
likeEscape(s) {
|
|
3352
3418
|
return s.replace(/[\\%_]/g, ch => '\\' + ch);
|
|
3353
3419
|
}
|
|
3420
|
+
/**
|
|
3421
|
+
* True when `targetPath` (a computed `graph/`/`vectors/` JSON path) does NOT land strictly
|
|
3422
|
+
* inside `containerDir` as a real file — i.e. writing there would be writing to a directory,
|
|
3423
|
+
* not a file inside one. Guards `writeGraphToDisk`/`writeVectorsToDisk` against a malformed
|
|
3424
|
+
* node whose `file_path` IS a directory (the workspace root, or a repo root itself): observed
|
|
3425
|
+
* in production as an EISDIR crash on `devsmind sync`, because `toRepoRelativePath` collapses
|
|
3426
|
+
* to `''` (workspace root) or `'{repo}/'` (a repo root) for those, which `diskRelPath` then
|
|
3427
|
+
* turns into `''`/a trailing-slash string/`'..'` depending on which branch collapsed — every
|
|
3428
|
+
* one of which makes `path.join(containerDir, diskRelPath)` resolve to `containerDir` itself
|
|
3429
|
+
* or some directory already inside it, never a fresh `.json` file. Multiple checks on purpose:
|
|
3430
|
+
* the empty/trailing-slash/`..`-relative cases catch it structurally (works even on a brand
|
|
3431
|
+
* new brain where nothing exists on disk yet, where an EISDIR would instead be a silent
|
|
3432
|
+
* file-where-directory-belongs corruption); the existsSync+isDirectory check is a catch-all
|
|
3433
|
+
* for any other collapse shape not foreseen above (existsSync is checked first specifically so
|
|
3434
|
+
* a normal not-yet-written target — the common case — never reaches statSync at all). Callers
|
|
3435
|
+
* already wrap their whole body in a try/catch that logs and returns, so nothing extra is
|
|
3436
|
+
* needed here for a statSync that throws for some other reason (a permission error, a race).
|
|
3437
|
+
* `devsmind analyze --fix` deprecates the node actually causing this; this only ever refuses
|
|
3438
|
+
* the write, never touches the node.
|
|
3439
|
+
*/
|
|
3440
|
+
isDegenerateDiskJsonPath(containerDir, diskRelPath, targetPath) {
|
|
3441
|
+
if (!diskRelPath || diskRelPath.endsWith('/') || diskRelPath.endsWith('\\'))
|
|
3442
|
+
return true;
|
|
3443
|
+
const relToContainer = path.relative(containerDir, targetPath);
|
|
3444
|
+
if (relToContainer === '' || relToContainer.startsWith('..'))
|
|
3445
|
+
return true;
|
|
3446
|
+
return fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory();
|
|
3447
|
+
}
|
|
3448
|
+
/**
|
|
3449
|
+
* True when `filePath` ITSELF can never resolve to a valid `graph/`/`vectors/` JSON target —
|
|
3450
|
+
* i.e. `isDegenerateDiskJsonPath`'s first two checks (empty/trailing-slash/`..`-collapsing
|
|
3451
|
+
* `diskRelPath`), never its third (existsSync+isDirectory, which is about a stray directory
|
|
3452
|
+
* left on disk, not about the file_path being bad — that case is exactly what
|
|
3453
|
+
* `findStrayOutputDirs` fixes, and can legitimately happen for a perfectly healthy file_path,
|
|
3454
|
+
* so it must NOT be treated as permanent here). Deliberately does no filesystem I/O — pure
|
|
3455
|
+
* string logic, cheap enough to call for every distinct file_path a full `syncToDisk` touches.
|
|
3456
|
+
* Used to recognize a file_path that's structurally unfixable no matter how many times a write
|
|
3457
|
+
* is retried, as opposed to one that's merely blocked by removable disk cruft.
|
|
3458
|
+
*/
|
|
3459
|
+
isFilePathStructurallyDegenerate(filePath) {
|
|
3460
|
+
const workspaceRoot = (0, config_1.canonicalizePath)(path.dirname(this.dbPath));
|
|
3461
|
+
const absPath = (0, config_1.canonicalizePath)(filePath);
|
|
3462
|
+
const repoRelPath = this.toRepoRelativePath(absPath);
|
|
3463
|
+
const diskRelPath = repoRelPath.replace(/^\{([^}]+)\}/, '$1').replace(/\.[^/.]+$/, '.json');
|
|
3464
|
+
if (!diskRelPath || diskRelPath.endsWith('/') || diskRelPath.endsWith('\\'))
|
|
3465
|
+
return true;
|
|
3466
|
+
const graphDir = path.join(workspaceRoot, 'graph');
|
|
3467
|
+
const relToContainer = path.relative(graphDir, path.join(graphDir, diskRelPath));
|
|
3468
|
+
return relToContainer === '' || relToContainer.startsWith('..');
|
|
3469
|
+
}
|
|
3354
3470
|
writeGraphToDisk(filePath) {
|
|
3355
3471
|
try {
|
|
3356
3472
|
if (!filePath)
|
|
@@ -3361,7 +3477,12 @@ class DevMindDatabase {
|
|
|
3361
3477
|
const repoRelPath = this.toRepoRelativePath(absPath);
|
|
3362
3478
|
// E.g., "{harrir-web}/app/page.tsx" -> "graph/harrir-web/app/page.json"
|
|
3363
3479
|
const diskRelPath = repoRelPath.replace(/^\{([^}]+)\}/, '$1').replace(/\.[^/.]+$/, '.json');
|
|
3364
|
-
const
|
|
3480
|
+
const graphDir = path.join(workspaceRoot, 'graph');
|
|
3481
|
+
const graphJsonPath = path.join(graphDir, diskRelPath);
|
|
3482
|
+
if (this.isDegenerateDiskJsonPath(graphDir, diskRelPath, graphJsonPath)) {
|
|
3483
|
+
console.warn(`⚠️ Skipped writing graph JSON: a node's file_path resolves to a directory, not a file (${absPath}) — run \`devsmind analyze --fix\` to clean it up.`);
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3365
3486
|
// Get all nodes in this file (active AND deprecated). A node's file_path is either
|
|
3366
3487
|
// exactly this absolute path, or (for the rare node spanning multiple files) a ", "-joined
|
|
3367
3488
|
// list containing it. We anchor on the FULL absolute path with ", " boundaries and escape
|
|
@@ -3442,7 +3563,14 @@ class DevMindDatabase {
|
|
|
3442
3563
|
const absPath = (0, config_1.canonicalizePath)(filePath);
|
|
3443
3564
|
const repoRelPath = this.toRepoRelativePath(absPath);
|
|
3444
3565
|
const diskRelPath = repoRelPath.replace(/^\{([^}]+)\}/, '$1').replace(/\.[^/.]+$/, '.json');
|
|
3445
|
-
const
|
|
3566
|
+
const vectorsDir = path.join(workspaceRoot, 'vectors');
|
|
3567
|
+
const vectorsJsonPath = path.join(vectorsDir, diskRelPath);
|
|
3568
|
+
// Same guard as writeGraphToDisk — see isDegenerateDiskJsonPath's comment for why this
|
|
3569
|
+
// happens and what it means.
|
|
3570
|
+
if (this.isDegenerateDiskJsonPath(vectorsDir, diskRelPath, vectorsJsonPath)) {
|
|
3571
|
+
console.warn(`⚠️ Skipped writing vectors JSON: a node's file_path resolves to a directory, not a file (${absPath}) — run \`devsmind analyze --fix\` to clean it up.`);
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3446
3574
|
const absLower = absPath.toLowerCase();
|
|
3447
3575
|
const absEscLower = this.likeEscape(absPath).toLowerCase();
|
|
3448
3576
|
const stmt = this.db.prepare(`
|
|
@@ -3474,20 +3602,50 @@ class DevMindDatabase {
|
|
|
3474
3602
|
console.warn('⚠️ SQLite warning: Failed to write vectors JSON to disk:', err);
|
|
3475
3603
|
}
|
|
3476
3604
|
}
|
|
3477
|
-
/**
|
|
3605
|
+
/**
|
|
3606
|
+
* Force-syncs all database nodes, vectors, and workflows to disk JSON files. This is the
|
|
3607
|
+
* write-back half of `devsmind sync` (paired with `syncFromDisk`, which reads `vectors/*.json`
|
|
3608
|
+
* into `node_vectors` — see its comment). Vectors are re-written here for the same reason
|
|
3609
|
+
* graph JSON is: normal operation already keeps `vectors/` current (`writeVectorsToDisk` runs
|
|
3610
|
+
* immediately alongside every embedding write), but `sync` exists precisely for the abnormal
|
|
3611
|
+
* case — recovering a `.devmind` after `vectors/*.json` was deleted/corrupted independently of
|
|
3612
|
+
* the DB, or after a past write silently failed (e.g. the directory-typed file_path bug
|
|
3613
|
+
* `isDegenerateDiskJsonPath` now catches) — so leaving vectors out of the force-resync
|
|
3614
|
+
* defeated half the point of running it.
|
|
3615
|
+
*
|
|
3616
|
+
* A file_path shared by at least one ACTIVE node is always re-attempted, deprecated or not —
|
|
3617
|
+
* a deprecated node sharing a real file with a live one still needs its `deprecated:1` flag
|
|
3618
|
+
* kept current on disk. A file_path held ONLY by deprecated nodes is skipped when it's also
|
|
3619
|
+
* `isFilePathStructurallyDegenerate` (a directory rather than a file — `deprecateNode` already
|
|
3620
|
+
* wrote it once, at the moment it was deprecated, so nothing new would come of retrying):
|
|
3621
|
+
* without this, a node deprecated for exactly this reason produced the identical "Skipped
|
|
3622
|
+
* writing graph JSON" warning on every single future `sync`, forever, since deprecating a node
|
|
3623
|
+
* never clears its (already-garbage) file_path — indistinguishable from the bug never having
|
|
3624
|
+
* been fixed at all.
|
|
3625
|
+
*/
|
|
3478
3626
|
syncToDisk() {
|
|
3479
3627
|
try {
|
|
3480
|
-
const rows = this.db.prepare('SELECT DISTINCT file_path FROM nodes').all();
|
|
3481
|
-
const
|
|
3628
|
+
const rows = this.db.prepare('SELECT DISTINCT file_path, deprecated FROM nodes').all();
|
|
3629
|
+
const activeFilePaths = new Set();
|
|
3630
|
+
const allFilePaths = new Set();
|
|
3482
3631
|
for (const row of rows) {
|
|
3483
|
-
if (row.file_path)
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3632
|
+
if (!row.file_path)
|
|
3633
|
+
continue;
|
|
3634
|
+
for (const p of row.file_path.split(',').map(s => s.trim()).filter(Boolean)) {
|
|
3635
|
+
allFilePaths.add(p);
|
|
3636
|
+
if (!row.deprecated)
|
|
3637
|
+
activeFilePaths.add(p);
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
const filePaths = new Set();
|
|
3641
|
+
for (const p of allFilePaths) {
|
|
3642
|
+
if (activeFilePaths.has(p) || !this.isFilePathStructurallyDegenerate(p)) {
|
|
3643
|
+
filePaths.add(p);
|
|
3487
3644
|
}
|
|
3488
3645
|
}
|
|
3489
3646
|
for (const filePath of filePaths) {
|
|
3490
3647
|
this.writeGraphToDisk(filePath);
|
|
3648
|
+
this.writeVectorsToDisk(filePath);
|
|
3491
3649
|
}
|
|
3492
3650
|
const workflowRows = this.db.prepare('SELECT id FROM workflows').all();
|
|
3493
3651
|
for (const row of workflowRows) {
|