devsmind-mcp 2.3.0 → 3.0.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 +326 -234
- package/dist/cli/activity.d.ts +9 -0
- package/dist/cli/activity.js +69 -0
- package/dist/cli/activity.js.map +1 -0
- package/dist/cli/describe.d.ts +31 -0
- package/dist/cli/describe.js +201 -0
- package/dist/cli/describe.js.map +1 -0
- package/dist/cli/diff.d.ts +14 -0
- package/dist/cli/diff.js +161 -0
- package/dist/cli/diff.js.map +1 -0
- package/dist/cli/embed.d.ts +6 -0
- package/dist/cli/embed.js +109 -0
- package/dist/cli/embed.js.map +1 -0
- package/dist/cli/extract-agent.d.ts +66 -0
- package/dist/cli/extract-agent.js +192 -0
- package/dist/cli/extract-agent.js.map +1 -0
- package/dist/cli/feedback.d.ts +11 -0
- package/dist/cli/feedback.js +90 -0
- package/dist/cli/feedback.js.map +1 -0
- package/dist/cli/index.js +128 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +30 -0
- package/dist/cli/init.js +98 -18
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/integrations/memory-topics.d.ts +44 -0
- package/dist/cli/integrations/memory-topics.js +295 -0
- package/dist/cli/integrations/memory-topics.js.map +1 -0
- package/dist/cli/integrations/memory.d.ts +11 -5
- package/dist/cli/integrations/memory.js +132 -36
- package/dist/cli/integrations/memory.js.map +1 -1
- package/dist/cli/integrations/prompt.d.ts +8 -0
- package/dist/cli/integrations/prompt.js +24 -3
- package/dist/cli/integrations/prompt.js.map +1 -1
- package/dist/cli/integrations/registry.d.ts +19 -5
- package/dist/cli/integrations/registry.js +23 -13
- package/dist/cli/integrations/registry.js.map +1 -1
- package/dist/cli/llm-client.d.ts +116 -0
- package/dist/cli/llm-client.js +322 -0
- package/dist/cli/llm-client.js.map +1 -0
- package/dist/cli/prune.js +4 -3
- package/dist/cli/prune.js.map +1 -1
- package/dist/cli/rule.d.ts +22 -2
- package/dist/cli/rule.js +116 -56
- package/dist/cli/rule.js.map +1 -1
- package/dist/cli/runner.d.ts +21 -0
- package/dist/cli/runner.js +94 -276
- package/dist/cli/runner.js.map +1 -1
- package/dist/cli/sync-progress.d.ts +9 -0
- package/dist/cli/sync-progress.js +41 -0
- package/dist/cli/sync-progress.js.map +1 -0
- package/dist/cli/sync.js +5 -2
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/view.js +3 -2
- package/dist/cli/view.js.map +1 -1
- package/dist/cli/workflow.js +26 -20
- package/dist/cli/workflow.js.map +1 -1
- package/dist/db/activity.d.ts +200 -0
- package/dist/db/activity.js +347 -0
- package/dist/db/activity.js.map +1 -0
- package/dist/db/analyze.js +25 -9
- package/dist/db/analyze.js.map +1 -1
- package/dist/db/database.d.ts +607 -79
- package/dist/db/database.js +1936 -488
- package/dist/db/database.js.map +1 -1
- package/dist/db/edges.d.ts +24 -0
- package/dist/db/edges.js +84 -0
- package/dist/db/edges.js.map +1 -1
- package/dist/db/embedder.d.ts +39 -0
- package/dist/db/embedder.js +0 -0
- package/dist/db/embedder.js.map +1 -0
- package/dist/db/feedback.d.ts +128 -0
- package/dist/db/feedback.js +182 -0
- package/dist/db/feedback.js.map +1 -0
- package/dist/db/file-diff.d.ts +32 -0
- package/dist/db/file-diff.js +110 -0
- package/dist/db/file-diff.js.map +1 -0
- package/dist/db/grep.d.ts +78 -0
- package/dist/db/grep.js +475 -0
- package/dist/db/grep.js.map +1 -0
- package/dist/db/message-revert.d.ts +63 -0
- package/dist/db/message-revert.js +258 -0
- package/dist/db/message-revert.js.map +1 -0
- package/dist/db/revert.d.ts +31 -0
- package/dist/db/revert.js +108 -0
- package/dist/db/revert.js.map +1 -0
- package/dist/db/schema.d.ts +49 -4
- package/dist/db/schema.js +125 -73
- package/dist/db/schema.js.map +1 -1
- package/dist/db/search-index.d.ts +65 -0
- package/dist/db/search-index.js +74 -0
- package/dist/db/search-index.js.map +1 -0
- package/dist/db/staging.d.ts +90 -5
- package/dist/db/staging.js +148 -22
- package/dist/db/staging.js.map +1 -1
- package/dist/mcp/server.d.ts +29 -7
- package/dist/mcp/server.js +2755 -745
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/vendor/3d-force-graph.min.js +5 -0
- package/dist/mcp/vendor/force-graph.min.js +5 -0
- package/dist/mcp/vendor/model/model_int8.onnx +0 -0
- package/dist/mcp/vendor/model/vocab.txt +30522 -0
- package/dist/mcp/vendor/three.min.js +7 -0
- package/dist/mcp/view.css +419 -0
- package/dist/mcp/view.html +161 -0
- package/dist/mcp/view.js +245 -0
- package/dist/mcp/view_chat.js +382 -0
- package/dist/mcp/view_graph.js +576 -0
- package/dist/mcp/visualizer.d.ts +25 -2
- package/dist/mcp/visualizer.js +31 -4
- package/dist/mcp/visualizer.js.map +1 -1
- package/dist/utils/ast.d.ts +187 -0
- package/dist/utils/ast.js +640 -20
- package/dist/utils/ast.js.map +1 -1
- package/dist/utils/diff.d.ts +44 -0
- package/dist/utils/diff.js +78 -0
- package/dist/utils/diff.js.map +1 -0
- package/dist/utils/edit.d.ts +47 -0
- package/dist/utils/edit.js +196 -0
- package/dist/utils/edit.js.map +1 -0
- package/dist/utils/scanner.js +12 -4
- package/dist/utils/scanner.js.map +1 -1
- package/dist/utils/tokenize.d.ts +45 -0
- package/dist/utils/tokenize.js +129 -0
- package/dist/utils/tokenize.js.map +1 -0
- package/dist/utils/version.d.ts +14 -0
- package/dist/utils/version.js +61 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +18 -5
- package/dist/mcp/visualizer_2d.html +0 -635
- package/dist/mcp/visualizer_3d.html +0 -613
package/dist/db/grep.js
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isDefaultIgnoredFile = isDefaultIgnoredFile;
|
|
37
|
+
exports.escapeRegExp = escapeRegExp;
|
|
38
|
+
exports.grepRepos = grepRepos;
|
|
39
|
+
exports.rankGrepHits = rankGrepHits;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
/**
|
|
43
|
+
* A fast, dependency-free filesystem grep — the "last resort" file layer of `search_nodes`,
|
|
44
|
+
* and the thing that lets it finally cover files the graph never indexes (CSS, JSON, config,
|
|
45
|
+
* markup, un-indexed code). Deliberately self-contained: no DB, no MCP, so it can be unit-tested
|
|
46
|
+
* in isolation and reasoned about purely as "given roots + a pattern, return where it appears".
|
|
47
|
+
*
|
|
48
|
+
* The pattern is a REAL regex — not escaped, not split on a delimiter. An agent can pass exactly
|
|
49
|
+
* what it would give `grep`: alternation (`a|b`), escaped literals (`item\.liked`), character
|
|
50
|
+
* classes, anything. Earlier versions took a list of literal `keywords`, escaped each one, and
|
|
51
|
+
* OR-joined them — which meant an agent passing its OWN already-correct regex (e.g.
|
|
52
|
+
* `item\.liked`) got it re-escaped into a search for a literal backslash, silently matching
|
|
53
|
+
* nothing. That mismatch between what the caller wrote and what the tool actually searched for
|
|
54
|
+
* is the whole reason for this rewrite.
|
|
55
|
+
*
|
|
56
|
+
* Performance is the whole point: the tool is only worth using if it's as fast as grep, or the
|
|
57
|
+
* AI just reaches for grep instead. So this does ONE walk over each repo, tests the pattern in a
|
|
58
|
+
* single compiled regex per line, skips junk dirs / binary / oversized files before ever reading
|
|
59
|
+
* them, and hard-stops at a deadline with a partial result rather than ever blowing the budget.
|
|
60
|
+
*
|
|
61
|
+
* Walking and reading are both SYNCHRONOUS (`fs.readdirSync`/`fs.statSync`/`fs.readFileSync`), not
|
|
62
|
+
* `fs.promises` + bounded concurrency as an earlier version did. That sounds like it should be
|
|
63
|
+
* slower — it isn't. Every `fs.promises` call queues onto Node's libuv threadpool, which defaults
|
|
64
|
+
* to just 4 threads; "bounded concurrency" in JS-land doesn't mean bounded OS-level parallelism,
|
|
65
|
+
* it means potentially thousands of promises competing for 4 real workers, and on Windows especially
|
|
66
|
+
* (antivirus adds latency to every filesystem call) that queuing dominated wall-clock time. Measured
|
|
67
|
+
* against a real 8-repo production checkout: the async version took multiple MINUTES per search on
|
|
68
|
+
* some queries; sync took low hundreds of milliseconds for the identical walk. Sync fs calls bypass
|
|
69
|
+
* the threadpool entirely — a brief main-thread block for a fast operation beats a "concurrent" one
|
|
70
|
+
* queued behind a 4-worker bottleneck.
|
|
71
|
+
*/
|
|
72
|
+
/** Directories never worth searching — build output, deps, VCS internals. Mirrors scanner.ts. */
|
|
73
|
+
const ALWAYS_IGNORED = [
|
|
74
|
+
'node_modules', '.git', 'dist', 'build', 'out', '.next',
|
|
75
|
+
'__pycache__', '.venv', 'venv', 'coverage', '.turbo',
|
|
76
|
+
'.cache', '.idea', '.vscode', '.devmind',
|
|
77
|
+
// Mobile/native build & dependency caches — not JS/Python-shaped, so the list above misses
|
|
78
|
+
// them entirely. Left out originally, these are exactly the giant, non-source directories
|
|
79
|
+
// (hundreds of thousands of files in Pods/DerivedData on a real iOS checkout) that a
|
|
80
|
+
// path-blind grep walk would otherwise crawl on every single search.
|
|
81
|
+
'.dart_tool', '.gradle', '.symlinks', 'Pods', 'DerivedData', '.expo', 'Carthage'
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* Individual FILES never worth grepping, matched on basename. Every entry here is machine-written:
|
|
85
|
+
* a dependency resolution dump or a build artifact. They are the single biggest source of false
|
|
86
|
+
* positives in a real search — a lockfile mentions every package name in the tree, so a pattern
|
|
87
|
+
* like `alipay|Alipay` matches `yarn.lock` and `package-lock.json` purely because a dependency is
|
|
88
|
+
* named that, ranking them alongside the actual source the agent was looking for.
|
|
89
|
+
*
|
|
90
|
+
* Note what is deliberately NOT here: `.env`. The file-level doc comment above, the `search_nodes`
|
|
91
|
+
* tool description, and the generated agent memory all advertise the files bucket as how you find
|
|
92
|
+
* "CSS, JSON, .env, markup" — "which env var drives this" is a documented, intended use. Excluding
|
|
93
|
+
* it by default would silently reverse a shipped contract. A user who wants it gone can still put
|
|
94
|
+
* it in `ignored_paths`, which IS honored per-file now (see `shouldIgnorePath`).
|
|
95
|
+
*
|
|
96
|
+
* Exact basenames, not globs, on purpose: `ignored_paths` has no glob engine (init.ts filters glob
|
|
97
|
+
* lines out of .gitignore import and tells the user so), and a built-in list that spoke a language
|
|
98
|
+
* the user's own config couldn't would be its own trap. `DENIED_SUFFIXES` covers the few cases
|
|
99
|
+
* that genuinely need a wildcard, via plain `endsWith`.
|
|
100
|
+
*/
|
|
101
|
+
const DENIED_BASENAMES = new Set([
|
|
102
|
+
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'npm-shrinkwrap.json', 'bun.lockb',
|
|
103
|
+
'composer.lock', 'Gemfile.lock', 'poetry.lock', 'Cargo.lock', 'Podfile.lock', 'pubspec.lock',
|
|
104
|
+
'go.sum'
|
|
105
|
+
]);
|
|
106
|
+
/** Generated/derived files, matched on suffix — minified bundles and source maps. */
|
|
107
|
+
const DENIED_SUFFIXES = ['.min.js', '.min.css', '.map'];
|
|
108
|
+
/**
|
|
109
|
+
* True if a path is a lockfile or build artifact excluded from search by default. Exported so
|
|
110
|
+
* `searchNodes` can tell an agent that explicitly scoped `path` AT such a file WHY it got nothing
|
|
111
|
+
* back, rather than leaving it to read as "the pattern isn't in there".
|
|
112
|
+
*/
|
|
113
|
+
function isDefaultIgnoredFile(filePath) {
|
|
114
|
+
const base = path.basename(filePath);
|
|
115
|
+
return DENIED_BASENAMES.has(base) || DENIED_SUFFIXES.some(sfx => base.endsWith(sfx));
|
|
116
|
+
}
|
|
117
|
+
/** Files above this are almost never hand-written source worth grepping (minified bundles, data dumps). */
|
|
118
|
+
const MAX_FILE_BYTES = 1_000_000;
|
|
119
|
+
/** Bytes sniffed from the head of a file to decide "is this binary" (a NUL byte ⇒ binary). */
|
|
120
|
+
const BINARY_SNIFF_BYTES = 4096;
|
|
121
|
+
/** Caps matches recorded per line — a pathological line (a minified bundle that slipped past the
|
|
122
|
+
* size/binary filters, or a pattern like `.` against a long line) shouldn't blow up memory. */
|
|
123
|
+
const MAX_MATCHES_PER_LINE = 20;
|
|
124
|
+
/**
|
|
125
|
+
* Path-segment-aware, mirroring scanner.ts's `shouldIgnore` exactly (kept as a separate copy,
|
|
126
|
+
* not a shared import — grep.ts is deliberately dependency-free, see the file-level doc comment).
|
|
127
|
+
*
|
|
128
|
+
* This used to be a bare `ALWAYS_IGNORED.includes(name) || extraIgnored.includes(name)` — an
|
|
129
|
+
* EXACT match against just the current directory's basename. That silently failed to honor any
|
|
130
|
+
* multi-segment `ignored_paths` entry (e.g. "ios/Pods", or a path picked via the `devsmind init`
|
|
131
|
+
* folder browser, which stores a path like "ios/Pods" rather than the bare name "Pods"): walking
|
|
132
|
+
* into a directory literally named "Pods" tested `"ios/Pods".includes("Pods")` against the wrong
|
|
133
|
+
* variable — `extraIgnored.includes(name)` checks array membership of "Pods" against the literal
|
|
134
|
+
* string "ios/Pods", which is never true. The directory was walked (and every file in it read)
|
|
135
|
+
* on every single search regardless of the exclusion the user explicitly configured. Same root
|
|
136
|
+
* cause the file-level doc comment already documents for enumeration cost in general: this was
|
|
137
|
+
* the biggest reason a single un-ignored deep native-deps folder could dominate `search_nodes`
|
|
138
|
+
* latency even after the user had "already excluded it."
|
|
139
|
+
*
|
|
140
|
+
* Named `shouldIgnorePath`, not `shouldIgnoreDir`, because it is applied to FILE entries too now.
|
|
141
|
+
* It always could be — the matching is on the full path, so `".../package-lock.json"` correctly
|
|
142
|
+
* satisfies `endsWith("/package-lock.json")`. It simply was never called on the file branch of
|
|
143
|
+
* the walk, which meant every FILE entry a user had put in `ignored_paths` was silently inert for
|
|
144
|
+
* search. That was not a rare configuration: `devsmind init`'s own preset list is entirely file
|
|
145
|
+
* names (package-lock.json, yarn.lock, tsconfig.json…), and .gitignore import passes literal file
|
|
146
|
+
* names straight through. Users had excluded these, seen them keep showing up in every result,
|
|
147
|
+
* and had no way to tell the exclusion was being dropped on the floor. `scanner.ts` applies the
|
|
148
|
+
* equivalent check to files correctly; grep.ts was the outlier.
|
|
149
|
+
*
|
|
150
|
+
* The leading-slash strip matters for the same class of reason: `.gitignore` conventionally writes
|
|
151
|
+
* a repo-root anchor as `/dist`, and `isLiteralIgnorePattern` accepts it (there is no glob
|
|
152
|
+
* metacharacter in it), so it lands in `ignored_paths` verbatim. Stripping only the TRAILING slash
|
|
153
|
+
* left the test as `normalized.includes('//dist/')` — a doubled slash that can never occur in a
|
|
154
|
+
* real path, so the entry matched nothing, forever, in silence.
|
|
155
|
+
*/
|
|
156
|
+
function shouldIgnorePath(fullPath, extraIgnored) {
|
|
157
|
+
const normalized = fullPath.replace(/\\/g, '/');
|
|
158
|
+
const allIgnored = [...ALWAYS_IGNORED, ...extraIgnored];
|
|
159
|
+
return allIgnored.some(pattern => {
|
|
160
|
+
const p = pattern.replace(/\\/g, '/').replace(/^\//, '').replace(/\/$/, '');
|
|
161
|
+
return normalized.includes(`/${p}/`) || normalized.endsWith(`/${p}`);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** Escapes a literal string so it matches as text, not as a regex pattern. Exported so callers
|
|
165
|
+
* building a regex out of LITERAL terms (e.g. `searchNodes` deriving one from query tokens when
|
|
166
|
+
* no explicit `pattern` was given) can reuse the exact same escaping grep.ts uses internally. */
|
|
167
|
+
function escapeRegExp(s) {
|
|
168
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Recursively lists every candidate file under a root, skipping junk dirs. Enumeration only — no
|
|
172
|
+
* file contents are read here.
|
|
173
|
+
*
|
|
174
|
+
* `root` may be a single FILE, not just a directory — this is what makes `path` scoping to one
|
|
175
|
+
* file work: `readdirSync` throws on a file, so that case is checked up front and short-circuits
|
|
176
|
+
* to a one-element result instead of falling into the directory walk.
|
|
177
|
+
*
|
|
178
|
+
* SYNCHRONOUS on purpose, not `fsp.readdir` fanned out via `Promise.all`. Every `fs.promises` call
|
|
179
|
+
* queues onto Node's libuv threadpool, which defaults to just 4 threads — an unbounded
|
|
180
|
+
* `Promise.all(subdirs.map(walk))` doesn't give the OS real parallelism, it just piles hundreds or
|
|
181
|
+
* thousands of directory-listing promises onto that tiny pool. On Windows in particular (real-time
|
|
182
|
+
* antivirus scanning adds latency to every single filesystem call), that queuing turned a walk of
|
|
183
|
+
* real 8-repo, ~3500-file trees — 32ms with plain synchronous `readdirSync` — into multi-MINUTE
|
|
184
|
+
* enumeration times, measured against a real production checkout. `readdirSync` bypasses the
|
|
185
|
+
* threadpool entirely (it's one direct syscall per directory), which is why it doesn't have this
|
|
186
|
+
* problem. A brief synchronous block for a fast operation beats a "concurrent" one queued behind a
|
|
187
|
+
* 4-worker bottleneck.
|
|
188
|
+
*
|
|
189
|
+
* Deadline-checked between every directory (not just, as before, only in the file-read phase) —
|
|
190
|
+
* `listFiles` alone used to have NO way to bail out early, so a single pathological repo could
|
|
191
|
+
* blow the entire search budget before the read phase's own deadline check ever ran. Returns
|
|
192
|
+
* whatever was found so far plus `truncated: true` if the deadline hit mid-walk.
|
|
193
|
+
*/
|
|
194
|
+
function listFiles(root, extraIgnored, deadline) {
|
|
195
|
+
let rootStat;
|
|
196
|
+
try {
|
|
197
|
+
rootStat = fs.statSync(root);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return { files: [], truncated: false }; // doesn't exist / unreadable — nothing to search
|
|
201
|
+
}
|
|
202
|
+
if (rootStat.isFile())
|
|
203
|
+
return { files: [root], truncated: false };
|
|
204
|
+
const out = [];
|
|
205
|
+
let truncated = false;
|
|
206
|
+
function walk(dir) {
|
|
207
|
+
if (truncated)
|
|
208
|
+
return;
|
|
209
|
+
if (timeMonotonic() > deadline) {
|
|
210
|
+
truncated = true;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let entries;
|
|
214
|
+
try {
|
|
215
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return; // unreadable dir — skip, don't fail the whole search
|
|
219
|
+
}
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
if (truncated)
|
|
222
|
+
return;
|
|
223
|
+
const fullPath = path.join(dir, entry.name);
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
if (!shouldIgnorePath(fullPath, extraIgnored))
|
|
226
|
+
walk(fullPath);
|
|
227
|
+
}
|
|
228
|
+
else if (entry.isFile()) {
|
|
229
|
+
// Files are filtered here, at enumeration, not later in `grepFile` — a lockfile is
|
|
230
|
+
// typically a few hundred KB of non-binary text, so it clears every check `grepFile`
|
|
231
|
+
// makes (size, NUL sniff) and gets read and scanned in full on every single search.
|
|
232
|
+
if (!isDefaultIgnoredFile(fullPath) && !shouldIgnorePath(fullPath, extraIgnored)) {
|
|
233
|
+
out.push(fullPath);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
walk(root);
|
|
239
|
+
return { files: out, truncated };
|
|
240
|
+
}
|
|
241
|
+
/** True if the head of the buffer contains a NUL byte — the standard cheap "is binary" heuristic. */
|
|
242
|
+
function looksBinary(buf) {
|
|
243
|
+
const n = Math.min(buf.length, BINARY_SNIFF_BYTES);
|
|
244
|
+
for (let i = 0; i < n; i++)
|
|
245
|
+
if (buf[i] === 0)
|
|
246
|
+
return true;
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Reads one file and collects every line matching the regex. Returns [] on any read error, binary
|
|
251
|
+
* content, or oversize — a single bad file never breaks the search.
|
|
252
|
+
*
|
|
253
|
+
* `matcher` MUST carry the `g` flag (the caller builds it once and reuses it across every file) —
|
|
254
|
+
* without it there is no way to find every match on a line, only the first. `lastIndex` is reset
|
|
255
|
+
* per line since a stateful global regex otherwise silently skips matches or scans stale offsets
|
|
256
|
+
* from the PREVIOUS line.
|
|
257
|
+
*
|
|
258
|
+
* Zero-length matches are a real hazard with an arbitrary caller-supplied pattern (e.g. `x*`
|
|
259
|
+
* against a line with no "x" still "matches" at every position with length 0): `exec` never
|
|
260
|
+
* advances `lastIndex` on its own for those, so a naive loop spins forever at the same index.
|
|
261
|
+
* Guarded by force-advancing whenever `lastIndex` didn't move — the standard fix for this exact
|
|
262
|
+
* MDN-documented `RegExp.exec` footgun.
|
|
263
|
+
*
|
|
264
|
+
* SYNCHRONOUS on purpose (`fs.statSync`/`fs.readFileSync`), same reasoning as `listFiles`: this
|
|
265
|
+
* used to be `fsp.stat`/`fsp.readFile` run through a 24-way `mapPool`, which sounds like real
|
|
266
|
+
* parallelism but every one of those calls queues onto Node's 4-thread libuv threadpool — on a
|
|
267
|
+
* real repo tree (thousands of files, many of them non-source assets a `public/`-style directory
|
|
268
|
+
* pulls in) that queuing, not the actual I/O, was the dominant cost, especially on Windows where
|
|
269
|
+
* antivirus adds latency per filesystem call. Measured on a real production checkout: converting
|
|
270
|
+
* this phase from bounded-async to plain sync took what could run into MINUTES down to comfortably
|
|
271
|
+
* under the search budget. "Bounded concurrency" isn't a meaningful concept for sync calls anyway
|
|
272
|
+
* — they're inherently one-at-a-time — so the caller now just loops with a deadline check between
|
|
273
|
+
* files instead of pooling workers.
|
|
274
|
+
*/
|
|
275
|
+
function grepFile(filePath, matcher) {
|
|
276
|
+
let stat;
|
|
277
|
+
try {
|
|
278
|
+
stat = fs.statSync(filePath);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
if (!stat.isFile() || stat.size === 0 || stat.size > MAX_FILE_BYTES)
|
|
284
|
+
return [];
|
|
285
|
+
let buf;
|
|
286
|
+
try {
|
|
287
|
+
buf = fs.readFileSync(filePath);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
if (looksBinary(buf))
|
|
293
|
+
return [];
|
|
294
|
+
const text = buf.toString('utf-8');
|
|
295
|
+
const lines = text.split('\n');
|
|
296
|
+
const hits = [];
|
|
297
|
+
for (let i = 0; i < lines.length; i++) {
|
|
298
|
+
const line = lines[i];
|
|
299
|
+
matcher.lastIndex = 0;
|
|
300
|
+
let m;
|
|
301
|
+
let countOnLine = 0;
|
|
302
|
+
while (countOnLine < MAX_MATCHES_PER_LINE && (m = matcher.exec(line)) !== null) {
|
|
303
|
+
if (m[0].length > 0) {
|
|
304
|
+
hits.push({ file_path: filePath, line_number: i + 1, line_content: line.slice(0, 400), matched_text: m[0].toLowerCase() });
|
|
305
|
+
countOnLine++;
|
|
306
|
+
}
|
|
307
|
+
// Zero-length match guard (see doc comment) — applies whether or not this iteration
|
|
308
|
+
// recorded a hit, since `lastIndex` can stall on an empty match either way.
|
|
309
|
+
if (matcher.lastIndex === m.index)
|
|
310
|
+
matcher.lastIndex++;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return hits;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Greps every configured repo root (or, when `opts.scopePath` is given, just that one folder or
|
|
317
|
+
* file) for `pattern` — a real regex, used as-is. Returns raw hits plus whether the deadline
|
|
318
|
+
* forced an early, partial stop. Ranking is a separate pure step (`rankGrepHits`).
|
|
319
|
+
*
|
|
320
|
+
* Invalid regex throws `Invalid regex pattern: <message>` immediately (same shape as the
|
|
321
|
+
* precedent already established by `DevMindDatabase.searchCode`) rather than surfacing as an
|
|
322
|
+
* opaque failure deeper in the walk.
|
|
323
|
+
*/
|
|
324
|
+
async function grepRepos(repoRoots, pattern, opts = {}) {
|
|
325
|
+
const trimmedPattern = pattern.trim();
|
|
326
|
+
const hasRoots = opts.scopePath ? true : repoRoots.filter(Boolean).length > 0;
|
|
327
|
+
if (!trimmedPattern || !hasRoots) {
|
|
328
|
+
return { hits: [], truncated: false, files_scanned: 0 };
|
|
329
|
+
}
|
|
330
|
+
const extraIgnored = opts.ignoredPaths ?? [];
|
|
331
|
+
// No default cap — a 3s deadline was silently truncating results on repos that legitimately
|
|
332
|
+
// take longer than that to walk, which read as "grep just didn't find it" with no signal that
|
|
333
|
+
// anything was cut short. Callers that DO want a bound (e.g. a latency-sensitive UI) should
|
|
334
|
+
// pass an explicit timeoutMs; `search_nodes` (the only current caller) intentionally doesn't —
|
|
335
|
+
// deliberate even now that `pattern` is a real regex: an unbounded walk against `node_modules`
|
|
336
|
+
// et al is already impossible (ALWAYS_IGNORED / shouldIgnoreDir below run BEFORE any matching,
|
|
337
|
+
// independent of the pattern), so a slow catastrophic-backtrack pattern still only ever runs
|
|
338
|
+
// against real project source, not the whole disk.
|
|
339
|
+
const timeoutMs = opts.timeoutMs ?? Infinity;
|
|
340
|
+
const deadline = timeMonotonic() + timeoutMs;
|
|
341
|
+
const caseInsensitive = opts.caseInsensitive !== false;
|
|
342
|
+
let matcher;
|
|
343
|
+
try {
|
|
344
|
+
matcher = new RegExp(pattern, caseInsensitive ? 'gi' : 'g');
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
throw new Error(`Invalid regex pattern: ${err.message}`);
|
|
348
|
+
}
|
|
349
|
+
// Scoped to one folder/file → that's the only walk root. The scope itself is checked against
|
|
350
|
+
// the ignore rules here (walk() below only checks CHILDREN it descends into, never the root it
|
|
351
|
+
// starts from) — otherwise a scope path pointing straight AT an ignored dir (or a file inside
|
|
352
|
+
// one) would bypass the ignore list entirely instead of correctly finding nothing.
|
|
353
|
+
//
|
|
354
|
+
// The default file denylist is enforced here too, deliberately: "explicit scoping wins" sounds
|
|
355
|
+
// right but would be inconsistent, since a scope pointing at an ignored DIRECTORY has always
|
|
356
|
+
// correctly found nothing. Making files the exception would mean one function enforced two
|
|
357
|
+
// opposite rules. The confusing part of an empty result is not the emptiness, it's the silence
|
|
358
|
+
// — so `searchNodes` pairs this with a hint saying the path was excluded by default, rather
|
|
359
|
+
// than letting it read as "the pattern isn't in there".
|
|
360
|
+
const walkRoots = opts.scopePath ? [opts.scopePath] : repoRoots.filter(Boolean);
|
|
361
|
+
const allFiles = [];
|
|
362
|
+
let truncated = false;
|
|
363
|
+
for (const root of walkRoots) {
|
|
364
|
+
if (truncated)
|
|
365
|
+
break;
|
|
366
|
+
if (opts.scopePath && (shouldIgnorePath(root, extraIgnored) || isDefaultIgnoredFile(root)))
|
|
367
|
+
continue;
|
|
368
|
+
const result = listFiles(root, extraIgnored, deadline);
|
|
369
|
+
allFiles.push(...result.files);
|
|
370
|
+
if (result.truncated)
|
|
371
|
+
truncated = true;
|
|
372
|
+
}
|
|
373
|
+
const hits = [];
|
|
374
|
+
let scanned = 0;
|
|
375
|
+
// Read+scan — synchronous (see grepFile's doc comment for why), deadline-checked between every
|
|
376
|
+
// file so a pathological repo yields a partial result instead of hanging, same as enumeration
|
|
377
|
+
// above. This is the whole point of the "as fast as grep" goal.
|
|
378
|
+
for (const file of allFiles) {
|
|
379
|
+
if (timeMonotonic() > deadline) {
|
|
380
|
+
truncated = true;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
const fileHits = grepFile(file, matcher);
|
|
384
|
+
scanned++;
|
|
385
|
+
if (fileHits.length)
|
|
386
|
+
hits.push(...fileHits);
|
|
387
|
+
}
|
|
388
|
+
return { hits, truncated, files_scanned: scanned };
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Ranks raw hits into a capped, ordered PAGE of files — the anti-firehose gate — plus the TRUE
|
|
392
|
+
* total file count before that cap, so a caller can tell "that's everything" from "more exists,
|
|
393
|
+
* ask for the next page" instead of guessing from a bare capped list. Unranked hits across 8
|
|
394
|
+
* repos can be hundreds; dumping them recreates the exact "15-20 low-confidence results I have to
|
|
395
|
+
* eyeball-filter" complaint that made the AI distrust the tool. So: group by file, score by match
|
|
396
|
+
* count weighted toward RARER matched strings (IDF-shaped, same instinct as BM25), reward files
|
|
397
|
+
* matching MULTIPLE distinct strings, sort with a stable tiebreaker, and page.
|
|
398
|
+
*/
|
|
399
|
+
function rankGrepHits(hits, opts = {}) {
|
|
400
|
+
const maxFiles = opts.maxFiles ?? 25;
|
|
401
|
+
const maxSampleLines = opts.maxSampleLines ?? 5;
|
|
402
|
+
const offset = opts.offset ?? 0;
|
|
403
|
+
if (hits.length === 0)
|
|
404
|
+
return { files: [], total: 0 };
|
|
405
|
+
// Document frequency per matched string: in how many distinct files does each one appear? A
|
|
406
|
+
// string in few files is a stronger signal than one that's everywhere — the rarity weight below.
|
|
407
|
+
const filesPerText = new Map();
|
|
408
|
+
for (const h of hits) {
|
|
409
|
+
let set = filesPerText.get(h.matched_text);
|
|
410
|
+
if (!set) {
|
|
411
|
+
set = new Set();
|
|
412
|
+
filesPerText.set(h.matched_text, set);
|
|
413
|
+
}
|
|
414
|
+
set.add(h.file_path);
|
|
415
|
+
}
|
|
416
|
+
const totalFiles = new Set(hits.map(h => h.file_path)).size;
|
|
417
|
+
const rarity = (text) => {
|
|
418
|
+
const df = filesPerText.get(text)?.size ?? 1;
|
|
419
|
+
// log-shaped IDF: rarer ⇒ higher, but never below a small floor so a common string still counts.
|
|
420
|
+
return Math.max(Math.log(1 + totalFiles / df), 0.3);
|
|
421
|
+
};
|
|
422
|
+
const byFile = new Map();
|
|
423
|
+
for (const h of hits) {
|
|
424
|
+
let acc = byFile.get(h.file_path);
|
|
425
|
+
if (!acc) {
|
|
426
|
+
acc = { total: 0, counts: {}, lines: [] };
|
|
427
|
+
byFile.set(h.file_path, acc);
|
|
428
|
+
}
|
|
429
|
+
acc.total++;
|
|
430
|
+
acc.counts[h.matched_text] = (acc.counts[h.matched_text] || 0) + 1;
|
|
431
|
+
if (acc.lines.length < maxSampleLines)
|
|
432
|
+
acc.lines.push({ line_number: h.line_number, line_content: h.line_content });
|
|
433
|
+
}
|
|
434
|
+
const ranked = Array.from(byFile.entries()).map(([file_path, acc]) => {
|
|
435
|
+
const distinct = Object.keys(acc.counts).length;
|
|
436
|
+
// Base: sum over matched strings of (saturating count × that string's rarity). The
|
|
437
|
+
// saturating `1 + log(count)` stops 50 hits of one string from running away, and rarer
|
|
438
|
+
// strings weigh more. Then multiply by `distinct` — the number of DISTINCT matched strings
|
|
439
|
+
// this file contains — because coverage beats frequency: a file matching two different parts
|
|
440
|
+
// of the pattern is almost always more relevant than one matching a single part many times.
|
|
441
|
+
// Rarity still differentiates within a coverage level, so a single hit of an ultra-rare
|
|
442
|
+
// string can still outrank two hits of two common ones.
|
|
443
|
+
let base = 0;
|
|
444
|
+
for (const [text, count] of Object.entries(acc.counts)) {
|
|
445
|
+
const tf = 1 + Math.log(count);
|
|
446
|
+
base += tf * rarity(text);
|
|
447
|
+
}
|
|
448
|
+
const score = base * distinct;
|
|
449
|
+
return {
|
|
450
|
+
file_path,
|
|
451
|
+
total_matches: acc.total,
|
|
452
|
+
match_counts: acc.counts,
|
|
453
|
+
distinct_matches: distinct,
|
|
454
|
+
score,
|
|
455
|
+
sample_lines: acc.lines
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
// Stable final tiebreaker (file_path) — without one, equal-scoring files have no guaranteed
|
|
459
|
+
// order, so paginating with offset/limit could repeat or skip rows across calls whenever the
|
|
460
|
+
// sort happens to land them differently.
|
|
461
|
+
ranked.sort((a, b) => b.score - a.score ||
|
|
462
|
+
b.distinct_matches - a.distinct_matches ||
|
|
463
|
+
b.total_matches - a.total_matches ||
|
|
464
|
+
a.file_path.localeCompare(b.file_path));
|
|
465
|
+
return { files: ranked.slice(offset, offset + maxFiles), total: ranked.length };
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* A monotonic millisecond clock. `Date.now()` is banned in some execution contexts here and can
|
|
469
|
+
* jump backwards on a clock adjustment; `process.hrtime` is monotonic and always available in Node.
|
|
470
|
+
*/
|
|
471
|
+
function timeMonotonic() {
|
|
472
|
+
const [s, ns] = process.hrtime();
|
|
473
|
+
return s * 1000 + ns / 1e6;
|
|
474
|
+
}
|
|
475
|
+
//# sourceMappingURL=grep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grep.js","sourceRoot":"","sources":["../../src/db/grep.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface RevertOutcome {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
/** Messages actually flipped to 'reverted' this call, oldest-affected-first. */
|
|
4
|
+
reverted: string[];
|
|
5
|
+
/** Set when the cascade stopped before finishing everything requested. */
|
|
6
|
+
blocked_at?: {
|
|
7
|
+
message_id: string;
|
|
8
|
+
reason: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface UnrevertOutcome {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
message_id?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Outcome of reverting/un-reverting one file's edits within one message. */
|
|
17
|
+
export interface FileRevertOutcome {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
/** Ids of the edits actually flipped this call. */
|
|
21
|
+
edit_ids?: string[];
|
|
22
|
+
}
|
|
23
|
+
/** Outcome of reverting/un-reverting exactly one edit. */
|
|
24
|
+
export interface EditRevertOutcome {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Reverts `messageId` and every message after it that isn't already fully reverted — the stack
|
|
30
|
+
* model: later messages were built on top of earlier ones, so undoing one partway through the
|
|
31
|
+
* stack undoes everything above it too, newest first. Stops at the first message that can't be
|
|
32
|
+
* fully reverted (a drifted edit) and reports which; everything reverted before that point in
|
|
33
|
+
* this call stays reverted. A `partial` message (touched by an earlier file/edit-level revert)
|
|
34
|
+
* counts as "not yet fully reverted" and is swept up here too, finishing the job on it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function revertMessage(devmindPath: string, messageId: string): RevertOutcome;
|
|
37
|
+
/**
|
|
38
|
+
* Re-applies exactly one message: the OLDEST currently-**fully**-reverted one. Un-revert restores
|
|
39
|
+
* up the stack in the order things came off it — out-of-order restoration would recreate the same
|
|
40
|
+
* "later edit built on code that isn't there yet" hazard the revert direction guards against, so
|
|
41
|
+
* it's refused rather than attempted. A `partial` message wasn't produced by that cascade — it was
|
|
42
|
+
* touched by a surgical file/edit-level revert — so it can be un-reverted independently of the
|
|
43
|
+
* fully-reverted stack's ordering.
|
|
44
|
+
*/
|
|
45
|
+
export declare function unrevertMessage(devmindPath: string, messageId: string): UnrevertOutcome;
|
|
46
|
+
/**
|
|
47
|
+
* Reverts just the edits ONE file picked up within ONE message, leaving the message's other
|
|
48
|
+
* files untouched. Newest-edit-first within that file, atomic (rolls back this operation's own
|
|
49
|
+
* partial progress on failure, same as the whole-message path) — never leaves this one file
|
|
50
|
+
* half-reverted, though the message as a whole is expected to end up `partial`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function revertMessageFile(devmindPath: string, messageId: string, filePath: string): FileRevertOutcome;
|
|
53
|
+
/** Symmetric to revertMessageFile: oldest-edit-first, same atomic rollback. */
|
|
54
|
+
export declare function unrevertMessageFile(devmindPath: string, messageId: string, filePath: string): FileRevertOutcome;
|
|
55
|
+
/**
|
|
56
|
+
* Reverts exactly ONE edit, identified by its stable `id`, regardless of what else in the message
|
|
57
|
+
* or file is applied. Safe by construction: if a later edit (to the same node, in this message or
|
|
58
|
+
* a later one) already changed this text further, this edit's `after` string won't be found on
|
|
59
|
+
* disk anymore and the exact-match guard in applyEdit refuses rather than guessing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function revertMessageEdit(devmindPath: string, messageId: string, editId: string): EditRevertOutcome;
|
|
62
|
+
/** Symmetric to revertMessageEdit. */
|
|
63
|
+
export declare function unrevertMessageEdit(devmindPath: string, messageId: string, editId: string): EditRevertOutcome;
|