grepmax 0.13.0 → 0.13.2

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 CHANGED
@@ -164,15 +164,15 @@ gmax "query" [options]
164
164
 
165
165
  ## Background Daemon
166
166
 
167
- A single daemon watches all registered projects via native OS file events (FSEvents/inotify). Changes are detected in sub-second and incrementally reindexed.
167
+ A single daemon watches all registered projects via native OS file events (FSEvents/inotify). Changes are detected in sub-second and incrementally reindexed. All writes to LanceDB are routed through the daemon via IPC, eliminating lock contention.
168
168
 
169
169
  ```bash
170
- gmax watch --daemon -b # Start daemon
170
+ gmax watch --daemon -b # Start daemon manually
171
171
  gmax watch stop # Stop daemon
172
172
  gmax status # See all projects + watcher status
173
173
  ```
174
174
 
175
- The daemon auto-starts via agent plugins and shuts down after 30 minutes of inactivity.
175
+ The daemon auto-starts when you run `gmax add`, `gmax index`, `gmax remove`, or `gmax summarize`. It shuts down after 30 minutes of inactivity.
176
176
 
177
177
  ## Architecture
178
178
 
@@ -118,7 +118,7 @@ Examples:
118
118
  yield (0, setup_helpers_1.ensureSetup)();
119
119
  yield (0, grammar_loader_1.ensureGrammars)(console.log, { silent: true });
120
120
  const { spinner, onProgress } = (0, sync_helpers_1.createIndexingSpinner)(projectRoot, `Adding ${projectName}...`);
