grepmax 0.4.0 → 0.5.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/README.md +17 -3
- package/dist/commands/doctor.js +46 -9
- package/dist/commands/mcp.js +17 -2
- package/dist/commands/search.js +5 -3
- package/dist/commands/summarize.js +83 -0
- package/dist/commands/watch.js +20 -9
- package/dist/index.js +2 -0
- package/dist/lib/index/index-config.js +7 -2
- package/dist/lib/index/syncer.js +89 -0
- package/dist/lib/index/watcher.js +51 -14
- package/dist/lib/store/vector-db.js +15 -0
- package/dist/lib/utils/watcher-registry.js +11 -0
- package/dist/lib/workers/orchestrator.js +1 -18
- package/dist/lib/workers/summarize/llm-client.js +8 -66
- package/mlx-embed-server/summarizer.py +10 -5
- package/package.json +2 -2
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/skills/gmax/SKILL.md +48 -25
package/README.md
CHANGED
|
@@ -24,7 +24,8 @@ Natural-language search that works like `grep`. Fast, local, and built for codin
|
|
|
24
24
|
- **Role Detection:** Distinguishes `ORCHESTRATION` (high-level logic) from `DEFINITION` (types/classes).
|
|
25
25
|
- **Local & Private:** 100% local embeddings via ONNX (CPU) or MLX (Apple Silicon GPU).
|
|
26
26
|
- **Centralized Index:** One database at `~/.gmax/` — index once, search from anywhere.
|
|
27
|
-
- **
|
|
27
|
+
- **LLM Summaries:** Optional Qwen3-Coder generates one-line descriptions per code chunk at index time.
|
|
28
|
+
- **Agent-Ready:** Pointer mode returns metadata (symbol, role, calls, summary) — no code snippets, ~80% fewer tokens.
|
|
28
29
|
|
|
29
30
|
## Quick Start
|
|
30
31
|
|
|
@@ -99,8 +100,8 @@ In our public benchmarks, `grepmax` can save about 20% of your LLM tokens and de
|
|
|
99
100
|
|
|
100
101
|
| Tool | Description |
|
|
101
102
|
| --- | --- |
|
|
102
|
-
| `semantic_search` |
|
|
103
|
-
| `search_all` | Search ALL indexed code across every directory. |
|
|
103
|
+
| `semantic_search` | Code search by meaning. Returns pointers (symbol, file:line, role, calls, summary) by default. Use `root` for cross-directory search, `detail: "code"` for snippets. |
|
|
104
|
+
| `search_all` | Search ALL indexed code across every directory. Same pointer format. |
|
|
104
105
|
| `code_skeleton` | Collapsed file structure (~4x fewer tokens than reading the full file) |
|
|
105
106
|
| `trace_calls` | Call graph — who calls a symbol and what it calls (unscoped, crosses project boundaries) |
|
|
106
107
|
| `list_symbols` | List indexed functions, classes, and types with definition locations |
|
|
@@ -228,6 +229,19 @@ On Macs with Apple Silicon, gmax defaults to MLX for GPU-accelerated embeddings.
|
|
|
228
229
|
|
|
229
230
|
To force CPU mode: `GMAX_EMBED_MODE=cpu gmax index`
|
|
230
231
|
|
|
232
|
+
### LLM Summaries
|
|
233
|
+
|
|
234
|
+
gmax can generate one-line natural language descriptions for every code chunk using a local LLM (Qwen3-Coder-30B-A3B via MLX). Summaries are pre-computed at index time and stored in LanceDB — zero latency at search time.
|
|
235
|
+
|
|
236
|
+
The summarizer server runs on port `8101` and auto-starts alongside the embed server. If unavailable, indexing proceeds without summaries.
|
|
237
|
+
|
|
238
|
+
Example search output with summaries:
|
|
239
|
+
```
|
|
240
|
+
handleAuth [exported ORCH C:8] src/auth/handler.ts:45-90
|
|
241
|
+
Validates JWT from Authorization header, checks RBAC permissions, returns 401 on failure
|
|
242
|
+
parent:AuthController calls:validateToken,checkRole,respond
|
|
243
|
+
```
|
|
244
|
+
|
|
231
245
|
## Configuration
|
|
232
246
|
|
|
233
247
|
### Ignoring Files
|
package/dist/commands/doctor.js
CHANGED
|
@@ -48,16 +48,17 @@ const os = __importStar(require("node:os"));
|
|
|
48
48
|
const path = __importStar(require("node:path"));
|
|
49
49
|
const commander_1 = require("commander");
|
|
50
50
|
const config_1 = require("../config");
|
|
51
|
+
const index_config_1 = require("../lib/index/index-config");
|
|
51
52
|
const exit_1 = require("../lib/utils/exit");
|
|
52
53
|
const project_root_1 = require("../lib/utils/project-root");
|
|
53
54
|
exports.doctor = new commander_1.Command("doctor")
|
|
54
55
|
.description("Check gmax health and paths")
|
|
55
56
|
.action(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
+
var _a;
|
|
56
58
|
console.log("🏥 gmax Doctor\n");
|
|
57
59
|
const root = config_1.PATHS.globalRoot;
|
|
58
60
|
const models = config_1.PATHS.models;
|
|
59
61
|
const grammars = config_1.PATHS.grammars;
|
|
60
|
-
const modelIds = [config_1.MODEL_IDS.embed, config_1.MODEL_IDS.colbert];
|
|
61
62
|
const checkDir = (name, p) => {
|
|
62
63
|
const exists = fs.existsSync(p);
|
|
63
64
|
const symbol = exists ? "✅" : "❌";
|
|
@@ -66,18 +67,20 @@ exports.doctor = new commander_1.Command("doctor")
|
|
|
66
67
|
checkDir("Root", root);
|
|
67
68
|
checkDir("Models", models);
|
|
68
69
|
checkDir("Grammars", grammars);
|
|
69
|
-
const
|
|
70
|
+
const globalConfig = (0, index_config_1.readGlobalConfig)();
|
|
71
|
+
const tier = (_a = config_1.MODEL_TIERS[globalConfig.modelTier]) !== null && _a !== void 0 ? _a : config_1.MODEL_TIERS.small;
|
|
72
|
+
const embedModel = globalConfig.embedMode === "gpu" ? tier.mlxModel : tier.onnxModel;
|
|
73
|
+
console.log(`\nEmbed mode: ${globalConfig.embedMode} | Model tier: ${globalConfig.modelTier} (${tier.vectorDim}d)`);
|
|
74
|
+
console.log(`Embed model: ${embedModel}`);
|
|
75
|
+
console.log(`ColBERT model: ${config_1.MODEL_IDS.colbert}`);
|
|
76
|
+
const modelStatuses = [embedModel, config_1.MODEL_IDS.colbert].map((id) => {
|
|
70
77
|
const modelPath = path.join(models, ...id.split("/"));
|
|
71
78
|
return { id, path: modelPath, exists: fs.existsSync(modelPath) };
|
|
72
79
|
});
|
|
73
|
-
modelStatuses.forEach(({ id,
|
|
74
|
-
const symbol = exists ? "✅" : "
|
|
75
|
-
console.log(`${symbol}
|
|
80
|
+
modelStatuses.forEach(({ id, exists }) => {
|
|
81
|
+
const symbol = exists ? "✅" : "⚠️ ";
|
|
82
|
+
console.log(`${symbol} ${id}: ${exists ? "downloaded" : "will download on first use"}`);
|
|
76
83
|
});
|
|
77
|
-
const missingModels = modelStatuses.filter(({ exists }) => !exists);
|
|
78
|
-
if (missingModels.length > 0) {
|
|
79
|
-
console.log("❌ Some models are missing; gmax will try bundled copies first, then download.");
|
|
80
|
-
}
|
|
81
84
|
console.log(`\nLocal Project: ${process.cwd()}`);
|
|
82
85
|
const projectRoot = (0, project_root_1.findProjectRoot)(process.cwd());
|
|
83
86
|
if (projectRoot) {
|
|
@@ -87,6 +90,40 @@ exports.doctor = new commander_1.Command("doctor")
|
|
|
87
90
|
else {
|
|
88
91
|
console.log(`ℹ️ No index found in current directory (run 'gmax index' to create one)`);
|
|
89
92
|
}
|
|
93
|
+
// Check MLX embed server
|
|
94
|
+
const embedUp = yield fetch("http://127.0.0.1:8100/health")
|
|
95
|
+
.then((r) => r.ok)
|
|
96
|
+
.catch(() => false);
|
|
97
|
+
console.log(`${embedUp ? "✅" : "⚠️ "} MLX Embed: ${embedUp ? "running (port 8100)" : "not running"}`);
|
|
98
|
+
// Check summarizer server
|
|
99
|
+
const summarizerUp = yield fetch("http://127.0.0.1:8101/health")
|
|
100
|
+
.then((r) => r.ok)
|
|
101
|
+
.catch(() => false);
|
|
102
|
+
console.log(`${summarizerUp ? "✅" : "⚠️ "} Summarizer: ${summarizerUp ? "running (port 8101)" : "not running"}`);
|
|
103
|
+
// Check summary coverage
|
|
104
|
+
try {
|
|
105
|
+
const { VectorDB } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
|
|
106
|
+
const db = new VectorDB(config_1.PATHS.lancedbDir);
|
|
107
|
+
const table = yield db.ensureTable();
|
|
108
|
+
const totalChunks = yield table.countRows();
|
|
109
|
+
if (totalChunks > 0) {
|
|
110
|
+
const withSummary = (yield table
|
|
111
|
+
.query()
|
|
112
|
+
.where("length(summary) > 5")
|
|
113
|
+
.select(["id"])
|
|
114
|
+
.toArray()).length;
|
|
115
|
+
const pct = Math.round((withSummary / totalChunks) * 100);
|
|
116
|
+
const symbol = pct >= 90 ? "✅" : pct > 0 ? "⚠️ " : "❌";
|
|
117
|
+
console.log(`${symbol} Summary coverage: ${withSummary}/${totalChunks} (${pct}%)`);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.log("ℹ️ No indexed chunks yet");
|
|
121
|
+
}
|
|
122
|
+
yield db.close();
|
|
123
|
+
}
|
|
124
|
+
catch (_b) {
|
|
125
|
+
console.log("⚠️ Could not check summary coverage");
|
|
126
|
+
}
|
|
90
127
|
console.log(`\nSystem: ${os.platform()} ${os.arch()} | Node: ${process.version}`);
|
|
91
128
|
console.log("\nIf you see ✅ everywhere, you are ready to search!");
|
|
92
129
|
yield (0, exit_1.gracefulExit)();
|
package/dist/commands/mcp.js
CHANGED
|
@@ -585,19 +585,34 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
585
585
|
}
|
|
586
586
|
function handleIndexStatus() {
|
|
587
587
|
return __awaiter(this, void 0, void 0, function* () {
|
|
588
|
-
var _a, _b, _c;
|
|
588
|
+
var _a, _b, _c, _d;
|
|
589
589
|
try {
|
|
590
590
|
const config = (0, index_config_1.readIndexConfig)(config_1.PATHS.configPath);
|
|
591
591
|
const projects = (0, project_registry_1.listProjects)();
|
|
592
592
|
const db = getVectorDb();
|
|
593
593
|
const stats = yield db.getStats();
|
|
594
594
|
const fileCount = yield db.getDistinctFileCount();
|
|
595
|
+
// Watcher status
|
|
596
|
+
const watcher = (0, watcher_registry_1.getWatcherCoveringPath)(projectRoot);
|
|
597
|
+
let watcherLine = "Watcher: not running";
|
|
598
|
+
if (watcher) {
|
|
599
|
+
const status = (_a = watcher.status) !== null && _a !== void 0 ? _a : "unknown";
|
|
600
|
+
const root = path.basename(watcher.projectRoot);
|
|
601
|
+
const reindex = watcher.lastReindex
|
|
602
|
+
? `last reindex: ${Math.round((Date.now() - watcher.lastReindex) / 60000)}m ago`
|
|
603
|
+
: "";
|
|
604
|
+
watcherLine = `Watcher: ${status} (${root}/)${reindex ? ` ${reindex}` : ""}`;
|
|
605
|
+
if (status === "syncing") {
|
|
606
|
+
watcherLine += " — search results may be incomplete";
|
|
607
|
+
}
|
|
608
|
+
}
|
|
595
609
|
const lines = [
|
|
596
610
|
`Index: ~/.gmax/lancedb (${stats.chunks} chunks, ${fileCount} files)`,
|
|
597
|
-
`Model: ${(
|
|
611
|
+
`Model: ${(_b = config === null || config === void 0 ? void 0 : config.embedModel) !== null && _b !== void 0 ? _b : "unknown"} (${(_c = config === null || config === void 0 ? void 0 : config.vectorDim) !== null && _c !== void 0 ? _c : "?"}d, ${(_d = config === null || config === void 0 ? void 0 : config.embedMode) !== null && _d !== void 0 ? _d : "unknown"})`,
|
|
598
612
|
(config === null || config === void 0 ? void 0 : config.indexedAt)
|
|
599
613
|
? `Last indexed: ${config.indexedAt}`
|
|
600
614
|
: "",
|
|
615
|
+
watcherLine,
|
|
601
616
|
"",
|
|
602
617
|
"Indexed directories:",
|
|
603
618
|
...projects.map((p) => { var _a; return ` ${p.name}\t${p.root}\t${(_a = p.lastIndexed) !== null && _a !== void 0 ? _a : "unknown"}`; }),
|
package/dist/commands/search.js
CHANGED
|
@@ -122,6 +122,7 @@ function toCompactHits(data) {
|
|
|
122
122
|
? chunk.defined_symbols.toArray().slice(0, 3)
|
|
123
123
|
: [],
|
|
124
124
|
preview: getPreviewText(chunk),
|
|
125
|
+
summary: typeof chunk.summary === "string" ? chunk.summary : undefined,
|
|
125
126
|
};
|
|
126
127
|
});
|
|
127
128
|
}
|
|
@@ -174,12 +175,12 @@ function padL(s, w) {
|
|
|
174
175
|
return " ".repeat(n) + s;
|
|
175
176
|
}
|
|
176
177
|
function formatCompactTSV(hits, projectRoot, query) {
|
|
177
|
-
var _a;
|
|
178
|
+
var _a, _b;
|
|
178
179
|
if (!hits.length)
|
|
179
180
|
return "No matches found.";
|
|
180
181
|
const lines = [];
|
|
181
182
|
lines.push(`gmax hits\tquery=${query}\tcount=${hits.length}`);
|
|
182
|
-
lines.push("path\tlines\tscore\trole\tconf\tdefined");
|
|
183
|
+
lines.push("path\tlines\tscore\trole\tconf\tdefined\tsummary");
|
|
183
184
|
for (const hit of hits) {
|
|
184
185
|
const relPath = path.isAbsolute(hit.path)
|
|
185
186
|
? path.relative(projectRoot, hit.path)
|
|
@@ -188,7 +189,8 @@ function formatCompactTSV(hits, projectRoot, query) {
|
|
|
188
189
|
const role = compactRole(hit.role);
|
|
189
190
|
const conf = compactConf(hit.confidence);
|
|
190
191
|
const defs = ((_a = hit.defined) !== null && _a !== void 0 ? _a : []).join(",");
|
|
191
|
-
|
|
192
|
+
const summary = (_b = hit.summary) !== null && _b !== void 0 ? _b : "";
|
|
193
|
+
lines.push([relPath, hit.range, score, role, conf, defs, summary].join("\t"));
|
|
192
194
|
}
|
|
193
195
|
return lines.join("\n");
|
|
194
196
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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.summarize = void 0;
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
const commander_1 = require("commander");
|
|
48
|
+
const sync_helpers_1 = require("../lib/index/sync-helpers");
|
|
49
|
+
const syncer_1 = require("../lib/index/syncer");
|
|
50
|
+
const vector_db_1 = require("../lib/store/vector-db");
|
|
51
|
+
const exit_1 = require("../lib/utils/exit");
|
|
52
|
+
const project_root_1 = require("../lib/utils/project-root");
|
|
53
|
+
exports.summarize = new commander_1.Command("summarize")
|
|
54
|
+
.description("Generate LLM summaries for indexed chunks without re-indexing")
|
|
55
|
+
.option("-p, --path <dir>", "Only summarize chunks under this directory")
|
|
56
|
+
.action((options) => __awaiter(void 0, void 0, void 0, function* () {
|
|
57
|
+
const paths = (0, project_root_1.ensureProjectPaths)(process.cwd());
|
|
58
|
+
const vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
|
|
59
|
+
const rootPrefix = options.path
|
|
60
|
+
? `${path.resolve(options.path)}/`
|
|
61
|
+
: "";
|
|
62
|
+
const { spinner } = (0, sync_helpers_1.createIndexingSpinner)("", "Summarizing...");
|
|
63
|
+
try {
|
|
64
|
+
const count = yield (0, syncer_1.generateSummaries)(vectorDb, rootPrefix, (done, total) => {
|
|
65
|
+
spinner.text = `Summarizing... (${done}/${total})`;
|
|
66
|
+
});
|
|
67
|
+
if (count > 0) {
|
|
68
|
+
spinner.succeed(`Summarized ${count} chunks`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
spinner.succeed("All chunks already have summaries (or summarizer unavailable)");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
76
|
+
spinner.fail(`Summarization failed: ${msg}`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
yield vectorDb.close();
|
|
81
|
+
yield (0, exit_1.gracefulExit)();
|
|
82
|
+
}
|
|
83
|
+
}));
|
package/dist/commands/watch.js
CHANGED
|
@@ -99,21 +99,31 @@ exports.watch = new commander_1.Command("watch")
|
|
|
99
99
|
// Propagate project root to worker processes
|
|
100
100
|
process.env.GMAX_PROJECT_ROOT = paths.root;
|
|
101
101
|
console.log(`[watch:${projectName}] Starting...`);
|
|
102
|
-
//
|
|
102
|
+
// Register early so MCP can see status
|
|
103
|
+
(0, watcher_registry_1.registerWatcher)({
|
|
104
|
+
pid: process.pid,
|
|
105
|
+
projectRoot,
|
|
106
|
+
startTime: Date.now(),
|
|
107
|
+
status: "syncing",
|
|
108
|
+
});
|
|
109
|
+
// Initial sync if this directory isn't indexed yet
|
|
103
110
|
const vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
const table = yield vectorDb.ensureTable();
|
|
112
|
+
const prefix = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
|
|
113
|
+
const indexed = yield table
|
|
114
|
+
.query()
|
|
115
|
+
.select(["id"])
|
|
116
|
+
.where(`path LIKE '${prefix}%'`)
|
|
117
|
+
.limit(1)
|
|
118
|
+
.toArray();
|
|
119
|
+
if (indexed.length === 0) {
|
|
120
|
+
console.log(`[watch:${projectName}] No index found for ${projectRoot}, running initial sync...`);
|
|
106
121
|
yield (0, syncer_1.initialSync)({ projectRoot });
|
|
107
122
|
console.log(`[watch:${projectName}] Initial sync complete.`);
|
|
108
123
|
}
|
|
124
|
+
(0, watcher_registry_1.updateWatcherStatus)(process.pid, "watching");
|
|
109
125
|
// Open resources for watcher
|
|
110
126
|
const metaCache = new meta_cache_1.MetaCache(paths.lmdbPath);
|
|
111
|
-
// Register
|
|
112
|
-
(0, watcher_registry_1.registerWatcher)({
|
|
113
|
-
pid: process.pid,
|
|
114
|
-
projectRoot,
|
|
115
|
-
startTime: Date.now(),
|
|
116
|
-
});
|
|
117
127
|
// Start watching
|
|
118
128
|
const watcher = (0, watcher_1.startWatcher)({
|
|
119
129
|
projectRoot,
|
|
@@ -123,6 +133,7 @@ exports.watch = new commander_1.Command("watch")
|
|
|
123
133
|
onReindex: (files, ms) => {
|
|
124
134
|
console.log(`[watch:${projectName}] Reindexed ${files} file${files !== 1 ? "s" : ""} (${(ms / 1000).toFixed(1)}s)`);
|
|
125
135
|
lastActivity = Date.now();
|
|
136
|
+
(0, watcher_registry_1.updateWatcherStatus)(process.pid, "watching", Date.now());
|
|
126
137
|
},
|
|
127
138
|
});
|
|
128
139
|
console.log(`[watch:${projectName}] File watcher active`);
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ const search_1 = require("./commands/search");
|
|
|
49
49
|
const serve_1 = require("./commands/serve");
|
|
50
50
|
const setup_1 = require("./commands/setup");
|
|
51
51
|
const skeleton_1 = require("./commands/skeleton");
|
|
52
|
+
const summarize_1 = require("./commands/summarize");
|
|
52
53
|
const symbols_1 = require("./commands/symbols");
|
|
53
54
|
const trace_1 = require("./commands/trace");
|
|
54
55
|
const watch_1 = require("./commands/watch");
|
|
@@ -82,5 +83,6 @@ commander_1.program.addCommand(droid_1.installDroid);
|
|
|
82
83
|
commander_1.program.addCommand(droid_1.uninstallDroid);
|
|
83
84
|
commander_1.program.addCommand(opencode_1.installOpencode);
|
|
84
85
|
commander_1.program.addCommand(opencode_1.uninstallOpencode);
|
|
86
|
+
commander_1.program.addCommand(summarize_1.summarize);
|
|
85
87
|
commander_1.program.addCommand(doctor_1.doctor);
|
|
86
88
|
commander_1.program.parse();
|
|
@@ -51,16 +51,21 @@ const path = __importStar(require("node:path"));
|
|
|
51
51
|
const config_1 = require("../../config");
|
|
52
52
|
const GLOBAL_CONFIG_PATH = path.join(config_1.PATHS.globalRoot, "config.json");
|
|
53
53
|
function readGlobalConfig() {
|
|
54
|
+
const defaultEmbedMode = process.arch === "arm64" && process.platform === "darwin" ? "gpu" : "cpu";
|
|
54
55
|
try {
|
|
55
56
|
const raw = fs.readFileSync(GLOBAL_CONFIG_PATH, "utf-8");
|
|
56
|
-
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
// Ensure embedMode has a default even if missing from stored config
|
|
59
|
+
if (!parsed.embedMode)
|
|
60
|
+
parsed.embedMode = defaultEmbedMode;
|
|
61
|
+
return parsed;
|
|
57
62
|
}
|
|
58
63
|
catch (_a) {
|
|
59
64
|
const tier = config_1.MODEL_TIERS[config_1.DEFAULT_MODEL_TIER];
|
|
60
65
|
return {
|
|
61
66
|
modelTier: config_1.DEFAULT_MODEL_TIER,
|
|
62
67
|
vectorDim: tier.vectorDim,
|
|
63
|
-
embedMode:
|
|
68
|
+
embedMode: defaultEmbedMode,
|
|
64
69
|
};
|
|
65
70
|
}
|
|
66
71
|
}
|
package/dist/lib/index/syncer.js
CHANGED
|
@@ -49,6 +49,7 @@ var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
|
49
49
|
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
50
50
|
};
|
|
51
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.generateSummaries = generateSummaries;
|
|
52
53
|
exports.initialSync = initialSync;
|
|
53
54
|
const fs = __importStar(require("node:fs"));
|
|
54
55
|
const path = __importStar(require("node:path"));
|
|
@@ -62,6 +63,69 @@ const project_root_1 = require("../utils/project-root");
|
|
|
62
63
|
const pool_1 = require("../workers/pool");
|
|
63
64
|
const index_config_1 = require("./index-config");
|
|
64
65
|
const walker_1 = require("./walker");
|
|
66
|
+
function generateSummaries(db, pathPrefix, onProgress) {
|
|
67
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
68
|
+
let summarizeChunks;
|
|
69
|
+
try {
|
|
70
|
+
const mod = yield Promise.resolve().then(() => __importStar(require("../workers/summarize/llm-client")));
|
|
71
|
+
summarizeChunks = mod.summarizeChunks;
|
|
72
|
+
}
|
|
73
|
+
catch (_a) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
// Quick availability check
|
|
77
|
+
const test = yield summarizeChunks([
|
|
78
|
+
{ code: "test", language: "ts", file: "test" },
|
|
79
|
+
]);
|
|
80
|
+
if (!test)
|
|
81
|
+
return 0;
|
|
82
|
+
const table = yield db.ensureTable();
|
|
83
|
+
const rows = yield table
|
|
84
|
+
.query()
|
|
85
|
+
.select(["id", "path", "content", "defined_symbols"])
|
|
86
|
+
.where(`path LIKE '${pathPrefix}%' AND (summary IS NULL OR summary = '')`)
|
|
87
|
+
.limit(50000)
|
|
88
|
+
.toArray();
|
|
89
|
+
if (rows.length === 0)
|
|
90
|
+
return 0;
|
|
91
|
+
let summarized = 0;
|
|
92
|
+
const BATCH_SIZE = 5;
|
|
93
|
+
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
|
94
|
+
const batch = rows.slice(i, i + BATCH_SIZE);
|
|
95
|
+
const chunks = batch.map((r) => {
|
|
96
|
+
var _a;
|
|
97
|
+
const defs = Array.isArray(r.defined_symbols)
|
|
98
|
+
? r.defined_symbols.filter((s) => typeof s === "string")
|
|
99
|
+
: typeof ((_a = r.defined_symbols) === null || _a === void 0 ? void 0 : _a.toArray) === "function"
|
|
100
|
+
? r.defined_symbols.toArray()
|
|
101
|
+
: [];
|
|
102
|
+
return {
|
|
103
|
+
code: String(r.content || ""),
|
|
104
|
+
language: path.extname(String(r.path || "")).replace(/^\./, "") || "unknown",
|
|
105
|
+
file: String(r.path || ""),
|
|
106
|
+
symbols: defs,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
const summaries = yield summarizeChunks(chunks);
|
|
110
|
+
if (!summaries)
|
|
111
|
+
break;
|
|
112
|
+
const ids = [];
|
|
113
|
+
const values = [];
|
|
114
|
+
for (let j = 0; j < batch.length; j++) {
|
|
115
|
+
if (summaries[j]) {
|
|
116
|
+
ids.push(String(batch[j].id));
|
|
117
|
+
values.push(summaries[j]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (ids.length > 0) {
|
|
121
|
+
yield db.updateRows(ids, "summary", values);
|
|
122
|
+
summarized += ids.length;
|
|
123
|
+
}
|
|
124
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress(summarized, rows.length);
|
|
125
|
+
}
|
|
126
|
+
return summarized;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
65
129
|
function flushBatch(db, meta, vectors, pendingMeta, pendingDeletes, dryRun) {
|
|
66
130
|
return __awaiter(this, void 0, void 0, function* () {
|
|
67
131
|
if (dryRun)
|
|
@@ -388,6 +452,31 @@ function initialSync(options) {
|
|
|
388
452
|
metaCache.delete(p);
|
|
389
453
|
});
|
|
390
454
|
}
|
|
455
|
+
// --- Summary post-processing (sequential, single process) ---
|
|
456
|
+
if (!dryRun && indexed > 0) {
|
|
457
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress({
|
|
458
|
+
processed,
|
|
459
|
+
indexed,
|
|
460
|
+
total,
|
|
461
|
+
filePath: "Generating summaries...",
|
|
462
|
+
});
|
|
463
|
+
const summarized = yield generateSummaries(vectorDb, rootPrefix, (count, chunkTotal) => {
|
|
464
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress({
|
|
465
|
+
processed: count,
|
|
466
|
+
indexed,
|
|
467
|
+
total: chunkTotal,
|
|
468
|
+
filePath: `Summarizing... (${count}/${chunkTotal})`,
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
if (summarized > 0) {
|
|
472
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress({
|
|
473
|
+
processed,
|
|
474
|
+
indexed,
|
|
475
|
+
total,
|
|
476
|
+
filePath: `Summarized ${summarized} chunks`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
391
480
|
// Write model config so future runs can detect model changes
|
|
392
481
|
if (!dryRun) {
|
|
393
482
|
(0, index_config_1.writeIndexConfig)(paths.configPath);
|
|
@@ -50,6 +50,7 @@ const chokidar_1 = require("chokidar");
|
|
|
50
50
|
const file_utils_1 = require("../utils/file-utils");
|
|
51
51
|
const lock_1 = require("../utils/lock");
|
|
52
52
|
const pool_1 = require("../workers/pool");
|
|
53
|
+
const llm_client_1 = require("../workers/summarize/llm-client");
|
|
53
54
|
// Chokidar ignored — must exclude heavy directories to keep FD count low.
|
|
54
55
|
// On macOS, chokidar uses FSEvents (single FD) but falls back to fs.watch()
|
|
55
56
|
// (one FD per directory) if FSEvents isn't available or for some subdirs.
|
|
@@ -103,6 +104,7 @@ function startWatcher(opts) {
|
|
|
103
104
|
pending.clear();
|
|
104
105
|
const start = Date.now();
|
|
105
106
|
let reindexed = 0;
|
|
107
|
+
const changedIds = [];
|
|
106
108
|
try {
|
|
107
109
|
const lock = yield (0, lock_1.acquireWriterLockWithRetry)(dataDir, {
|
|
108
110
|
maxRetries: 3,
|
|
@@ -115,10 +117,9 @@ function startWatcher(opts) {
|
|
|
115
117
|
const metaUpdates = new Map();
|
|
116
118
|
const metaDeletes = [];
|
|
117
119
|
for (const [absPath, event] of batch) {
|
|
118
|
-
const relPath = path.relative(projectRoot, absPath);
|
|
119
120
|
if (event === "unlink") {
|
|
120
|
-
deletes.push(
|
|
121
|
-
metaDeletes.push(
|
|
121
|
+
deletes.push(absPath);
|
|
122
|
+
metaDeletes.push(absPath);
|
|
122
123
|
reindexed++;
|
|
123
124
|
continue;
|
|
124
125
|
}
|
|
@@ -128,9 +129,9 @@ function startWatcher(opts) {
|
|
|
128
129
|
if (!(0, file_utils_1.isIndexableFile)(absPath, stats.size))
|
|
129
130
|
continue;
|
|
130
131
|
// Check if content actually changed via hash
|
|
131
|
-
const cached = metaCache.get(
|
|
132
|
+
const cached = metaCache.get(absPath);
|
|
132
133
|
const result = yield pool.processFile({
|
|
133
|
-
path:
|
|
134
|
+
path: absPath,
|
|
134
135
|
absolutePath: absPath,
|
|
135
136
|
});
|
|
136
137
|
const metaEntry = {
|
|
@@ -139,33 +140,36 @@ function startWatcher(opts) {
|
|
|
139
140
|
size: result.size,
|
|
140
141
|
};
|
|
141
142
|
if (cached && cached.hash === result.hash) {
|
|
142
|
-
|
|
143
|
-
metaUpdates.set(relPath, metaEntry);
|
|
143
|
+
metaUpdates.set(absPath, metaEntry);
|
|
144
144
|
continue;
|
|
145
145
|
}
|
|
146
146
|
if (result.shouldDelete) {
|
|
147
|
-
deletes.push(
|
|
148
|
-
metaUpdates.set(
|
|
147
|
+
deletes.push(absPath);
|
|
148
|
+
metaUpdates.set(absPath, metaEntry);
|
|
149
149
|
reindexed++;
|
|
150
150
|
continue;
|
|
151
151
|
}
|
|
152
152
|
// Delete old vectors, insert new
|
|
153
|
-
deletes.push(
|
|
153
|
+
deletes.push(absPath);
|
|
154
154
|
if (result.vectors.length > 0) {
|
|
155
155
|
vectors.push(...result.vectors);
|
|
156
|
+
// Track IDs of new vectors for summarization
|
|
157
|
+
for (const v of result.vectors) {
|
|
158
|
+
changedIds.push(v.id);
|
|
159
|
+
}
|
|
156
160
|
}
|
|
157
|
-
metaUpdates.set(
|
|
161
|
+
metaUpdates.set(absPath, metaEntry);
|
|
158
162
|
reindexed++;
|
|
159
163
|
}
|
|
160
164
|
catch (err) {
|
|
161
165
|
const code = err === null || err === void 0 ? void 0 : err.code;
|
|
162
166
|
if (code === "ENOENT") {
|
|
163
|
-
deletes.push(
|
|
164
|
-
metaDeletes.push(
|
|
167
|
+
deletes.push(absPath);
|
|
168
|
+
metaDeletes.push(absPath);
|
|
165
169
|
reindexed++;
|
|
166
170
|
}
|
|
167
171
|
else {
|
|
168
|
-
console.error(`[watch] Failed to process ${
|
|
172
|
+
console.error(`[watch] Failed to process ${absPath}:`, err);
|
|
169
173
|
}
|
|
170
174
|
}
|
|
171
175
|
}
|
|
@@ -187,6 +191,39 @@ function startWatcher(opts) {
|
|
|
187
191
|
finally {
|
|
188
192
|
yield lock.release();
|
|
189
193
|
}
|
|
194
|
+
// Summarize new/changed chunks outside the lock (sequential, no GPU contention)
|
|
195
|
+
if (changedIds.length > 0) {
|
|
196
|
+
try {
|
|
197
|
+
const table = yield vectorDb.ensureTable();
|
|
198
|
+
for (const id of changedIds) {
|
|
199
|
+
const escaped = id.replace(/'/g, "''");
|
|
200
|
+
const rows = yield table
|
|
201
|
+
.query()
|
|
202
|
+
.select(["id", "path", "content"])
|
|
203
|
+
.where(`id = '${escaped}'`)
|
|
204
|
+
.limit(1)
|
|
205
|
+
.toArray();
|
|
206
|
+
if (rows.length === 0)
|
|
207
|
+
continue;
|
|
208
|
+
const r = rows[0];
|
|
209
|
+
const lang = path.extname(String(r.path || "")).replace(/^\./, "") ||
|
|
210
|
+
"unknown";
|
|
211
|
+
const summaries = yield (0, llm_client_1.summarizeChunks)([
|
|
212
|
+
{
|
|
213
|
+
code: String(r.content || ""),
|
|
214
|
+
language: lang,
|
|
215
|
+
file: String(r.path || ""),
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
if (summaries === null || summaries === void 0 ? void 0 : summaries[0]) {
|
|
219
|
+
yield vectorDb.updateRows([id], "summary", [summaries[0]]);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (_a) {
|
|
224
|
+
// Summarizer unavailable — skip silently
|
|
225
|
+
}
|
|
226
|
+
}
|
|
190
227
|
if (reindexed > 0) {
|
|
191
228
|
const duration = Date.now() - start;
|
|
192
229
|
onReindex === null || onReindex === void 0 ? void 0 : onReindex(reindexed, duration);
|
|
@@ -314,6 +314,21 @@ class VectorDB {
|
|
|
314
314
|
}
|
|
315
315
|
});
|
|
316
316
|
}
|
|
317
|
+
updateRows(ids, field, values) {
|
|
318
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
319
|
+
var _a;
|
|
320
|
+
if (!ids.length)
|
|
321
|
+
return;
|
|
322
|
+
const table = yield this.ensureTable();
|
|
323
|
+
for (let i = 0; i < ids.length; i++) {
|
|
324
|
+
const escaped = ids[i].replace(/'/g, "''");
|
|
325
|
+
yield table.update({
|
|
326
|
+
where: `id = '${escaped}'`,
|
|
327
|
+
values: { [field]: (_a = values[i]) !== null && _a !== void 0 ? _a : "" },
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
317
332
|
deletePathsWithPrefix(prefix) {
|
|
318
333
|
return __awaiter(this, void 0, void 0, function* () {
|
|
319
334
|
const table = yield this.ensureTable();
|
|
@@ -41,6 +41,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
42
|
exports.isProcessRunning = isProcessRunning;
|
|
43
43
|
exports.registerWatcher = registerWatcher;
|
|
44
|
+
exports.updateWatcherStatus = updateWatcherStatus;
|
|
44
45
|
exports.unregisterWatcher = unregisterWatcher;
|
|
45
46
|
exports.getWatcherForProject = getWatcherForProject;
|
|
46
47
|
exports.getWatcherCoveringPath = getWatcherCoveringPath;
|
|
@@ -76,6 +77,16 @@ function registerWatcher(info) {
|
|
|
76
77
|
entries.push(info);
|
|
77
78
|
saveRegistry(entries);
|
|
78
79
|
}
|
|
80
|
+
function updateWatcherStatus(pid, status, lastReindex) {
|
|
81
|
+
const entries = loadRegistry();
|
|
82
|
+
const match = entries.find((e) => e.pid === pid);
|
|
83
|
+
if (match) {
|
|
84
|
+
match.status = status;
|
|
85
|
+
if (lastReindex)
|
|
86
|
+
match.lastReindex = lastReindex;
|
|
87
|
+
saveRegistry(entries);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
79
90
|
function unregisterWatcher(pid) {
|
|
80
91
|
const entries = loadRegistry().filter((e) => e.pid !== pid);
|
|
81
92
|
saveRegistry(entries);
|
|
@@ -49,7 +49,6 @@ const transformers_1 = require("@huggingface/transformers");
|
|
|
49
49
|
const ort = __importStar(require("onnxruntime-node"));
|
|
50
50
|
const uuid_1 = require("uuid");
|
|
51
51
|
const config_1 = require("../../config");
|
|
52
|
-
const llm_client_1 = require("./summarize/llm-client");
|
|
53
52
|
const chunker_1 = require("../index/chunker");
|
|
54
53
|
const skeleton_1 = require("../skeleton");
|
|
55
54
|
const file_utils_1 = require("../utils/file-utils");
|
|
@@ -214,23 +213,7 @@ class WorkerOrchestrator {
|
|
|
214
213
|
if (!chunks.length)
|
|
215
214
|
return { vectors: [], hash, mtimeMs, size };
|
|
216
215
|
const preparedChunks = this.toPreparedChunks(input.path, hash, chunks, skeletonResult.success ? skeletonResult.skeleton : undefined);
|
|
217
|
-
|
|
218
|
-
const lang = path.extname(input.path).replace(/^\./, "") || "unknown";
|
|
219
|
-
const [hybrids, summaries] = yield Promise.all([
|
|
220
|
-
this.computeHybrid(preparedChunks.map((chunk) => chunk.content), onProgress),
|
|
221
|
-
(0, llm_client_1.summarizeChunks)(preparedChunks.map((c) => ({
|
|
222
|
-
code: c.content,
|
|
223
|
-
language: lang,
|
|
224
|
-
file: c.path,
|
|
225
|
-
}))),
|
|
226
|
-
]);
|
|
227
|
-
// Attach summaries if available
|
|
228
|
-
if (summaries) {
|
|
229
|
-
for (let i = 0; i < preparedChunks.length; i++) {
|
|
230
|
-
if (summaries[i])
|
|
231
|
-
preparedChunks[i].summary = summaries[i];
|
|
232
|
-
}
|
|
233
|
-
}
|
|
216
|
+
const hybrids = yield this.computeHybrid(preparedChunks.map((chunk) => chunk.content), onProgress);
|
|
234
217
|
const vectors = preparedChunks.map((chunk, idx) => {
|
|
235
218
|
var _a;
|
|
236
219
|
const hybrid = (_a = hybrids[idx]) !== null && _a !== void 0 ? _a : {
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* LLM summarizer HTTP client.
|
|
4
4
|
* Talks to the MLX summarizer server to generate code summaries.
|
|
5
5
|
* Returns null if server isn't running — caller skips summaries gracefully.
|
|
6
|
+
*
|
|
7
|
+
* Called from the main syncer process (not worker processes) to avoid
|
|
8
|
+
* GPU contention from multiple concurrent workers.
|
|
6
9
|
*/
|
|
7
10
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
11
|
if (k2 === undefined) k2 = k;
|
|
@@ -48,14 +51,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
48
51
|
};
|
|
49
52
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
53
|
exports.summarizeChunks = summarizeChunks;
|
|
51
|
-
exports.resetSummarizerCache = resetSummarizerCache;
|
|
52
54
|
const http = __importStar(require("node:http"));
|
|
53
55
|
const SUMMARY_PORT = parseInt(process.env.GMAX_SUMMARY_PORT || "8101", 10);
|
|
54
56
|
const SUMMARY_HOST = "127.0.0.1";
|
|
55
|
-
const SUMMARY_TIMEOUT_MS = 120000;
|
|
56
|
-
let summarizerAvailable = null;
|
|
57
|
-
let lastCheck = 0;
|
|
58
|
-
const CHECK_INTERVAL_MS = 5000; // short cache — retry quickly if server just started
|
|
57
|
+
const SUMMARY_TIMEOUT_MS = 120000;
|
|
59
58
|
function postJSON(path, body) {
|
|
60
59
|
return new Promise((resolve) => {
|
|
61
60
|
const payload = JSON.stringify(body);
|
|
@@ -91,75 +90,18 @@ function postJSON(path, body) {
|
|
|
91
90
|
req.end();
|
|
92
91
|
});
|
|
93
92
|
}
|
|
94
|
-
function isSummarizerUp() {
|
|
95
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
96
|
-
const now = Date.now();
|
|
97
|
-
if (summarizerAvailable !== null && now - lastCheck < CHECK_INTERVAL_MS) {
|
|
98
|
-
return summarizerAvailable;
|
|
99
|
-
}
|
|
100
|
-
const result = yield new Promise((resolve) => {
|
|
101
|
-
const req = http.get({
|
|
102
|
-
hostname: SUMMARY_HOST,
|
|
103
|
-
port: SUMMARY_PORT,
|
|
104
|
-
path: "/health",
|
|
105
|
-
timeout: 5000,
|
|
106
|
-
}, (res) => {
|
|
107
|
-
res.resume();
|
|
108
|
-
resolve(res.statusCode === 200);
|
|
109
|
-
});
|
|
110
|
-
req.on("error", () => resolve(false));
|
|
111
|
-
req.on("timeout", () => {
|
|
112
|
-
req.destroy();
|
|
113
|
-
resolve(false);
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
summarizerAvailable = result;
|
|
117
|
-
lastCheck = now;
|
|
118
|
-
return result;
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
93
|
/**
|
|
122
94
|
* Generate summaries for code chunks via the local LLM server.
|
|
123
|
-
* Sends one chunk at a time. Skips health check — just tries the request.
|
|
124
|
-
* If the server is busy, the TCP connection queues until it's ready.
|
|
125
95
|
* Returns string[] on success, null if server unavailable.
|
|
126
96
|
*/
|
|
127
97
|
function summarizeChunks(chunks) {
|
|
128
98
|
return __awaiter(this, void 0, void 0, function* () {
|
|
129
|
-
var _a;
|
|
130
99
|
if (chunks.length === 0)
|
|
131
100
|
return [];
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
if (!summarizerAvailable)
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
if (summarizerAvailable === false) {
|
|
139
|
-
// Recheck periodically
|
|
140
|
-
const now = Date.now();
|
|
141
|
-
if (now - lastCheck < CHECK_INTERVAL_MS)
|
|
142
|
-
return null;
|
|
143
|
-
summarizerAvailable = yield isSummarizerUp();
|
|
144
|
-
if (!summarizerAvailable)
|
|
145
|
-
return null;
|
|
101
|
+
const { ok, data } = yield postJSON("/summarize", { chunks });
|
|
102
|
+
if (!ok || !(data === null || data === void 0 ? void 0 : data.summaries)) {
|
|
103
|
+
return null;
|
|
146
104
|
}
|
|
147
|
-
|
|
148
|
-
for (const chunk of chunks) {
|
|
149
|
-
const { ok, data } = yield postJSON("/summarize", {
|
|
150
|
-
chunks: [chunk],
|
|
151
|
-
});
|
|
152
|
-
if (!ok || !((_a = data === null || data === void 0 ? void 0 : data.summaries) === null || _a === void 0 ? void 0 : _a[0])) {
|
|
153
|
-
summaries.push("");
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
summaries.push(data.summaries[0]);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return summaries;
|
|
105
|
+
return data.summaries;
|
|
160
106
|
});
|
|
161
107
|
}
|
|
162
|
-
function resetSummarizerCache() {
|
|
163
|
-
summarizerAvailable = null;
|
|
164
|
-
lastCheck = 0;
|
|
165
|
-
}
|
|
@@ -50,8 +50,12 @@ SYSTEM_PROMPT = """You are a code summarizer. Given a code chunk, produce exactl
|
|
|
50
50
|
Be specific about business logic, services, and side effects. Do not describe syntax.
|
|
51
51
|
Do not use phrases like "This function" or "This code". Start with a verb."""
|
|
52
52
|
|
|
53
|
-
def build_prompt(code: str, language: str, file: str) -> str:
|
|
54
|
-
|
|
53
|
+
def build_prompt(code: str, language: str, file: str, symbols: list[str] | None = None) -> str:
|
|
54
|
+
parts = [f"Language: {language}", f"File: {file}"]
|
|
55
|
+
if symbols:
|
|
56
|
+
parts.append(f"Defines: {', '.join(symbols)}")
|
|
57
|
+
parts.append(f"\n```\n{code}\n```")
|
|
58
|
+
return "\n".join(parts)
|
|
55
59
|
|
|
56
60
|
|
|
57
61
|
def is_port_in_use(port: int) -> bool:
|
|
@@ -59,11 +63,11 @@ def is_port_in_use(port: int) -> bool:
|
|
|
59
63
|
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
60
64
|
|
|
61
65
|
|
|
62
|
-
def summarize_chunk(code: str, language: str, file: str) -> str:
|
|
66
|
+
def summarize_chunk(code: str, language: str, file: str, symbols: list[str] | None = None) -> str:
|
|
63
67
|
"""Generate a one-line summary for a code chunk."""
|
|
64
68
|
messages = [
|
|
65
69
|
{"role": "system", "content": SYSTEM_PROMPT},
|
|
66
|
-
{"role": "user", "content": build_prompt(code, language, file)},
|
|
70
|
+
{"role": "user", "content": build_prompt(code, language, file, symbols)},
|
|
67
71
|
]
|
|
68
72
|
prompt = tokenizer.apply_chat_template(
|
|
69
73
|
messages, tokenize=False, add_generation_prompt=True
|
|
@@ -106,6 +110,7 @@ class ChunkInput(BaseModel):
|
|
|
106
110
|
code: str
|
|
107
111
|
language: str = "unknown"
|
|
108
112
|
file: str = ""
|
|
113
|
+
symbols: list[str] = []
|
|
109
114
|
|
|
110
115
|
|
|
111
116
|
class SummarizeRequest(BaseModel):
|
|
@@ -125,7 +130,7 @@ async def summarize(request: SummarizeRequest) -> SummarizeResponse:
|
|
|
125
130
|
async with _mlx_lock:
|
|
126
131
|
for chunk in request.chunks:
|
|
127
132
|
try:
|
|
128
|
-
summary = summarize_chunk(chunk.code, chunk.language, chunk.file)
|
|
133
|
+
summary = summarize_chunk(chunk.code, chunk.language, chunk.file, chunk.symbols or None)
|
|
129
134
|
summaries.append(summary)
|
|
130
135
|
except Exception as e:
|
|
131
136
|
summaries.append(f"(summary failed: {e})")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grepmax",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"author": "Robert Owens <robowens@me.com>",
|
|
5
5
|
"homepage": "https://github.com/reowens/grepmax",
|
|
6
6
|
"bugs": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"NOTICE"
|
|
30
30
|
],
|
|
31
31
|
"license": "Apache-2.0",
|
|
32
|
-
"description": "
|
|
32
|
+
"description": "Semantic code search for coding agents. Local embeddings, LLM summaries, call graph tracing.",
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@clack/prompts": "^1.1.0",
|
|
35
35
|
"@huggingface/transformers": "^3.8.0",
|
|
@@ -6,63 +6,86 @@ allowed-tools: "mcp__grepmax__semantic_search, mcp__grepmax__search_all, mcp__gr
|
|
|
6
6
|
|
|
7
7
|
## What gmax does
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Semantic code search — finds code by meaning, not just strings.
|
|
10
10
|
|
|
11
|
-
- grep/ripgrep: exact string match
|
|
12
|
-
- gmax: concept match,
|
|
11
|
+
- grep/ripgrep: exact string match
|
|
12
|
+
- gmax: concept match ("where do we handle auth?", "how does booking flow work?")
|
|
13
13
|
|
|
14
14
|
## MCP tools
|
|
15
15
|
|
|
16
16
|
### semantic_search
|
|
17
|
-
Search code by meaning.
|
|
18
|
-
- `query` (required): Natural language. Be specific — more words = better results.
|
|
19
|
-
- `limit` (optional): Max results (default 3, max 50)
|
|
20
|
-
- `root` (optional): Directory to search. Defaults to project root. Use to search a parent directory (e.g. `root: "../"` to search the monorepo).
|
|
21
|
-
- `path` (optional): Restrict to path prefix (e.g. "src/auth/")
|
|
22
|
-
- `detail` (optional): `"pointer"` (default) or `"code"` (adds 4-line numbered snippets)
|
|
23
|
-
- `min_score` (optional): Filter by minimum relevance score (0-1)
|
|
24
|
-
- `max_per_file` (optional): Cap results per file for diversity
|
|
17
|
+
Search code by meaning. Two output modes:
|
|
25
18
|
|
|
26
|
-
**
|
|
19
|
+
**Pointer mode (default)** — returns metadata + LLM-generated summary per result:
|
|
27
20
|
```
|
|
28
21
|
handleAuth [exported ORCH C:8] src/auth/handler.ts:45-90
|
|
22
|
+
Validates JWT from Authorization header, checks RBAC permissions, returns 401 on failure
|
|
29
23
|
parent:AuthController calls:validateToken,checkRole,respond
|
|
30
24
|
```
|
|
31
25
|
|
|
32
|
-
**
|
|
26
|
+
**Code mode (`detail: "code"`)** — includes 4-line numbered code snippets:
|
|
27
|
+
```
|
|
28
|
+
handleAuth [exported ORCH C:8] src/auth/handler.ts:45-90
|
|
29
|
+
Validates JWT from Authorization header, checks RBAC permissions, returns 401 on failure
|
|
30
|
+
parent:AuthController calls:validateToken,checkRole,respond
|
|
31
|
+
45│ const token = req.headers.get("Authorization");
|
|
32
|
+
46│ const claims = await validateToken(token);
|
|
33
|
+
47│ if (!claims) return unauthorized();
|
|
34
|
+
48│ const allowed = await checkRole(claims.role, req.path);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
- `query` (required): Natural language. More words = better results.
|
|
39
|
+
- `limit` (optional): Max results (default 3, max 50)
|
|
40
|
+
- `root` (optional): Directory to search. Use `root: "../"` to search a parent directory.
|
|
41
|
+
- `path` (optional): Restrict to path prefix (e.g. "src/auth/")
|
|
42
|
+
- `detail` (optional): `"pointer"` (default) or `"code"`
|
|
43
|
+
- `min_score` (optional): Filter by minimum relevance score (0-1)
|
|
44
|
+
- `max_per_file` (optional): Cap results per file for diversity
|
|
45
|
+
|
|
46
|
+
**When to use which mode:**
|
|
47
|
+
- `pointer` — navigation, finding locations, understanding architecture
|
|
48
|
+
- `code` — comparing implementations, finding duplicates, checking syntax
|
|
33
49
|
|
|
34
50
|
### search_all
|
|
35
|
-
Search ALL indexed code across every directory. Same
|
|
51
|
+
Search ALL indexed code across every directory. Same modes as semantic_search.
|
|
36
52
|
|
|
37
53
|
### code_skeleton
|
|
38
|
-
|
|
54
|
+
File structure — signatures with bodies collapsed (~4x fewer tokens).
|
|
39
55
|
- `target` (required): File path relative to project root
|
|
40
56
|
|
|
41
57
|
### trace_calls
|
|
42
|
-
|
|
43
|
-
- `symbol` (required): Function/method/class name
|
|
58
|
+
Call graph — who calls a symbol and what it calls. Unscoped — follows calls across all indexed directories.
|
|
59
|
+
- `symbol` (required): Function/method/class name
|
|
44
60
|
|
|
45
61
|
### list_symbols
|
|
46
62
|
List indexed symbols with definition locations.
|
|
47
|
-
- `pattern` (optional): Filter by name
|
|
48
|
-
- `limit` (optional): Max results (default 20
|
|
63
|
+
- `pattern` (optional): Filter by name
|
|
64
|
+
- `limit` (optional): Max results (default 20)
|
|
49
65
|
- `path` (optional): Only symbols under this path prefix
|
|
50
66
|
|
|
51
67
|
### index_status
|
|
52
|
-
Check centralized index health —
|
|
68
|
+
Check centralized index health — chunks, files, indexed directories, model info.
|
|
53
69
|
|
|
54
70
|
## Workflow
|
|
55
71
|
|
|
56
|
-
1. **
|
|
72
|
+
1. **Search** — `semantic_search` to find relevant code (pointers by default)
|
|
57
73
|
2. **Read** — `Read file:line` for the specific ranges you need
|
|
58
|
-
3. **
|
|
59
|
-
4. **
|
|
74
|
+
3. **Compare** — `semantic_search` with `detail: "code"` when comparing implementations
|
|
75
|
+
4. **Trace** — `trace_calls` to understand call flow across files
|
|
76
|
+
5. **Skeleton** — `code_skeleton` before reading large files
|
|
77
|
+
|
|
78
|
+
## If results seem stale
|
|
60
79
|
|
|
61
|
-
|
|
80
|
+
1. Check `index_status` — if watcher shows "syncing", results may be incomplete. Wait for it.
|
|
81
|
+
2. To force a re-index: `Bash(gmax index)` (indexes current directory)
|
|
82
|
+
3. To add summaries without re-indexing: `Bash(gmax summarize)`
|
|
83
|
+
4. Do NOT use `gmax reindex` — it doesn't exist.
|
|
62
84
|
|
|
63
85
|
## Tips
|
|
64
86
|
|
|
65
87
|
- More words = better results. "auth" is vague. "where does the server validate JWT tokens" is specific.
|
|
66
|
-
- ORCH results contain the logic — prioritize
|
|
88
|
+
- ORCH results contain the logic — prioritize over DEF/IMPL.
|
|
89
|
+
- Summaries tell you what the code does without reading it. Use them to decide what to Read.
|
|
67
90
|
- Use `root` to search parent directories (monorepo, workspace).
|
|
68
91
|
- Use `search_all` sparingly — it searches everything indexed.
|