grepmax 0.26.16 → 0.26.19

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.16",
13
+ "version": "0.26.19",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
@@ -54,6 +54,7 @@ const sync_helpers_1 = require("../lib/index/sync-helpers");
54
54
  const syncer_1 = require("../lib/index/syncer");
55
55
  const setup_helpers_1 = require("../lib/setup/setup-helpers");
56
56
  const vector_db_1 = require("../lib/store/vector-db");
57
+ const autostart_1 = require("../lib/utils/autostart");
57
58
  const blocked_roots_1 = require("../lib/utils/blocked-roots");
58
59
  const exit_1 = require("../lib/utils/exit");
59
60
  const project_marker_1 = require("../lib/utils/project-marker");
@@ -98,6 +99,12 @@ Examples:
98
99
  process.exitCode = 1;
99
100
  return;
100
101
  }
102
+ if ((0, blocked_roots_1.isGitWorktreeRoot)(projectRoot)) {
103
+ logBlockedAttempt("git_worktree", projectRoot);
104
+ console.error(`Refusing to add ${projectRoot}: ${blocked_roots_1.WORKTREE_REFUSAL}`);
105
+ process.exitCode = 1;
106
+ return;
107
+ }
101
108
  // Check if already registered
102
109
  const existing = (0, project_registry_1.getProject)(projectRoot);
103
110
  if (existing) {
@@ -220,6 +227,12 @@ Examples:
220
227
  }