121
- const { isDaemonRunning, sendStreamingCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
121
+ const { ensureDaemonRunning, sendStreamingCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
122
122
  const pendingEntry = {
123
123
  root: projectRoot,
124
124
  name: projectName,
@@ -129,7 +129,7 @@ Examples:
129
129
  chunkCount: 0,
130
130
  status: "error",
131
131
  };
132
- if (yield isDaemonRunning()) {
132
+ if (yield ensureDaemonRunning()) {
133
133
  // Daemon mode: IPC streaming
134
134
  try {
135
135
  const done = yield sendStreamingCommand({ cmd: "add", root: projectRoot }, (msg) => {
@@ -100,8 +100,8 @@ Examples:
100
100
  }
101
101
  // Ensure grammars are present before indexing (silent if already exist)
102
102
  yield (0, grammar_loader_1.ensureGrammars)(console.log, { silent: true });
103
- const { isDaemonRunning, sendStreamingCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
104
- if (yield isDaemonRunning()) {
103
+ const { ensureDaemonRunning, sendStreamingCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
104
+ if (yield ensureDaemonRunning()) {
105
105
  // Daemon mode: IPC streaming — daemon handles watcher pause/resume internally
106
106
  const { spinner, onProgress } = (0, sync_helpers_1.createIndexingSpinner)(projectRoot, "Indexing...", { verbose: options.verbose });
107
107
  try {
@@ -0,0 +1,143 @@
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
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.llm = void 0;
46
+ const path = __importStar(require("node:path"));
47
+ const commander_1 = require("commander");
48
+ const exit_1 = require("../lib/utils/exit");
49
+ function showStatus() {
50
+ return __awaiter(this, void 0, void 0, function* () {
51
+ const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
52
+ if (!(yield isDaemonRunning())) {
53
+ console.log("LLM server: not running (daemon not started)");
54
+ return;
55
+ }
56
+ const resp = yield sendDaemonCommand({ cmd: "llm-status" });
57
+ if (!resp.ok) {
58
+ console.error("Failed to get LLM status:", resp.error);
59
+ process.exitCode = 1;
60
+ return;
61
+ }
62
+ if (resp.running) {
63
+ const model = path.basename(String(resp.model));
64
+ const uptime = Number(resp.uptime) || 0;
65
+ const mins = Math.floor(uptime / 60);
66
+ const secs = uptime % 60;
67
+ console.log(`LLM server: running (PID: ${resp.pid}, port: ${resp.port})`);
68
+ console.log(` Model: ${model}`);
69
+ console.log(` Uptime: ${mins}m ${secs}s`);
70
+ }
71
+ else {
72
+ console.log("LLM server: not running");
73
+ }
74
+ });
75
+ }
76
+ exports.llm = new commander_1.Command("llm")
77
+ .description("Manage the local LLM server (llama-server)")
78
+ .action(() => __awaiter(void 0, void 0, void 0, function* () {
79
+ try {
80
+ yield showStatus();
81
+ }
82
+ finally {
83
+ yield (0, exit_1.gracefulExit)();
84
+ }
85
+ }));
86
+ exports.llm
87
+ .command("start")
88
+ .description("Start the LLM server")
89
+ .action(() => __awaiter(void 0, void 0, void 0, function* () {
90
+ try {
91
+ const { ensureDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
92
+ if (!(yield ensureDaemonRunning())) {
93
+ console.error("Failed to start daemon");
94
+ process.exitCode = 1;
95
+ return;
96
+ }
97
+ console.log("Starting LLM server...");
98
+ const resp = yield sendDaemonCommand({ cmd: "llm-start" }, { timeoutMs: 90000 });
99
+ if (!resp.ok) {
100
+ console.error(`Failed: ${resp.error}`);
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ const model = path.basename(String(resp.model));
105
+ console.log(`LLM server ready (PID: ${resp.pid}, port: ${resp.port}, model: ${model})`);
106
+ }
107
+ finally {
108
+ yield (0, exit_1.gracefulExit)();
109
+ }
110
+ }));
111
+ exports.llm
112
+ .command("stop")
113
+ .description("Stop the LLM server")
114
+ .action(() => __awaiter(void 0, void 0, void 0, function* () {
115
+ try {
116
+ const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
117
+ if (!(yield isDaemonRunning())) {
118
+ console.log("Daemon not running");
119
+ return;
120
+ }
121
+ const resp = yield sendDaemonCommand({ cmd: "llm-stop" });
122
+ if (!resp.ok) {
123
+ console.error(`Failed: ${resp.error}`);
124
+ process.exitCode = 1;
125
+ return;
126
+ }
127
+ console.log("LLM server stopped");
128
+ }
129
+ finally {
130
+ yield (0, exit_1.gracefulExit)();
131
+ }
132
+ }));
133
+ exports.llm
134
+ .command("status")
135
+ .description("Show LLM server status")
136
+ .action(() => __awaiter(void 0, void 0, void 0, function* () {
137
+ try {
138
+ yield showStatus();
139
+ }
140
+ finally {
141
+ yield (0, exit_1.gracefulExit)();
142
+ }
143
+ }));
package/dist/config.js CHANGED
@@ -103,6 +103,9 @@ exports.PATHS = {
103
103
  lmdbPath: path.join(GLOBAL_ROOT, "cache", "meta.lmdb"),
104
104
  configPath: path.join(GLOBAL_ROOT, "config.json"),
105
105
  lockDir: GLOBAL_ROOT,
106
+ // LLM server (llama-server)
107
+ llmPidFile: path.join(GLOBAL_ROOT, "llm-server.pid"),
108
+ llmLogFile: path.join(GLOBAL_ROOT, "logs", "llm-server.log"),
106
109
  };
107
110
  exports.MAX_FILE_SIZE_BYTES = 1024 * 1024 * 2; // 2MB limit for indexing
108
111
  // Extensions we consider for indexing to avoid binary noise and improve relevance.
package/dist/index.js CHANGED
@@ -50,6 +50,7 @@ const impact_1 = require("./commands/impact");
50
50
  const droid_1 = require("./commands/droid");
51
51
  const index_1 = require("./commands/index");
52
52
  const list_1 = require("./commands/list");
53
+ const llm_1 = require("./commands/llm");
53
54
  const mcp_1 = require("./commands/mcp");
54
55
  const peek_1 = require("./commands/peek");
55
56
  const project_1 = require("./commands/project");
@@ -110,6 +111,7 @@ commander_1.program.addCommand(serve_1.serve);
110
111
  commander_1.program.addCommand(watch_1.watch);
111
112
  commander_1.program.addCommand(mcp_1.mcp);
112
113
  commander_1.program.addCommand(summarize_1.summarize);
114
+ commander_1.program.addCommand(llm_1.llm);
113
115
  // Setup & diagnostics
114
116
  commander_1.program.addCommand(setup_1.setup);
115
117
  commander_1.program.addCommand(config_1.config);
@@ -60,6 +60,7 @@ const vector_db_1 = require("../store/vector-db");
60
60
  const process_1 = require("../utils/process");
61
61
  const project_registry_1 = require("../utils/project-registry");
62
62
  const watcher_store_1 = require("../utils/watcher-store");
63
+ const server_1 = require("../llm/server");
63
64
  const ipc_handler_1 = require("./ipc-handler");
64
65
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
65
66
  const HEARTBEAT_INTERVAL_MS = 60 * 1000;
@@ -78,6 +79,7 @@ class Daemon {
78
79
  this.shuttingDown = false;
79
80
  this.pendingOps = new Set();
80
81
  this.projectLocks = new Map();
82
+ this.llmServer = null;
81
83
  }
82
84
  start() {
83
85
  return __awaiter(this, void 0, void 0, function* () {
@@ -124,7 +126,9 @@ class Daemon {
124
126
  console.error("[daemon] Failed to open shared resources:", err);
125
127
  throw err;
126
128
  }
127
- // 6. Register daemon (only after resources are open)
129
+ // 6. LLM server manager (constructed, not started starts on first request)
130
+ this.llmServer = new server_1.LlmServer();
131
+ // 7. Register daemon (only after resources are open)
128
132
  (0, watcher_store_1.registerDaemon)(process.pid);
129
133
  // 7. Subscribe to all registered projects (skip missing directories)
130
134
  const projects = (0, project_registry_1.listProjects)().filter((p) => p.status === "indexed");
@@ -323,6 +327,8 @@ class Daemon {
323
327
  (0, ipc_handler_1.writeDone)(conn, { ok: false, error: "daemon resources not ready" });
324
328
  return;
325
329
  }
330
+ const ac = new AbortController();
331
+ conn.on("close", () => ac.abort());
326
332
  this.vectorDb.pauseMaintenanceLoop();
327
333
  let lastProgressTime = 0;
328
334
  try {
@@ -330,6 +336,7 @@ class Daemon {
330
336
  projectRoot: root,
331
337
  vectorDb: this.vectorDb,
332
338
  metaCache: this.metaCache,
339
+ signal: ac.signal,
333
340
  onProgress: (info) => {
334
341
  this.resetActivity();
335
342
  const now = Date.now();
@@ -383,6 +390,8 @@ class Daemon {
383
390
  yield sub.unsubscribe();
384
391
  this.subscriptions.delete(root);
385
392
  }
393
+ const ac = new AbortController();
394
+ conn.on("close", () => ac.abort());
386
395
  this.vectorDb.pauseMaintenanceLoop();
387
396
  let lastProgressTime = 0;
388
397
  try {
@@ -392,6 +401,7 @@ class Daemon {
392
401
  dryRun: opts.dryRun,
393
402
  vectorDb: this.vectorDb,
394
403
  metaCache: this.metaCache,
404
+ signal: ac.signal,
395
405
  onProgress: (info) => {
396
406
  this.resetActivity();
397
407
  const now = Date.now();
@@ -490,9 +500,48 @@ class Daemon {
490
500
  }));
491
501
  });
492
502
  }
503
+ // --- LLM server management ---
504
+ llmStart() {
505
+ return __awaiter(this, void 0, void 0, function* () {
506
+ if (!this.llmServer)
507
+ return { ok: false, error: "daemon not initialized" };
508
+ try {
509
+ yield this.llmServer.start();
510
+ this.resetActivity();
511
+ return Object.assign({ ok: true }, this.llmServer.getStatus());
512
+ }
513
+ catch (err) {
514
+ const msg = err instanceof Error ? err.message : String(err);
515
+ return { ok: false, error: msg };
516
+ }
517
+ });
518
+ }
519
+ llmStop() {
520
+ return __awaiter(this, void 0, void 0, function* () {
521
+ if (!this.llmServer)
522
+ return { ok: false, error: "daemon not initialized" };
523
+ try {
524
+ yield this.llmServer.stop();
525
+ return { ok: true };
526
+ }
527
+ catch (err) {
528
+ const msg = err instanceof Error ? err.message : String(err);
529
+ return { ok: false, error: msg };
530
+ }
531
+ });
532
+ }
533
+ llmStatus() {
534
+ if (!this.llmServer)
535
+ return { ok: false, error: "daemon not initialized" };
536
+ return Object.assign({ ok: true }, this.llmServer.getStatus());
537
+ }
538
+ llmTouch() {
539
+ var _a;
540
+ (_a = this.llmServer) === null || _a === void 0 ? void 0 : _a.touchIdle();
541
+ }
493
542
  shutdown() {
494
543
  return __awaiter(this, void 0, void 0, function* () {
495
- var _a, _b, _c;
544
+ var _a, _b, _c, _d;
496
545
  if (this.shuttingDown)
497
546
  return;
498
547
  this.shuttingDown = true;
@@ -505,29 +554,34 @@ class Daemon {
505
554
  for (const processor of this.processors.values()) {
506
555
  yield processor.close();
507
556
  }
557
+ // Stop LLM server if running
558
+ try {
559
+ yield ((_a = this.llmServer) === null || _a === void 0 ? void 0 : _a.stop());
560
+ }
561
+ catch (_e) { }
508
562
  // Unsubscribe all watchers
509
563
  for (const sub of this.subscriptions.values()) {
510
564
  try {
511
565
  yield sub.unsubscribe();
512
566
  }
513
- catch (_d) { }
567
+ catch (_f) { }
514
568
  }
515
569
  this.subscriptions.clear();
516
570
  // Close server + socket + PID file + lock
517
- (_a = this.server) === null || _a === void 0 ? void 0 : _a.close();
571
+ (_b = this.server) === null || _b === void 0 ? void 0 : _b.close();
518
572
  try {
519
573
  fs.unlinkSync(config_1.PATHS.daemonSocket);
520
574
  }
521
- catch (_e) { }
575
+ catch (_g) { }
522
576
  try {
523
577
  fs.unlinkSync(config_1.PATHS.daemonPidFile);
524
578
  }
525
- catch (_f) { }
579
+ catch (_h) { }
526
580
  if (this.releaseLock) {
527
581
  try {
528
582
  yield this.releaseLock();
529
583
  }
530
- catch (_g) { }
584
+ catch (_j) { }
531
585
  this.releaseLock = null;
532
586
  }
533
587
  // Unregister all
@@ -538,13 +592,13 @@ class Daemon {
538
592
  this.processors.clear();
539
593
  // Close shared resources
540
594
  try {
541
- yield ((_b = this.metaCache) === null || _b === void 0 ? void 0 : _b.close());
595
+ yield ((_c = this.metaCache) === null || _c === void 0 ? void 0 : _c.close());
542
596
  }
543
- catch (_h) { }
597
+ catch (_k) { }
544
598
  try {
545
- yield ((_c = this.vectorDb) === null || _c === void 0 ? void 0 : _c.close());
599
+ yield ((_d = this.vectorDb) === null || _d === void 0 ? void 0 : _d.close());
546
600
  }
547
- catch (_j) { }
601
+ catch (_l) { }
548
602
  console.log("[daemon] Shutdown complete");
549
603
  });
550
604
  }
@@ -101,6 +101,13 @@ function handleCommand(daemon, cmd, conn) {
101
101
  });
102
102
  return null;
103
103
  }
104
+ // --- LLM server management ---
105
+ case "llm-start":
106
+ return yield daemon.llmStart();
107
+ case "llm-stop":
108
+ return yield daemon.llmStop();
109
+ case "llm-status":
110
+ return daemon.llmStatus();
104
111
  default:
105
112
  return { ok: false, error: `unknown command: ${cmd.cmd}` };
106
113
  }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLlmConfig = getLlmConfig;
4
+ const DEFAULT_MODEL = "/Volumes/External/models/huggingface/hub/models--unsloth--Qwen3.5-35B-A3B-GGUF/Qwen3.5-35B-A3B-Q4_K_M.gguf";
5
+ function envInt(key, fallback) {
6
+ const v = process.env[key];
7
+ if (!v)
8
+ return fallback;
9
+ const n = Number.parseInt(v, 10);
10
+ return Number.isFinite(n) && n > 0 ? n : fallback;
11
+ }
12
+ function getLlmConfig() {
13
+ var _a, _b, _c;
14
+ return {
15
+ model: (_a = process.env.GMAX_LLM_MODEL) !== null && _a !== void 0 ? _a : DEFAULT_MODEL,
16
+ binary: (_b = process.env.GMAX_LLM_BINARY) !== null && _b !== void 0 ? _b : "llama-server",
17
+ host: (_c = process.env.GMAX_LLM_HOST) !== null && _c !== void 0 ? _c : "127.0.0.1",
18
+ port: envInt("GMAX_LLM_PORT", 8079),
19
+ ctxSize: envInt("GMAX_LLM_CTX_SIZE", 16384),
20
+ ngl: envInt("GMAX_LLM_NGL", 99),
21
+ maxTokens: envInt("GMAX_LLM_MAX_TOKENS", 8192),
22
+ idleTimeoutMin: envInt("GMAX_LLM_IDLE_TIMEOUT", 30),
23
+ startupWaitSec: envInt("GMAX_LLM_STARTUP_WAIT", 60),
24
+ };
25
+ }
@@ -0,0 +1,261 @@
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
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.LlmServer = void 0;
46
+ const node_child_process_1 = require("node:child_process");
47
+ const fs = __importStar(require("node:fs"));
48
+ const http = __importStar(require("node:http"));
49
+ const config_1 = require("../../config");
50
+ const log_rotate_1 = require("../utils/log-rotate");
51
+ const config_2 = require("./config");
52
+ const HEALTH_TIMEOUT_MS = 2000;
53
+ const POLL_INTERVAL_MS = 500;
54
+ const STOP_GRACE_MS = 5000;
55
+ const IDLE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
56
+ class LlmServer {
57
+ constructor() {
58
+ this.lastRequestTime = 0;
59
+ this.startTime = 0;
60
+ this.idleTimer = null;
61
+ this.config = (0, config_2.getLlmConfig)();
62
+ }
63
+ /** HTTP GET /v1/models — returns true if llama-server is responding. */
64
+ healthy() {
65
+ return new Promise((resolve) => {
66
+ const req = http.get({
67
+ hostname: this.config.host,
68
+ port: this.config.port,
69
+ path: "/v1/models",
70
+ timeout: HEALTH_TIMEOUT_MS,
71
+ }, (res) => {
72
+ res.resume();
73
+ resolve(res.statusCode === 200);
74
+ });
75
+ req.on("error", () => resolve(false));
76
+ req.on("timeout", () => {
77
+ req.destroy();
78
+ resolve(false);
79
+ });
80
+ });
81
+ }
82
+ /** Start llama-server, poll until ready, start idle watchdog. */
83
+ start() {
84
+ return __awaiter(this, void 0, void 0, function* () {
85
+ if (yield this.healthy())
86
+ return;
87
+ // Validate binary
88
+ const binary = this.config.binary;
89
+ try {
90
+ (0, node_child_process_1.execSync)(`which ${binary}`, { stdio: "ignore" });
91
+ }
92
+ catch (_a) {
93
+ throw new Error(`llama-server binary not found: "${binary}". Install llama.cpp or set GMAX_LLM_BINARY`);
94
+ }
95
+ // Validate model file
96
+ if (!fs.existsSync(this.config.model)) {
97
+ throw new Error(`Model file not found: "${this.config.model}". Set GMAX_LLM_MODEL to a valid .gguf path`);
98
+ }
99
+ const logFd = (0, log_rotate_1.openRotatedLog)(config_1.PATHS.llmLogFile);
100
+ const child = (0, node_child_process_1.spawn)(binary, [
101
+ "-m", this.config.model,
102
+ "--host", this.config.host,
103
+ "--port", String(this.config.port),
104
+ "-ngl", String(this.config.ngl),
105
+ "--ctx-size", String(this.config.ctxSize),
106
+ ], { detached: true, stdio: ["ignore", logFd, logFd] });
107
+ child.unref();
108
+ fs.closeSync(logFd);
109
+ const pid = child.pid;
110
+ if (!pid) {
111
+ throw new Error("Failed to spawn llama-server — no PID returned");
112
+ }
113
+ fs.writeFileSync(config_1.PATHS.llmPidFile, String(pid));
114
+ console.log(`[llm] Starting llama-server (PID: ${pid}, port: ${this.config.port})`);
115
+ // Poll until ready
116
+ const deadline = Date.now() + this.config.startupWaitSec * 1000;
117
+ while (Date.now() < deadline) {
118
+ yield new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
119
+ // Check if process died
120
+ try {
121
+ process.kill(pid, 0);
122
+ }
123
+ catch (_b) {
124
+ throw new Error(`llama-server process died during startup — check ${config_1.PATHS.llmLogFile}`);
125
+ }
126
+ if (yield this.healthy()) {
127
+ this.startTime = Date.now();
128
+ this.lastRequestTime = Date.now();
129
+ this.startIdleWatchdog();
130
+ console.log("[llm] Server ready");
131
+ return;
132
+ }
133
+ }
134
+ // Timeout — kill the process
135
+ try {
136
+ process.kill(pid, "SIGKILL");
137
+ }
138
+ catch (_c) { }
139
+ try {
140
+ fs.unlinkSync(config_1.PATHS.llmPidFile);
141
+ }
142
+ catch (_d) { }
143
+ throw new Error(`llama-server startup timed out after ${this.config.startupWaitSec}s — check ${config_1.PATHS.llmLogFile}`);
144
+ });
145
+ }
146
+ /** Stop llama-server gracefully (SIGTERM → wait → SIGKILL). */
147
+ stop() {
148
+ return __awaiter(this, void 0, void 0, function* () {
149
+ this.stopIdleWatchdog();
150
+ const pid = this.readPid();
151
+ if (!pid)
152
+ return;
153
+ // Check if alive
154
+ try {
155
+ process.kill(pid, 0);
156
+ }
157
+ catch (_a) {
158
+ this.cleanupPidFile();
159
+ return;
160
+ }
161
+ // SIGTERM
162
+ try {
163
+ process.kill(pid, "SIGTERM");
164
+ }
165
+ catch (_b) { }
166
+ // Wait up to 5s
167
+ const deadline = Date.now() + STOP_GRACE_MS;
168
+ while (Date.now() < deadline) {
169
+ yield new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
170
+ try {
171
+ process.kill(pid, 0);
172
+ }
173
+ catch (_c) {
174
+ // Process exited
175
+ this.cleanupPidFile();
176
+ console.log(`[llm] Server stopped (PID: ${pid})`);
177
+ return;
178
+ }
179
+ }
180
+ // Force kill
181
+ try {
182
+ process.kill(pid, "SIGKILL");
183
+ }
184
+ catch (_d) { }
185
+ this.cleanupPidFile();
186
+ console.log(`[llm] Server force-killed (PID: ${pid})`);
187
+ });
188
+ }
189
+ /** Start if not running. */
190
+ ensure() {
191
+ return __awaiter(this, void 0, void 0, function* () {
192
+ if (yield this.healthy()) {
193
+ this.touchIdle();
194
+ return;
195
+ }
196
+ yield this.start();
197
+ });
198
+ }
199
+ /** Mark activity — resets idle timer. Called by inference endpoints. */
200
+ touchIdle() {
201
+ this.lastRequestTime = Date.now();
202
+ }
203
+ /** Get current status for IPC/CLI display. */
204
+ getStatus() {
205
+ const pid = this.readPid();
206
+ const alive = pid ? this.isAlive(pid) : false;
207
+ return {
208
+ running: alive,
209
+ pid: alive ? pid : null,
210
+ port: this.config.port,
211
+ model: this.config.model,
212
+ uptime: alive && this.startTime ? Math.floor((Date.now() - this.startTime) / 1000) : 0,
213
+ };
214
+ }
215
+ startIdleWatchdog() {
216
+ this.stopIdleWatchdog();
217
+ const timeoutMs = this.config.idleTimeoutMin * 60 * 1000;
218
+ this.idleTimer = setInterval(() => __awaiter(this, void 0, void 0, function* () {
219
+ if (this.lastRequestTime === 0)
220
+ return;
221
+ if (Date.now() - this.lastRequestTime > timeoutMs) {
222
+ console.log(`[llm] Server idle for ${this.config.idleTimeoutMin}min, shutting down`);
223
+ yield this.stop();
224
+ }
225
+ }), IDLE_CHECK_INTERVAL_MS);
226
+ this.idleTimer.unref();
227
+ }
228
+ stopIdleWatchdog() {
229
+ if (this.idleTimer) {
230
+ clearInterval(this.idleTimer);
231
+ this.idleTimer = null;
232
+ }
233
+ }
234
+ readPid() {
235
+ try {
236
+ const raw = fs.readFileSync(config_1.PATHS.llmPidFile, "utf-8").trim();
237
+ const pid = Number.parseInt(raw, 10);
238
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
239
+ }
240
+ catch (_a) {
241
+ return null;
242
+ }
243
+ }
244
+ isAlive(pid) {
245
+ try {
246
+ process.kill(pid, 0);
247
+ return true;
248
+ }
249
+ catch (_a) {
250
+ return false;
251
+ }
252
+ }
253
+ cleanupPidFile() {
254
+ try {
255
+ fs.unlinkSync(config_1.PATHS.llmPidFile);
256
+ }
257
+ catch (_a) { }
258
+ this.startTime = 0;
259
+ }
260
+ }
261
+ exports.LlmServer = LlmServer;
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.sendDaemonCommand = sendDaemonCommand;
46
46
  exports.isDaemonRunning = isDaemonRunning;
47
+ exports.ensureDaemonRunning = ensureDaemonRunning;
47
48
  exports.sendStreamingCommand = sendStreamingCommand;
48
49
  const net = __importStar(require("node:net"));
49
50
  const config_1 = require("../../config");
@@ -107,6 +108,26 @@ function isDaemonRunning() {
107
108
  return resp.ok === true;
108
109
  });
109
110
  }
111
+ /**
112
+ * Ensure the daemon is running — start it if needed, poll up to 5s.
113
+ * Returns true if daemon is ready, false if it couldn't be started.
114
+ */
115
+ function ensureDaemonRunning() {
116
+ return __awaiter(this, void 0, void 0, function* () {
117
+ if (yield isDaemonRunning())
118
+ return true;
119
+ const { spawnDaemon } = yield Promise.resolve().then(() => __importStar(require("./daemon-launcher")));
120
+ const pid = spawnDaemon();
121
+ if (!pid)
122
+ return false;
123
+ for (let i = 0; i < 25; i++) {
124
+ yield new Promise((r) => setTimeout(r, 200));
125
+ if (yield isDaemonRunning())
126
+ return true;
127
+ }
128
+ return false;
129
+ });
130
+ }
110
131
  const DEFAULT_STREAMING_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
111
132
  /**
112
133
  * Send a streaming command to the daemon. The daemon streams
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
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.13.0",
3
+ "version": "0.13.2",
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",
@@ -24,7 +24,7 @@ Bash(gmax "auth handler" --role ORCHESTRATION --lang ts --agent -m 3)
24
24
 
25
25
  ## Project management
26
26
 
27
- Projects must be added before search works:
27
+ Projects must be added before search works. These commands auto-start the daemon if not running:
28
28
 
29
29
  ```
30
30
  gmax add # add + index current directory