grepmax 0.26.36 → 0.26.37
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.
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "grepmax",
|
|
11
11
|
"source": "./plugins/grepmax",
|
|
12
12
|
"description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
|
|
13
|
-
"version": "0.26.
|
|
13
|
+
"version": "0.26.37",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Robert Owens",
|
|
16
16
|
"email": "robowens@me.com"
|
package/README.md
CHANGED
|
@@ -216,6 +216,7 @@ The daemon watches only projects that are in use: each Claude Code session (its
|
|
|
216
216
|
```bash
|
|
217
217
|
gmax watch --daemon -b # Start daemon manually
|
|
218
218
|
gmax watch stop # Stop daemon
|
|
219
|
+
gmax watch restart # Stop it, wait for it to exit, start a fresh one (--json)
|
|
219
220
|
gmax status # See all projects + watcher status
|
|
220
221
|
```
|
|
221
222
|
|
|
@@ -440,7 +441,7 @@ gmax doctor --fix # Auto-repair (compact, prune, fix locks)
|
|
|
440
441
|
gmax doctor --agent # Machine-readable health output
|
|
441
442
|
gmax index # Reindex (auto-detects and repairs cache/vector mismatches)
|
|
442
443
|
gmax index --reset # Full reindex from scratch
|
|
443
|
-
gmax watch
|
|
444
|
+
gmax watch restart # Restart daemon
|
|
444
445
|
```
|
|
445
446
|
|
|
446
447
|
`gmax doctor` reports ANN index state. `ANN: vector index not built` is normal with the default exact-search configuration.
|
package/dist/commands/watch.js
CHANGED
|
@@ -52,6 +52,7 @@ const syncer_1 = require("../lib/index/syncer");
|
|
|
52
52
|
const watcher_1 = require("../lib/index/watcher");
|
|
53
53
|
const meta_cache_1 = require("../lib/store/meta-cache");
|
|
54
54
|
const vector_db_1 = require("../lib/store/vector-db");
|
|
55
|
+
const daemon_restart_1 = require("../lib/utils/daemon-restart");
|
|
55
56
|
const exit_1 = require("../lib/utils/exit");
|
|
56
57
|
const filter_builder_1 = require("../lib/utils/filter-builder");
|
|
57
58
|
const log_rotate_1 = require("../lib/utils/log-rotate");
|
|
@@ -131,20 +132,10 @@ exports.watch = new commander_1.Command("watch")
|
|
|
131
132
|
// competitor; the startup code would only kill the busy peer.
|
|
132
133
|
process.exit(0);
|
|
133
134
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
let child;
|
|
135
|
+
const { spawnDaemonProcess } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-launcher")));
|
|
136
|
+
let started;
|
|
137
137
|
try {
|
|
138
|
-
|
|
139
|
-
detached: true,
|
|
140
|
-
stdio: ["ignore", out, out],
|
|
141
|
-
cwd: process.cwd(),
|
|
142
|
-
env: Object.assign(Object.assign({}, process.env), { GMAX_BACKGROUND: "true" }),
|
|
143
|
-
});
|
|
144
|
-
yield new Promise((resolve, reject) => {
|
|
145
|
-
child.once("spawn", resolve);
|
|
146
|
-
child.once("error", reject);
|
|
147
|
-
});
|
|
138
|
+
started = yield spawnDaemonProcess();
|
|
148
139
|
}
|
|
149
140
|
catch (error) {
|
|
150
141
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -152,14 +143,7 @@ exports.watch = new commander_1.Command("watch")
|
|
|
152
143
|
process.exitCode = 1;
|
|
153
144
|
return;
|
|
154
145
|
}
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
fs.closeSync(out);
|
|
158
|
-
}
|
|
159
|
-
catch (_c) { }
|
|
160
|
-
}
|
|
161
|
-
child.unref();
|
|
162
|
-
console.log(`Daemon started (PID: ${child.pid}, log: ${logFile})`);
|
|
146
|
+
console.log(`Daemon started (PID: ${started.pid}, log: ${started.logFile})`);
|
|
163
147
|
process.exit(0);
|
|
164
148
|
}
|
|
165
149
|
// Daemon foreground
|
|
@@ -210,7 +194,7 @@ exports.watch = new commander_1.Command("watch")
|
|
|
210
194
|
try {
|
|
211
195
|
yield daemon.shutdown();
|
|
212
196
|
}
|
|
213
|
-
catch (
|
|
197
|
+
catch (_c) { }
|
|
214
198
|
process.exitCode = 1;
|
|
215
199
|
return;
|
|
216
200
|
}
|
|
@@ -258,7 +242,7 @@ exports.watch = new commander_1.Command("watch")
|
|
|
258
242
|
try {
|
|
259
243
|
fs.closeSync(out);
|
|
260
244
|
}
|
|
261
|
-
catch (
|
|
245
|
+
catch (_d) { }
|
|
262
246
|
}
|
|
263
247
|
child.unref();
|
|
264
248
|
console.log(`Watcher started for ${projectName} (PID: ${child.pid}, log: ${logFile})`);
|
|
@@ -413,6 +397,85 @@ exports.watch
|
|
|
413
397
|
}
|
|
414
398
|
yield (0, exit_1.gracefulExit)();
|
|
415
399
|
}));
|
|
400
|
+
exports.watch
|
|
401
|
+
.command("restart")
|
|
402
|
+
.description("Stop the daemon and start a fresh one; waits for the old one to exit")
|
|
403
|
+
.option("--json", "Machine-readable result", false)
|
|
404
|
+
.action((options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
405
|
+
const emit = (result) => {
|
|
406
|
+
if (options.json)
|
|
407
|
+
console.log(JSON.stringify(result));
|
|
408
|
+
else if (result.ok)
|
|
409
|
+
console.log((0, daemon_restart_1.formatRestartResult)(result));
|
|
410
|
+
else
|
|
411
|
+
console.error((0, daemon_restart_1.formatRestartResult)(result));
|
|
412
|
+
};
|
|
413
|
+
const refuse = (error) => {
|
|
414
|
+
emit({
|
|
415
|
+
ok: false,
|
|
416
|
+
previousPid: null,
|
|
417
|
+
stoppedVia: null,
|
|
418
|
+
pid: null,
|
|
419
|
+
version: null,
|
|
420
|
+
workers: null,
|
|
421
|
+
workerThreads: null,
|
|
422
|
+
adopted: false,
|
|
423
|
+
readyMs: null,
|
|
424
|
+
error,
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
// Refused before anything is stopped: a daemon that cannot be started
|
|
428
|
+
// again from here must not be taken down.
|
|
429
|
+
if (process.env.GMAX_SECONDARY_STORE === "1") {
|
|
430
|
+
refuse("this project's index lives on an external drive and never runs a daemon");
|
|
431
|
+
yield (0, exit_1.gracefulExit)(2);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const { storeWriteDeniedNotice } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/store-access")));
|
|
435
|
+
const denied = storeWriteDeniedNotice();
|
|
436
|
+
if (denied) {
|
|
437
|
+
refuse(denied);
|
|
438
|
+
yield (0, exit_1.gracefulExit)(2);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const client = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
|
|
442
|
+
const { spawnDaemonProcess } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-launcher")));
|
|
443
|
+
let parentCmd = "?";
|
|
444
|
+
try {
|
|
445
|
+
const { execSync } = yield Promise.resolve().then(() => __importStar(require("node:child_process")));
|
|
446
|
+
parentCmd = execSync(`ps -o command= -p ${process.ppid}`, {
|
|
447
|
+
encoding: "utf8",
|
|
448
|
+
}).trim();
|
|
449
|
+
}
|
|
450
|
+
catch (_a) { }
|
|
451
|
+
const result = yield (0, daemon_restart_1.restartDaemon)({
|
|
452
|
+
ping: (timeoutMs) => client.sendDaemonCommand({ cmd: "ping" }, { timeoutMs }),
|
|
453
|
+
status: () => client.sendDaemonCommand({ cmd: "status" }),
|
|
454
|
+
requestShutdown: () => client.sendDaemonCommand({
|
|
455
|
+
cmd: "shutdown",
|
|
456
|
+
reason: "gmax-watch-restart",
|
|
457
|
+
from_pid: process.pid,
|
|
458
|
+
from_ppid: process.ppid,
|
|
459
|
+
from_argv: process.argv.slice(0, 4),
|
|
460
|
+
from_parent_cmd: parentCmd,
|
|
461
|
+
}, { timeoutMs: 10000 }),
|
|
462
|
+
liveDaemonPidFromFiles: () => client.isDaemonHeartbeatFresh() ? client.readDaemonPid() : null,
|
|
463
|
+
isAlive: watcher_store_1.isProcessRunning,
|
|
464
|
+
terminate: (pid) => process.kill(pid, "SIGTERM"),
|
|
465
|
+
spawn: () => __awaiter(void 0, void 0, void 0, function* () {
|
|
466
|
+
try {
|
|
467
|
+
return (yield spawnDaemonProcess()).pid;
|
|
468
|
+
}
|
|
469
|
+
catch (_a) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
}),
|
|
473
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
474
|
+
now: () => Date.now(),
|
|
475
|
+
});
|
|
476
|
+
emit(result);
|
|
477
|
+
yield (0, exit_1.gracefulExit)(result.ok ? 0 : 1);
|
|
478
|
+
}));
|
|
416
479
|
exports.watch
|
|
417
480
|
.command("stop")
|
|
418
481
|
.description("Stop watcher for current project")
|
|
@@ -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.spawnDaemon = spawnDaemon;
|
|
46
|
+
exports.spawnDaemonProcess = spawnDaemonProcess;
|
|
46
47
|
const node_child_process_1 = require("node:child_process");
|
|
47
48
|
const fs = __importStar(require("node:fs"));
|
|
48
49
|
const path = __importStar(require("node:path"));
|
|
@@ -82,3 +83,36 @@ function spawnDaemon() {
|
|
|
82
83
|
}
|
|
83
84
|
});
|
|
84
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Spawn the daemon itself (`watch --daemon`, not the `-b` launcher), detached,
|
|
88
|
+
* logging to daemon.log. Resolves to the daemon's own PID; throws when the
|
|
89
|
+
* spawn fails.
|
|
90
|
+
*/
|
|
91
|
+
function spawnDaemonProcess() {
|
|
92
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
93
|
+
const logFile = path.join(config_1.PATHS.logsDir, "daemon.log");
|
|
94
|
+
const out = (0, log_rotate_1.openRotatedLog)(logFile);
|
|
95
|
+
try {
|
|
96
|
+
const child = (0, node_child_process_1.spawn)(process.argv[0], [process.argv[1], "watch", "--daemon"], {
|
|
97
|
+
detached: true,
|
|
98
|
+
stdio: ["ignore", out, out],
|
|
99
|
+
cwd: process.cwd(),
|
|
100
|
+
env: Object.assign(Object.assign({}, process.env), { GMAX_BACKGROUND: "true" }),
|
|
101
|
+
});
|
|
102
|
+
yield new Promise((resolve, reject) => {
|
|
103
|
+
child.once("spawn", resolve);
|
|
104
|
+
child.once("error", reject);
|
|
105
|
+
});
|
|
106
|
+
child.unref();
|
|
107
|
+
if (child.pid === undefined)
|
|
108
|
+
throw new Error("spawned without a PID");
|
|
109
|
+
return { pid: child.pid, logFile };
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
try {
|
|
113
|
+
fs.closeSync(out);
|
|
114
|
+
}
|
|
115
|
+
catch (_a) { }
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.restartDaemon = restartDaemon;
|
|
13
|
+
exports.formatRestartResult = formatRestartResult;
|
|
14
|
+
// Matches the daemon's own drain grace (DRAIN_GRACE_MS in daemon-client): a
|
|
15
|
+
// graceful shutdown can legitimately run this long on a large index.
|
|
16
|
+
const DEFAULT_EXIT_TIMEOUT_MS = 90000;
|
|
17
|
+
const DEFAULT_READY_TIMEOUT_MS = 60000;
|
|
18
|
+
const DEFAULT_POLL_MS = 200;
|
|
19
|
+
function pidOf(resp) {
|
|
20
|
+
return typeof resp.pid === "number" && resp.pid > 0 ? resp.pid : null;
|
|
21
|
+
}
|
|
22
|
+
function restartDaemon(deps_1) {
|
|
23
|
+
return __awaiter(this, arguments, void 0, function* (deps, opts = {}) {
|
|
24
|
+
var _a, _b, _c, _d;
|
|
25
|
+
const exitTimeoutMs = (_a = opts.exitTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_EXIT_TIMEOUT_MS;
|
|
26
|
+
const readyTimeoutMs = (_b = opts.readyTimeoutMs) !== null && _b !== void 0 ? _b : DEFAULT_READY_TIMEOUT_MS;
|
|
27
|
+
const pollMs = (_c = opts.pollMs) !== null && _c !== void 0 ? _c : DEFAULT_POLL_MS;
|
|
28
|
+
const result = {
|
|
29
|
+
ok: false,
|
|
30
|
+
previousPid: null,
|
|
31
|
+
stoppedVia: null,
|
|
32
|
+
pid: null,
|
|
33
|
+
version: null,
|
|
34
|
+
workers: null,
|
|
35
|
+
workerThreads: null,
|
|
36
|
+
adopted: false,
|
|
37
|
+
readyMs: null,
|
|
38
|
+
};
|
|
39
|
+
// A busy daemon can miss a short ping while its heartbeat is still fresh;
|
|
40
|
+
// the files catch that case so it is stopped rather than raced.
|
|
41
|
+
const first = yield deps.ping(5000);
|
|
42
|
+
const previousPid = first.ok
|
|
43
|
+
? ((_d = pidOf(first)) !== null && _d !== void 0 ? _d : deps.liveDaemonPidFromFiles())
|
|
44
|
+
: deps.liveDaemonPidFromFiles();
|
|
45
|
+
result.previousPid = previousPid;
|
|
46
|
+
if (first.ok && previousPid === null) {
|
|
47
|
+
result.error =
|
|
48
|
+
"a daemon answered but did not name its PID; stop it with `gmax watch stop` first";
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
if (previousPid !== null) {
|
|
52
|
+
const shutdown = yield deps.requestShutdown();
|
|
53
|
+
if (shutdown.ok) {
|
|
54
|
+
result.stoppedVia = "ipc";
|
|
55
|
+
}
|
|
56
|
+
else if (deps.isAlive(previousPid)) {
|
|
57
|
+
// SIGTERM runs the same graceful shutdown as the IPC verb.
|
|
58
|
+
try {
|
|
59
|
+
deps.terminate(previousPid);
|
|
60
|
+
result.stoppedVia = "sigterm";
|
|
61
|
+
}
|
|
62
|
+
catch (_e) { }
|
|
63
|
+
}
|
|
64
|
+
const deadline = deps.now() + exitTimeoutMs;
|
|
65
|
+
while (deps.isAlive(previousPid)) {
|
|
66
|
+
if (deps.now() >= deadline) {
|
|
67
|
+
result.error = `the daemon (PID ${previousPid}) did not exit within ${Math.round(exitTimeoutMs / 1000)}s; it is still running and no second daemon was started`;
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
yield deps.sleep(pollMs);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const startedAt = deps.now();
|
|
74
|
+
// Something else (a session hook, an MCP server) may have started a daemon
|
|
75
|
+
// in the gap. Spawning beside it would only lose the lock race, so use it.
|
|
76
|
+
const gap = yield deps.ping(2000);
|
|
77
|
+
let spawnedPid = null;
|
|
78
|
+
if (gap.ok && pidOf(gap) !== previousPid) {
|
|
79
|
+
result.adopted = true;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
spawnedPid = yield deps.spawn();
|
|
83
|
+
if (spawnedPid === null) {
|
|
84
|
+
result.error = "the new daemon could not be spawned";
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const readyBy = startedAt + readyTimeoutMs;
|
|
89
|
+
for (;;) {
|
|
90
|
+
const resp = yield deps.ping(2000);
|
|
91
|
+
const pid = pidOf(resp);
|
|
92
|
+
if (resp.ok && pid !== null && pid !== previousPid) {
|
|
93
|
+
if (resp.ready !== false) {
|
|
94
|
+
result.pid = pid;
|
|
95
|
+
result.version = typeof resp.version === "string" ? resp.version : null;
|
|
96
|
+
if (spawnedPid !== null && pid !== spawnedPid)
|
|
97
|
+
result.adopted = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (spawnedPid !== null && !resp.ok && !deps.isAlive(spawnedPid)) {
|
|
102
|
+
result.error = `the new daemon (PID ${spawnedPid}) exited during startup; see ~/.gmax/logs/daemon.log`;
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
if (deps.now() >= readyBy) {
|
|
106
|
+
result.error = `the new daemon did not become ready within ${Math.round(readyTimeoutMs / 1000)}s${spawnedPid !== null ? ` (PID ${spawnedPid})` : ""}`;
|
|
107
|
+
result.pid = spawnedPid;
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
yield deps.sleep(pollMs);
|
|
111
|
+
}
|
|
112
|
+
result.readyMs = deps.now() - startedAt;
|
|
113
|
+
const status = yield deps.status();
|
|
114
|
+
if (status.ok) {
|
|
115
|
+
if (typeof status.workers === "number")
|
|
116
|
+
result.workers = status.workers;
|
|
117
|
+
const threads = status.workerThreads;
|
|
118
|
+
if (threads && typeof threads.value === "number") {
|
|
119
|
+
result.workerThreads = threads.value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
result.ok = true;
|
|
123
|
+
return result;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function formatRestartResult(r) {
|
|
127
|
+
var _a;
|
|
128
|
+
const lines = [];
|
|
129
|
+
if (r.previousPid !== null) {
|
|
130
|
+
lines.push(`Stopped daemon (PID ${r.previousPid}).`);
|
|
131
|
+
}
|
|
132
|
+
else if (r.ok || r.pid !== null) {
|
|
133
|
+
lines.push("No daemon was running.");
|
|
134
|
+
}
|
|
135
|
+
if (!r.ok) {
|
|
136
|
+
lines.push(`Restart failed: ${(_a = r.error) !== null && _a !== void 0 ? _a : "unknown error"}.`);
|
|
137
|
+
return lines.join("\n");
|
|
138
|
+
}
|
|
139
|
+
const parts = [`PID ${r.pid}`];
|
|
140
|
+
if (r.version)
|
|
141
|
+
parts.push(`v${r.version}`);
|
|
142
|
+
if (r.workerThreads !== null)
|
|
143
|
+
parts.push(`${r.workerThreads} worker threads`);
|
|
144
|
+
if (r.workers !== null) {
|
|
145
|
+
parts.push(`${r.workers} worker${r.workers === 1 ? "" : "s"} running`);
|
|
146
|
+
}
|
|
147
|
+
if (r.readyMs !== null)
|
|
148
|
+
parts.push(`ready in ${(r.readyMs / 1000).toFixed(1)}s`);
|
|
149
|
+
lines.push(`${r.adopted ? "Daemon already started by another caller" : "Daemon started"} (${parts.join(", ")}).`);
|
|
150
|
+
return lines.join("\n");
|
|
151
|
+
}
|
package/package.json
CHANGED