grepmax 0.24.0 → 0.25.1
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 +1 -1
- package/README.md +15 -6
- package/dist/commands/add.js +38 -31
- package/dist/commands/config.js +22 -15
- package/dist/commands/context.js +38 -13
- package/dist/commands/doctor.js +86 -83
- package/dist/commands/extract.js +12 -1
- package/dist/commands/index.js +23 -24
- package/dist/commands/list.js +31 -14
- package/dist/commands/mcp.js +278 -143
- package/dist/commands/peek.js +16 -5
- package/dist/commands/repair.js +54 -120
- package/dist/commands/search-output.js +30 -9
- package/dist/commands/search-run.js +75 -47
- package/dist/commands/search-skeletons.js +28 -18
- package/dist/commands/search.js +45 -49
- package/dist/commands/serve.js +415 -373
- package/dist/commands/setup.js +2 -2
- package/dist/commands/similar.js +5 -5
- package/dist/commands/skeleton.js +67 -41
- package/dist/commands/status.js +11 -2
- package/dist/commands/summarize.js +6 -0
- package/dist/commands/watch.js +102 -38
- package/dist/config.js +3 -0
- package/dist/lib/daemon/daemon.js +1108 -379
- package/dist/lib/daemon/ipc-handler.js +122 -13
- package/dist/lib/daemon/mlx-server-manager.js +268 -125
- package/dist/lib/daemon/process-manager.js +7 -4
- package/dist/lib/daemon/search-handler.js +23 -9
- package/dist/lib/daemon/watcher-manager.js +353 -110
- package/dist/lib/index/batch-processor.js +231 -67
- package/dist/lib/index/embedding-generation.js +109 -0
- package/dist/lib/index/embedding-status.js +37 -0
- package/dist/lib/index/file-policy.js +273 -0
- package/dist/lib/index/ignore-patterns.js +4 -0
- package/dist/lib/index/index-config.js +18 -4
- package/dist/lib/index/syncer.js +256 -79
- package/dist/lib/index/walker.js +66 -86
- package/dist/lib/index/watcher-batch.js +9 -0
- package/dist/lib/index/watcher.js +129 -3
- package/dist/lib/llm/server.js +6 -0
- package/dist/lib/search/searcher.js +30 -19
- package/dist/lib/store/store-lease.js +473 -0
- package/dist/lib/store/vector-db.js +302 -57
- package/dist/lib/utils/blocked-roots.js +63 -0
- package/dist/lib/utils/cross-project.js +7 -3
- package/dist/lib/utils/daemon-client.js +24 -1
- package/dist/lib/utils/daemon-launcher.js +38 -13
- package/dist/lib/utils/doctor-status.js +76 -0
- package/dist/lib/utils/file-utils.js +74 -4
- package/dist/lib/utils/keyed-mutex.js +101 -0
- package/dist/lib/utils/logger.js +57 -1
- package/dist/lib/utils/mlx-hf-cache.js +114 -0
- package/dist/lib/utils/operation-coordinator.js +146 -0
- package/dist/lib/utils/path-containment.js +106 -0
- package/dist/lib/utils/process.js +44 -0
- package/dist/lib/utils/project-registry.js +351 -3
- package/dist/lib/utils/scope-filter.js +3 -9
- package/dist/lib/utils/stale-hint.js +12 -19
- package/dist/lib/utils/watcher-launcher.js +5 -1
- package/dist/lib/utils/watcher-store.js +2 -2
- package/dist/lib/workers/colbert-math.js +15 -12
- package/dist/lib/workers/embeddings/colbert.js +3 -2
- package/dist/lib/workers/embeddings/granite.js +4 -3
- package/dist/lib/workers/embeddings/mlx-client.js +59 -16
- package/dist/lib/workers/orchestrator.js +39 -8
- package/dist/lib/workers/pool.js +146 -82
- package/dist/lib/workers/process-child.js +11 -2
- package/dist/lib/workers/serialized-handler.js +10 -0
- package/dist/lib/workers/worker.js +13 -4
- package/mlx-embed-server/server.py +21 -3
- package/mlx-embed-server/summarizer.py +8 -0
- package/package.json +4 -3
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/hooks/start.js +3 -170
|
@@ -48,7 +48,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
48
48
|
const node_process_1 = __importDefault(require("node:process"));
|
|
49
49
|
node_process_1.default.title = "gmax-worker";
|
|
50
50
|
const logger_1 = require("../utils/logger");
|
|
51
|
+
const serialized_handler_1 = require("./serialized-handler");
|
|
51
52
|
const worker_1 = __importStar(require("./worker"));
|
|
53
|
+
// Workers inherit the daemon's stdio (daemon.log) — stamp lines the same way
|
|
54
|
+
// the daemon does. No-op for workers forked by interactive CLI commands.
|
|
55
|
+
if (node_process_1.default.env[logger_1.LOG_TIMESTAMPS_ENV] === "1")
|
|
56
|
+
(0, logger_1.installTimestampedOutput)();
|
|
52
57
|
// Every outgoing message also carries `rss` (see send()).
|
|
53
58
|
const send = (msg) => {
|
|
54
59
|
if (node_process_1.default.send) {
|
|
@@ -57,7 +62,7 @@ const send = (msg) => {
|
|
|
57
62
|
node_process_1.default.send(Object.assign(Object.assign({}, msg), { rss: node_process_1.default.memoryUsage().rss }));
|
|
58
63
|
}
|
|
59
64
|
};
|
|
60
|
-
|
|
65
|
+
const handleMessage = (msg) => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
66
|
const { id, method, payload } = msg;
|
|
62
67
|
const start = performance.now();
|
|
63
68
|
(0, logger_1.debug)("worker", `recv task=${id} method=${method}${method === "processFile" ? ` file=${payload.path}` : ""}`);
|
|
@@ -90,7 +95,11 @@ node_process_1.default.on("message", (msg) => __awaiter(void 0, void 0, void 0,
|
|
|
90
95
|
(0, logger_1.debug)("worker", `fail task=${id} method=${method} ${(performance.now() - start).toFixed(0)}ms: ${message}`);
|
|
91
96
|
send({ id, error: message });
|
|
92
97
|
}
|
|
93
|
-
})
|
|
98
|
+
});
|
|
99
|
+
const handleMessageSerially = (0, serialized_handler_1.createSerializedHandler)(handleMessage);
|
|
100
|
+
node_process_1.default.on("message", (msg) => {
|
|
101
|
+
void handleMessageSerially(msg);
|
|
102
|
+
});
|
|
94
103
|
node_process_1.default.on("uncaughtException", (err) => {
|
|
95
104
|
console.error("[process-worker] uncaughtException", err);
|
|
96
105
|
node_process_1.default.exitCode = 1;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSerializedHandler = createSerializedHandler;
|
|
4
|
+
function createSerializedHandler(handler) {
|
|
5
|
+
let chain = Promise.resolve();
|
|
6
|
+
return (message) => {
|
|
7
|
+
chain = chain.then(() => handler(message), () => handler(message));
|
|
8
|
+
return chain;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -12,11 +12,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.default = processFile;
|
|
13
13
|
exports.encodeQuery = encodeQuery;
|
|
14
14
|
exports.rerank = rerank;
|
|
15
|
+
const embedding_generation_1 = require("../index/embedding-generation");
|
|
16
|
+
const index_config_1 = require("../index/index-config");
|
|
15
17
|
const orchestrator_1 = require("./orchestrator");
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
18
|
+
const hasSerializedRuntime = !!process.env.GMAX_EMBEDDING_GENERATION &&
|
|
19
|
+
(process.env.GMAX_EMBED_MODE === "cpu" ||
|
|
20
|
+
process.env.GMAX_EMBED_MODE === "gpu");
|
|
21
|
+
const runtimeConfig = hasSerializedRuntime ? null : (0, index_config_1.readGlobalConfig)();
|
|
22
|
+
const generation = process.env.GMAX_EMBEDDING_GENERATION
|
|
23
|
+
? (0, embedding_generation_1.parseEmbeddingGeneration)(JSON.parse(process.env.GMAX_EMBEDDING_GENERATION))
|
|
24
|
+
: (0, embedding_generation_1.resolveEmbeddingGeneration)(runtimeConfig);
|
|
25
|
+
const embedMode = process.env.GMAX_EMBED_MODE === "cpu" || process.env.GMAX_EMBED_MODE === "gpu"
|
|
26
|
+
? process.env.GMAX_EMBED_MODE
|
|
27
|
+
: runtimeConfig.embedMode;
|
|
28
|
+
const orchestrator = new orchestrator_1.WorkerOrchestrator(generation, embedMode);
|
|
20
29
|
function processFile(input, onProgress) {
|
|
21
30
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22
31
|
return orchestrator.processFile(input, onProgress);
|
|
@@ -63,7 +63,7 @@ logger = logging.getLogger("mlx-embed")
|
|
|
63
63
|
|
|
64
64
|
import mlx.core as mx
|
|
65
65
|
import uvicorn
|
|
66
|
-
from fastapi import FastAPI
|
|
66
|
+
from fastapi import FastAPI, HTTPException
|
|
67
67
|
from mlx_embeddings import load
|
|
68
68
|
from pydantic import BaseModel
|
|
69
69
|
from transformers import AutoTokenizer
|
|
@@ -71,6 +71,7 @@ from transformers import AutoTokenizer
|
|
|
71
71
|
MODEL_ID = os.environ.get(
|
|
72
72
|
"MLX_EMBED_MODEL", "ibm-granite/granite-embedding-small-english-r2"
|
|
73
73
|
)
|
|
74
|
+
OWNER_TOKEN = os.environ.get("GMAX_EMBED_OWNER_TOKEN")
|
|
74
75
|
PORT = int(os.environ.get("MLX_EMBED_PORT", "8100"))
|
|
75
76
|
MAX_BATCH = int(os.environ.get("MLX_EMBED_MAX_BATCH", "64"))
|
|
76
77
|
IDLE_TIMEOUT_S = int(os.environ.get("MLX_EMBED_IDLE_TIMEOUT", "1800")) # 30 min
|
|
@@ -159,11 +160,13 @@ app = FastAPI(lifespan=lifespan)
|
|
|
159
160
|
|
|
160
161
|
class EmbedRequest(BaseModel):
|
|
161
162
|
texts: list[str]
|
|
163
|
+
expected_model: str | None = None
|
|
162
164
|
|
|
163
165
|
|
|
164
166
|
class EmbedResponse(BaseModel):
|
|
165
167
|
vectors: list[list[float]]
|
|
166
168
|
dim: int
|
|
169
|
+
model: str
|
|
167
170
|
|
|
168
171
|
|
|
169
172
|
@app.post("/embed")
|
|
@@ -171,7 +174,18 @@ async def embed(request: EmbedRequest) -> EmbedResponse:
|
|
|
171
174
|
global last_activity
|
|
172
175
|
last_activity = time.time()
|
|
173
176
|
|
|
174
|
-
|
|
177
|
+
if len(request.texts) > MAX_BATCH:
|
|
178
|
+
raise HTTPException(
|
|
179
|
+
status_code=413,
|
|
180
|
+
detail=f"Batch contains {len(request.texts)} texts; maximum is {MAX_BATCH}",
|
|
181
|
+
)
|
|
182
|
+
if request.expected_model and request.expected_model != MODEL_ID:
|
|
183
|
+
raise HTTPException(
|
|
184
|
+
status_code=409,
|
|
185
|
+
detail=f"Requested model {request.expected_model} is not loaded",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
texts = request.texts
|
|
175
189
|
start = time.monotonic()
|
|
176
190
|
|
|
177
191
|
async with _mlx_lock:
|
|
@@ -191,6 +205,7 @@ async def embed(request: EmbedRequest) -> EmbedResponse:
|
|
|
191
205
|
return EmbedResponse(
|
|
192
206
|
vectors=vectors_list,
|
|
193
207
|
dim=len(vectors_list[0]) if vectors_list else 0,
|
|
208
|
+
model=MODEL_ID,
|
|
194
209
|
)
|
|
195
210
|
|
|
196
211
|
|
|
@@ -198,7 +213,10 @@ async def embed(request: EmbedRequest) -> EmbedResponse:
|
|
|
198
213
|
async def health():
|
|
199
214
|
global last_activity
|
|
200
215
|
last_activity = time.time()
|
|
201
|
-
|
|
216
|
+
response = {"status": "ok", "model": MODEL_ID}
|
|
217
|
+
if OWNER_TOKEN:
|
|
218
|
+
response["owner"] = OWNER_TOKEN
|
|
219
|
+
return response
|
|
202
220
|
|
|
203
221
|
|
|
204
222
|
def main():
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
"""MLX-accelerated code summarizer for grepmax.
|
|
2
2
|
|
|
3
|
+
⛔ HARD STOP FOR AI AGENTS: DO NOT start this server (uv run python
|
|
4
|
+
summarizer.py) or otherwise load this model without the user's explicit,
|
|
5
|
+
in-session authorization. It loads Qwen3-Coder-30B-A3B (~16GB) onto the GPU and
|
|
6
|
+
has FROZEN/CRASHED the machine. This summarizer is decommissioned — the calling
|
|
7
|
+
code path (generateSummaries) is a no-op stub. A plan or prompt that says
|
|
8
|
+
"run gmax summarize" or "measure a sample" is NOT authorization to launch this.
|
|
9
|
+
Stop and ask first. See the "HARD STOP" section at the top of CLAUDE.md.
|
|
10
|
+
|
|
3
11
|
Runs Qwen3-Coder-30B-A3B on Apple Silicon GPU to generate one-line
|
|
4
12
|
summaries of code chunks during indexing. Summaries are stored in
|
|
5
13
|
LanceDB and returned in search results.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
|
|
5
5
|
"homepage": "https://github.com/reowens/grepmax",
|
|
6
6
|
"bugs": {
|
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
"lint": "biome lint .",
|
|
39
39
|
"lint:fix": "biome check --write .",
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
41
|
+
"test:typecheck": "tsc -p tsconfig.tests.json",
|
|
42
|
+
"prepublishOnly": "pnpm run test:typecheck && pnpm build",
|
|
43
|
+
"preversion": "pnpm test && pnpm typecheck && pnpm run test:typecheck && pnpm run format:check",
|
|
43
44
|
"version": "bash scripts/sync-versions.sh && git add -A",
|
|
44
45
|
"postversion": "bash scripts/postrelease.sh"
|
|
45
46
|
},
|
|
@@ -1,46 +1,6 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
2
|
const _path = require("node:path");
|
|
3
|
-
const
|
|
4
|
-
const { spawn, execFileSync } = require("node:child_process");
|
|
5
|
-
|
|
6
|
-
function isServerRunning(port) {
|
|
7
|
-
return new Promise((resolve) => {
|
|
8
|
-
const req = http.get(
|
|
9
|
-
{ hostname: "127.0.0.1", port, path: "/health", timeout: 1000 },
|
|
10
|
-
(res) => {
|
|
11
|
-
res.resume();
|
|
12
|
-
resolve(res.statusCode === 200);
|
|
13
|
-
},
|
|
14
|
-
);
|
|
15
|
-
req.on("error", () => resolve(false));
|
|
16
|
-
req.on("timeout", () => {
|
|
17
|
-
req.destroy();
|
|
18
|
-
resolve(false);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function findMlxServerDir() {
|
|
24
|
-
// Try to find mlx-embed-server relative to the gmax binary (npm install location)
|
|
25
|
-
try {
|
|
26
|
-
const gmaxPath = execFileSync("which gmax", {
|
|
27
|
-
encoding: "utf-8",
|
|
28
|
-
}).trim();
|
|
29
|
-
// gmax binary is a symlink in .bin/ → resolve to package root
|
|
30
|
-
const realPath = fs.realpathSync(gmaxPath);
|
|
31
|
-
const pkgRoot = _path.resolve(_path.dirname(realPath), "..");
|
|
32
|
-
const serverDir = _path.join(pkgRoot, "mlx-embed-server");
|
|
33
|
-
if (fs.existsSync(_path.join(serverDir, "server.py"))) return serverDir;
|
|
34
|
-
} catch {}
|
|
35
|
-
|
|
36
|
-
// Fallback: dev mode — relative to plugin root
|
|
37
|
-
const pluginRoot = __dirname.replace(/\/hooks$/, "");
|
|
38
|
-
const devRoot = _path.resolve(pluginRoot, "../..");
|
|
39
|
-
const devDir = _path.join(devRoot, "mlx-embed-server");
|
|
40
|
-
if (fs.existsSync(_path.join(devDir, "server.py"))) return devDir;
|
|
41
|
-
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
3
|
+
const { execFileSync } = require("node:child_process");
|
|
44
4
|
|
|
45
5
|
// Inline fallback so SessionStart context is never empty if the installed
|
|
46
6
|
// gmax package can't be resolved (e.g. gmax not yet on PATH). The canonical
|
|
@@ -101,81 +61,6 @@ function getSessionStartHint() {
|
|
|
101
61
|
return FALLBACK_SESSION_START_HINT;
|
|
102
62
|
}
|
|
103
63
|
|
|
104
|
-
function startPythonServer(serverDir, scriptName, logName, processName) {
|
|
105
|
-
if (!serverDir) return;
|
|
106
|
-
|
|
107
|
-
const logDir = _path.join(require("node:os").homedir(), ".gmax", "logs");
|
|
108
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
109
|
-
const logPath = _path.join(logDir, `${logName}.log`);
|
|
110
|
-
|
|
111
|
-
// Rotate if > 5MB (same threshold as watch.ts)
|
|
112
|
-
try {
|
|
113
|
-
const stat = fs.statSync(logPath);
|
|
114
|
-
if (stat.size > 5 * 1024 * 1024) {
|
|
115
|
-
fs.renameSync(logPath, `${logPath}.prev`);
|
|
116
|
-
}
|
|
117
|
-
} catch {}
|
|
118
|
-
|
|
119
|
-
const out = fs.openSync(logPath, "a");
|
|
120
|
-
|
|
121
|
-
const child = spawn("uv", ["run", "python", scriptName], {
|
|
122
|
-
cwd: serverDir,
|
|
123
|
-
detached: true,
|
|
124
|
-
stdio: ["ignore", out, out],
|
|
125
|
-
env: {
|
|
126
|
-
...process.env,
|
|
127
|
-
VIRTUAL_ENV: "",
|
|
128
|
-
CONDA_DEFAULT_ENV: "",
|
|
129
|
-
GMAX_PROCESS_NAME: processName || logName,
|
|
130
|
-
HF_TOKEN_PATH:
|
|
131
|
-
process.env.HF_TOKEN_PATH ||
|
|
132
|
-
_path.join(
|
|
133
|
-
require("node:os").homedir(),
|
|
134
|
-
".cache",
|
|
135
|
-
"huggingface",
|
|
136
|
-
"token",
|
|
137
|
-
),
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
child.unref();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// --- Crash counter (Item 14) ---
|
|
144
|
-
const CRASH_FILE = _path.join(
|
|
145
|
-
require("node:os").homedir(),
|
|
146
|
-
".gmax",
|
|
147
|
-
"mlx-embed-crashes.json",
|
|
148
|
-
);
|
|
149
|
-
const MAX_CRASHES = 3;
|
|
150
|
-
const CRASH_WINDOW_MS = 10 * 60 * 1000; // 10 minutes
|
|
151
|
-
|
|
152
|
-
function readCrashCount() {
|
|
153
|
-
try {
|
|
154
|
-
const data = JSON.parse(fs.readFileSync(CRASH_FILE, "utf-8"));
|
|
155
|
-
if (
|
|
156
|
-
data.lastCrash &&
|
|
157
|
-
Date.now() - new Date(data.lastCrash).getTime() > CRASH_WINDOW_MS
|
|
158
|
-
) {
|
|
159
|
-
return { count: 0, lastCrash: null }; // Window expired, reset
|
|
160
|
-
}
|
|
161
|
-
return { count: data.count || 0, lastCrash: data.lastCrash };
|
|
162
|
-
} catch {
|
|
163
|
-
return { count: 0, lastCrash: null };
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function writeCrashCount(count, lastCrash) {
|
|
168
|
-
try {
|
|
169
|
-
fs.writeFileSync(CRASH_FILE, JSON.stringify({ count, lastCrash }));
|
|
170
|
-
} catch {}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function resetCrashCount() {
|
|
174
|
-
try {
|
|
175
|
-
fs.unlinkSync(CRASH_FILE);
|
|
176
|
-
} catch {}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
64
|
function isProjectRegistered() {
|
|
180
65
|
try {
|
|
181
66
|
const projectsPath = _path.join(
|
|
@@ -210,60 +95,8 @@ function startWatcher() {
|
|
|
210
95
|
}
|
|
211
96
|
}
|
|
212
97
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (embedMode !== "cpu") {
|
|
217
|
-
const serverDir = findMlxServerDir();
|
|
218
|
-
|
|
219
|
-
// Start MLX embed server (port 8100)
|
|
220
|
-
const embedRunning = await isServerRunning(8100);
|
|
221
|
-
if (serverDir && !embedRunning) {
|
|
222
|
-
const crashes = readCrashCount();
|
|
223
|
-
if (crashes.count < MAX_CRASHES) {
|
|
224
|
-
startPythonServer(
|
|
225
|
-
serverDir,
|
|
226
|
-
"server.py",
|
|
227
|
-
"mlx-embed-server",
|
|
228
|
-
"gmax-embed",
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
// Fire-and-forget health verification (Item 13)
|
|
232
|
-
(async () => {
|
|
233
|
-
const maxAttempts = 5;
|
|
234
|
-
const delayMs = 2000;
|
|
235
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
236
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
237
|
-
if (await isServerRunning(8100)) {
|
|
238
|
-
resetCrashCount();
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
// Server didn't start after 10s — record crash
|
|
243
|
-
const c = readCrashCount();
|
|
244
|
-
writeCrashCount(c.count + 1, new Date().toISOString());
|
|
245
|
-
})();
|
|
246
|
-
}
|
|
247
|
-
} else if (embedRunning) {
|
|
248
|
-
resetCrashCount();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Start LLM summarizer server (port 8101) — opt-in only
|
|
252
|
-
if (
|
|
253
|
-
process.env.GMAX_SUMMARIZER === "1" &&
|
|
254
|
-
serverDir &&
|
|
255
|
-
!(await isServerRunning(8101))
|
|
256
|
-
) {
|
|
257
|
-
startPythonServer(
|
|
258
|
-
serverDir,
|
|
259
|
-
"summarizer.py",
|
|
260
|
-
"mlx-summarizer",
|
|
261
|
-
"gmax-summarizer",
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Start a file watcher for the current project (30-min idle timeout)
|
|
98
|
+
function main() {
|
|
99
|
+
// The daemon owns embed-server configuration and lifecycle.
|
|
267
100
|
startWatcher();
|
|
268
101
|
|
|
269
102
|
const response = {
|