221
228
  else {
222
229
  // Fallback: direct mode with lock
230
+ const notice = (0, autostart_1.autostartDisabledNotice)();
231
+ if (notice) {
232
+ spinner.stop();
233
+ console.log(notice);
234
+ spinner.start();
235
+ }
223
236
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
224
237
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
225
238
  try {
@@ -51,6 +51,7 @@ const sync_helpers_1 = require("../lib/index/sync-helpers");
51
51
  const syncer_1 = require("../lib/index/syncer");
52
52
  const setup_helpers_1 = require("../lib/setup/setup-helpers");
53
53
  const vector_db_1 = require("../lib/store/vector-db");
54
+ const autostart_1 = require("../lib/utils/autostart");
54
55
  const exit_1 = require("../lib/utils/exit");
55
56
  const project_registry_1 = require("../lib/utils/project-registry");
56
57
  const project_root_1 = require("../lib/utils/project-root");
@@ -149,6 +150,9 @@ Examples:
149
150
  }
150
151
  else {
151
152
  // Fallback: direct mode with lock — stop any watcher first
153
+ const notice = (0, autostart_1.autostartDisabledNotice)();
154
+ if (notice)
155
+ console.log(notice);
152
156
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
153
157
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
154
158
  let restartWatcher = false;
package/dist/config.js CHANGED
@@ -229,6 +229,10 @@ exports.PATHS = {
229
229
  // never SIGKILLs a peer mid-cleanup once the peer has already dropped its
230
230
  // socket/PID/lock liveness markers.
231
231
  daemonDrainingFile: path.join(GLOBAL_ROOT, "daemon.draining"),
232
+ // Kill switch for implicit daemon auto-start (see lib/utils/autostart.ts).
233
+ // Its presence stops hooks and CLI commands from reviving a quarantined
234
+ // daemon; explicit `gmax watch --daemon` still works.
235
+ autostartDisabledFile: path.join(GLOBAL_ROOT, "autostart-disabled"),
232
236
  // Centralized index storage — one database for all indexed directories
233
237
  lancedbDir: path.join(GLOBAL_ROOT, "lancedb"),
234
238
  cacheDir: path.join(GLOBAL_ROOT, "cache"),
@@ -59,6 +59,7 @@ const server_1 = require("../llm/server");
59
59
  const meta_cache_1 = require("../store/meta-cache");
60
60
  const store_lease_1 = require("../store/store-lease");
61
61
  const vector_db_1 = require("../store/vector-db");
62
+ const blocked_roots_1 = require("../utils/blocked-roots");
62
63
  const daemon_client_1 = require("../utils/daemon-client");
63
64
  const daemon_launcher_1 = require("../utils/daemon-launcher");
64
65
  const kernel_zone_1 = require("../utils/kernel-zone");
@@ -789,6 +790,10 @@ class Daemon {
789
790
  return;
790
791
  }
791
792
  const project = (0, project_registry_1.getProject)(root);
793
+ if (!project && (0, blocked_roots_1.isGitWorktreeRoot)(root)) {
794
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: blocked_roots_1.WORKTREE_REFUSAL });
795
+ return;
796
+ }
792
797
  if ((project === null || project === void 0 ? void 0 : project.status) === "indexed") {
793
798
  if (!this.activeGeneration ||
794
799
  (0, embedding_generation_1.compareEmbeddingGeneration)(project, this.activeGeneration)
@@ -854,6 +859,10 @@ class Daemon {
854
859
  (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
855
860
  return;
856
861
  }
862
+ if (!(0, project_registry_1.getProject)(root) && (0, blocked_roots_1.isGitWorktreeRoot)(root)) {
863
+ (0, ipc_handler_1.writeDone)(conn, { ok: false, error: blocked_roots_1.WORKTREE_REFUSAL });
864
+ return;
865
+ }
857
866
  if (!(0, project_registry_1.getProject)(root)) {
858
867
  (0, project_registry_1.registerProject)({
859
868
  root,
@@ -47,6 +47,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
48
  exports.StoreLease = exports.StoreLeaseTimeoutError = void 0;
49
49
  exports.storeLeasePaths = storeLeasePaths;
50
+ exports.defaultProbeOwner = defaultProbeOwner;
50
51
  exports.hasStoreExclusiveIntent = hasStoreExclusiveIntent;
51
52
  const node_child_process_1 = require("node:child_process");
52
53
  const node_crypto_1 = require("node:crypto");
@@ -97,19 +98,37 @@ function currentProcessStart(pid) {
97
98
  return fallback;
98
99
  }
99
100
  }
100
- function defaultProbeOwner(owner) {
101
+ const defaultProbeDeps = {
102
+ signal: (pid) => {
103
+ process.kill(pid, 0);
104
+ },
105
+ processStartOf: (pid) => (0, node_child_process_1.execFileSync)("ps", ["-p", String(pid), "-o", "lstart="], {
106
+ encoding: "utf8",
107
+ timeout: 1000,
108
+ }).trim(),
109
+ };
110
+ function defaultProbeOwner(owner, deps = defaultProbeDeps) {
101
111
  try {
102
- process.kill(owner.pid, 0);
112
+ deps.signal(owner.pid);
103
113
  }
104
114
  catch (error) {
105
- return error.code === "ESRCH"
106
- ? "dead"
107
- : "unknown";
115
+ const code = error.code;
116
+ if (code === "ESRCH")
117
+ return "dead";
118
+ // EPERM means the PID exists but is not signalable by us -- the signature
119
+ // of a recycled PID that now belongs to another user. That is not evidence
120
+ // the lease owner is alive, so fall through to the processStart comparison
121
+ // below, which `ps` answers across user boundaries. Returning "unknown"
122
+ // here instead would make the reader marker immortal and hang every
123
+ // exclusive operation for the life of the store.
124
+ if (code !== "EPERM")
125
+ return "unknown";
108
126
  }
109
127
  if (owner.processStart.startsWith(`pid:${owner.pid}:started:`))
110
128
  return "same";
111
129
  try {
112
- const start = (0, node_child_process_1.execFileSync)("ps", ["-p", String(owner.pid), "-o", "lstart="], { encoding: "utf8", timeout: 1000 }).trim();
130
+ const start = deps.processStartOf(owner.pid);
131
+ // Reserve "unknown" for a start-time lookup that could not answer.
113
132
  if (!start)
114
133
  return "unknown";
115
134
  return start === owner.processStart ? "same" : "reused";
@@ -0,0 +1,81 @@
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.autostartDisabledReason = autostartDisabledReason;
37
+ exports.isAutostartDisabled = isAutostartDisabled;
38
+ exports.autostartDisabledNotice = autostartDisabledNotice;
39
+ const fs = __importStar(require("node:fs"));
40
+ const config_1 = require("../../config");
41
+ /**
42
+ * Kill switch for *implicit* daemon auto-start. Create
43
+ * ~/.gmax/autostart-disabled (or export GMAX_NO_AUTOSTART=1) to keep session
44
+ * hooks and ordinary commands (`gmax add`, `gmax index`, search, MCP) from
45
+ * reviving the daemon — needed when the host is quarantined and the daemon's
46
+ * write volume is the thing under investigation.
47
+ *
48
+ * Explicit `gmax watch --daemon` is never gated: typing it is the user asking
49
+ * for a daemon in that moment.
50
+ *
51
+ * plugins/grepmax/hooks/start.js carries a plain-JS copy of these semantics
52
+ * (env var checked first, then the file) because a SessionStart hook cannot
53
+ * import from dist. Keep the two in sync.
54
+ */
55
+ function autostartDisabledReason() {
56
+ if (process.env.GMAX_NO_AUTOSTART === "1")
57
+ return "env";
58
+ try {
59
+ return fs.existsSync(config_1.PATHS.autostartDisabledFile) ? "file" : null;
60
+ }
61
+ catch (_a) {
62
+ return null;
63
+ }
64
+ }
65
+ function isAutostartDisabled() {
66
+ return autostartDisabledReason() !== null;
67
+ }
68
+ /**
69
+ * One-line notice for commands that fell back to in-process work because the
70
+ * kill switch is on, or null when it isn't. Names the specific undo step so
71
+ * the message is actionable whichever way autostart was disabled.
72
+ */
73
+ function autostartDisabledNotice() {
74
+ const reason = autostartDisabledReason();
75
+ if (!reason)
76
+ return null;
77
+ const undo = reason === "env"
78
+ ? "unset GMAX_NO_AUTOSTART"
79
+ : `rm ${config_1.PATHS.autostartDisabledFile}`;
80
+ return `Daemon autostart is disabled — running in-process. Re-enable with: ${undo}`;
81
+ }
@@ -33,9 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.BLOCKED_ROOTS_DESCRIPTION = void 0;
36
+ exports.WORKTREE_REFUSAL = exports.BLOCKED_ROOTS_DESCRIPTION = void 0;
37
37
  exports.getBlockedProjectRoots = getBlockedProjectRoots;
38
38
  exports.isBlockedProjectRoot = isBlockedProjectRoot;
39
+ exports.isGitWorktreeRoot = isGitWorktreeRoot;
40
+ const fs = __importStar(require("node:fs"));
39
41
  const os = __importStar(require("node:os"));
40
42
  const path = __importStar(require("node:path"));
41
43
  exports.BLOCKED_ROOTS_DESCRIPTION = "home, /, /Users, /tmp, /private, /var, /usr, /opt, /etc, /System, /Library, /Applications";
@@ -61,3 +63,27 @@ function getBlockedProjectRoots() {
61
63
  function isBlockedProjectRoot(root) {
62
64
  return getBlockedProjectRoots().has(path.resolve(root));
63
65
  }
66
+ /**
67
+ * A linked git worktree has a `.git` FILE (not a directory) pointing back into
68
+ * the main repo's `.git/worktrees/<name>`. Indexing one duplicates the whole
69
+ * corpus of the repo it was cut from — three platform worktrees once put 680k
70
+ * duplicate chunks in the index and pinned seven embed workers at ~1GB each.
71
+ */
72
+ function isGitWorktreeRoot(root) {
73
+ const gitPath = path.join(path.resolve(root), ".git");
74
+ let contents;
75
+ try {
76
+ if (!fs.statSync(gitPath).isFile())
77
+ return false;
78
+ contents = fs.readFileSync(gitPath, "utf8");
79
+ }
80
+ catch (_a) {
81
+ return false;
82
+ }
83
+ const match = /^gitdir:\s*(.+)$/m.exec(contents);
84
+ if (!(match === null || match === void 0 ? void 0 : match[1]))
85
+ return false;
86
+ const gitdir = match[1].trim().replace(/\\/g, "/");
87
+ return /(^|\/)\.git\/worktrees\//.test(gitdir);
88
+ }
89
+ exports.WORKTREE_REFUSAL = "gmax does not index git worktrees — they duplicate the repository they were cut from. Add the main repository root instead.";
@@ -269,12 +269,19 @@ function isDaemonDraining(pid) {
269
269
  /**
270
270
  * Ensure the daemon is running — start it if needed, then wait for resources.
271
271
  * Returns true if daemon is ready, false if it couldn't be started.
272
+ *
273
+ * This is the implicit spawn path, so it honors the autostart kill switch: with
274
+ * it on, an already-running daemon is still used, but a dead one is not
275
+ * revived and callers fall back to in-process work.
272
276
  */
273
277
  function ensureDaemonRunning() {
274
278
  return __awaiter(this, void 0, void 0, function* () {
275
279
  if (yield isDaemonReady())
276
280
  return true;
277
281
  if (!(yield isDaemonRunning())) {
282
+ const { isAutostartDisabled } = yield Promise.resolve().then(() => __importStar(require("./autostart")));
283
+ if (isAutostartDisabled())
284
+ return false;
278
285
  const { spawnDaemon } = yield Promise.resolve().then(() => __importStar(require("./daemon-launcher")));
279
286
  const pid = yield spawnDaemon();
280
287
  if (!pid)
@@ -63,6 +63,7 @@ const path = __importStar(require("node:path"));
63
63
  const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
64
64
  const config_1 = require("../../config");
65
65
  const embedding_generation_1 = require("../index/embedding-generation");
66
+ const blocked_roots_1 = require("./blocked-roots");
66
67
  const REGISTRY_PATH = path.join(config_1.PATHS.globalRoot, "projects.json");
67
68
  const REBUILD_JOURNAL_PATH = path.join(config_1.PATHS.globalRoot, "rebuild-journal.json");
68
69
  class ProjectRegistryConflictError extends Error {
@@ -207,6 +208,10 @@ function registerProject(entry) {
207
208
  }
208
209
  withRegistryLock(() => {
209
210
  const entries = loadRegistry();
211
+ if (!entries.some((e) => e.root === entry.root) &&
212
+ (0, blocked_roots_1.isGitWorktreeRoot)(entry.root)) {
213
+ throw new Error(`${blocked_roots_1.WORKTREE_REFUSAL} (${entry.root})`);
214
+ }
210
215
  let canonicalRoot;
211
216
  try {
212
217
  canonicalRoot = fs.realpathSync(entry.root);
@@ -16,6 +16,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.launchWatcher = launchWatcher;
18
18
  const node_child_process_1 = require("node:child_process");
19
+ const autostart_1 = require("./autostart");
19
20
  const daemon_client_1 = require("./daemon-client");
20
21
  const daemon_launcher_1 = require("./daemon-launcher");
21
22
  const project_registry_1 = require("./project-registry");
@@ -48,7 +49,13 @@ function launchWatcher(projectRoot) {
48
49
  if (resp.ok && typeof resp.pid === "number") {
49
50
  return { ok: true, pid: resp.pid, reused: true };
50
51
  }
51
- // 4. Daemon not running — try to start it, poll until ready
52
+ // 4. Daemon not running — try to start it, poll until ready.
53
+ // Both remaining steps spawn a background process, so stop here when the
54
+ // autostart kill switch is on: a quarantined daemon must stay down.
55
+ const notice = (0, autostart_1.autostartDisabledNotice)();
56
+ if (notice) {
57
+ return { ok: false, reason: "autostart-disabled", message: notice };
58
+ }
52
59
  const error = resp.error;
53
60
  if (error === "ENOENT" || error === "ECONNREFUSED") {
54
61
  const daemonPid = yield (0, daemon_launcher_1.spawnDaemon)();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.16",
3
+ "version": "0.26.19",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.26.16",
3
+ "version": "0.26.19",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",
@@ -82,6 +82,9 @@ function isProjectRegistered() {
82
82
  // GMAX_NO_AUTOSTART=1) to keep sessions from reviving the daemon — needed when the
83
83
  // host is quarantined after a kernel-zone incident and the daemon's write volume is
84
84
  // the thing under investigation. Delete the file to restore normal behavior.
85
+ //
86
+ // The CLI honors the same switch via src/lib/utils/autostart.ts (PATHS.autostartDisabledFile).
87
+ // This copy exists because a hook cannot import from dist — keep the two in sync.
85
88
  function isAutostartDisabled() {
86
89
  if (process.env.GMAX_NO_AUTOSTART === "1") return true;
87
90
  try {