grepmax 0.19.0 → 0.20.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/.claude-plugin/marketplace.json +23 -0
- package/README.md +1 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/mcp.js +7 -7
- package/dist/commands/plugin.js +35 -21
- package/dist/commands/project.js +1 -1
- package/dist/commands/remove.js +9 -2
- package/dist/commands/setup.js +4 -2
- package/dist/commands/status.js +1 -1
- package/dist/commands/symbols.js +1 -1
- package/dist/commands/watch.js +24 -4
- package/dist/lib/daemon/daemon.js +10 -0
- package/dist/lib/daemon/ipc-handler.js +6 -0
- package/dist/lib/daemon/watcher-manager.js +18 -2
- package/dist/lib/graph/graph-builder.js +2 -2
- package/dist/lib/graph/impact.js +6 -6
- package/dist/lib/index/batch-processor.js +10 -1
- package/dist/lib/index/syncer.js +15 -4
- package/dist/lib/index/watcher-batch.js +10 -1
- package/dist/lib/llm/config.js +2 -1
- package/dist/lib/llm/investigate.js +40 -6
- package/dist/lib/llm/server.js +9 -2
- package/dist/lib/llm/tools.js +1 -1
- package/dist/lib/search/pagerank.js +1 -1
- package/dist/lib/search/searcher.js +6 -6
- package/dist/lib/store/vector-db.js +21 -10
- package/dist/lib/utils/daemon-client.js +34 -0
- package/dist/lib/utils/filter-builder.js +22 -0
- package/dist/lib/utils/scope-filter.js +3 -5
- package/dist/lib/workers/embeddings/granite.js +4 -1
- package/dist/lib/workers/pool.js +21 -1
- package/package.json +2 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "grepmax",
|
|
4
|
+
"owner": {
|
|
5
|
+
"name": "Robert Owens",
|
|
6
|
+
"email": "robowens@me.com"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "grepmax",
|
|
11
|
+
"source": "./plugins/grepmax",
|
|
12
|
+
"description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
|
|
13
|
+
"version": "0.20.0",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Robert Owens",
|
|
16
|
+
"email": "robowens@me.com"
|
|
17
|
+
},
|
|
18
|
+
"skills": [
|
|
19
|
+
"./skills/grepmax"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ gmax "where do we handle auth?" --agent # Semantic search (compact output)
|
|
|
61
61
|
gmax extract handleAuth # Full function body with line numbers
|
|
62
62
|
gmax peek handleAuth # Signature + callers + callees
|
|
63
63
|
gmax trace handleAuth -d 2 # Call graph (2-hop)
|
|
64
|
-
gmax skeleton src/lib/
|
|
64
|
+
gmax skeleton src/lib/auth.ts # File structure (bodies collapsed)
|
|
65
65
|
gmax symbols auth # List indexed symbols
|
|
66
66
|
```
|
|
67
67
|
|
package/dist/commands/context.js
CHANGED
|
@@ -316,7 +316,7 @@ exports.context = new commander_1.Command("context")
|
|
|
316
316
|
allSymbols.add(s);
|
|
317
317
|
}
|
|
318
318
|
if (allSymbols.size > 0) {
|
|
319
|
-
const pathScope =
|
|
319
|
+
const pathScope = (0, filter_builder_1.pathStartsWith)(`${projectRoot}/`);
|
|
320
320
|
const relatedCounts = new Map();
|
|
321
321
|
const searchedFiles = new Set(uniqueFiles);
|
|
322
322
|
for (const sym of [...allSymbols].slice(0, 20)) {
|
package/dist/commands/mcp.js
CHANGED
|
@@ -773,7 +773,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
773
773
|
"is_exported",
|
|
774
774
|
"defined_symbols",
|
|
775
775
|
])
|
|
776
|
-
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND
|
|
776
|
+
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
|
|
777
777
|
.limit(10)
|
|
778
778
|
.toArray();
|
|
779
779
|
if (rows.length === 0) {
|
|
@@ -867,7 +867,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
867
867
|
const metaRows = yield table
|
|
868
868
|
.query()
|
|
869
869
|
.select(["is_exported", "start_line", "end_line"])
|
|
870
|
-
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND
|
|
870
|
+
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
|
|
871
871
|
.limit(1)
|
|
872
872
|
.toArray();
|
|
873
873
|
const exported = metaRows.length > 0 && Boolean(metaRows[0].is_exported);
|
|
@@ -982,7 +982,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
982
982
|
const defRows = yield table
|
|
983
983
|
.query()
|
|
984
984
|
.select(["path", "start_line", "is_exported"])
|
|
985
|
-
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND
|
|
985
|
+
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
|
|
986
986
|
.limit(1)
|
|
987
987
|
.toArray();
|
|
988
988
|
if (defRows.length === 0) {
|
|
@@ -1037,7 +1037,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1037
1037
|
"referenced_symbols",
|
|
1038
1038
|
"is_exported",
|
|
1039
1039
|
])
|
|
1040
|
-
.where(
|
|
1040
|
+
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
1041
1041
|
.limit(500000)
|
|
1042
1042
|
.toArray();
|
|
1043
1043
|
if (rows.length === 0) {
|
|
@@ -1221,7 +1221,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1221
1221
|
const absPrefix = path.isAbsolute(pathPrefix)
|
|
1222
1222
|
? pathPrefix
|
|
1223
1223
|
: path.resolve(projectRoot, pathPrefix);
|
|
1224
|
-
query = query.where(
|
|
1224
|
+
query = query.where((0, filter_builder_1.pathStartsWith)((0, filter_builder_1.normalizePath)(absPrefix)));
|
|
1225
1225
|
}
|
|
1226
1226
|
const rows = yield query.toArray();
|
|
1227
1227
|
const map = new Map();
|
|
@@ -1388,7 +1388,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1388
1388
|
"defined_symbols",
|
|
1389
1389
|
"referenced_symbols",
|
|
1390
1390
|
])
|
|
1391
|
-
.where(
|
|
1391
|
+
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
1392
1392
|
.limit(200000)
|
|
1393
1393
|
.toArray();
|
|
1394
1394
|
if (rows.length === 0) {
|
|
@@ -1829,7 +1829,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1829
1829
|
"role",
|
|
1830
1830
|
"_distance",
|
|
1831
1831
|
])
|
|
1832
|
-
.where(
|
|
1832
|
+
.where((0, filter_builder_1.pathStartsWith)(`${projectRoot}/`))
|
|
1833
1833
|
.limit(limit + 5)
|
|
1834
1834
|
.toArray();
|
|
1835
1835
|
let filtered = results.filter((r) => !(r.path === source.path && r.start_line === source.start_line));
|
package/dist/commands/plugin.js
CHANGED
|
@@ -43,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.plugin = void 0;
|
|
46
|
+
exports.installAll = installAll;
|
|
46
47
|
const node_child_process_1 = require("node:child_process");
|
|
47
48
|
const fs = __importStar(require("node:fs"));
|
|
48
49
|
const os = __importStar(require("node:os"));
|
|
@@ -139,6 +140,39 @@ function getClients() {
|
|
|
139
140
|
},
|
|
140
141
|
];
|
|
141
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Install gmax plugins for every detected client; returns the number installed.
|
|
145
|
+
* Deliberately does NOT call gracefulExit — callers decide how to finish. This
|
|
146
|
+
* lets `gmax setup` keep running (and print its outro) after installing, while
|
|
147
|
+
* the `add` subcommand still exits the process itself.
|
|
148
|
+
*/
|
|
149
|
+
function installAll() {
|
|
150
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
151
|
+
const clients = getClients();
|
|
152
|
+
console.log("gmax plugin add — detecting clients...\n");
|
|
153
|
+
let installed = 0;
|
|
154
|
+
for (const client of clients) {
|
|
155
|
+
if (!client.detect()) {
|
|
156
|
+
console.log(` skip ${client.name} — not found`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
yield client.install();
|
|
161
|
+
installed++;
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
console.error(` FAIL ${client.name} — ${err instanceof Error ? err.message : String(err)}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (installed === 0) {
|
|
168
|
+
console.log("\nNo supported clients found. Install one of: claude, opencode, codex, droid");
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.log(`\n${installed} plugin(s) installed.`);
|
|
172
|
+
}
|
|
173
|
+
return installed;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
142
176
|
// --- Subcommands ---
|
|
143
177
|
const addCmd = new commander_1.Command("add")
|
|
144
178
|
.description("Install or update gmax plugins")
|
|
@@ -164,27 +198,7 @@ const addCmd = new commander_1.Command("add")
|
|
|
164
198
|
return;
|
|
165
199
|
}
|
|
166
200
|
// Install all detected
|
|
167
|
-
|
|
168
|
-
let installed = 0;
|
|
169
|
-
for (const client of clients) {
|
|
170
|
-
if (!client.detect()) {
|
|
171
|
-
console.log(` skip ${client.name} — not found`);
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
try {
|
|
175
|
-
yield client.install();
|
|
176
|
-
installed++;
|
|
177
|
-
}
|
|
178
|
-
catch (err) {
|
|
179
|
-
console.error(` FAIL ${client.name} — ${err instanceof Error ? err.message : String(err)}`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (installed === 0) {
|
|
183
|
-
console.log("\nNo supported clients found. Install one of: claude, opencode, codex, droid");
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
console.log(`\n${installed} plugin(s) installed.`);
|
|
187
|
-
}
|
|
201
|
+
yield installAll();
|
|
188
202
|
yield (0, exit_1.gracefulExit)();
|
|
189
203
|
}));
|
|
190
204
|
const removeCmd = new commander_1.Command("remove")
|
package/dist/commands/project.js
CHANGED
|
@@ -79,7 +79,7 @@ exports.project = new commander_1.Command("project")
|
|
|
79
79
|
"defined_symbols",
|
|
80
80
|
"referenced_symbols",
|
|
81
81
|
])
|
|
82
|
-
.where(
|
|
82
|
+
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
83
83
|
.limit(200000)
|
|
84
84
|
.toArray();
|
|
85
85
|
if (rows.length === 0) {
|
package/dist/commands/remove.js
CHANGED
|
@@ -133,10 +133,17 @@ Examples:
|
|
|
133
133
|
(0, watcher_store_1.unregisterWatcher)(watcher.pid);
|
|
134
134
|
}
|
|
135
135
|
const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
|
|
136
|
+
// Slash-terminate so removing /repo/app can't also delete the sibling
|
|
137
|
+
// /repo/app2 — VectorDB matches `path LIKE prefix%` and MetaCache
|
|
138
|
+
// matches `key.startsWith(prefix)`, both of which bleed across siblings
|
|
139
|
+
// without the trailing slash. Mirrors the daemon remove path.
|
|
140
|
+
const rootPrefix = projectRoot.endsWith("/")
|
|
141
|
+
? projectRoot
|
|
142
|
+
: `${projectRoot}/`;
|
|
136
143
|
vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
|
|
137
|
-
yield vectorDb.deletePathsWithPrefix(
|
|
144
|
+
yield vectorDb.deletePathsWithPrefix(rootPrefix);
|
|
138
145
|
metaCache = new meta_cache_1.MetaCache(paths.lmdbPath);
|
|
139
|
-
const keys = yield metaCache.getKeysWithPrefix(
|
|
146
|
+
const keys = yield metaCache.getKeysWithPrefix(rootPrefix);
|
|
140
147
|
for (const key of keys) {
|
|
141
148
|
metaCache.delete(key);
|
|
142
149
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -154,8 +154,10 @@ exports.setup = new commander_1.Command("setup")
|
|
|
154
154
|
initialValue: true,
|
|
155
155
|
});
|
|
156
156
|
if (!p.isCancel(installPlugins) && installPlugins) {
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
// Call installAll() directly, not `plugin` (bare status) or the `add`
|
|
158
|
+
// subcommand — both gracefulExit() before setup's outro can run.
|
|
159
|
+
const { installAll } = yield Promise.resolve().then(() => __importStar(require("./plugin")));
|
|
160
|
+
yield installAll();
|
|
159
161
|
}
|
|
160
162
|
p.outro(`Ready — ${selectedTier.label}, ${embedMode === "gpu" ? "GPU" : "CPU"} mode`);
|
|
161
163
|
yield (0, exit_1.gracefulExit)();
|
package/dist/commands/status.js
CHANGED
|
@@ -121,7 +121,7 @@ Examples:
|
|
|
121
121
|
const rows = yield table
|
|
122
122
|
.query()
|
|
123
123
|
.select(["id"])
|
|
124
|
-
.where(
|
|
124
|
+
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
125
125
|
.toArray();
|
|
126
126
|
chunkCounts.set(project.root, rows.length);
|
|
127
127
|
}
|
package/dist/commands/symbols.js
CHANGED
|
@@ -86,7 +86,7 @@ function collectSymbols(options) {
|
|
|
86
86
|
const absPrefix = path.isAbsolute(options.pathPrefix)
|
|
87
87
|
? options.pathPrefix
|
|
88
88
|
: path.resolve(options.projectRoot, options.pathPrefix);
|
|
89
|
-
query = query.where(
|
|
89
|
+
query = query.where((0, filter_builder_1.pathStartsWith)((0, filter_builder_1.normalizePath)(absPrefix)));
|
|
90
90
|
}
|
|
91
91
|
const rows = yield query.toArray();
|
|
92
92
|
const map = new Map();
|
package/dist/commands/watch.js
CHANGED
|
@@ -81,7 +81,7 @@ exports.watch = new commander_1.Command("watch")
|
|
|
81
81
|
// Skip spawn if daemon already running at the same version.
|
|
82
82
|
// If version mismatches (e.g. after npm install -g), shut down the old
|
|
83
83
|
// daemon so we can start a fresh one with the new code.
|
|
84
|
-
const { isDaemonRunning, isDaemonHeartbeatFresh, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
|
|
84
|
+
const { isDaemonRunning, isDaemonHeartbeatFresh, sendDaemonCommand, readDaemonPid, waitForProcessExit, } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
|
|
85
85
|
if (yield isDaemonRunning()) {
|
|
86
86
|
const cliVersion = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf-8")).version;
|
|
87
87
|
const resp = yield sendDaemonCommand({ cmd: "ping" });
|
|
@@ -89,6 +89,12 @@ exports.watch = new commander_1.Command("watch")
|
|
|
89
89
|
process.exit(0);
|
|
90
90
|
}
|
|
91
91
|
console.log(`Daemon version mismatch (${resp.version} → ${cliVersion}), restarting...`);
|
|
92
|
+
// Capture the old PID before shutdown removes the PID file, so we
|
|
93
|
+
// can wait for the process to fully EXIT rather than guessing with a
|
|
94
|
+
// fixed sleep. Spawning the successor while the old daemon is still
|
|
95
|
+
// draining lets the successor's killStaleProcesses() classify it as
|
|
96
|
+
// stale and SIGKILL it mid-cleanup.
|
|
97
|
+
const oldPid = readDaemonPid();
|
|
92
98
|
yield sendDaemonCommand({
|
|
93
99
|
cmd: "shutdown",
|
|
94
100
|
reason: "version-mismatch",
|
|
@@ -97,8 +103,15 @@ exports.watch = new commander_1.Command("watch")
|
|
|
97
103
|
from_version: cliVersion,
|
|
98
104
|
from_argv: process.argv.slice(0, 4),
|
|
99
105
|
});
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
if (oldPid) {
|
|
107
|
+
const exited = yield waitForProcessExit(oldPid, 20000);
|
|
108
|
+
if (!exited) {
|
|
109
|
+
console.log(`Old daemon (PID ${oldPid}) still draining after 20s — starting anyway`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
yield new Promise((r) => setTimeout(r, 2000));
|
|
114
|
+
}
|
|
102
115
|
}
|
|
103
116
|
else if (isDaemonHeartbeatFresh()) {
|
|
104
117
|
// Ping failed but daemon.lock mtime is fresh — another daemon is
|
|
@@ -131,6 +144,13 @@ exports.watch = new commander_1.Command("watch")
|
|
|
131
144
|
process.exit(0);
|
|
132
145
|
}
|
|
133
146
|
console.error("[daemon] Failed to start:", err);
|
|
147
|
+
// Tear down any half-opened state — the listening socket, PID, and
|
|
148
|
+
// lock — so we don't leave a zombie that answers pings but has no
|
|
149
|
+
// resources. The socket also keeps the event loop alive otherwise.
|
|
150
|
+
try {
|
|
151
|
+
yield daemon.shutdown();
|
|
152
|
+
}
|
|
153
|
+
catch (_c) { }
|
|
134
154
|
process.exitCode = 1;
|
|
135
155
|
return;
|
|
136
156
|
}
|
|
@@ -201,7 +221,7 @@ exports.watch = new commander_1.Command("watch")
|
|
|
201
221
|
const indexed = yield table
|
|
202
222
|
.query()
|
|
203
223
|
.select(["id"])
|
|
204
|
-
.where(
|
|
224
|
+
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
205
225
|
.limit(1)
|
|
206
226
|
.toArray();
|
|
207
227
|
if (indexed.length === 0) {
|
|
@@ -116,6 +116,10 @@ class Daemon {
|
|
|
116
116
|
this.heartbeatTick = 0;
|
|
117
117
|
this.shuttingDown = false;
|
|
118
118
|
this.recycling = false;
|
|
119
|
+
// False until LanceDB + MetaCache are open. The socket starts listening early
|
|
120
|
+
// (so liveness probes succeed during slow init), so commands that need those
|
|
121
|
+
// resources must be gated on this to avoid hitting null stores mid-startup.
|
|
122
|
+
this.ready = false;
|
|
119
123
|
this.processManager = new process_manager_1.ProcessManager({
|
|
120
124
|
getShuttingDown: () => this.shuttingDown,
|
|
121
125
|
});
|
|
@@ -253,6 +257,8 @@ class Daemon {
|
|
|
253
257
|
this.vectorDb.startMaintenanceLoop();
|
|
254
258
|
console.log("[daemon] Opening MetaCache:", config_1.PATHS.lmdbPath);
|
|
255
259
|
this.metaCache = new meta_cache_1.MetaCache(config_1.PATHS.lmdbPath);
|
|
260
|
+
// Resources are open — only now may resource-dependent IPC commands run.
|
|
261
|
+
this.ready = true;
|
|
256
262
|
}
|
|
257
263
|
catch (err) {
|
|
258
264
|
console.error("[daemon] Failed to open shared resources:", err);
|
|
@@ -499,6 +505,10 @@ class Daemon {
|
|
|
499
505
|
uptime() {
|
|
500
506
|
return Math.floor((Date.now() - this.startTime) / 1000);
|
|
501
507
|
}
|
|
508
|
+
/** True once shared resources (LanceDB + MetaCache) are open. */
|
|
509
|
+
isReady() {
|
|
510
|
+
return this.ready;
|
|
511
|
+
}
|
|
502
512
|
getDiskPressure() {
|
|
503
513
|
var _a, _b;
|
|
504
514
|
return (_b = (_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.diskPressure) !== null && _b !== void 0 ? _b : "unknown";
|
|
@@ -105,6 +105,12 @@ function handleCommand(daemon, cmd, conn) {
|
|
|
105
105
|
var _a, _b, _c, _d, _e;
|
|
106
106
|
try {
|
|
107
107
|
(0, logger_1.debug)("daemon", `ipc cmd=${cmd.cmd}${cmd.root ? ` root=${cmd.root}` : ""}`);
|
|
108
|
+
// The socket listens before LanceDB/MetaCache are open so liveness probes
|
|
109
|
+
// succeed during slow startup. Gate resource-dependent commands until those
|
|
110
|
+
// stores exist; ping/shutdown must always work (probes + restart).
|
|
111
|
+
if (cmd.cmd !== "ping" && cmd.cmd !== "shutdown" && !daemon.isReady()) {
|
|
112
|
+
return { ok: false, error: "daemon initializing" };
|
|
113
|
+
}
|
|
108
114
|
switch (cmd.cmd) {
|
|
109
115
|
case "ping":
|
|
110
116
|
return {
|
|
@@ -368,12 +368,15 @@ class WatcherManager {
|
|
|
368
368
|
const bn = path.basename(absPath).toLowerCase();
|
|
369
369
|
if (!INDEXABLE_EXTENSIONS.has(ext) && !INDEXABLE_EXTENSIONS.has(bn))
|
|
370
370
|
continue;
|
|
371
|
-
seenPaths.add(absPath);
|
|
372
371
|
try {
|
|
373
372
|
const stats = yield fs.promises.stat(absPath);
|
|
374
|
-
// Skip files that are too large or empty — they'll never be indexed
|
|
373
|
+
// Skip files that are too large or empty — they'll never be indexed.
|
|
374
|
+
// Leave them OUT of seenPaths so a file that became empty/oversized is
|
|
375
|
+
// treated as deleted by the purge sweep below and its now-stale chunks
|
|
376
|
+
// are removed, rather than lingering as unsearchable-but-present rows.
|
|
375
377
|
if (stats.size === 0 || stats.size > MAX_FILE_SIZE_BYTES)
|
|
376
378
|
continue;
|
|
379
|
+
seenPaths.add(absPath);
|
|
377
380
|
const cached = metaCache.get(absPath);
|
|
378
381
|
if (!isFileCached(cached, stats)) {
|
|
379
382
|
// Fast path: if only mtime changed but size is identical and we have a hash,
|
|
@@ -439,6 +442,19 @@ class WatcherManager {
|
|
|
439
442
|
}
|
|
440
443
|
unwatchProject(root) {
|
|
441
444
|
return __awaiter(this, void 0, void 0, function* () {
|
|
445
|
+
// Stop poll-mode timers + their FSEvents recovery probe first, so a removed
|
|
446
|
+
// project can't keep scanning until full daemon shutdown. These live
|
|
447
|
+
// independently of the processor, so clear them even on the early return.
|
|
448
|
+
const pollInterval = this.pollIntervals.get(root);
|
|
449
|
+
if (pollInterval) {
|
|
450
|
+
clearInterval(pollInterval);
|
|
451
|
+
this.pollIntervals.delete(root);
|
|
452
|
+
}
|
|
453
|
+
const recoveryTimer = this.pollRecoveryTimers.get(root);
|
|
454
|
+
if (recoveryTimer) {
|
|
455
|
+
clearInterval(recoveryTimer);
|
|
456
|
+
this.pollRecoveryTimers.delete(root);
|
|
457
|
+
}
|
|
442
458
|
const processor = this.deps.processors.get(root);
|
|
443
459
|
if (!processor)
|
|
444
460
|
return;
|
|
@@ -27,10 +27,10 @@ class GraphBuilder {
|
|
|
27
27
|
scopeWhere(condition) {
|
|
28
28
|
let result = condition;
|
|
29
29
|
if (this.pathPrefix) {
|
|
30
|
-
result = `${result} AND
|
|
30
|
+
result = `${result} AND ${(0, filter_builder_1.pathStartsWith)(this.pathPrefix)}`;
|
|
31
31
|
}
|
|
32
32
|
for (const ex of this.excludePrefixes) {
|
|
33
|
-
result = `${result} AND
|
|
33
|
+
result = `${result} AND ${(0, filter_builder_1.pathNotStartsWith)(ex)}`;
|
|
34
34
|
}
|
|
35
35
|
return result;
|
|
36
36
|
}
|
package/dist/lib/graph/impact.js
CHANGED
|
@@ -66,10 +66,10 @@ function expandFileSymbols(symbols, vectorDb, projectRoot, excludePrefixes) {
|
|
|
66
66
|
return symbols;
|
|
67
67
|
const table = yield vectorDb.ensureTable();
|
|
68
68
|
const prefix = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
|
|
69
|
-
let where = `array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbols[0])}') AND
|
|
69
|
+
let where = `array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbols[0])}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`;
|
|
70
70
|
for (const ex of excludePrefixes !== null && excludePrefixes !== void 0 ? excludePrefixes : []) {
|
|
71
71
|
const exNorm = ex.endsWith("/") ? ex : `${ex}/`;
|
|
72
|
-
where += ` AND
|
|
72
|
+
where += ` AND ${(0, filter_builder_1.pathNotStartsWith)(exNorm)}`;
|
|
73
73
|
}
|
|
74
74
|
// Find the file that defines this symbol
|
|
75
75
|
const defRows = yield table
|
|
@@ -146,10 +146,10 @@ function findImportFallbackTests(expandedSymbols, originalSymbols, vectorDb, pro
|
|
|
146
146
|
// test body's referenced_symbols ends up empty).
|
|
147
147
|
const table = yield vectorDb.ensureTable();
|
|
148
148
|
const prefix = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
|
|
149
|
-
let pathScope =
|
|
149
|
+
let pathScope = (0, filter_builder_1.pathStartsWith)(prefix);
|
|
150
150
|
for (const ex of excludePrefixes !== null && excludePrefixes !== void 0 ? excludePrefixes : []) {
|
|
151
151
|
const exNorm = ex.endsWith("/") ? ex : `${ex}/`;
|
|
152
|
-
pathScope += ` AND
|
|
152
|
+
pathScope += ` AND ${(0, filter_builder_1.pathNotStartsWith)(exNorm)}`;
|
|
153
153
|
}
|
|
154
154
|
// Textual matching runs on the ORIGINAL targets only: matching co-defined
|
|
155
155
|
// file symbols (helpers like `log`) textually drags in every test file
|
|
@@ -203,10 +203,10 @@ function walkCallers(symbol, graphBuilder, testHits, currentHop, maxDepth, visit
|
|
|
203
203
|
function findDependents(symbols_1, vectorDb_1, projectRoot_1, excludePaths_1) {
|
|
204
204
|
return __awaiter(this, arguments, void 0, function* (symbols, vectorDb, projectRoot, excludePaths, limit = 10, excludePrefixes) {
|
|
205
205
|
const table = yield vectorDb.ensureTable();
|
|
206
|
-
let pathScope =
|
|
206
|
+
let pathScope = (0, filter_builder_1.pathStartsWith)(`${projectRoot}/`);
|
|
207
207
|
for (const ex of excludePrefixes !== null && excludePrefixes !== void 0 ? excludePrefixes : []) {
|
|
208
208
|
const exNorm = ex.endsWith("/") ? ex : `${ex}/`;
|
|
209
|
-
pathScope += ` AND
|
|
209
|
+
pathScope += ` AND ${(0, filter_builder_1.pathNotStartsWith)(exNorm)}`;
|
|
210
210
|
}
|
|
211
211
|
const counts = new Map();
|
|
212
212
|
for (const sym of symbols) {
|
|
@@ -177,8 +177,17 @@ class ProjectBatchProcessor {
|
|
|
177
177
|
// change or add
|
|
178
178
|
try {
|
|
179
179
|
const stats = yield fs.promises.stat(absPath);
|
|
180
|
-
if (!(0, file_utils_1.isIndexableFile)(absPath, stats.size))
|
|
180
|
+
if (!(0, file_utils_1.isIndexableFile)(absPath, stats.size)) {
|
|
181
|
+
// File became non-indexable (emptied or now too large). If we
|
|
182
|
+
// indexed it before, drop its chunks + meta so search stops
|
|
183
|
+
// returning stale content; otherwise there's nothing to clean up.
|
|
184
|
+
if (this.metaCache.get(absPath)) {
|
|
185
|
+
deletes.push(absPath);
|
|
186
|
+
metaDeletes.push(absPath);
|
|
187
|
+
reindexed++;
|
|
188
|
+
}
|
|
181
189
|
continue;
|
|
190
|
+
}
|
|
182
191
|
const cached = this.metaCache.get(absPath);
|
|
183
192
|
if ((0, cache_check_1.isFileCached)(cached, stats)) {
|
|
184
193
|
continue;
|
package/dist/lib/index/syncer.js
CHANGED
|
@@ -74,13 +74,24 @@ function flushBatch(db, meta, vectors, pendingMeta, pendingDeletes, dryRun) {
|
|
|
74
74
|
return __awaiter(this, void 0, void 0, function* () {
|
|
75
75
|
if (dryRun)
|
|
76
76
|
return;
|
|
77
|
-
// 1.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
// 1. Insert the new vectors FIRST, then delete the old chunks for those paths
|
|
78
|
+
// (excluding the just-inserted ids). Deleting first would leave a file
|
|
79
|
+
// unsearchable if the insert then fails — the old, still-valid chunks would
|
|
80
|
+
// already be gone. Mirrors batch-processor's insert-first flush. Paths in
|
|
81
|
+
// pendingDeletes with no new vectors (emptied / non-indexable files) match
|
|
82
|
+
// no excluded id, so all their old chunks are removed.
|
|
83
|
+
const newIds = vectors.map((v) => v.id);
|
|
81
84
|
if (vectors.length > 0) {
|
|
82
85
|
yield db.insertBatch(vectors);
|
|
83
86
|
}
|
|
87
|
+
if (pendingDeletes.length > 0) {
|
|
88
|
+
if (newIds.length > 0) {
|
|
89
|
+
yield db.deletePathsExcludingIds(pendingDeletes, newIds);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
yield db.deletePaths(pendingDeletes);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
84
95
|
// 2. Update MetaCache only after VectorDB write succeeds
|
|
85
96
|
for (const [p, entry] of pendingMeta.entries()) {
|
|
86
97
|
meta.put(p, entry);
|
|
@@ -67,8 +67,17 @@ function processBatchCore(batch, metaCache, pool, signal) {
|
|
|
67
67
|
}
|
|
68
68
|
try {
|
|
69
69
|
const stats = yield fs.promises.stat(absPath);
|
|
70
|
-
if (!(0, file_utils_1.isIndexableFile)(absPath, stats.size))
|
|
70
|
+
if (!(0, file_utils_1.isIndexableFile)(absPath, stats.size)) {
|
|
71
|
+
// File became non-indexable (emptied or now too large). If we indexed
|
|
72
|
+
// it before, drop its chunks + meta so search stops returning stale
|
|
73
|
+
// content; otherwise there's nothing to clean up.
|
|
74
|
+
if (metaCache.get(absPath)) {
|
|
75
|
+
deletes.push(absPath);
|
|
76
|
+
metaDeletes.push(absPath);
|
|
77
|
+
reindexed++;
|
|
78
|
+
}
|
|
71
79
|
continue;
|
|
80
|
+
}
|
|
72
81
|
const cached = metaCache.get(absPath);
|
|
73
82
|
if ((0, cache_check_1.isFileCached)(cached, stats)) {
|
|
74
83
|
continue;
|
package/dist/lib/llm/config.js
CHANGED
|
@@ -10,7 +10,7 @@ function envInt(key, fallback) {
|
|
|
10
10
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
11
11
|
}
|
|
12
12
|
function getLlmConfig() {
|
|
13
|
-
var _a, _b, _c;
|
|
13
|
+
var _a, _b, _c, _d;
|
|
14
14
|
return {
|
|
15
15
|
model: (_a = process.env.GMAX_LLM_MODEL) !== null && _a !== void 0 ? _a : DEFAULT_MODEL,
|
|
16
16
|
binary: (_b = process.env.GMAX_LLM_BINARY) !== null && _b !== void 0 ? _b : "llama-server",
|
|
@@ -21,5 +21,6 @@ function getLlmConfig() {
|
|
|
21
21
|
maxTokens: envInt("GMAX_LLM_MAX_TOKENS", 8192),
|
|
22
22
|
idleTimeoutMin: envInt("GMAX_LLM_IDLE_TIMEOUT", 30),
|
|
23
23
|
startupWaitSec: envInt("GMAX_LLM_STARTUP_WAIT", 60),
|
|
24
|
+
reasoningFormat: (_d = process.env.GMAX_LLM_REASONING_FORMAT) !== null && _d !== void 0 ? _d : "deepseek",
|
|
24
25
|
};
|
|
25
26
|
}
|
|
@@ -45,6 +45,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
45
45
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
46
|
};
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.looksLikeRawToolCall = looksLikeRawToolCall;
|
|
49
|
+
exports.toolCallLeakHint = toolCallLeakHint;
|
|
50
|
+
exports.finalizeAnswer = finalizeAnswer;
|
|
48
51
|
exports.investigate = investigate;
|
|
49
52
|
const path = __importStar(require("node:path"));
|
|
50
53
|
const openai_1 = __importDefault(require("openai"));
|
|
@@ -58,6 +61,37 @@ const tools_1 = require("./tools");
|
|
|
58
61
|
function stripThinkTags(text) {
|
|
59
62
|
return text.replace(/<think(?:ing)?>[\s\S]*?<\/think(?:ing)?>/g, "").trim();
|
|
60
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Detect raw model tool-call markup leaking into `message.content`.
|
|
66
|
+
*
|
|
67
|
+
* Some local models (notably Qwen3.5-35B-A3B, which emits Qwen-XML tool calls)
|
|
68
|
+
* trip a known llama.cpp `peg-native` parser bug: the call is never extracted
|
|
69
|
+
* into structured `tool_calls` and instead leaks into `content` as raw markup
|
|
70
|
+
* like `<tool_call>…`, `<function=peek>…`, or `<parameter=…>`. Returning that
|
|
71
|
+
* verbatim as the "answer" is what this guards against.
|
|
72
|
+
*/
|
|
73
|
+
function looksLikeRawToolCall(text) {
|
|
74
|
+
return /<tool_call\b|<\/?function[=\s>]|<parameter[=\s>]/i.test(text);
|
|
75
|
+
}
|
|
76
|
+
/** Guidance shown when a non-tool-calling model leaks raw tool-call markup. */
|
|
77
|
+
function toolCallLeakHint(modelName) {
|
|
78
|
+
return (`The investigate tool needs a model that emits structured tool calls, but ` +
|
|
79
|
+
`"${modelName}" returned raw tool-call markup instead. This is a known ` +
|
|
80
|
+
`llama.cpp limitation for Qwen-XML tool-calling models — switch GMAX_LLM_MODEL ` +
|
|
81
|
+
`to a Hermes-JSON tool-calling model (e.g. Qwen3-30B-A3B-Instruct-2507), or ` +
|
|
82
|
+
`use review_commit / the gmax CLI search tools for this question.`);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Turn a model message's content into the final answer: detect a tool-call leak
|
|
86
|
+
* (return a clear hint), otherwise strip reasoning tags.
|
|
87
|
+
*/
|
|
88
|
+
function finalizeAnswer(content, modelName) {
|
|
89
|
+
if (content && looksLikeRawToolCall(content)) {
|
|
90
|
+
return toolCallLeakHint(modelName);
|
|
91
|
+
}
|
|
92
|
+
const stripped = content ? stripThinkTags(content) : "";
|
|
93
|
+
return stripped || "(no response)";
|
|
94
|
+
}
|
|
61
95
|
function investigate(opts) {
|
|
62
96
|
return __awaiter(this, void 0, void 0, function* () {
|
|
63
97
|
var _a, _b, _c;
|
|
@@ -116,9 +150,7 @@ function investigate(opts) {
|
|
|
116
150
|
if (verbose) {
|
|
117
151
|
process.stderr.write(`[R${round}] Final answer (${roundMs}ms)\n`);
|
|
118
152
|
}
|
|
119
|
-
const answer = message.content
|
|
120
|
-
? stripThinkTags(message.content)
|
|
121
|
-
: "(no response)";
|
|
153
|
+
const answer = finalizeAnswer(message.content, modelName);
|
|
122
154
|
return {
|
|
123
155
|
answer,
|
|
124
156
|
rounds,
|
|
@@ -194,6 +226,10 @@ function investigate(opts) {
|
|
|
194
226
|
process.stderr.write(`[R${maxRounds}] Round cap reached, forcing final answer\n`);
|
|
195
227
|
}
|
|
196
228
|
messages.push({ role: "user", content: prompts_1.FORCE_FINAL_MESSAGE });
|
|
229
|
+
// No tools on the synthesis call. Note: tool_choice:"none" does NOT stop this
|
|
230
|
+
// model from emitting a stray tool call here (verified 2026-06-28 — it still
|
|
231
|
+
// leaks raw markup into content), so the finalizeAnswer guard is what protects
|
|
232
|
+
// this path rather than a request-shape tweak.
|
|
197
233
|
const response = yield client.chat.completions.create({
|
|
198
234
|
model: modelName,
|
|
199
235
|
messages,
|
|
@@ -202,9 +238,7 @@ function investigate(opts) {
|
|
|
202
238
|
if (!((_c = response.choices) === null || _c === void 0 ? void 0 : _c.length)) {
|
|
203
239
|
throw new Error("LLM returned empty response");
|
|
204
240
|
}
|
|
205
|
-
const answer = response.choices[0].message.content
|
|
206
|
-
? stripThinkTags(response.choices[0].message.content)
|
|
207
|
-
: "(no response)";
|
|
241
|
+
const answer = finalizeAnswer(response.choices[0].message.content, modelName);
|
|
208
242
|
return {
|
|
209
243
|
answer,
|
|
210
244
|
rounds: rounds + 1,
|
package/dist/lib/llm/server.js
CHANGED
|
@@ -143,7 +143,7 @@ class LlmServer {
|
|
|
143
143
|
throw new Error(`Model file not found: "${this.config.model}". Set GMAX_LLM_MODEL to a valid .gguf path`);
|
|
144
144
|
}
|
|
145
145
|
const logFd = (0, log_rotate_1.openRotatedLog)(config_1.PATHS.llmLogFile);
|
|
146
|
-
const
|
|
146
|
+
const args = [
|
|
147
147
|
"-m",
|
|
148
148
|
this.config.model,
|
|
149
149
|
"--host",
|
|
@@ -154,7 +154,14 @@ class LlmServer {
|
|
|
154
154
|
String(this.config.ngl),
|
|
155
155
|
"--ctx-size",
|
|
156
156
|
String(this.config.ctxSize),
|
|
157
|
-
]
|
|
157
|
+
];
|
|
158
|
+
if (this.config.reasoningFormat) {
|
|
159
|
+
args.push("--reasoning-format", this.config.reasoningFormat);
|
|
160
|
+
}
|
|
161
|
+
const child = (0, node_child_process_1.spawn)(binary, args, {
|
|
162
|
+
detached: true,
|
|
163
|
+
stdio: ["ignore", logFd, logFd],
|
|
164
|
+
});
|
|
158
165
|
child.unref();
|
|
159
166
|
fs.closeSync(logFd);
|
|
160
167
|
const pid = child.pid;
|
package/dist/lib/llm/tools.js
CHANGED
|
@@ -244,7 +244,7 @@ function executePeek(args, ctx) {
|
|
|
244
244
|
const metaRows = yield table
|
|
245
245
|
.query()
|
|
246
246
|
.select(["is_exported", "start_line", "end_line"])
|
|
247
|
-
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND
|
|
247
|
+
.where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
|
|
248
248
|
.limit(1)
|
|
249
249
|
.toArray();
|
|
250
250
|
const exported = metaRows.length > 0 && Boolean(metaRows[0].is_exported);
|
|
@@ -147,7 +147,7 @@ function buildGraphFromDb(db, pathPrefix) {
|
|
|
147
147
|
const rows = yield table
|
|
148
148
|
.query()
|
|
149
149
|
.select(["defined_symbols", "referenced_symbols"])
|
|
150
|
-
.where(
|
|
150
|
+
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
151
151
|
.toArray();
|
|
152
152
|
const nodes = new Set();
|
|
153
153
|
const edges = new Map();
|
|
@@ -51,7 +51,7 @@ function buildWhereClause(pathPrefix, filters, searchIntent) {
|
|
|
51
51
|
var _a;
|
|
52
52
|
const parts = [];
|
|
53
53
|
if (pathPrefix) {
|
|
54
|
-
parts.push(
|
|
54
|
+
parts.push((0, filter_builder_1.pathStartsWith)((0, filter_builder_1.normalizePath)(pathPrefix)));
|
|
55
55
|
}
|
|
56
56
|
const fileFilter = filters === null || filters === void 0 ? void 0 : filters.file;
|
|
57
57
|
if (typeof fileFilter === "string" && fileFilter) {
|
|
@@ -62,7 +62,7 @@ function buildWhereClause(pathPrefix, filters, searchIntent) {
|
|
|
62
62
|
const absExclude = pathPrefix
|
|
63
63
|
? (0, filter_builder_1.normalizePath)(pathPrefix + excludeFilter)
|
|
64
64
|
: excludeFilter;
|
|
65
|
-
parts.push(
|
|
65
|
+
parts.push((0, filter_builder_1.pathNotStartsWith)(absExclude));
|
|
66
66
|
}
|
|
67
67
|
// New array-shape: pre-resolved absolute exclude prefixes from
|
|
68
68
|
// resolveScope (or daemon IPC). Each becomes its own NOT LIKE clause.
|
|
@@ -71,7 +71,7 @@ function buildWhereClause(pathPrefix, filters, searchIntent) {
|
|
|
71
71
|
for (const p of excludePrefixes) {
|
|
72
72
|
if (typeof p === "string" && p) {
|
|
73
73
|
const norm = (0, filter_builder_1.normalizePath)(p);
|
|
74
|
-
parts.push(
|
|
74
|
+
parts.push((0, filter_builder_1.pathNotStartsWith)(norm));
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -84,7 +84,7 @@ function buildWhereClause(pathPrefix, filters, searchIntent) {
|
|
|
84
84
|
for (const p of inPrefixes) {
|
|
85
85
|
if (typeof p === "string" && p) {
|
|
86
86
|
const norm = (0, filter_builder_1.normalizePath)(p);
|
|
87
|
-
clauses.push(
|
|
87
|
+
clauses.push((0, filter_builder_1.pathStartsWith)(norm));
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
if (clauses.length === 1)
|
|
@@ -106,7 +106,7 @@ function buildWhereClause(pathPrefix, filters, searchIntent) {
|
|
|
106
106
|
const roots = projectRoots.split(",");
|
|
107
107
|
const clauses = roots.map((r) => {
|
|
108
108
|
const prefix = r.endsWith("/") ? r : `${r}/`;
|
|
109
|
-
return
|
|
109
|
+
return (0, filter_builder_1.pathStartsWith)(prefix);
|
|
110
110
|
});
|
|
111
111
|
parts.push(`(${clauses.join(" OR ")})`);
|
|
112
112
|
}
|
|
@@ -114,7 +114,7 @@ function buildWhereClause(pathPrefix, filters, searchIntent) {
|
|
|
114
114
|
if (typeof excludeRoots === "string" && excludeRoots) {
|
|
115
115
|
for (const r of excludeRoots.split(",")) {
|
|
116
116
|
const prefix = r.endsWith("/") ? r : `${r}/`;
|
|
117
|
-
parts.push(
|
|
117
|
+
parts.push((0, filter_builder_1.pathNotStartsWith)(prefix));
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
const defFilter = filters === null || filters === void 0 ? void 0 : filters.def;
|
|
@@ -48,6 +48,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
48
48
|
const lancedb = __importStar(require("@lancedb/lancedb"));
|
|
49
49
|
const apache_arrow_1 = require("apache-arrow");
|
|
50
50
|
const config_1 = require("../../config");
|
|
51
|
+
const index_config_1 = require("../index/index-config");
|
|
51
52
|
const cleanup_1 = require("../utils/cleanup");
|
|
52
53
|
const filter_builder_1 = require("../utils/filter-builder");
|
|
53
54
|
const logger_1 = require("../utils/logger");
|
|
@@ -86,7 +87,10 @@ class VectorDB {
|
|
|
86
87
|
this.activeWrites = 0;
|
|
87
88
|
this.writeDrainResolve = null;
|
|
88
89
|
this.compactingPromise = null;
|
|
89
|
-
|
|
90
|
+
// Default to the configured tier's dim (not the hard-wired small-tier 384)
|
|
91
|
+
// so a `standard`-tier index actually stores 768d vectors. An explicit
|
|
92
|
+
// arg still wins (eval scripts, tests).
|
|
93
|
+
this.vectorDim = vectorDim !== null && vectorDim !== void 0 ? vectorDim : (0, index_config_1.readGlobalConfig)().vectorDim;
|
|
90
94
|
this.unregisterCleanup = (0, cleanup_1.registerCleanup)(() => this.close());
|
|
91
95
|
}
|
|
92
96
|
/**
|
|
@@ -411,11 +415,13 @@ class VectorDB {
|
|
|
411
415
|
// Callers (syncer flushBatch) splice records before passing — they're never reused.
|
|
412
416
|
for (const rec of records) {
|
|
413
417
|
const vec = toNumberArray(rec.vector);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
418
|
+
// Never silently pad/truncate: a width mismatch means the embedding tier
|
|
419
|
+
// and this table disagree (wrong model wired, or a stale index after a
|
|
420
|
+
// tier change). Reshaping would store garbage that scores meaninglessly.
|
|
421
|
+
// Fail loudly and point at the fix instead.
|
|
422
|
+
if (vec.length !== this.vectorDim) {
|
|
423
|
+
throw new Error(`Vector dimension mismatch: got ${vec.length}d, expected ${this.vectorDim}d. ` +
|
|
424
|
+
"The embedding model tier likely changed without a rebuild — run `gmax index --reset`.");
|
|
419
425
|
}
|
|
420
426
|
rec.vector = vec;
|
|
421
427
|
rec.colbert = toBuffer(rec.colbert);
|
|
@@ -676,7 +682,7 @@ class VectorDB {
|
|
|
676
682
|
const rows = yield table
|
|
677
683
|
.query()
|
|
678
684
|
.select(["id"])
|
|
679
|
-
.where(
|
|
685
|
+
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
680
686
|
.limit(1)
|
|
681
687
|
.toArray();
|
|
682
688
|
return rows.length > 0;
|
|
@@ -686,7 +692,7 @@ class VectorDB {
|
|
|
686
692
|
return __awaiter(this, void 0, void 0, function* () {
|
|
687
693
|
const table = yield this.ensureTable();
|
|
688
694
|
const prefix = pathPrefix.endsWith("/") ? pathPrefix : `${pathPrefix}/`;
|
|
689
|
-
return table.countRows(
|
|
695
|
+
return table.countRows((0, filter_builder_1.pathStartsWith)(prefix));
|
|
690
696
|
});
|
|
691
697
|
}
|
|
692
698
|
countDistinctFilesForPath(pathPrefix) {
|
|
@@ -701,7 +707,7 @@ class VectorDB {
|
|
|
701
707
|
const rows = yield table
|
|
702
708
|
.query()
|
|
703
709
|
.select(["path"])
|
|
704
|
-
.where(
|
|
710
|
+
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
705
711
|
.toArray();
|
|
706
712
|
const unique = new Set();
|
|
707
713
|
for (const r of rows) {
|
|
@@ -804,7 +810,12 @@ class VectorDB {
|
|
|
804
810
|
return __awaiter(this, void 0, void 0, function* () {
|
|
805
811
|
this.ensureDiskOk();
|
|
806
812
|
const table = yield this.ensureTable();
|
|
807
|
-
|
|
813
|
+
// Slash-terminate so a project root can't bleed into a sibling
|
|
814
|
+
// (`/repo/app` must not delete `/repo/app2`), and use starts_with so `_`/`%`
|
|
815
|
+
// in the path are literal, not LIKE wildcards. Destructive path — keep this
|
|
816
|
+
// self-protective even if a caller forgets to normalize.
|
|
817
|
+
const dirPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
818
|
+
yield this.withWriteGate(() => table.delete((0, filter_builder_1.pathStartsWith)(dirPrefix)));
|
|
808
819
|
});
|
|
809
820
|
}
|
|
810
821
|
drop() {
|
|
@@ -45,6 +45,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
45
45
|
exports.sendDaemonCommand = sendDaemonCommand;
|
|
46
46
|
exports.isDaemonRunning = isDaemonRunning;
|
|
47
47
|
exports.isDaemonHeartbeatFresh = isDaemonHeartbeatFresh;
|
|
48
|
+
exports.readDaemonPid = readDaemonPid;
|
|
49
|
+
exports.waitForProcessExit = waitForProcessExit;
|
|
48
50
|
exports.ensureDaemonRunning = ensureDaemonRunning;
|
|
49
51
|
exports.sendStreamingCommand = sendStreamingCommand;
|
|
50
52
|
const fs = __importStar(require("node:fs"));
|
|
@@ -152,6 +154,38 @@ function isDaemonHeartbeatFresh() {
|
|
|
152
154
|
return false;
|
|
153
155
|
}
|
|
154
156
|
}
|
|
157
|
+
/** Read the daemon's PID from the PID file, or null if absent/invalid. */
|
|
158
|
+
function readDaemonPid() {
|
|
159
|
+
try {
|
|
160
|
+
const pid = parseInt(fs.readFileSync(config_1.PATHS.daemonPidFile, "utf-8").trim(), 10);
|
|
161
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
162
|
+
}
|
|
163
|
+
catch (_a) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Poll until a process has fully exited, or the timeout elapses. Used before
|
|
169
|
+
* spawning a successor daemon: shutdown() drops the socket/lock immediately but
|
|
170
|
+
* keeps draining in-flight work, and a successor that starts in that window
|
|
171
|
+
* classifies the still-draining predecessor as stale and SIGKILLs it. Waiting
|
|
172
|
+
* for the actual process exit closes that race. Returns true if it exited.
|
|
173
|
+
*/
|
|
174
|
+
function waitForProcessExit(pid, timeoutMs) {
|
|
175
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
176
|
+
const deadline = Date.now() + timeoutMs;
|
|
177
|
+
while (Date.now() < deadline) {
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pid, 0);
|
|
180
|
+
}
|
|
181
|
+
catch (_a) {
|
|
182
|
+
return true; // ESRCH — process is gone
|
|
183
|
+
}
|
|
184
|
+
yield new Promise((r) => setTimeout(r, 100));
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
155
189
|
/**
|
|
156
190
|
* Ensure the daemon is running — start it if needed, poll up to 5s.
|
|
157
191
|
* Returns true if daemon is ready, false if it couldn't be started.
|
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.escapeSqlString = escapeSqlString;
|
|
4
|
+
exports.pathStartsWith = pathStartsWith;
|
|
5
|
+
exports.pathNotStartsWith = pathNotStartsWith;
|
|
4
6
|
exports.normalizePath = normalizePath;
|
|
5
7
|
function escapeSqlString(str) {
|
|
6
8
|
// LanceDB (via DataFusion) treats backslashes literally in standard strings.
|
|
7
9
|
// We only need to escape single quotes by doubling them.
|
|
8
10
|
return str.replace(/'/g, "''");
|
|
9
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* SQL predicate matching rows whose `path` begins with `prefix`.
|
|
14
|
+
*
|
|
15
|
+
* Use this instead of `path LIKE '<prefix>%'` for any path-prefix scope.
|
|
16
|
+
* `escapeSqlString` only neutralizes quotes, not LIKE metacharacters, so a
|
|
17
|
+
* prefix containing `_` (matches any single char) or `%` (matches anything)
|
|
18
|
+
* would silently match — and delete — across sibling projects
|
|
19
|
+
* (`/repo/my_app/` would match `/repo/myXapp/`). `starts_with()` has no
|
|
20
|
+
* wildcard semantics, so the prefix is matched literally.
|
|
21
|
+
*
|
|
22
|
+
* Callers are responsible for trailing-slash boundary correctness: pass
|
|
23
|
+
* `/repo/app/` (not `/repo/app`) so the scope can't bleed into `/repo/app2/`.
|
|
24
|
+
*/
|
|
25
|
+
function pathStartsWith(prefix) {
|
|
26
|
+
return `starts_with(path, '${escapeSqlString(prefix)}')`;
|
|
27
|
+
}
|
|
28
|
+
/** Negation of {@link pathStartsWith} — excludes paths under `prefix`. */
|
|
29
|
+
function pathNotStartsWith(prefix) {
|
|
30
|
+
return `NOT ${pathStartsWith(prefix)}`;
|
|
31
|
+
}
|
|
10
32
|
/**
|
|
11
33
|
* Normalizes a path to use forward slashes, ensuring consistency across platforms.
|
|
12
34
|
* @param p The path to normalize
|
|
@@ -92,14 +92,12 @@ function buildScopeWhere(scope, condition) {
|
|
|
92
92
|
const parts = [];
|
|
93
93
|
if (condition)
|
|
94
94
|
parts.push(condition);
|
|
95
|
-
parts.push(
|
|
95
|
+
parts.push((0, filter_builder_1.pathStartsWith)(scope.pathPrefix));
|
|
96
96
|
for (const ex of scope.excludePrefixes) {
|
|
97
|
-
parts.push(
|
|
97
|
+
parts.push((0, filter_builder_1.pathNotStartsWith)(ex));
|
|
98
98
|
}
|
|
99
99
|
if (scope.inPrefixes.length > 0) {
|
|
100
|
-
const ors = scope.inPrefixes
|
|
101
|
-
.map((p) => `path LIKE '${(0, filter_builder_1.escapeSqlString)(p)}%'`)
|
|
102
|
-
.join(" OR ");
|
|
100
|
+
const ors = scope.inPrefixes.map((p) => (0, filter_builder_1.pathStartsWith)(p)).join(" OR ");
|
|
103
101
|
parts.push(`(${ors})`);
|
|
104
102
|
}
|
|
105
103
|
return parts.join(" AND ");
|
|
@@ -63,7 +63,10 @@ class GraniteModel {
|
|
|
63
63
|
this.vectorDimensions = vectorDim !== null && vectorDim !== void 0 ? vectorDim : config_1.CONFIG.VECTOR_DIM;
|
|
64
64
|
}
|
|
65
65
|
resolvePaths() {
|
|
66
|
-
|
|
66
|
+
// Honor the active tier's ONNX model (propagated from the pool); fall back
|
|
67
|
+
// to the small-tier default when unset (standalone/test invocations).
|
|
68
|
+
const modelId = process.env.GMAX_EMBED_ONNX_MODEL || config_1.MODEL_IDS.embed;
|
|
69
|
+
const basePath = path.join(CACHE_DIR, modelId);
|
|
67
70
|
const onnxDir = path.join(basePath, "onnx");
|
|
68
71
|
const candidates = ["model_q4.onnx", "model.onnx"];
|
|
69
72
|
for (const candidate of candidates) {
|
package/dist/lib/workers/pool.js
CHANGED
|
@@ -43,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.WorkerPool = void 0;
|
|
46
|
+
exports.embeddingEnv = embeddingEnv;
|
|
46
47
|
exports.getWorkerPool = getWorkerPool;
|
|
47
48
|
exports.destroyWorkerPool = destroyWorkerPool;
|
|
48
49
|
exports.isWorkerPoolInitialized = isWorkerPoolInitialized;
|
|
@@ -54,6 +55,7 @@ const childProcess = __importStar(require("node:child_process"));
|
|
|
54
55
|
const fs = __importStar(require("node:fs"));
|
|
55
56
|
const path = __importStar(require("node:path"));
|
|
56
57
|
const config_1 = require("../../config");
|
|
58
|
+
const index_config_1 = require("../index/index-config");
|
|
57
59
|
const logger_1 = require("../utils/logger");
|
|
58
60
|
function reviveBufferLike(input) {
|
|
59
61
|
if (input &&
|
|
@@ -110,6 +112,22 @@ const FORCE_KILL_GRACE_MS = 200;
|
|
|
110
112
|
// escalate to SIGKILL. ~5s is well above ONNX teardown time but short
|
|
111
113
|
// enough that the reap loop self-heals within a minute.
|
|
112
114
|
const REAP_FORCE_KILL_GRACE_MS = 5000;
|
|
115
|
+
/**
|
|
116
|
+
* Embedding env derived from the active model tier, injected into every spawned
|
|
117
|
+
* worker so the worker's VectorDB dim (GMAX_VECTOR_DIM) and CPU ONNX model
|
|
118
|
+
* (GMAX_EMBED_ONNX_MODEL) match the configured tier. Without this, workers fall
|
|
119
|
+
* back to the hard-wired small tier (384d) regardless of config. One spot covers
|
|
120
|
+
* every worker user (index, add, search, daemon, mcp). Merged so process.env can
|
|
121
|
+
* override — see the spread order at fork().
|
|
122
|
+
*/
|
|
123
|
+
function embeddingEnv() {
|
|
124
|
+
const { modelTier } = (0, index_config_1.readGlobalConfig)();
|
|
125
|
+
const ids = (0, index_config_1.getModelIdsForTier)(modelTier);
|
|
126
|
+
return {
|
|
127
|
+
GMAX_VECTOR_DIM: String(ids.vectorDim),
|
|
128
|
+
GMAX_EMBED_ONNX_MODEL: ids.embed,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
113
131
|
class ProcessWorker {
|
|
114
132
|
constructor(modulePath, execArgv, maxMemoryMb) {
|
|
115
133
|
this.modulePath = modulePath;
|
|
@@ -127,9 +145,11 @@ class ProcessWorker {
|
|
|
127
145
|
// fire for the same crash.
|
|
128
146
|
this.cleanedUp = false;
|
|
129
147
|
const memArgs = maxMemoryMb ? [`--max-old-space-size=${maxMemoryMb}`] : [];
|
|
148
|
+
// embeddingEnv() first so a manually-exported GMAX_VECTOR_DIM /
|
|
149
|
+
// GMAX_EMBED_ONNX_MODEL in process.env still wins (spread last).
|
|
130
150
|
this.child = childProcess.fork(modulePath, {
|
|
131
151
|
execArgv: [...memArgs, ...execArgv],
|
|
132
|
-
env: Object.assign({}, process.env),
|
|
152
|
+
env: Object.assign(Object.assign({}, embeddingEnv()), process.env),
|
|
133
153
|
});
|
|
134
154
|
}
|
|
135
155
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
|
|
5
5
|
"homepage": "https://github.com/reowens/grepmax",
|
|
6
6
|
"bugs": {
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
],
|
|
66
66
|
"files": [
|
|
67
67
|
"dist",
|
|
68
|
+
".claude-plugin",
|
|
68
69
|
"plugins",
|
|
69
70
|
"scripts/postinstall.js",
|
|
70
71
|
"mlx-embed-server",
|