suture-mcp 0.1.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/LICENSE +21 -0
- package/README.md +246 -0
- package/dist/cli.cjs +3033 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3006 -0
- package/dist/index.cjs +2908 -0
- package/dist/index.d.cts +634 -0
- package/dist/index.d.ts +634 -0
- package/dist/index.js +2841 -0
- package/package.json +74 -0
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,3033 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
27
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
28
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
|
+
|
|
30
|
+
// src/cli.ts
|
|
31
|
+
var path9 = __toESM(require("path"), 1);
|
|
32
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
33
|
+
var readline2 = __toESM(require("readline/promises"), 1);
|
|
34
|
+
var import_node_stream2 = require("stream");
|
|
35
|
+
var import_node_process = require("process");
|
|
36
|
+
|
|
37
|
+
// src/substrate/sqlite.ts
|
|
38
|
+
var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
|
|
39
|
+
function rowToEpisode(row) {
|
|
40
|
+
return {
|
|
41
|
+
id: row.id,
|
|
42
|
+
projectId: row.projectId ?? void 0,
|
|
43
|
+
content: JSON.parse(row.content),
|
|
44
|
+
entities: JSON.parse(row.entities),
|
|
45
|
+
edges: JSON.parse(row.edges),
|
|
46
|
+
validFrom: new Date(row.validFrom),
|
|
47
|
+
validTo: row.validTo !== null ? new Date(row.validTo) : null,
|
|
48
|
+
tier: row.tier,
|
|
49
|
+
rir: JSON.parse(row.rir),
|
|
50
|
+
source: row.source,
|
|
51
|
+
confidence: row.confidence ?? void 0,
|
|
52
|
+
status: row.status ?? "active",
|
|
53
|
+
visibility: row.visibility ?? "local",
|
|
54
|
+
tags: row.tags ? JSON.parse(row.tags) : void 0,
|
|
55
|
+
provenance: row.provenance ? JSON.parse(row.provenance) : void 0,
|
|
56
|
+
curationReason: row.curationReason ?? void 0,
|
|
57
|
+
pinned: row.pinned !== null && row.pinned !== void 0 ? row.pinned === 1 : void 0
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
var SqliteSubstrate = class {
|
|
61
|
+
db;
|
|
62
|
+
constructor(path10) {
|
|
63
|
+
this.db = new import_better_sqlite3.default(path10);
|
|
64
|
+
this.db.pragma("journal_mode = WAL");
|
|
65
|
+
this.db.pragma("busy_timeout = 5000");
|
|
66
|
+
this.db.exec(`
|
|
67
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
68
|
+
key TEXT PRIMARY KEY,
|
|
69
|
+
value TEXT NOT NULL
|
|
70
|
+
);
|
|
71
|
+
CREATE TABLE IF NOT EXISTS episodes (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
projectId TEXT,
|
|
74
|
+
content TEXT NOT NULL,
|
|
75
|
+
entities TEXT NOT NULL,
|
|
76
|
+
edges TEXT NOT NULL,
|
|
77
|
+
validFrom INTEGER NOT NULL,
|
|
78
|
+
validTo INTEGER,
|
|
79
|
+
tier TEXT NOT NULL,
|
|
80
|
+
rir TEXT NOT NULL,
|
|
81
|
+
source TEXT NOT NULL,
|
|
82
|
+
confidence REAL,
|
|
83
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
84
|
+
visibility TEXT NOT NULL DEFAULT 'local',
|
|
85
|
+
tags TEXT,
|
|
86
|
+
provenance TEXT,
|
|
87
|
+
curationReason TEXT,
|
|
88
|
+
pinned INTEGER NOT NULL DEFAULT 0
|
|
89
|
+
);
|
|
90
|
+
CREATE TABLE IF NOT EXISTS discovery_sessions (
|
|
91
|
+
id TEXT PRIMARY KEY,
|
|
92
|
+
projectId TEXT NOT NULL,
|
|
93
|
+
rootPath TEXT NOT NULL,
|
|
94
|
+
startedBy TEXT NOT NULL,
|
|
95
|
+
startedAt INTEGER NOT NULL,
|
|
96
|
+
status TEXT NOT NULL,
|
|
97
|
+
scanSummary TEXT NOT NULL,
|
|
98
|
+
coverage TEXT NOT NULL,
|
|
99
|
+
policyVersion TEXT NOT NULL
|
|
100
|
+
);
|
|
101
|
+
CREATE TABLE IF NOT EXISTS discovery_findings (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
sessionId TEXT NOT NULL,
|
|
104
|
+
projectId TEXT NOT NULL,
|
|
105
|
+
type TEXT NOT NULL,
|
|
106
|
+
claim TEXT NOT NULL,
|
|
107
|
+
payload TEXT NOT NULL,
|
|
108
|
+
evidence TEXT NOT NULL,
|
|
109
|
+
confidence REAL NOT NULL,
|
|
110
|
+
source TEXT NOT NULL,
|
|
111
|
+
freshness TEXT NOT NULL,
|
|
112
|
+
risk TEXT NOT NULL,
|
|
113
|
+
status TEXT NOT NULL,
|
|
114
|
+
createdAt INTEGER NOT NULL,
|
|
115
|
+
reason TEXT NOT NULL
|
|
116
|
+
);
|
|
117
|
+
CREATE TABLE IF NOT EXISTS hook_injection_ledger (
|
|
118
|
+
projectId TEXT NOT NULL,
|
|
119
|
+
sessionId TEXT NOT NULL,
|
|
120
|
+
client TEXT NOT NULL,
|
|
121
|
+
event TEXT NOT NULL,
|
|
122
|
+
lastHash TEXT NOT NULL,
|
|
123
|
+
lastInjectedAt INTEGER NOT NULL,
|
|
124
|
+
source TEXT NOT NULL,
|
|
125
|
+
reason TEXT NOT NULL,
|
|
126
|
+
PRIMARY KEY (projectId, sessionId, client, event)
|
|
127
|
+
);
|
|
128
|
+
CREATE TABLE IF NOT EXISTS hook_reinject_flags (
|
|
129
|
+
projectId TEXT NOT NULL,
|
|
130
|
+
sessionId TEXT NOT NULL,
|
|
131
|
+
client TEXT NOT NULL,
|
|
132
|
+
source TEXT NOT NULL,
|
|
133
|
+
reason TEXT NOT NULL,
|
|
134
|
+
createdAt INTEGER NOT NULL,
|
|
135
|
+
consumedAt INTEGER,
|
|
136
|
+
PRIMARY KEY (projectId, sessionId, client)
|
|
137
|
+
);
|
|
138
|
+
CREATE TABLE IF NOT EXISTS project_index_files (
|
|
139
|
+
projectId TEXT NOT NULL,
|
|
140
|
+
filePath TEXT NOT NULL,
|
|
141
|
+
hash TEXT NOT NULL,
|
|
142
|
+
kind TEXT NOT NULL,
|
|
143
|
+
indexedAt INTEGER NOT NULL,
|
|
144
|
+
estimatedTokens INTEGER NOT NULL DEFAULT 0,
|
|
145
|
+
PRIMARY KEY (projectId, filePath)
|
|
146
|
+
);
|
|
147
|
+
CREATE TABLE IF NOT EXISTS project_index_projects (
|
|
148
|
+
projectId TEXT PRIMARY KEY,
|
|
149
|
+
fingerprint TEXT NOT NULL,
|
|
150
|
+
rootPath TEXT NOT NULL,
|
|
151
|
+
packageName TEXT,
|
|
152
|
+
gitRemote TEXT,
|
|
153
|
+
firstIndexedAt INTEGER NOT NULL,
|
|
154
|
+
lastIndexedAt INTEGER NOT NULL,
|
|
155
|
+
indexedFiles INTEGER NOT NULL DEFAULT 0,
|
|
156
|
+
codeSymbols INTEGER NOT NULL DEFAULT 0
|
|
157
|
+
);
|
|
158
|
+
`);
|
|
159
|
+
this.migrate();
|
|
160
|
+
this.db.exec(`
|
|
161
|
+
CREATE INDEX IF NOT EXISTS episodes_active_project ON episodes(projectId, validTo, status);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS discovery_findings_session ON discovery_findings(sessionId);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS project_index_files_project ON project_index_files(projectId);
|
|
164
|
+
CREATE UNIQUE INDEX IF NOT EXISTS project_index_projects_fingerprint ON project_index_projects(fingerprint);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS project_index_projects_root ON project_index_projects(rootPath);
|
|
166
|
+
`);
|
|
167
|
+
}
|
|
168
|
+
migrate() {
|
|
169
|
+
const columns = new Set(
|
|
170
|
+
this.db.prepare(`PRAGMA table_info(episodes)`).all().map((c) => c.name)
|
|
171
|
+
);
|
|
172
|
+
const add = (name, definition) => {
|
|
173
|
+
if (!columns.has(name)) this.db.exec(`ALTER TABLE episodes ADD COLUMN ${name} ${definition}`);
|
|
174
|
+
};
|
|
175
|
+
add("projectId", "TEXT");
|
|
176
|
+
add("confidence", "REAL");
|
|
177
|
+
add("status", "TEXT NOT NULL DEFAULT 'active'");
|
|
178
|
+
add("visibility", "TEXT NOT NULL DEFAULT 'local'");
|
|
179
|
+
add("tags", "TEXT");
|
|
180
|
+
add("provenance", "TEXT");
|
|
181
|
+
add("curationReason", "TEXT");
|
|
182
|
+
add("pinned", "INTEGER NOT NULL DEFAULT 0");
|
|
183
|
+
this.db.prepare(
|
|
184
|
+
`INSERT OR REPLACE INTO schema_meta(key, value) VALUES ('schemaVersion', '2')`
|
|
185
|
+
).run();
|
|
186
|
+
}
|
|
187
|
+
upsert(episode) {
|
|
188
|
+
this.db.prepare(
|
|
189
|
+
`
|
|
190
|
+
INSERT OR REPLACE INTO episodes (
|
|
191
|
+
id, projectId, content, entities, edges, validFrom, validTo, tier, rir, source,
|
|
192
|
+
confidence, status, visibility, tags, provenance, curationReason, pinned
|
|
193
|
+
)
|
|
194
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
195
|
+
`
|
|
196
|
+
).run(
|
|
197
|
+
episode.id,
|
|
198
|
+
episode.projectId ?? null,
|
|
199
|
+
JSON.stringify(episode.content),
|
|
200
|
+
JSON.stringify(episode.entities),
|
|
201
|
+
JSON.stringify(episode.edges),
|
|
202
|
+
episode.validFrom.getTime(),
|
|
203
|
+
episode.validTo !== null ? episode.validTo.getTime() : null,
|
|
204
|
+
episode.tier,
|
|
205
|
+
JSON.stringify(episode.rir),
|
|
206
|
+
episode.source,
|
|
207
|
+
episode.confidence ?? null,
|
|
208
|
+
episode.status ?? "active",
|
|
209
|
+
episode.visibility ?? "local",
|
|
210
|
+
episode.tags ? JSON.stringify(episode.tags) : null,
|
|
211
|
+
episode.provenance ? JSON.stringify(episode.provenance) : null,
|
|
212
|
+
episode.curationReason ?? null,
|
|
213
|
+
episode.pinned ? 1 : 0
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
invalidate(id, validTo) {
|
|
217
|
+
this.db.prepare(`UPDATE episodes SET validTo = ?, status = 'invalidated' WHERE id = ?`).run(validTo.getTime(), id);
|
|
218
|
+
}
|
|
219
|
+
listByTier(tier) {
|
|
220
|
+
const rows = this.db.prepare(`SELECT * FROM episodes WHERE tier = ? AND validTo IS NULL AND status = 'active'`).all(tier);
|
|
221
|
+
return rows.map(rowToEpisode);
|
|
222
|
+
}
|
|
223
|
+
listAll() {
|
|
224
|
+
const rows = this.db.prepare(`SELECT * FROM episodes WHERE validTo IS NULL AND status = 'active'`).all();
|
|
225
|
+
return rows.map(rowToEpisode);
|
|
226
|
+
}
|
|
227
|
+
listActiveByIds(ids) {
|
|
228
|
+
if (ids.length === 0) return [];
|
|
229
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
230
|
+
const rows = this.db.prepare(`SELECT * FROM episodes WHERE id IN (${placeholders}) AND validTo IS NULL AND status = 'active'`).all(...ids);
|
|
231
|
+
const byId = new Map(rows.map((row) => [row.id, rowToEpisode(row)]));
|
|
232
|
+
return ids.flatMap((id) => {
|
|
233
|
+
const ep = byId.get(id);
|
|
234
|
+
return ep ? [ep] : [];
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
listAllRaw() {
|
|
238
|
+
return this.db.prepare(`SELECT * FROM episodes WHERE validTo IS NULL AND status = 'active'`).all();
|
|
239
|
+
}
|
|
240
|
+
transaction(fn) {
|
|
241
|
+
return this.db.transaction(fn)();
|
|
242
|
+
}
|
|
243
|
+
raw() {
|
|
244
|
+
return this.db;
|
|
245
|
+
}
|
|
246
|
+
close() {
|
|
247
|
+
this.db.close();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// src/server/mcp.ts
|
|
252
|
+
var path6 = __toESM(require("path"), 1);
|
|
253
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
254
|
+
var import_node_stream = require("stream");
|
|
255
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
256
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
257
|
+
|
|
258
|
+
// src/core/gate.ts
|
|
259
|
+
var fs = __toESM(require("fs"), 1);
|
|
260
|
+
var path = __toESM(require("path"), 1);
|
|
261
|
+
var SessionGate = class {
|
|
262
|
+
seen = /* @__PURE__ */ new Set();
|
|
263
|
+
sentinel;
|
|
264
|
+
constructor(stateDir) {
|
|
265
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
266
|
+
this.sentinel = path.join(stateDir, "reinject.flag");
|
|
267
|
+
}
|
|
268
|
+
shouldInject(contentHash) {
|
|
269
|
+
return !this.seen.has(contentHash) || fs.existsSync(this.sentinel);
|
|
270
|
+
}
|
|
271
|
+
markInjected(contentHash) {
|
|
272
|
+
this.seen.add(contentHash);
|
|
273
|
+
if (fs.existsSync(this.sentinel)) {
|
|
274
|
+
fs.unlinkSync(this.sentinel);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
forceReinject() {
|
|
278
|
+
fs.writeFileSync(this.sentinel, "");
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// src/search/memory-graph.ts
|
|
283
|
+
function normalizeName(name) {
|
|
284
|
+
return name.trim().toLowerCase();
|
|
285
|
+
}
|
|
286
|
+
var MemoryGraph = class {
|
|
287
|
+
db;
|
|
288
|
+
constructor(db) {
|
|
289
|
+
this.db = db;
|
|
290
|
+
this.db.exec(`
|
|
291
|
+
CREATE TABLE IF NOT EXISTS mg_entities (
|
|
292
|
+
id TEXT PRIMARY KEY,
|
|
293
|
+
name TEXT NOT NULL,
|
|
294
|
+
normalizedName TEXT,
|
|
295
|
+
kind TEXT NOT NULL,
|
|
296
|
+
attributes TEXT NOT NULL,
|
|
297
|
+
episodeId TEXT NOT NULL
|
|
298
|
+
);
|
|
299
|
+
CREATE INDEX IF NOT EXISTS mg_entities_name ON mg_entities(name);
|
|
300
|
+
CREATE INDEX IF NOT EXISTS mg_entities_normalized_name ON mg_entities(normalizedName);
|
|
301
|
+
CREATE TABLE IF NOT EXISTS mg_relations (
|
|
302
|
+
fromId TEXT NOT NULL,
|
|
303
|
+
toId TEXT NOT NULL,
|
|
304
|
+
label TEXT NOT NULL,
|
|
305
|
+
validFrom INTEGER NOT NULL,
|
|
306
|
+
validTo INTEGER,
|
|
307
|
+
episodeId TEXT NOT NULL
|
|
308
|
+
);
|
|
309
|
+
`);
|
|
310
|
+
const columns = new Set(
|
|
311
|
+
this.db.prepare(`PRAGMA table_info(mg_entities)`).all().map((c) => c.name)
|
|
312
|
+
);
|
|
313
|
+
if (!columns.has("normalizedName")) {
|
|
314
|
+
this.db.exec(`ALTER TABLE mg_entities ADD COLUMN normalizedName TEXT`);
|
|
315
|
+
this.db.exec(`UPDATE mg_entities SET normalizedName = lower(name) WHERE normalizedName IS NULL`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
index(episode) {
|
|
319
|
+
this.remove(episode.id);
|
|
320
|
+
const upsertEntity = this.db.prepare(
|
|
321
|
+
`INSERT OR REPLACE INTO mg_entities(id, name, normalizedName, kind, attributes, episodeId) VALUES (?, ?, ?, ?, ?, ?)`
|
|
322
|
+
);
|
|
323
|
+
const insertRelation = this.db.prepare(
|
|
324
|
+
`INSERT INTO mg_relations(fromId, toId, label, validFrom, validTo, episodeId) VALUES (?, ?, ?, ?, ?, ?)`
|
|
325
|
+
);
|
|
326
|
+
for (const entity of episode.entities) {
|
|
327
|
+
upsertEntity.run(
|
|
328
|
+
entity.id,
|
|
329
|
+
entity.name,
|
|
330
|
+
normalizeName(entity.name),
|
|
331
|
+
entity.kind,
|
|
332
|
+
JSON.stringify(entity.attributes),
|
|
333
|
+
episode.id
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
for (const edge of episode.edges) {
|
|
337
|
+
insertRelation.run(
|
|
338
|
+
edge.fromId,
|
|
339
|
+
edge.toId,
|
|
340
|
+
edge.label,
|
|
341
|
+
edge.validFrom.getTime(),
|
|
342
|
+
edge.validTo ? edge.validTo.getTime() : null,
|
|
343
|
+
episode.id
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
search(entityName, limit = 10) {
|
|
348
|
+
const rows = this.db.prepare(
|
|
349
|
+
`SELECT DISTINCT e.episodeId
|
|
350
|
+
FROM mg_entities e
|
|
351
|
+
JOIN episodes ep ON ep.id = e.episodeId
|
|
352
|
+
WHERE e.normalizedName = ?
|
|
353
|
+
AND ep.validTo IS NULL
|
|
354
|
+
AND ep.status = 'active'
|
|
355
|
+
LIMIT ?`
|
|
356
|
+
).all(normalizeName(entityName), limit);
|
|
357
|
+
return rows.map((r) => r.episodeId);
|
|
358
|
+
}
|
|
359
|
+
getRelations(entityId) {
|
|
360
|
+
const rows = this.db.prepare(`SELECT * FROM mg_relations WHERE fromId = ?`).all(entityId);
|
|
361
|
+
return rows.map((r) => ({
|
|
362
|
+
fromId: r.fromId,
|
|
363
|
+
toId: r.toId,
|
|
364
|
+
label: r.label,
|
|
365
|
+
validFrom: new Date(r.validFrom),
|
|
366
|
+
validTo: r.validTo !== null ? new Date(r.validTo) : null
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
remove(episodeId) {
|
|
370
|
+
this.db.prepare(`DELETE FROM mg_entities WHERE episodeId = ?`).run(episodeId);
|
|
371
|
+
this.db.prepare(`DELETE FROM mg_relations WHERE episodeId = ?`).run(episodeId);
|
|
372
|
+
}
|
|
373
|
+
clear() {
|
|
374
|
+
this.db.prepare(`DELETE FROM mg_entities`).run();
|
|
375
|
+
this.db.prepare(`DELETE FROM mg_relations`).run();
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// src/search/code-graph.ts
|
|
380
|
+
var import_node_module = require("module");
|
|
381
|
+
var require2 = (0, import_node_module.createRequire)(importMetaUrl);
|
|
382
|
+
var Parser = require2("tree-sitter");
|
|
383
|
+
var { typescript: tsLang } = require2("tree-sitter-typescript");
|
|
384
|
+
var KIND_MAP = {
|
|
385
|
+
function_declaration: "function",
|
|
386
|
+
method_definition: "function",
|
|
387
|
+
class_declaration: "class",
|
|
388
|
+
interface_declaration: "interface",
|
|
389
|
+
type_alias_declaration: "type",
|
|
390
|
+
lexical_declaration: "variable"
|
|
391
|
+
};
|
|
392
|
+
var CodeGraph = class {
|
|
393
|
+
db;
|
|
394
|
+
parser;
|
|
395
|
+
constructor(db) {
|
|
396
|
+
this.db = db;
|
|
397
|
+
this.ensureSchema();
|
|
398
|
+
this.parser = new Parser();
|
|
399
|
+
this.parser.setLanguage(tsLang);
|
|
400
|
+
}
|
|
401
|
+
ensureSchema() {
|
|
402
|
+
const existingColumns = this.db.prepare(`PRAGMA table_info(cg_symbols)`).all();
|
|
403
|
+
const primaryKey = existingColumns.filter((column) => column.pk > 0).sort((a, b) => a.pk - b.pk).map((column) => column.name).join(",");
|
|
404
|
+
if (existingColumns.length > 0 && primaryKey !== "name,filePath,line") {
|
|
405
|
+
this.db.exec(`
|
|
406
|
+
DROP TABLE IF EXISTS cg_symbols;
|
|
407
|
+
DROP TABLE IF EXISTS cg_symbols_fts;
|
|
408
|
+
`);
|
|
409
|
+
}
|
|
410
|
+
this.db.exec(`
|
|
411
|
+
CREATE TABLE IF NOT EXISTS cg_symbols (
|
|
412
|
+
name TEXT NOT NULL,
|
|
413
|
+
kind TEXT NOT NULL,
|
|
414
|
+
filePath TEXT NOT NULL,
|
|
415
|
+
line INTEGER NOT NULL,
|
|
416
|
+
PRIMARY KEY (name, filePath, line)
|
|
417
|
+
);
|
|
418
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS cg_symbols_fts USING fts5(
|
|
419
|
+
name,
|
|
420
|
+
filePath UNINDEXED,
|
|
421
|
+
line UNINDEXED,
|
|
422
|
+
tokenize='trigram'
|
|
423
|
+
);
|
|
424
|
+
`);
|
|
425
|
+
}
|
|
426
|
+
indexFile(filePath, source) {
|
|
427
|
+
this.removeFile(filePath);
|
|
428
|
+
const tree = this.parser.parse(source);
|
|
429
|
+
const upsertSym = this.db.prepare(
|
|
430
|
+
`INSERT OR REPLACE INTO cg_symbols(name, kind, filePath, line) VALUES (?, ?, ?, ?)`
|
|
431
|
+
);
|
|
432
|
+
const upsertFts = this.db.prepare(
|
|
433
|
+
`INSERT INTO cg_symbols_fts(name, filePath, line) VALUES (?, ?, ?)`
|
|
434
|
+
);
|
|
435
|
+
const walk = (node) => {
|
|
436
|
+
const kind = KIND_MAP[node.type];
|
|
437
|
+
if (kind) {
|
|
438
|
+
let name;
|
|
439
|
+
if (node.type === "lexical_declaration") {
|
|
440
|
+
const declarator = node.children.find(
|
|
441
|
+
(c) => c.type === "variable_declarator"
|
|
442
|
+
);
|
|
443
|
+
name = declarator?.childForFieldName("name")?.text;
|
|
444
|
+
} else {
|
|
445
|
+
name = node.childForFieldName("name")?.text;
|
|
446
|
+
}
|
|
447
|
+
if (name) {
|
|
448
|
+
const line2 = node.startPosition.row + 1;
|
|
449
|
+
upsertSym.run(name, kind, filePath, line2);
|
|
450
|
+
upsertFts.run(name, filePath, line2);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
for (const child of node.children) {
|
|
454
|
+
walk(child);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
walk(tree.rootNode);
|
|
458
|
+
}
|
|
459
|
+
search(query, limit = 10) {
|
|
460
|
+
const normalized = query.trim().slice(0, 200);
|
|
461
|
+
if (!normalized) return [];
|
|
462
|
+
const safeQuery = `"${normalized.replace(/"/g, '""')}"`;
|
|
463
|
+
let rows;
|
|
464
|
+
try {
|
|
465
|
+
rows = this.db.prepare(
|
|
466
|
+
`SELECT s.name, s.kind, s.filePath, s.line
|
|
467
|
+
FROM cg_symbols_fts f
|
|
468
|
+
JOIN cg_symbols s ON s.name = f.name AND s.filePath = f.filePath AND s.line = f.line
|
|
469
|
+
WHERE cg_symbols_fts MATCH ?
|
|
470
|
+
ORDER BY rank
|
|
471
|
+
LIMIT ?`
|
|
472
|
+
).all(safeQuery, limit);
|
|
473
|
+
} catch {
|
|
474
|
+
return [];
|
|
475
|
+
}
|
|
476
|
+
return rows.map((r) => ({
|
|
477
|
+
name: r.name,
|
|
478
|
+
kind: r.kind,
|
|
479
|
+
filePath: r.filePath,
|
|
480
|
+
line: r.line
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
countSymbolsForFile(filePath) {
|
|
484
|
+
const row = this.db.prepare(`SELECT COUNT(*) AS count FROM cg_symbols WHERE filePath = ?`).get(filePath);
|
|
485
|
+
return row?.count ?? 0;
|
|
486
|
+
}
|
|
487
|
+
removeFile(filePath) {
|
|
488
|
+
this.db.prepare(`DELETE FROM cg_symbols WHERE filePath = ?`).run(filePath);
|
|
489
|
+
this.db.prepare(`DELETE FROM cg_symbols_fts WHERE filePath = ?`).run(filePath);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// src/search/fts5.ts
|
|
494
|
+
var Fts5Index = class {
|
|
495
|
+
db;
|
|
496
|
+
constructor(db) {
|
|
497
|
+
this.db = db;
|
|
498
|
+
this.db.exec(`
|
|
499
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS episodes_fts USING fts5(
|
|
500
|
+
id UNINDEXED,
|
|
501
|
+
text,
|
|
502
|
+
tokenize="trigram"
|
|
503
|
+
)
|
|
504
|
+
`);
|
|
505
|
+
}
|
|
506
|
+
index(episode) {
|
|
507
|
+
this.remove(episode.id);
|
|
508
|
+
this.db.prepare(`INSERT OR REPLACE INTO episodes_fts(id, text) VALUES (?, ?)`).run(episode.id, JSON.stringify(episode.content));
|
|
509
|
+
}
|
|
510
|
+
search(query, limit = 10) {
|
|
511
|
+
const normalized = query.trim().slice(0, 200);
|
|
512
|
+
if (!normalized) return [];
|
|
513
|
+
const safeQuery = `"${normalized.replace(/"/g, '""')}"`;
|
|
514
|
+
try {
|
|
515
|
+
const rows = this.db.prepare(
|
|
516
|
+
`SELECT id FROM episodes_fts WHERE episodes_fts MATCH ? ORDER BY rank LIMIT ?`
|
|
517
|
+
).all(safeQuery, limit);
|
|
518
|
+
return rows.map((r) => r.id);
|
|
519
|
+
} catch {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
remove(id) {
|
|
524
|
+
this.db.prepare(`DELETE FROM episodes_fts WHERE id = ?`).run(id);
|
|
525
|
+
}
|
|
526
|
+
clear() {
|
|
527
|
+
this.db.prepare(`DELETE FROM episodes_fts`).run();
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// src/search/router.ts
|
|
532
|
+
var SearchRouter = class {
|
|
533
|
+
memory;
|
|
534
|
+
code;
|
|
535
|
+
fts;
|
|
536
|
+
constructor(memory, code, fts) {
|
|
537
|
+
this.memory = memory;
|
|
538
|
+
this.code = code;
|
|
539
|
+
this.fts = fts;
|
|
540
|
+
}
|
|
541
|
+
search(query, limit = 10) {
|
|
542
|
+
const graphIds = this.memory.search(query, limit);
|
|
543
|
+
const ftsIds = this.fts.search(query, limit);
|
|
544
|
+
const codeSymbols = this.code.search(query, limit);
|
|
545
|
+
const seen = /* @__PURE__ */ new Set();
|
|
546
|
+
const episodeIds = [];
|
|
547
|
+
for (const id of [...graphIds, ...ftsIds]) {
|
|
548
|
+
if (!seen.has(id)) {
|
|
549
|
+
seen.add(id);
|
|
550
|
+
episodeIds.push(id);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const hasGraph = graphIds.length > 0;
|
|
554
|
+
const hasFts = ftsIds.length > 0;
|
|
555
|
+
const hasCode = codeSymbols.length > 0;
|
|
556
|
+
const backendCount = (hasGraph ? 1 : 0) + (hasFts ? 1 : 0) + (hasCode ? 1 : 0);
|
|
557
|
+
let source;
|
|
558
|
+
if (backendCount > 1) {
|
|
559
|
+
source = "combined";
|
|
560
|
+
} else if (hasGraph) {
|
|
561
|
+
source = "graph";
|
|
562
|
+
} else if (hasCode) {
|
|
563
|
+
source = "code";
|
|
564
|
+
} else {
|
|
565
|
+
source = "fts5";
|
|
566
|
+
}
|
|
567
|
+
return { episodeIds, codeSymbols, source };
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
// src/curator/rir.ts
|
|
572
|
+
var RirScorer = class {
|
|
573
|
+
recency(createdAt) {
|
|
574
|
+
const hoursSince = (Date.now() - createdAt.getTime()) / 36e5;
|
|
575
|
+
return Math.exp(-0.1 * hoursSince);
|
|
576
|
+
}
|
|
577
|
+
importance(content) {
|
|
578
|
+
return Math.min(Math.max(JSON.stringify(content).length / 500, 0.35), 1);
|
|
579
|
+
}
|
|
580
|
+
relevance(content, goal) {
|
|
581
|
+
if (!goal) return 1;
|
|
582
|
+
const text = JSON.stringify(content).toLowerCase();
|
|
583
|
+
const goalWords = goal.toLowerCase().split(/\s+/).filter(Boolean);
|
|
584
|
+
if (goalWords.length === 0) return 1;
|
|
585
|
+
const matched = goalWords.filter((w) => text.includes(w)).length;
|
|
586
|
+
return Math.min(matched / goalWords.length, 1);
|
|
587
|
+
}
|
|
588
|
+
score(episode) {
|
|
589
|
+
return {
|
|
590
|
+
recency: this.recency(episode.validFrom),
|
|
591
|
+
importance: this.importance(episode.content),
|
|
592
|
+
relevance: this.relevance(episode.content)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// src/curator/semantic.ts
|
|
598
|
+
var import_nanoid = require("nanoid");
|
|
599
|
+
var SemanticCurator = class {
|
|
600
|
+
async scoreImportance(content) {
|
|
601
|
+
return Math.min(Math.max(JSON.stringify(content).length / 500, 0.35), 1);
|
|
602
|
+
}
|
|
603
|
+
async extractEntities(content) {
|
|
604
|
+
if (content.kind === "entity") {
|
|
605
|
+
return [{ id: (0, import_nanoid.nanoid)(), name: content.name, kind: "entity", attributes: content.attributes }];
|
|
606
|
+
}
|
|
607
|
+
const text = "text" in content ? content.text : "what" in content ? content.what : "claim" in content ? content.claim : "choice" in content ? content.choice : "";
|
|
608
|
+
const candidates = Array.from(new Set(text.match(/\b[A-Z][A-Za-z0-9_-]{2,}\b/g) ?? []));
|
|
609
|
+
return candidates.slice(0, 8).map((name) => ({
|
|
610
|
+
id: (0, import_nanoid.nanoid)(),
|
|
611
|
+
name,
|
|
612
|
+
kind: "concept",
|
|
613
|
+
attributes: { extractedBy: "semantic-fallback" }
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// src/tiers/manager.ts
|
|
619
|
+
function tierForRir(rir) {
|
|
620
|
+
if (rir > 0.7) return "core";
|
|
621
|
+
if (rir > 0.3) return "recall";
|
|
622
|
+
if (rir > 0.1) return "archive";
|
|
623
|
+
return "delete";
|
|
624
|
+
}
|
|
625
|
+
var TierManager = class {
|
|
626
|
+
constructor(substrate, scorer, curator, indexes) {
|
|
627
|
+
this.substrate = substrate;
|
|
628
|
+
this.scorer = scorer;
|
|
629
|
+
this.curator = curator;
|
|
630
|
+
this.indexes = indexes;
|
|
631
|
+
}
|
|
632
|
+
substrate;
|
|
633
|
+
scorer;
|
|
634
|
+
curator;
|
|
635
|
+
indexes;
|
|
636
|
+
async runCuration() {
|
|
637
|
+
const summary = {
|
|
638
|
+
promoted: 0,
|
|
639
|
+
retained: 0,
|
|
640
|
+
archived: 0,
|
|
641
|
+
deleted: 0,
|
|
642
|
+
actions: []
|
|
643
|
+
};
|
|
644
|
+
const episodes = this.substrate.listAll();
|
|
645
|
+
for (const episode of episodes) {
|
|
646
|
+
const scores = this.scorer.score(episode);
|
|
647
|
+
const importance = await this.curator.scoreImportance(episode.content);
|
|
648
|
+
const rir = scores.recency * importance * scores.relevance;
|
|
649
|
+
const target = tierForRir(rir);
|
|
650
|
+
if (target === "delete") {
|
|
651
|
+
this.substrate.invalidate(episode.id, /* @__PURE__ */ new Date());
|
|
652
|
+
this.indexes?.removeEpisode(episode.id);
|
|
653
|
+
summary.deleted++;
|
|
654
|
+
summary.actions.push({
|
|
655
|
+
action: "invalidated",
|
|
656
|
+
detail: `${episode.id} scored RIR ${rir.toFixed(3)}`,
|
|
657
|
+
projectId: episode.projectId
|
|
658
|
+
});
|
|
659
|
+
} else {
|
|
660
|
+
if (target === "core" && episode.tier !== "core") {
|
|
661
|
+
summary.promoted++;
|
|
662
|
+
} else if (target === "archive" && episode.tier !== "archive") {
|
|
663
|
+
summary.archived++;
|
|
664
|
+
} else {
|
|
665
|
+
summary.retained++;
|
|
666
|
+
}
|
|
667
|
+
const updated = {
|
|
668
|
+
...episode,
|
|
669
|
+
tier: target,
|
|
670
|
+
rir: { ...scores, importance },
|
|
671
|
+
curationReason: `RIR ${rir.toFixed(3)} mapped to ${target}`
|
|
672
|
+
};
|
|
673
|
+
this.substrate.upsert(updated);
|
|
674
|
+
this.indexes?.indexEpisode(updated);
|
|
675
|
+
summary.actions.push({
|
|
676
|
+
action: target === "core" ? "promoted" : target === "archive" ? "archived" : "retained",
|
|
677
|
+
detail: `${episode.id} mapped to ${target} at RIR ${rir.toFixed(3)}`,
|
|
678
|
+
projectId: episode.projectId
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return summary;
|
|
683
|
+
}
|
|
684
|
+
getCoreBlock() {
|
|
685
|
+
const episodes = this.substrate.listByTier("core");
|
|
686
|
+
episodes.sort(
|
|
687
|
+
(a, b) => b.rir.recency * b.rir.importance * b.rir.relevance - a.rir.recency * a.rir.importance * a.rir.relevance
|
|
688
|
+
);
|
|
689
|
+
const LIMIT = 2048;
|
|
690
|
+
let result = "";
|
|
691
|
+
for (const ep of episodes) {
|
|
692
|
+
const line2 = `[${ep.tier}] ${JSON.stringify(ep.content)}
|
|
693
|
+
`;
|
|
694
|
+
if (result.length + line2.length > LIMIT) break;
|
|
695
|
+
result += line2;
|
|
696
|
+
}
|
|
697
|
+
return result;
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// src/services/index-manager.ts
|
|
702
|
+
var IndexManager = class {
|
|
703
|
+
constructor(memory, code, fts) {
|
|
704
|
+
this.memory = memory;
|
|
705
|
+
this.code = code;
|
|
706
|
+
this.fts = fts;
|
|
707
|
+
}
|
|
708
|
+
memory;
|
|
709
|
+
code;
|
|
710
|
+
fts;
|
|
711
|
+
indexEpisode(episode) {
|
|
712
|
+
this.fts.index(episode);
|
|
713
|
+
this.memory.index(episode);
|
|
714
|
+
}
|
|
715
|
+
removeEpisode(episodeId) {
|
|
716
|
+
this.fts.remove(episodeId);
|
|
717
|
+
this.memory.remove(episodeId);
|
|
718
|
+
}
|
|
719
|
+
rebuildEpisodes(episodes) {
|
|
720
|
+
this.fts.clear();
|
|
721
|
+
this.memory.clear();
|
|
722
|
+
for (const episode of episodes) {
|
|
723
|
+
this.indexEpisode(episode);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
indexCodeFile(filePath, source) {
|
|
727
|
+
this.code.indexFile(filePath, source);
|
|
728
|
+
}
|
|
729
|
+
removeCodeFile(filePath) {
|
|
730
|
+
this.code.removeFile(filePath);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// src/server/tools.ts
|
|
735
|
+
var path4 = __toESM(require("path"), 1);
|
|
736
|
+
var import_node_crypto = require("crypto");
|
|
737
|
+
var import_zod2 = require("zod");
|
|
738
|
+
|
|
739
|
+
// src/discovery/engine.ts
|
|
740
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
741
|
+
var path3 = __toESM(require("path"), 1);
|
|
742
|
+
var import_nanoid2 = require("nanoid");
|
|
743
|
+
|
|
744
|
+
// src/security/scanner.ts
|
|
745
|
+
var path2 = __toESM(require("path"), 1);
|
|
746
|
+
var SECRET_PATTERNS = [
|
|
747
|
+
{ name: "private_key", pattern: /-----BEGIN (?:RSA |EC |OPENSSH |)?PRIVATE KEY-----/gi },
|
|
748
|
+
{ name: "github_token", pattern: /gh[pousr]_[A-Za-z0-9_]{20,}/g },
|
|
749
|
+
{ name: "openai_key", pattern: /sk-[A-Za-z0-9_-]{20,}/g },
|
|
750
|
+
{ name: "aws_access_key", pattern: /AKIA[0-9A-Z]{16}/g },
|
|
751
|
+
{ name: "env_secret", pattern: /\b(?:SECRET|TOKEN|PASSWORD|API_KEY|PRIVATE_KEY)\s*=\s*["']?[^"'\s]+/gi }
|
|
752
|
+
];
|
|
753
|
+
function isWithin(root, target) {
|
|
754
|
+
const relative4 = path2.relative(root, target);
|
|
755
|
+
return relative4 === "" || !relative4.startsWith("..") && !path2.isAbsolute(relative4);
|
|
756
|
+
}
|
|
757
|
+
var SecurityScanner = class {
|
|
758
|
+
scanText(text) {
|
|
759
|
+
const reasons = SECRET_PATTERNS.filter(({ pattern }) => text.match(pattern)).map(({ name }) => name);
|
|
760
|
+
let redactedText = text;
|
|
761
|
+
for (const { pattern } of SECRET_PATTERNS) {
|
|
762
|
+
redactedText = redactedText.replace(pattern, "[REDACTED]");
|
|
763
|
+
}
|
|
764
|
+
return { blocked: reasons.length > 0, reasons, redactedText };
|
|
765
|
+
}
|
|
766
|
+
isIgnoredPath(filePath) {
|
|
767
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
768
|
+
return /(^|\/)(node_modules|dist|build|coverage|\.git|\.next|\.cache|vendor)(\/|$)/.test(normalized);
|
|
769
|
+
}
|
|
770
|
+
canExportTo(targetPath, stateDir) {
|
|
771
|
+
const target = path2.resolve(targetPath);
|
|
772
|
+
const state = path2.resolve(stateDir);
|
|
773
|
+
const tmp = path2.resolve("/tmp");
|
|
774
|
+
return isWithin(state, target) || isWithin(tmp, target);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
// src/discovery/policy.ts
|
|
779
|
+
var LOW_RISK = /* @__PURE__ */ new Set([
|
|
780
|
+
"language",
|
|
781
|
+
"framework",
|
|
782
|
+
"package_manager",
|
|
783
|
+
"script",
|
|
784
|
+
"command",
|
|
785
|
+
"entrypoint",
|
|
786
|
+
"test_strategy",
|
|
787
|
+
"directory_role",
|
|
788
|
+
"dependency"
|
|
789
|
+
]);
|
|
790
|
+
var HIGH_RISK = /* @__PURE__ */ new Set([
|
|
791
|
+
"architecture_note",
|
|
792
|
+
"decision",
|
|
793
|
+
"risk",
|
|
794
|
+
"deployment_note"
|
|
795
|
+
]);
|
|
796
|
+
var DiscoveryPolicy = class {
|
|
797
|
+
constructor(security = new SecurityScanner()) {
|
|
798
|
+
this.security = security;
|
|
799
|
+
}
|
|
800
|
+
security;
|
|
801
|
+
version = "2026-05-secure-project-map";
|
|
802
|
+
decide(input2) {
|
|
803
|
+
const scan = this.security.scanText(`${input2.claim}
|
|
804
|
+
${JSON.stringify(input2.payload ?? {})}`);
|
|
805
|
+
if (scan.blocked) {
|
|
806
|
+
return {
|
|
807
|
+
status: "blocked",
|
|
808
|
+
risk: "high",
|
|
809
|
+
reason: `blocked sensitive content: ${scan.reasons.join(", ")}`
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
const hasEvidence = (input2.evidence ?? []).length > 0;
|
|
813
|
+
if (LOW_RISK.has(input2.type) && hasEvidence) {
|
|
814
|
+
return {
|
|
815
|
+
status: "auto_accepted",
|
|
816
|
+
risk: "low",
|
|
817
|
+
reason: "low-risk project-map finding with evidence"
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
if (HIGH_RISK.has(input2.type)) {
|
|
821
|
+
return {
|
|
822
|
+
status: "queued",
|
|
823
|
+
risk: "high",
|
|
824
|
+
reason: "high-impact claim requires review or stronger confirmation"
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
status: "queued",
|
|
829
|
+
risk: "medium",
|
|
830
|
+
reason: hasEvidence ? "finding requires review before durable memory" : "finding lacks evidence and was queued as low-confidence"
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// src/discovery/engine.ts
|
|
836
|
+
var INITIAL_COVERAGE = {
|
|
837
|
+
languages: 0,
|
|
838
|
+
scripts: 0,
|
|
839
|
+
entrypoints: 0,
|
|
840
|
+
tests: 0,
|
|
841
|
+
docs: 0,
|
|
842
|
+
queued: 0,
|
|
843
|
+
blocked: 0,
|
|
844
|
+
accepted: 0
|
|
845
|
+
};
|
|
846
|
+
var EXT_LANGUAGE = {
|
|
847
|
+
".ts": "TypeScript",
|
|
848
|
+
".tsx": "TypeScript React",
|
|
849
|
+
".js": "JavaScript",
|
|
850
|
+
".jsx": "JavaScript React",
|
|
851
|
+
".py": "Python",
|
|
852
|
+
".rs": "Rust",
|
|
853
|
+
".go": "Go",
|
|
854
|
+
".java": "Java",
|
|
855
|
+
".rb": "Ruby",
|
|
856
|
+
".php": "PHP"
|
|
857
|
+
};
|
|
858
|
+
function projectIdFor(rootPath) {
|
|
859
|
+
return path3.basename(path3.resolve(rootPath)).replace(/[^A-Za-z0-9_.-]/g, "-") || "project";
|
|
860
|
+
}
|
|
861
|
+
function rowToSession(row) {
|
|
862
|
+
return {
|
|
863
|
+
id: row.id,
|
|
864
|
+
projectId: row.projectId,
|
|
865
|
+
rootPath: row.rootPath,
|
|
866
|
+
startedBy: row.startedBy,
|
|
867
|
+
startedAt: new Date(row.startedAt),
|
|
868
|
+
status: row.status,
|
|
869
|
+
scanSummary: JSON.parse(row.scanSummary),
|
|
870
|
+
coverage: JSON.parse(row.coverage),
|
|
871
|
+
policyVersion: row.policyVersion
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function rowToFinding(row) {
|
|
875
|
+
return {
|
|
876
|
+
id: row.id,
|
|
877
|
+
sessionId: row.sessionId,
|
|
878
|
+
projectId: row.projectId,
|
|
879
|
+
type: row.type,
|
|
880
|
+
claim: row.claim,
|
|
881
|
+
payload: JSON.parse(row.payload),
|
|
882
|
+
evidence: JSON.parse(row.evidence),
|
|
883
|
+
confidence: row.confidence,
|
|
884
|
+
source: row.source,
|
|
885
|
+
freshness: row.freshness,
|
|
886
|
+
risk: row.risk,
|
|
887
|
+
status: row.status,
|
|
888
|
+
createdAt: new Date(row.createdAt),
|
|
889
|
+
reason: row.reason
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
var DiscoveryEngine = class {
|
|
893
|
+
constructor(substrate, policy = new DiscoveryPolicy(), security = new SecurityScanner()) {
|
|
894
|
+
this.substrate = substrate;
|
|
895
|
+
this.policy = policy;
|
|
896
|
+
this.security = security;
|
|
897
|
+
}
|
|
898
|
+
substrate;
|
|
899
|
+
policy;
|
|
900
|
+
security;
|
|
901
|
+
start(rootPath, startedBy = "agent", projectId = projectIdFor(rootPath)) {
|
|
902
|
+
const resolved = path3.resolve(rootPath);
|
|
903
|
+
const session = {
|
|
904
|
+
id: (0, import_nanoid2.nanoid)(),
|
|
905
|
+
projectId,
|
|
906
|
+
rootPath: resolved,
|
|
907
|
+
startedBy,
|
|
908
|
+
startedAt: /* @__PURE__ */ new Date(),
|
|
909
|
+
status: "active",
|
|
910
|
+
scanSummary: {},
|
|
911
|
+
coverage: { ...INITIAL_COVERAGE },
|
|
912
|
+
policyVersion: this.policy.version
|
|
913
|
+
};
|
|
914
|
+
this.substrate.raw().prepare(
|
|
915
|
+
`INSERT INTO discovery_sessions(id, projectId, rootPath, startedBy, startedAt, status, scanSummary, coverage, policyVersion)
|
|
916
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
917
|
+
).run(
|
|
918
|
+
session.id,
|
|
919
|
+
session.projectId,
|
|
920
|
+
session.rootPath,
|
|
921
|
+
session.startedBy,
|
|
922
|
+
session.startedAt.getTime(),
|
|
923
|
+
session.status,
|
|
924
|
+
JSON.stringify(session.scanSummary),
|
|
925
|
+
JSON.stringify(session.coverage),
|
|
926
|
+
session.policyVersion
|
|
927
|
+
);
|
|
928
|
+
return session;
|
|
929
|
+
}
|
|
930
|
+
getSession(sessionId) {
|
|
931
|
+
const row = this.substrate.raw().prepare(`SELECT * FROM discovery_sessions WHERE id = ?`).get(sessionId);
|
|
932
|
+
return row ? rowToSession(row) : null;
|
|
933
|
+
}
|
|
934
|
+
findings(sessionId) {
|
|
935
|
+
const rows = this.substrate.raw().prepare(`SELECT * FROM discovery_findings WHERE sessionId = ? ORDER BY createdAt ASC`).all(sessionId);
|
|
936
|
+
return rows.map(rowToFinding);
|
|
937
|
+
}
|
|
938
|
+
submit(sessionId, input2) {
|
|
939
|
+
const session = this.getSession(sessionId);
|
|
940
|
+
if (!session) throw new Error(`Unknown discovery session: ${sessionId}`);
|
|
941
|
+
const decision = this.policy.decide(input2);
|
|
942
|
+
const scan = this.security.scanText(`${input2.claim}
|
|
943
|
+
${JSON.stringify(input2.payload ?? {})}`);
|
|
944
|
+
const finding = {
|
|
945
|
+
id: (0, import_nanoid2.nanoid)(),
|
|
946
|
+
sessionId,
|
|
947
|
+
projectId: session.projectId,
|
|
948
|
+
type: input2.type,
|
|
949
|
+
claim: scan.blocked ? scan.redactedText.split("\n")[0] : input2.claim,
|
|
950
|
+
payload: scan.blocked ? { redacted: true } : input2.payload ?? {},
|
|
951
|
+
evidence: input2.evidence ?? [],
|
|
952
|
+
confidence: input2.confidence ?? (decision.status === "auto_accepted" ? 0.85 : 0.45),
|
|
953
|
+
source: input2.source ?? "agent",
|
|
954
|
+
freshness: input2.freshness ?? "fresh",
|
|
955
|
+
risk: decision.risk,
|
|
956
|
+
status: decision.status,
|
|
957
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
958
|
+
reason: decision.reason
|
|
959
|
+
};
|
|
960
|
+
this.substrate.raw().prepare(
|
|
961
|
+
`INSERT INTO discovery_findings(
|
|
962
|
+
id, sessionId, projectId, type, claim, payload, evidence, confidence,
|
|
963
|
+
source, freshness, risk, status, createdAt, reason
|
|
964
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
965
|
+
).run(
|
|
966
|
+
finding.id,
|
|
967
|
+
finding.sessionId,
|
|
968
|
+
finding.projectId,
|
|
969
|
+
finding.type,
|
|
970
|
+
finding.claim,
|
|
971
|
+
JSON.stringify(finding.payload),
|
|
972
|
+
JSON.stringify(finding.evidence),
|
|
973
|
+
finding.confidence,
|
|
974
|
+
finding.source,
|
|
975
|
+
finding.freshness,
|
|
976
|
+
finding.risk,
|
|
977
|
+
finding.status,
|
|
978
|
+
finding.createdAt.getTime(),
|
|
979
|
+
finding.reason
|
|
980
|
+
);
|
|
981
|
+
this.updateCoverage(sessionId);
|
|
982
|
+
return finding;
|
|
983
|
+
}
|
|
984
|
+
scan(sessionId) {
|
|
985
|
+
const session = this.getSession(sessionId);
|
|
986
|
+
if (!session) throw new Error(`Unknown discovery session: ${sessionId}`);
|
|
987
|
+
const root = session.rootPath;
|
|
988
|
+
const findings = [];
|
|
989
|
+
const languages = /* @__PURE__ */ new Set();
|
|
990
|
+
const topDirs = [];
|
|
991
|
+
let docs = 0;
|
|
992
|
+
let tests = 0;
|
|
993
|
+
const walk = (dir, depth) => {
|
|
994
|
+
if (depth > 3 || this.security.isIgnoredPath(dir)) return;
|
|
995
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
996
|
+
const full = path3.join(dir, entry.name);
|
|
997
|
+
const rel = path3.relative(root, full) || entry.name;
|
|
998
|
+
if (this.security.isIgnoredPath(rel)) continue;
|
|
999
|
+
if (entry.isDirectory()) {
|
|
1000
|
+
if (depth === 0) topDirs.push(entry.name);
|
|
1001
|
+
if (/test|spec/i.test(entry.name)) tests++;
|
|
1002
|
+
walk(full, depth + 1);
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
const ext = path3.extname(entry.name);
|
|
1006
|
+
if (EXT_LANGUAGE[ext]) languages.add(EXT_LANGUAGE[ext]);
|
|
1007
|
+
if (/^readme|\.md$/i.test(entry.name) || ext === ".md") docs++;
|
|
1008
|
+
if (/(\.test|\.spec)\.[tj]sx?$|test_/i.test(entry.name)) tests++;
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
walk(root, 0);
|
|
1012
|
+
for (const language of languages) {
|
|
1013
|
+
findings.push({
|
|
1014
|
+
type: "language",
|
|
1015
|
+
claim: `Project uses ${language}`,
|
|
1016
|
+
payload: { language },
|
|
1017
|
+
evidence: [{ kind: "file", path: root }],
|
|
1018
|
+
source: "scanner"
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
const packagePath = path3.join(root, "package.json");
|
|
1022
|
+
if (fs2.existsSync(packagePath)) {
|
|
1023
|
+
const pkg = JSON.parse(fs2.readFileSync(packagePath, "utf-8"));
|
|
1024
|
+
findings.push({
|
|
1025
|
+
type: "package_manager",
|
|
1026
|
+
claim: "Project has a package.json and uses the Node package ecosystem",
|
|
1027
|
+
payload: { packageManager: fs2.existsSync(path3.join(root, "package-lock.json")) ? "npm" : "node" },
|
|
1028
|
+
evidence: [{ kind: "file", path: packagePath }],
|
|
1029
|
+
source: "scanner"
|
|
1030
|
+
});
|
|
1031
|
+
for (const [name, command] of Object.entries(pkg.scripts ?? {})) {
|
|
1032
|
+
findings.push({
|
|
1033
|
+
type: "script",
|
|
1034
|
+
claim: `npm script ${name}: ${command}`,
|
|
1035
|
+
payload: { name, command },
|
|
1036
|
+
evidence: [{ kind: "package", path: packagePath, key: `scripts.${name}` }],
|
|
1037
|
+
source: "scanner"
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
if (pkg.main) {
|
|
1041
|
+
findings.push({
|
|
1042
|
+
type: "entrypoint",
|
|
1043
|
+
claim: `Package main entrypoint is ${pkg.main}`,
|
|
1044
|
+
payload: { main: pkg.main },
|
|
1045
|
+
evidence: [{ kind: "package", path: packagePath, key: "main" }],
|
|
1046
|
+
source: "scanner"
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
for (const dir of topDirs.slice(0, 20)) {
|
|
1051
|
+
findings.push({
|
|
1052
|
+
type: "directory_role",
|
|
1053
|
+
claim: `Top-level project directory: ${dir}`,
|
|
1054
|
+
payload: { directory: dir },
|
|
1055
|
+
evidence: [{ kind: "file", path: path3.join(root, dir) }],
|
|
1056
|
+
source: "scanner"
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
if (tests > 0) {
|
|
1060
|
+
findings.push({
|
|
1061
|
+
type: "test_strategy",
|
|
1062
|
+
claim: `Project has ${tests} visible test-related files or directories`,
|
|
1063
|
+
payload: { count: tests },
|
|
1064
|
+
evidence: [{ kind: "file", path: root }],
|
|
1065
|
+
source: "scanner"
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
if (docs > 0) {
|
|
1069
|
+
findings.push({
|
|
1070
|
+
type: "convention",
|
|
1071
|
+
claim: `Project has ${docs} visible documentation files`,
|
|
1072
|
+
payload: { count: docs },
|
|
1073
|
+
evidence: [{ kind: "file", path: root }],
|
|
1074
|
+
source: "scanner"
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
return findings.map((finding) => this.submit(sessionId, finding));
|
|
1078
|
+
}
|
|
1079
|
+
status(sessionId) {
|
|
1080
|
+
const session = this.getSession(sessionId);
|
|
1081
|
+
if (!session) throw new Error(`Unknown discovery session: ${sessionId}`);
|
|
1082
|
+
const findings = this.findings(sessionId);
|
|
1083
|
+
return { session, findings, coverage: this.coverageFor(findings) };
|
|
1084
|
+
}
|
|
1085
|
+
refresh(sessionId) {
|
|
1086
|
+
this.substrate.raw().prepare(`UPDATE discovery_sessions SET status = 'stale' WHERE id = ?`).run(sessionId);
|
|
1087
|
+
const session = this.getSession(sessionId);
|
|
1088
|
+
if (!session) throw new Error(`Unknown discovery session: ${sessionId}`);
|
|
1089
|
+
return session;
|
|
1090
|
+
}
|
|
1091
|
+
updateCoverage(sessionId) {
|
|
1092
|
+
const coverage = this.coverageFor(this.findings(sessionId));
|
|
1093
|
+
this.substrate.raw().prepare(`UPDATE discovery_sessions SET coverage = ? WHERE id = ?`).run(JSON.stringify(coverage), sessionId);
|
|
1094
|
+
}
|
|
1095
|
+
coverageFor(findings) {
|
|
1096
|
+
return {
|
|
1097
|
+
languages: findings.filter((f) => f.type === "language").length,
|
|
1098
|
+
scripts: findings.filter((f) => f.type === "script").length,
|
|
1099
|
+
entrypoints: findings.filter((f) => f.type === "entrypoint").length,
|
|
1100
|
+
tests: findings.filter((f) => f.type === "test_strategy").length,
|
|
1101
|
+
docs: findings.filter((f) => f.type === "convention").length,
|
|
1102
|
+
queued: findings.filter((f) => f.status === "queued").length,
|
|
1103
|
+
blocked: findings.filter((f) => f.status === "blocked").length,
|
|
1104
|
+
accepted: findings.filter((f) => f.status === "auto_accepted").length
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
// src/services/memory-store.ts
|
|
1110
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
1111
|
+
var import_nanoid3 = require("nanoid");
|
|
1112
|
+
function buildContent(kind, content) {
|
|
1113
|
+
switch (kind) {
|
|
1114
|
+
case "event":
|
|
1115
|
+
return { kind: "event", what: content };
|
|
1116
|
+
case "insight":
|
|
1117
|
+
return { kind: "insight", claim: content, confidence: 0.8 };
|
|
1118
|
+
case "decision":
|
|
1119
|
+
return { kind: "decision", choice: content, rationale: "" };
|
|
1120
|
+
case "entity":
|
|
1121
|
+
return { kind: "entity", name: content, attributes: {} };
|
|
1122
|
+
default:
|
|
1123
|
+
return { kind: "summary", text: content };
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
var MemoryStore = class {
|
|
1127
|
+
constructor(substrate, indexes, router, curator, security = new SecurityScanner(), telemetry) {
|
|
1128
|
+
this.substrate = substrate;
|
|
1129
|
+
this.indexes = indexes;
|
|
1130
|
+
this.router = router;
|
|
1131
|
+
this.curator = curator;
|
|
1132
|
+
this.security = security;
|
|
1133
|
+
this.telemetry = telemetry;
|
|
1134
|
+
}
|
|
1135
|
+
substrate;
|
|
1136
|
+
indexes;
|
|
1137
|
+
router;
|
|
1138
|
+
curator;
|
|
1139
|
+
security;
|
|
1140
|
+
telemetry;
|
|
1141
|
+
async remember(input2) {
|
|
1142
|
+
const scan = this.security.scanText(input2.content);
|
|
1143
|
+
if (scan.blocked) {
|
|
1144
|
+
throw new Error(`Refusing to remember sensitive content: ${scan.reasons.join(", ")}`);
|
|
1145
|
+
}
|
|
1146
|
+
const content = buildContent(input2.kind, scan.redactedText);
|
|
1147
|
+
const entities = await this.curator.extractEntities(content);
|
|
1148
|
+
const importance = await this.curator.scoreImportance(content);
|
|
1149
|
+
const episode = {
|
|
1150
|
+
id: (0, import_nanoid3.nanoid)(),
|
|
1151
|
+
projectId: input2.projectId,
|
|
1152
|
+
content,
|
|
1153
|
+
entities,
|
|
1154
|
+
edges: [],
|
|
1155
|
+
validFrom: /* @__PURE__ */ new Date(),
|
|
1156
|
+
validTo: null,
|
|
1157
|
+
tier: "recall",
|
|
1158
|
+
rir: { recency: 1, importance, relevance: 1 },
|
|
1159
|
+
source: input2.source ?? "user",
|
|
1160
|
+
confidence: input2.confidence ?? 0.8,
|
|
1161
|
+
status: "active",
|
|
1162
|
+
visibility: "local",
|
|
1163
|
+
tags: input2.tags,
|
|
1164
|
+
provenance: {
|
|
1165
|
+
sourceType: input2.sourceType ?? (input2.source === "discovery" ? "discovery" : "mcp"),
|
|
1166
|
+
source: input2.source ?? "user",
|
|
1167
|
+
evidence: input2.evidence
|
|
1168
|
+
},
|
|
1169
|
+
curationReason: "stored through MemoryStore with security scan and curator extraction"
|
|
1170
|
+
};
|
|
1171
|
+
this.substrate.transaction(() => {
|
|
1172
|
+
this.substrate.upsert(episode);
|
|
1173
|
+
this.indexes.indexEpisode(episode);
|
|
1174
|
+
});
|
|
1175
|
+
this.telemetry?.recordMemoryCreated();
|
|
1176
|
+
this.telemetry?.recordSavings(input2.content.length / 4);
|
|
1177
|
+
return episode;
|
|
1178
|
+
}
|
|
1179
|
+
forget(id) {
|
|
1180
|
+
this.substrate.transaction(() => {
|
|
1181
|
+
this.substrate.invalidate(id, /* @__PURE__ */ new Date());
|
|
1182
|
+
this.indexes.removeEpisode(id);
|
|
1183
|
+
});
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
search(query, limit = 10) {
|
|
1187
|
+
const raw = this.router.search(query, limit);
|
|
1188
|
+
const episodes = this.substrate.listActiveByIds(raw.episodeIds);
|
|
1189
|
+
const activeIds = new Set(episodes.map((ep) => ep.id));
|
|
1190
|
+
this.telemetry?.recordSavings(episodes.length * 500);
|
|
1191
|
+
return {
|
|
1192
|
+
...raw,
|
|
1193
|
+
episodeIds: raw.episodeIds.filter((id) => activeIds.has(id)),
|
|
1194
|
+
episodes,
|
|
1195
|
+
warnings: []
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
listActive() {
|
|
1199
|
+
return this.substrate.listAll();
|
|
1200
|
+
}
|
|
1201
|
+
exportActive(targetPath, stateDir) {
|
|
1202
|
+
if (!this.security.canExportTo(targetPath, stateDir)) {
|
|
1203
|
+
throw new Error("targetPath must be inside the Suture state directory or /tmp");
|
|
1204
|
+
}
|
|
1205
|
+
const episodes = this.substrate.listAllRaw();
|
|
1206
|
+
fs3.writeFileSync(targetPath, JSON.stringify(episodes, null, 2));
|
|
1207
|
+
return episodes.length;
|
|
1208
|
+
}
|
|
1209
|
+
indexCodeFile(filePath, source) {
|
|
1210
|
+
this.indexes.indexCodeFile(filePath, source);
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
// src/core/telemetry.ts
|
|
1215
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
1216
|
+
var import_zod = require("zod");
|
|
1217
|
+
var RECENT_LIMIT = 24;
|
|
1218
|
+
var CurationActionSchema = import_zod.z.object({
|
|
1219
|
+
at: import_zod.z.string(),
|
|
1220
|
+
action: import_zod.z.string(),
|
|
1221
|
+
detail: import_zod.z.string(),
|
|
1222
|
+
projectId: import_zod.z.string().optional()
|
|
1223
|
+
});
|
|
1224
|
+
var TelemetryStatsSchema = import_zod.z.object({
|
|
1225
|
+
tokensSaved: import_zod.z.number().default(0),
|
|
1226
|
+
contextRotationsAvoided: import_zod.z.number().default(0),
|
|
1227
|
+
totalInjections: import_zod.z.number().default(0),
|
|
1228
|
+
toolUsage: import_zod.z.record(import_zod.z.string(), import_zod.z.number()).default({}),
|
|
1229
|
+
memoriesCreated: import_zod.z.number().default(0),
|
|
1230
|
+
activeContextTokens: import_zod.z.number().default(0),
|
|
1231
|
+
indexedFiles: import_zod.z.number().default(0),
|
|
1232
|
+
skippedUnchangedFiles: import_zod.z.number().default(0),
|
|
1233
|
+
codeSymbolsIndexed: import_zod.z.number().default(0),
|
|
1234
|
+
projectFactsAccepted: import_zod.z.number().default(0),
|
|
1235
|
+
projectFactsQueued: import_zod.z.number().default(0),
|
|
1236
|
+
projectFactsRejected: import_zod.z.number().default(0),
|
|
1237
|
+
projectFactsStale: import_zod.z.number().default(0),
|
|
1238
|
+
projectFactsSuperseded: import_zod.z.number().default(0),
|
|
1239
|
+
estimatedTokensCached: import_zod.z.number().default(0),
|
|
1240
|
+
estimatedTokensAvoidedFromRereads: import_zod.z.number().default(0),
|
|
1241
|
+
searchHitsServedFromSubstrate: import_zod.z.number().default(0),
|
|
1242
|
+
skippedDuplicateInjections: import_zod.z.number().default(0),
|
|
1243
|
+
recentTokenSavings: import_zod.z.array(import_zod.z.number()).default([]),
|
|
1244
|
+
recentCurationActions: import_zod.z.array(CurationActionSchema).default([]),
|
|
1245
|
+
lastUpdate: import_zod.z.string().default(() => (/* @__PURE__ */ new Date()).toISOString())
|
|
1246
|
+
});
|
|
1247
|
+
var MODEL_COST_PROFILES = {
|
|
1248
|
+
conservative: 1,
|
|
1249
|
+
standard: 3,
|
|
1250
|
+
premium: 10
|
|
1251
|
+
};
|
|
1252
|
+
var TelemetryService = class {
|
|
1253
|
+
constructor(statePath) {
|
|
1254
|
+
this.statePath = statePath;
|
|
1255
|
+
this.stats = this.load();
|
|
1256
|
+
if (this.statePath && !fs4.existsSync(this.statePath)) {
|
|
1257
|
+
this.save();
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
statePath;
|
|
1261
|
+
stats;
|
|
1262
|
+
load() {
|
|
1263
|
+
if (this.statePath && fs4.existsSync(this.statePath)) {
|
|
1264
|
+
try {
|
|
1265
|
+
const data = JSON.parse(fs4.readFileSync(this.statePath, "utf-8"));
|
|
1266
|
+
return TelemetryStatsSchema.parse(data);
|
|
1267
|
+
} catch (e) {
|
|
1268
|
+
return TelemetryStatsSchema.parse({});
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return TelemetryStatsSchema.parse({});
|
|
1272
|
+
}
|
|
1273
|
+
save() {
|
|
1274
|
+
if (this.statePath) {
|
|
1275
|
+
this.stats.lastUpdate = (/* @__PURE__ */ new Date()).toISOString();
|
|
1276
|
+
fs4.writeFileSync(this.statePath, JSON.stringify(this.stats, null, 2));
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
recordToolUsage(toolName) {
|
|
1280
|
+
const current = this.stats.toolUsage[toolName] || 0;
|
|
1281
|
+
this.stats.toolUsage[toolName] = current + 1;
|
|
1282
|
+
this.save();
|
|
1283
|
+
}
|
|
1284
|
+
recordSavings(tokens) {
|
|
1285
|
+
const normalized = Math.max(0, Math.round(tokens));
|
|
1286
|
+
this.stats.tokensSaved += normalized;
|
|
1287
|
+
this.stats.recentTokenSavings = [...this.stats.recentTokenSavings, normalized].slice(-RECENT_LIMIT);
|
|
1288
|
+
this.save();
|
|
1289
|
+
}
|
|
1290
|
+
recordMemoryCreated() {
|
|
1291
|
+
this.stats.memoriesCreated += 1;
|
|
1292
|
+
this.save();
|
|
1293
|
+
}
|
|
1294
|
+
recordInjection(tokens) {
|
|
1295
|
+
this.stats.totalInjections += 1;
|
|
1296
|
+
this.stats.activeContextTokens = Math.max(0, Math.round(tokens));
|
|
1297
|
+
this.save();
|
|
1298
|
+
}
|
|
1299
|
+
recordSkippedInjection() {
|
|
1300
|
+
this.stats.skippedDuplicateInjections += 1;
|
|
1301
|
+
this.save();
|
|
1302
|
+
}
|
|
1303
|
+
recordIndexing(summary) {
|
|
1304
|
+
this.stats.indexedFiles += summary.indexedFiles ?? 0;
|
|
1305
|
+
this.stats.skippedUnchangedFiles += summary.skippedFiles ?? 0;
|
|
1306
|
+
this.stats.codeSymbolsIndexed += summary.symbols ?? 0;
|
|
1307
|
+
this.stats.projectFactsAccepted += summary.accepted ?? 0;
|
|
1308
|
+
this.stats.projectFactsQueued += summary.queued ?? 0;
|
|
1309
|
+
this.stats.projectFactsRejected += summary.rejected ?? 0;
|
|
1310
|
+
this.stats.projectFactsStale += summary.stale ?? 0;
|
|
1311
|
+
this.stats.projectFactsSuperseded += summary.superseded ?? 0;
|
|
1312
|
+
this.stats.estimatedTokensCached += Math.max(0, Math.round(summary.tokensCached ?? 0));
|
|
1313
|
+
this.stats.estimatedTokensAvoidedFromRereads += Math.max(0, Math.round(summary.tokensAvoidedFromRereads ?? 0));
|
|
1314
|
+
this.recordSavings((summary.tokensCached ?? 0) + (summary.tokensAvoidedFromRereads ?? 0));
|
|
1315
|
+
}
|
|
1316
|
+
recordSearchHits(count) {
|
|
1317
|
+
this.stats.searchHitsServedFromSubstrate += Math.max(0, Math.round(count));
|
|
1318
|
+
this.save();
|
|
1319
|
+
}
|
|
1320
|
+
recordCurationAction(action) {
|
|
1321
|
+
this.stats.recentCurationActions = [
|
|
1322
|
+
...this.stats.recentCurationActions,
|
|
1323
|
+
{ ...action, at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1324
|
+
].slice(-RECENT_LIMIT);
|
|
1325
|
+
this.save();
|
|
1326
|
+
}
|
|
1327
|
+
getStats() {
|
|
1328
|
+
return { ...this.stats };
|
|
1329
|
+
}
|
|
1330
|
+
estimateCostSavings(modelProfile = "standard", inputCostPerMillion) {
|
|
1331
|
+
const profileCost = MODEL_COST_PROFILES[modelProfile] ?? MODEL_COST_PROFILES.standard;
|
|
1332
|
+
const cost = inputCostPerMillion ?? profileCost;
|
|
1333
|
+
const estimatedTokens = Math.round(
|
|
1334
|
+
this.stats.estimatedTokensCached + this.stats.estimatedTokensAvoidedFromRereads
|
|
1335
|
+
);
|
|
1336
|
+
return {
|
|
1337
|
+
modelProfile,
|
|
1338
|
+
inputCostPerMillion: cost,
|
|
1339
|
+
estimatedTokens,
|
|
1340
|
+
estimatedUsd: estimatedTokens / 1e6 * cost,
|
|
1341
|
+
formula: `(${estimatedTokens} estimated avoided input tokens / 1,000,000) * $${cost.toFixed(2)}`
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
// src/server/tools.ts
|
|
1347
|
+
function ok(data) {
|
|
1348
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
1349
|
+
}
|
|
1350
|
+
function makeTools(substrate, gate, router, tierManager, memory, code, fts, services = {}) {
|
|
1351
|
+
const curator = services.curator ?? new SemanticCurator();
|
|
1352
|
+
const security = new SecurityScanner();
|
|
1353
|
+
const indexes = new IndexManager(memory, code, fts);
|
|
1354
|
+
const stateDir = services.stateDir ?? path4.dirname(substrate.raw().name ?? "/tmp");
|
|
1355
|
+
const telemetry = services.telemetry ?? new TelemetryService(path4.join(stateDir, "telemetry.json"));
|
|
1356
|
+
const memoryStore = services.memoryStore ?? new MemoryStore(substrate, indexes, router, curator, security, telemetry);
|
|
1357
|
+
const discovery = services.discovery ?? new DiscoveryEngine(substrate);
|
|
1358
|
+
return [
|
|
1359
|
+
{
|
|
1360
|
+
name: "remember",
|
|
1361
|
+
description: "Store a new memory episode",
|
|
1362
|
+
schema: {
|
|
1363
|
+
content: import_zod2.z.string(),
|
|
1364
|
+
kind: import_zod2.z.enum(["summary", "event", "insight", "decision", "entity"]).default("summary"),
|
|
1365
|
+
source: import_zod2.z.string().optional(),
|
|
1366
|
+
projectId: import_zod2.z.string().optional()
|
|
1367
|
+
},
|
|
1368
|
+
handler: async (args) => {
|
|
1369
|
+
telemetry.recordToolUsage("remember");
|
|
1370
|
+
const episode = await memoryStore.remember({
|
|
1371
|
+
content: args.content,
|
|
1372
|
+
kind: args.kind,
|
|
1373
|
+
source: args.source ?? "user",
|
|
1374
|
+
projectId: args.projectId
|
|
1375
|
+
});
|
|
1376
|
+
const curation = await tierManager.runCuration();
|
|
1377
|
+
indexes.rebuildEpisodes(substrate.listAll());
|
|
1378
|
+
for (const action of curation.actions.slice(-4)) telemetry.recordCurationAction(action);
|
|
1379
|
+
return ok({ id: episode.id });
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
name: "recall",
|
|
1384
|
+
description: "Retrieve the highest-RIR core memory block (\u2264 2 KB). Gated: only returns if core block changed since last injection. Pass isCompaction=true on PreCompact to force re-inject.",
|
|
1385
|
+
schema: {
|
|
1386
|
+
sessionId: import_zod2.z.string().optional(),
|
|
1387
|
+
isCompaction: import_zod2.z.boolean().default(false)
|
|
1388
|
+
},
|
|
1389
|
+
handler: async (args) => {
|
|
1390
|
+
telemetry.recordToolUsage("recall");
|
|
1391
|
+
if (args.isCompaction) gate.forceReinject();
|
|
1392
|
+
const block = tierManager.getCoreBlock();
|
|
1393
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(block).digest("hex");
|
|
1394
|
+
if (gate.shouldInject(hash)) {
|
|
1395
|
+
gate.markInjected(hash);
|
|
1396
|
+
telemetry.recordInjection(block.length / 4);
|
|
1397
|
+
return ok({ block });
|
|
1398
|
+
}
|
|
1399
|
+
telemetry.recordSkippedInjection();
|
|
1400
|
+
return ok({ block: "" });
|
|
1401
|
+
}
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
name: "search",
|
|
1405
|
+
description: "Search across memory graph, code graph, and FTS5 index",
|
|
1406
|
+
schema: {
|
|
1407
|
+
query: import_zod2.z.string(),
|
|
1408
|
+
limit: import_zod2.z.number().int().positive().optional()
|
|
1409
|
+
},
|
|
1410
|
+
handler: async (args) => {
|
|
1411
|
+
telemetry.recordToolUsage("search");
|
|
1412
|
+
const results = memoryStore.search(
|
|
1413
|
+
args.query,
|
|
1414
|
+
args.limit ?? 10
|
|
1415
|
+
);
|
|
1416
|
+
telemetry.recordSearchHits(results.episodeIds.length + results.codeSymbols.length);
|
|
1417
|
+
return ok({ results });
|
|
1418
|
+
}
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
name: "suture_stats",
|
|
1422
|
+
description: "Get comprehensive stats on Suture usage, token savings, and agent efficiency metrics",
|
|
1423
|
+
schema: {},
|
|
1424
|
+
handler: async () => ok(telemetry.getStats())
|
|
1425
|
+
},
|
|
1426
|
+
{
|
|
1427
|
+
name: "curate",
|
|
1428
|
+
description: "Trigger a curation cycle: score episodes by RIR and promote, demote, or delete",
|
|
1429
|
+
schema: {},
|
|
1430
|
+
handler: async () => {
|
|
1431
|
+
telemetry.recordToolUsage("curate");
|
|
1432
|
+
const curation = await tierManager.runCuration();
|
|
1433
|
+
indexes.rebuildEpisodes(substrate.listAll());
|
|
1434
|
+
for (const action of curation.actions.slice(-8)) telemetry.recordCurationAction(action);
|
|
1435
|
+
return ok({ ok: true, curation });
|
|
1436
|
+
}
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
name: "transfer",
|
|
1440
|
+
description: "Export all active (non-invalidated) episodes to a portable JSON file at the given absolute path",
|
|
1441
|
+
schema: { targetPath: import_zod2.z.string() },
|
|
1442
|
+
handler: async (args) => {
|
|
1443
|
+
telemetry.recordToolUsage("transfer");
|
|
1444
|
+
const targetPath = args.targetPath;
|
|
1445
|
+
if (!path4.isAbsolute(targetPath)) {
|
|
1446
|
+
throw new Error("targetPath must be an absolute path");
|
|
1447
|
+
}
|
|
1448
|
+
const exported = memoryStore.exportActive(targetPath, stateDir);
|
|
1449
|
+
return ok({ exported });
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
name: "forget",
|
|
1454
|
+
description: "Invalidate an active memory and remove it from search indexes",
|
|
1455
|
+
schema: { id: import_zod2.z.string() },
|
|
1456
|
+
handler: async (args) => {
|
|
1457
|
+
telemetry.recordToolUsage("forget");
|
|
1458
|
+
return ok({ ok: memoryStore.forget(args.id) });
|
|
1459
|
+
}
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
name: "memory_inspect",
|
|
1463
|
+
description: "List active memories for local review",
|
|
1464
|
+
schema: {
|
|
1465
|
+
limit: import_zod2.z.number().int().positive().optional()
|
|
1466
|
+
},
|
|
1467
|
+
handler: async (args) => {
|
|
1468
|
+
telemetry.recordToolUsage("memory_inspect");
|
|
1469
|
+
const limit = args.limit ?? 50;
|
|
1470
|
+
return ok({ episodes: memoryStore.listActive().slice(0, limit) });
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
name: "discovery_start",
|
|
1475
|
+
description: "Create a secure project discovery session and return setup guidance",
|
|
1476
|
+
schema: {
|
|
1477
|
+
projectPath: import_zod2.z.string(),
|
|
1478
|
+
startedBy: import_zod2.z.string().optional(),
|
|
1479
|
+
projectId: import_zod2.z.string().optional()
|
|
1480
|
+
},
|
|
1481
|
+
handler: async (args) => {
|
|
1482
|
+
telemetry.recordToolUsage("discovery_start");
|
|
1483
|
+
const session = discovery.start(
|
|
1484
|
+
args.projectPath,
|
|
1485
|
+
args.startedBy ?? "agent",
|
|
1486
|
+
args.projectId
|
|
1487
|
+
);
|
|
1488
|
+
return ok({
|
|
1489
|
+
session,
|
|
1490
|
+
resources: [
|
|
1491
|
+
"suture://resources/discovery/schema",
|
|
1492
|
+
"suture://resources/discovery/checklist",
|
|
1493
|
+
"suture://resources/discovery/security-policy"
|
|
1494
|
+
],
|
|
1495
|
+
ignoredPaths: ["node_modules", "dist", "build", "coverage", ".git", ".next", ".cache", "vendor"],
|
|
1496
|
+
requestedCategories: ["languages", "scripts", "entrypoints", "tests", "docs", "directory roles"]
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
name: "discovery_scan",
|
|
1502
|
+
description: "Run deterministic local project-map discovery for a session",
|
|
1503
|
+
schema: { sessionId: import_zod2.z.string() },
|
|
1504
|
+
handler: async (args) => {
|
|
1505
|
+
telemetry.recordToolUsage("discovery_scan");
|
|
1506
|
+
return ok({ findings: discovery.scan(args.sessionId) });
|
|
1507
|
+
}
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
name: "discovery_submit",
|
|
1511
|
+
description: "Submit one structured agent discovery finding with evidence and confidence",
|
|
1512
|
+
schema: {
|
|
1513
|
+
sessionId: import_zod2.z.string(),
|
|
1514
|
+
type: import_zod2.z.enum([
|
|
1515
|
+
"language",
|
|
1516
|
+
"framework",
|
|
1517
|
+
"package_manager",
|
|
1518
|
+
"script",
|
|
1519
|
+
"command",
|
|
1520
|
+
"entrypoint",
|
|
1521
|
+
"test_strategy",
|
|
1522
|
+
"directory_role",
|
|
1523
|
+
"key_module",
|
|
1524
|
+
"convention",
|
|
1525
|
+
"architecture_note",
|
|
1526
|
+
"decision",
|
|
1527
|
+
"risk",
|
|
1528
|
+
"dependency",
|
|
1529
|
+
"deployment_note"
|
|
1530
|
+
]),
|
|
1531
|
+
claim: import_zod2.z.string(),
|
|
1532
|
+
payload: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional(),
|
|
1533
|
+
evidence: import_zod2.z.array(import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown())).optional(),
|
|
1534
|
+
confidence: import_zod2.z.number().min(0).max(1).optional(),
|
|
1535
|
+
source: import_zod2.z.string().optional(),
|
|
1536
|
+
freshness: import_zod2.z.enum(["fresh", "unknown", "stale"]).optional()
|
|
1537
|
+
},
|
|
1538
|
+
handler: async (args) => {
|
|
1539
|
+
telemetry.recordToolUsage("discovery_submit");
|
|
1540
|
+
const finding = discovery.submit(args.sessionId, {
|
|
1541
|
+
type: args.type,
|
|
1542
|
+
claim: args.claim,
|
|
1543
|
+
payload: args.payload,
|
|
1544
|
+
evidence: args.evidence,
|
|
1545
|
+
confidence: args.confidence,
|
|
1546
|
+
source: args.source,
|
|
1547
|
+
freshness: args.freshness
|
|
1548
|
+
});
|
|
1549
|
+
return ok({ finding });
|
|
1550
|
+
}
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
name: "discovery_status",
|
|
1554
|
+
description: "Report discovery coverage, accepted findings, queued findings, and blocked findings",
|
|
1555
|
+
schema: { sessionId: import_zod2.z.string() },
|
|
1556
|
+
handler: async (args) => {
|
|
1557
|
+
telemetry.recordToolUsage("discovery_status");
|
|
1558
|
+
return ok(discovery.status(args.sessionId));
|
|
1559
|
+
}
|
|
1560
|
+
},
|
|
1561
|
+
{
|
|
1562
|
+
name: "discovery_apply",
|
|
1563
|
+
description: "Apply auto-accepted discovery findings into durable local memory",
|
|
1564
|
+
schema: { sessionId: import_zod2.z.string() },
|
|
1565
|
+
handler: async (args) => {
|
|
1566
|
+
telemetry.recordToolUsage("discovery_apply");
|
|
1567
|
+
const status = discovery.status(args.sessionId);
|
|
1568
|
+
let applied = 0;
|
|
1569
|
+
let skipped = 0;
|
|
1570
|
+
const active = substrate.listAll();
|
|
1571
|
+
const appliedSources = new Set(
|
|
1572
|
+
active.filter((episode) => episode.provenance?.sourceType === "discovery").map((episode) => episode.provenance?.source).filter((source) => Boolean(source))
|
|
1573
|
+
);
|
|
1574
|
+
for (const finding of status.findings) {
|
|
1575
|
+
if (finding.status !== "auto_accepted") continue;
|
|
1576
|
+
const source = `discovery:${finding.id}`;
|
|
1577
|
+
if (appliedSources.has(source)) {
|
|
1578
|
+
skipped++;
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
await memoryStore.remember({
|
|
1582
|
+
content: finding.claim,
|
|
1583
|
+
kind: "summary",
|
|
1584
|
+
source,
|
|
1585
|
+
sourceType: "discovery",
|
|
1586
|
+
projectId: finding.projectId,
|
|
1587
|
+
evidence: finding.evidence,
|
|
1588
|
+
confidence: finding.confidence,
|
|
1589
|
+
tags: ["discovery", finding.type]
|
|
1590
|
+
});
|
|
1591
|
+
appliedSources.add(source);
|
|
1592
|
+
applied++;
|
|
1593
|
+
}
|
|
1594
|
+
if (applied > 0) {
|
|
1595
|
+
substrate.raw().prepare(`UPDATE discovery_sessions SET status = 'applied' WHERE id = ?`).run(args.sessionId);
|
|
1596
|
+
}
|
|
1597
|
+
const curation = await tierManager.runCuration();
|
|
1598
|
+
indexes.rebuildEpisodes(substrate.listAll());
|
|
1599
|
+
for (const action of curation.actions.slice(-8)) telemetry.recordCurationAction(action);
|
|
1600
|
+
return ok({ applied, skipped });
|
|
1601
|
+
}
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
name: "discovery_refresh",
|
|
1605
|
+
description: "Mark a discovery session stale so an agent can refresh project context",
|
|
1606
|
+
schema: { sessionId: import_zod2.z.string() },
|
|
1607
|
+
handler: async (args) => {
|
|
1608
|
+
telemetry.recordToolUsage("discovery_refresh");
|
|
1609
|
+
return ok({ session: discovery.refresh(args.sessionId) });
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
];
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/server/prompts.ts
|
|
1616
|
+
var import_zod3 = require("zod");
|
|
1617
|
+
var DISCOVERY_SCHEMA = {
|
|
1618
|
+
type: "object",
|
|
1619
|
+
required: ["type", "claim", "evidence"],
|
|
1620
|
+
properties: {
|
|
1621
|
+
type: {
|
|
1622
|
+
enum: [
|
|
1623
|
+
"language",
|
|
1624
|
+
"framework",
|
|
1625
|
+
"package_manager",
|
|
1626
|
+
"script",
|
|
1627
|
+
"command",
|
|
1628
|
+
"entrypoint",
|
|
1629
|
+
"test_strategy",
|
|
1630
|
+
"directory_role",
|
|
1631
|
+
"key_module",
|
|
1632
|
+
"convention",
|
|
1633
|
+
"architecture_note",
|
|
1634
|
+
"decision",
|
|
1635
|
+
"risk",
|
|
1636
|
+
"dependency",
|
|
1637
|
+
"deployment_note"
|
|
1638
|
+
]
|
|
1639
|
+
},
|
|
1640
|
+
claim: { type: "string" },
|
|
1641
|
+
payload: { type: "object" },
|
|
1642
|
+
evidence: { type: "array" },
|
|
1643
|
+
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
1644
|
+
freshness: { enum: ["fresh", "unknown", "stale"] }
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
var DISCOVERY_CHECKLIST = [
|
|
1648
|
+
"Identify languages, frameworks, package manager, and primary scripts.",
|
|
1649
|
+
"Identify entrypoints, top-level directory roles, and test commands.",
|
|
1650
|
+
"Submit only structured JSON findings through discovery_submit.",
|
|
1651
|
+
"Attach file/package/GitHub evidence for every setup fact.",
|
|
1652
|
+
"Do not submit secrets, tokens, .env values, or private keys.",
|
|
1653
|
+
"Queue architecture decisions or undocumented assumptions as lower-confidence findings."
|
|
1654
|
+
];
|
|
1655
|
+
var DISCOVERY_SECURITY_POLICY = [
|
|
1656
|
+
"Sensitive content is blocked before persistence.",
|
|
1657
|
+
"Low-risk setup facts with evidence may be accepted automatically.",
|
|
1658
|
+
"Architecture claims, risks, and decisions require review or stronger confirmation.",
|
|
1659
|
+
"GitHub ingestion is explicit-only; broad automatic PR/issue ingestion is disabled."
|
|
1660
|
+
].join("\n");
|
|
1661
|
+
function registerDiscoveryPrompts(server) {
|
|
1662
|
+
server.registerResource(
|
|
1663
|
+
"suture-discovery-schema",
|
|
1664
|
+
"suture://resources/discovery/schema",
|
|
1665
|
+
{
|
|
1666
|
+
title: "Suture Discovery JSON Schema",
|
|
1667
|
+
description: "Strict schema for agent-submitted project discovery findings.",
|
|
1668
|
+
mimeType: "application/json"
|
|
1669
|
+
},
|
|
1670
|
+
(uri) => ({
|
|
1671
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(DISCOVERY_SCHEMA, null, 2) }]
|
|
1672
|
+
})
|
|
1673
|
+
);
|
|
1674
|
+
server.registerResource(
|
|
1675
|
+
"suture-discovery-checklist",
|
|
1676
|
+
"suture://resources/discovery/checklist",
|
|
1677
|
+
{
|
|
1678
|
+
title: "Suture Discovery Checklist",
|
|
1679
|
+
description: "Project-map checklist for connected agents.",
|
|
1680
|
+
mimeType: "text/plain"
|
|
1681
|
+
},
|
|
1682
|
+
(uri) => ({
|
|
1683
|
+
contents: [{ uri: uri.href, mimeType: "text/plain", text: DISCOVERY_CHECKLIST.join("\n") }]
|
|
1684
|
+
})
|
|
1685
|
+
);
|
|
1686
|
+
server.registerResource(
|
|
1687
|
+
"suture-discovery-security-policy",
|
|
1688
|
+
"suture://resources/discovery/security-policy",
|
|
1689
|
+
{
|
|
1690
|
+
title: "Suture Discovery Security Policy",
|
|
1691
|
+
description: "Security rules for discovery memory ingestion.",
|
|
1692
|
+
mimeType: "text/plain"
|
|
1693
|
+
},
|
|
1694
|
+
(uri) => ({
|
|
1695
|
+
contents: [{ uri: uri.href, mimeType: "text/plain", text: DISCOVERY_SECURITY_POLICY }]
|
|
1696
|
+
})
|
|
1697
|
+
);
|
|
1698
|
+
server.registerPrompt(
|
|
1699
|
+
"suture_project_map_discovery",
|
|
1700
|
+
{
|
|
1701
|
+
title: "Suture Project Map Discovery",
|
|
1702
|
+
description: "Guide a connected agent to produce secure, evidence-backed project setup findings.",
|
|
1703
|
+
argsSchema: {
|
|
1704
|
+
projectPath: import_zod3.z.string()
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1707
|
+
({ projectPath }) => ({
|
|
1708
|
+
messages: [
|
|
1709
|
+
{
|
|
1710
|
+
role: "user",
|
|
1711
|
+
content: {
|
|
1712
|
+
type: "text",
|
|
1713
|
+
text: [
|
|
1714
|
+
`Start Suture discovery for project path: ${projectPath}`,
|
|
1715
|
+
"Use discovery_start, discovery_scan, and discovery_submit.",
|
|
1716
|
+
"Submit project-map findings only as strict JSON matching suture://resources/discovery/schema.",
|
|
1717
|
+
"Prefer low-risk setup facts: languages, scripts, entrypoints, tests, docs, directory roles.",
|
|
1718
|
+
"Attach evidence for each claim. Do not submit secrets, tokens, .env values, or private keys."
|
|
1719
|
+
].join("\n")
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
]
|
|
1723
|
+
})
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// src/core/paths.ts
|
|
1728
|
+
var path5 = __toESM(require("path"), 1);
|
|
1729
|
+
var os = __toESM(require("os"), 1);
|
|
1730
|
+
var DEFAULT_STATE_DIR = process.env.SUTURE_STATE_DIR || path5.join(os.homedir(), ".suture");
|
|
1731
|
+
function resolveStateDir(explicitPath) {
|
|
1732
|
+
if (explicitPath) return path5.resolve(explicitPath);
|
|
1733
|
+
return DEFAULT_STATE_DIR;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// src/server/mcp.ts
|
|
1737
|
+
function createSyncStdout() {
|
|
1738
|
+
return new import_node_stream.Writable({
|
|
1739
|
+
write(chunk, _encoding, callback) {
|
|
1740
|
+
try {
|
|
1741
|
+
if (typeof chunk === "string") {
|
|
1742
|
+
fs5.writeSync(1, chunk);
|
|
1743
|
+
} else {
|
|
1744
|
+
fs5.writeSync(1, Buffer.from(chunk));
|
|
1745
|
+
}
|
|
1746
|
+
callback();
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
async function startMcpServer(stateDirInput, io = {}) {
|
|
1754
|
+
const stateDir = resolveStateDir(stateDirInput);
|
|
1755
|
+
fs5.mkdirSync(stateDir, { recursive: true });
|
|
1756
|
+
const substrate = new SqliteSubstrate(path6.join(stateDir, "suture.db"));
|
|
1757
|
+
const gate = new SessionGate(stateDir);
|
|
1758
|
+
const db = substrate.raw();
|
|
1759
|
+
const memory = new MemoryGraph(db);
|
|
1760
|
+
const code = new CodeGraph(db);
|
|
1761
|
+
const fts = new Fts5Index(db);
|
|
1762
|
+
const indexes = new IndexManager(memory, code, fts);
|
|
1763
|
+
const router = new SearchRouter(memory, code, fts);
|
|
1764
|
+
const scorer = new RirScorer();
|
|
1765
|
+
const curator = new SemanticCurator();
|
|
1766
|
+
const tierManager = new TierManager(substrate, scorer, curator, indexes);
|
|
1767
|
+
const server = new import_mcp.McpServer({ name: "suture-mcp", version: "0.1.0" });
|
|
1768
|
+
registerDiscoveryPrompts(server);
|
|
1769
|
+
for (const t of makeTools(substrate, gate, router, tierManager, memory, code, fts, { stateDir, curator })) {
|
|
1770
|
+
server.tool(t.name, t.schema, async (args) => t.handler(args));
|
|
1771
|
+
}
|
|
1772
|
+
const stdin = io.stdin ?? fs5.createReadStream("", { fd: 0, autoClose: false });
|
|
1773
|
+
const stdout = io.stdout ?? createSyncStdout();
|
|
1774
|
+
const transport = new import_stdio.StdioServerTransport(stdin, stdout);
|
|
1775
|
+
await server.connect(transport);
|
|
1776
|
+
stdin.resume();
|
|
1777
|
+
return {
|
|
1778
|
+
close: async () => {
|
|
1779
|
+
await transport.close();
|
|
1780
|
+
substrate.close();
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/services/project-indexer.ts
|
|
1786
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
1787
|
+
var path7 = __toESM(require("path"), 1);
|
|
1788
|
+
var import_node_crypto2 = require("crypto");
|
|
1789
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
1790
|
+
var LANGUAGE_BY_EXT = {
|
|
1791
|
+
".ts": "TypeScript",
|
|
1792
|
+
".tsx": "TypeScript React",
|
|
1793
|
+
".js": "JavaScript",
|
|
1794
|
+
".jsx": "JavaScript React"
|
|
1795
|
+
};
|
|
1796
|
+
var MANIFEST_NAMES = /* @__PURE__ */ new Set([
|
|
1797
|
+
"package.json",
|
|
1798
|
+
"tsconfig.json",
|
|
1799
|
+
"tsup.config.ts",
|
|
1800
|
+
"vite.config.ts",
|
|
1801
|
+
"vitest.config.ts",
|
|
1802
|
+
"next.config.js",
|
|
1803
|
+
"eslint.config.js"
|
|
1804
|
+
]);
|
|
1805
|
+
var SECRET_FILE_PATTERN = /(^|\/)(\.env|\.env\..*|.*\.pem|.*\.key)$/i;
|
|
1806
|
+
var LOCAL_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
|
|
1807
|
+
".agents",
|
|
1808
|
+
".codex",
|
|
1809
|
+
".suture",
|
|
1810
|
+
".suture-data",
|
|
1811
|
+
".suture-index-test",
|
|
1812
|
+
"--dir",
|
|
1813
|
+
"archive"
|
|
1814
|
+
]);
|
|
1815
|
+
var MAX_INDEX_FILES = 500;
|
|
1816
|
+
var MAX_CODE_FILE_BYTES = 5e5;
|
|
1817
|
+
function slug(value) {
|
|
1818
|
+
return value.replace(/[^A-Za-z0-9_.-]/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
1819
|
+
}
|
|
1820
|
+
function estimateTokens(text) {
|
|
1821
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
1822
|
+
}
|
|
1823
|
+
function hashText(text) {
|
|
1824
|
+
return (0, import_node_crypto2.createHash)("sha256").update(text).digest("hex");
|
|
1825
|
+
}
|
|
1826
|
+
function isLocalOnlyDirectory(name) {
|
|
1827
|
+
return name.startsWith(".") || LOCAL_DIRECTORY_NAMES.has(name);
|
|
1828
|
+
}
|
|
1829
|
+
function safeReadJson(filePath) {
|
|
1830
|
+
try {
|
|
1831
|
+
return JSON.parse(fs6.readFileSync(filePath, "utf-8"));
|
|
1832
|
+
} catch {
|
|
1833
|
+
return null;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
function readGitRemote(root) {
|
|
1837
|
+
const configPath = path7.join(root, ".git", "config");
|
|
1838
|
+
if (!fs6.existsSync(configPath)) return void 0;
|
|
1839
|
+
const config = fs6.readFileSync(configPath, "utf-8");
|
|
1840
|
+
const match = config.match(/\[remote "origin"\][\s\S]*?\n\s*url\s*=\s*(.+)/);
|
|
1841
|
+
return match?.[1]?.trim();
|
|
1842
|
+
}
|
|
1843
|
+
function readPackageName(pkg) {
|
|
1844
|
+
if (pkg && typeof pkg === "object" && "name" in pkg && typeof pkg.name === "string") {
|
|
1845
|
+
return pkg.name;
|
|
1846
|
+
}
|
|
1847
|
+
return void 0;
|
|
1848
|
+
}
|
|
1849
|
+
function buildIdentity(root, pkg) {
|
|
1850
|
+
const packageName = readPackageName(pkg);
|
|
1851
|
+
const gitRemote = readGitRemote(root);
|
|
1852
|
+
const basis = [
|
|
1853
|
+
gitRemote ? `git:${gitRemote}` : null,
|
|
1854
|
+
packageName ? `package:${packageName}` : null,
|
|
1855
|
+
gitRemote || packageName ? null : `path:${fs6.realpathSync(root)}`
|
|
1856
|
+
].filter((item) => Boolean(item));
|
|
1857
|
+
return {
|
|
1858
|
+
packageName,
|
|
1859
|
+
gitRemote,
|
|
1860
|
+
rootPath: root,
|
|
1861
|
+
basis
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
function fingerprintFor(identity) {
|
|
1865
|
+
return hashText(identity.basis.join("\n"));
|
|
1866
|
+
}
|
|
1867
|
+
function projectIdFor2(rootPath, identity, fingerprint) {
|
|
1868
|
+
const base = identity.packageName ?? path7.basename(path7.resolve(rootPath));
|
|
1869
|
+
return `${slug(base)}-${fingerprint.slice(0, 8)}`;
|
|
1870
|
+
}
|
|
1871
|
+
var ProjectIndexer = class {
|
|
1872
|
+
constructor(substrate, memoryStore, codeGraph, tierManager, telemetry, security = new SecurityScanner()) {
|
|
1873
|
+
this.substrate = substrate;
|
|
1874
|
+
this.memoryStore = memoryStore;
|
|
1875
|
+
this.codeGraph = codeGraph;
|
|
1876
|
+
this.tierManager = tierManager;
|
|
1877
|
+
this.telemetry = telemetry;
|
|
1878
|
+
this.security = security;
|
|
1879
|
+
}
|
|
1880
|
+
substrate;
|
|
1881
|
+
memoryStore;
|
|
1882
|
+
codeGraph;
|
|
1883
|
+
tierManager;
|
|
1884
|
+
telemetry;
|
|
1885
|
+
security;
|
|
1886
|
+
preview(projectPath) {
|
|
1887
|
+
const root = path7.resolve(projectPath);
|
|
1888
|
+
if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) {
|
|
1889
|
+
throw new Error(`Project path must be an existing directory: ${root}`);
|
|
1890
|
+
}
|
|
1891
|
+
const ignoredPaths = [];
|
|
1892
|
+
const manifestFiles = [];
|
|
1893
|
+
const codeFiles = [];
|
|
1894
|
+
const languages = /* @__PURE__ */ new Set();
|
|
1895
|
+
const topDirs = /* @__PURE__ */ new Set();
|
|
1896
|
+
let docs = 0;
|
|
1897
|
+
let tests = 0;
|
|
1898
|
+
const walk = (dir, depth) => {
|
|
1899
|
+
if (depth > 4 || codeFiles.length + manifestFiles.length >= MAX_INDEX_FILES) return;
|
|
1900
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1901
|
+
for (const entry of entries) {
|
|
1902
|
+
const full = path7.join(dir, entry.name);
|
|
1903
|
+
const rel = path7.relative(root, full) || entry.name;
|
|
1904
|
+
const normalizedRel = rel.replace(/\\/g, "/");
|
|
1905
|
+
if (entry.isDirectory() && isLocalOnlyDirectory(entry.name)) {
|
|
1906
|
+
ignoredPaths.push(normalizedRel);
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
if (this.security.isIgnoredPath(normalizedRel) || SECRET_FILE_PATTERN.test(normalizedRel)) {
|
|
1910
|
+
ignoredPaths.push(normalizedRel);
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if (entry.isDirectory()) {
|
|
1914
|
+
if (depth === 0) topDirs.add(entry.name);
|
|
1915
|
+
if (/test|spec/i.test(entry.name)) tests++;
|
|
1916
|
+
walk(full, depth + 1);
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
const ext = path7.extname(entry.name);
|
|
1920
|
+
if (LANGUAGE_BY_EXT[ext]) languages.add(LANGUAGE_BY_EXT[ext]);
|
|
1921
|
+
if (MANIFEST_NAMES.has(entry.name)) manifestFiles.push(full);
|
|
1922
|
+
if (CODE_EXTENSIONS.has(ext)) {
|
|
1923
|
+
const size = fs6.statSync(full).size;
|
|
1924
|
+
if (size <= MAX_CODE_FILE_BYTES) codeFiles.push(full);
|
|
1925
|
+
}
|
|
1926
|
+
if (ext === ".md") docs++;
|
|
1927
|
+
if (/(\.test|\.spec)\.[tj]sx?$|test_/i.test(entry.name)) tests++;
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
walk(root, 0);
|
|
1931
|
+
const packagePath = path7.join(root, "package.json");
|
|
1932
|
+
const pkg = safeReadJson(packagePath);
|
|
1933
|
+
const identity = buildIdentity(root, pkg);
|
|
1934
|
+
const fingerprint = fingerprintFor(identity);
|
|
1935
|
+
const existingIndex = this.getExistingIndex(fingerprint, root);
|
|
1936
|
+
const projectId = existingIndex?.projectId ?? projectIdFor2(root, identity, fingerprint);
|
|
1937
|
+
const findings = [];
|
|
1938
|
+
for (const language of languages) {
|
|
1939
|
+
findings.push({
|
|
1940
|
+
type: "language",
|
|
1941
|
+
claim: `Project uses ${language}`,
|
|
1942
|
+
payload: { language },
|
|
1943
|
+
evidence: [{ kind: "file", path: root }],
|
|
1944
|
+
source: "indexer"
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
if (pkg) {
|
|
1948
|
+
findings.push({
|
|
1949
|
+
type: "package_manager",
|
|
1950
|
+
claim: "Project has a package.json and uses the Node package ecosystem",
|
|
1951
|
+
payload: { packageManager: fs6.existsSync(path7.join(root, "package-lock.json")) ? "npm" : "node" },
|
|
1952
|
+
evidence: [{ kind: "package", path: packagePath, key: "name" }],
|
|
1953
|
+
source: "indexer"
|
|
1954
|
+
});
|
|
1955
|
+
for (const [name, command] of Object.entries(pkg.scripts ?? {})) {
|
|
1956
|
+
findings.push({
|
|
1957
|
+
type: "script",
|
|
1958
|
+
claim: `npm script ${name}: ${command}`,
|
|
1959
|
+
payload: { name, command },
|
|
1960
|
+
evidence: [{ kind: "package", path: packagePath, key: `scripts.${name}` }],
|
|
1961
|
+
source: "indexer"
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
if (pkg.main) {
|
|
1965
|
+
findings.push({
|
|
1966
|
+
type: "entrypoint",
|
|
1967
|
+
claim: `Package main entrypoint is ${pkg.main}`,
|
|
1968
|
+
payload: { main: pkg.main },
|
|
1969
|
+
evidence: [{ kind: "package", path: packagePath, key: "main" }],
|
|
1970
|
+
source: "indexer"
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
const dependencies = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} }).slice(0, 12);
|
|
1974
|
+
for (const dependency of dependencies) {
|
|
1975
|
+
findings.push({
|
|
1976
|
+
type: "dependency",
|
|
1977
|
+
claim: `Project depends on ${dependency}`,
|
|
1978
|
+
payload: { dependency },
|
|
1979
|
+
evidence: [{ kind: "package", path: packagePath, key: `dependencies.${dependency}` }],
|
|
1980
|
+
source: "indexer"
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
for (const dir of Array.from(topDirs).slice(0, 20)) {
|
|
1985
|
+
findings.push({
|
|
1986
|
+
type: "directory_role",
|
|
1987
|
+
claim: `Top-level project directory: ${dir}`,
|
|
1988
|
+
payload: { directory: dir },
|
|
1989
|
+
evidence: [{ kind: "file", path: path7.join(root, dir) }],
|
|
1990
|
+
source: "indexer"
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
if (tests > 0) {
|
|
1994
|
+
findings.push({
|
|
1995
|
+
type: "test_strategy",
|
|
1996
|
+
claim: `Project has ${tests} visible test-related files or directories`,
|
|
1997
|
+
payload: { count: tests },
|
|
1998
|
+
evidence: [{ kind: "file", path: root }],
|
|
1999
|
+
source: "indexer"
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
if (docs > 0) {
|
|
2003
|
+
findings.push({
|
|
2004
|
+
type: "convention",
|
|
2005
|
+
claim: `Project has ${docs} documentation files available as evidence but does not generate markdown memory files`,
|
|
2006
|
+
payload: { count: docs, rawMarkdownIngested: false },
|
|
2007
|
+
evidence: [{ kind: "file", path: root }],
|
|
2008
|
+
source: "indexer"
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
return {
|
|
2012
|
+
projectId,
|
|
2013
|
+
fingerprint,
|
|
2014
|
+
identity,
|
|
2015
|
+
existingIndex,
|
|
2016
|
+
projectPath: root,
|
|
2017
|
+
scanPatterns: ["package/config manifests", "project structure", "test signals", "TypeScript/JavaScript symbols"],
|
|
2018
|
+
ignoredPaths: Array.from(new Set(ignoredPaths)).slice(0, 40),
|
|
2019
|
+
manifestFiles,
|
|
2020
|
+
codeFiles,
|
|
2021
|
+
findings,
|
|
2022
|
+
estimatedTokensToCache: findings.reduce((total, finding) => total + estimateTokens(finding.claim), 0),
|
|
2023
|
+
estimatedTokensAvoidedFromRereads: 0
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
async apply(projectPath, options = {}) {
|
|
2027
|
+
const plan = this.preview(projectPath);
|
|
2028
|
+
const discovery = new DiscoveryEngine(this.substrate);
|
|
2029
|
+
const session = discovery.start(plan.projectPath, "suture-indexer", plan.projectId);
|
|
2030
|
+
const enabledIndexes = options.enabledFindingIndexes ? new Set(options.enabledFindingIndexes) : null;
|
|
2031
|
+
const selectedFindings = enabledIndexes ? plan.findings.filter((_, index) => enabledIndexes.has(index)) : plan.findings;
|
|
2032
|
+
const findings = selectedFindings.map((finding) => discovery.submit(session.id, finding));
|
|
2033
|
+
const acceptedFindings = findings.filter((finding) => finding.status === "auto_accepted");
|
|
2034
|
+
let applied = 0;
|
|
2035
|
+
let skippedDuplicateFacts = 0;
|
|
2036
|
+
for (const finding of acceptedFindings) {
|
|
2037
|
+
if (this.hasExistingFact(plan.projectId, finding.claim)) {
|
|
2038
|
+
skippedDuplicateFacts++;
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
await this.memoryStore.remember({
|
|
2042
|
+
content: finding.claim,
|
|
2043
|
+
kind: "summary",
|
|
2044
|
+
source: `project-index:${finding.id}`,
|
|
2045
|
+
sourceType: "discovery",
|
|
2046
|
+
projectId: plan.projectId,
|
|
2047
|
+
evidence: finding.evidence,
|
|
2048
|
+
confidence: finding.confidence,
|
|
2049
|
+
tags: ["project-index", finding.type]
|
|
2050
|
+
});
|
|
2051
|
+
applied++;
|
|
2052
|
+
}
|
|
2053
|
+
let indexedFiles = 0;
|
|
2054
|
+
let skippedFiles = 0;
|
|
2055
|
+
let codeSymbolsIndexed = 0;
|
|
2056
|
+
let estimatedTokensAvoidedFromRereads = 0;
|
|
2057
|
+
for (const filePath of plan.codeFiles) {
|
|
2058
|
+
const source = fs6.readFileSync(filePath, "utf-8");
|
|
2059
|
+
const hash = hashText(source);
|
|
2060
|
+
const rel = path7.relative(plan.projectPath, filePath).replace(/\\/g, "/");
|
|
2061
|
+
const previous = this.substrate.raw().prepare(`SELECT hash, estimatedTokens FROM project_index_files WHERE projectId = ? AND filePath = ?`).get(plan.projectId, rel);
|
|
2062
|
+
if (previous?.hash === hash) {
|
|
2063
|
+
skippedFiles++;
|
|
2064
|
+
estimatedTokensAvoidedFromRereads += previous.estimatedTokens;
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
this.memoryStore.indexCodeFile(rel, source);
|
|
2068
|
+
const symbols = this.codeGraph.countSymbolsForFile(rel);
|
|
2069
|
+
const estimatedTokens = estimateTokens(source);
|
|
2070
|
+
this.substrate.raw().prepare(
|
|
2071
|
+
`INSERT OR REPLACE INTO project_index_files(projectId, filePath, hash, kind, indexedAt, estimatedTokens)
|
|
2072
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
2073
|
+
).run(plan.projectId, rel, hash, "code", Date.now(), estimatedTokens);
|
|
2074
|
+
indexedFiles++;
|
|
2075
|
+
codeSymbolsIndexed += symbols;
|
|
2076
|
+
}
|
|
2077
|
+
const totalIndexedFiles = this.substrate.raw().prepare(`SELECT COUNT(*) AS count FROM project_index_files WHERE projectId = ?`).get(plan.projectId).count;
|
|
2078
|
+
this.recordProjectIndex(
|
|
2079
|
+
plan,
|
|
2080
|
+
totalIndexedFiles,
|
|
2081
|
+
codeSymbolsIndexed > 0 ? codeSymbolsIndexed : plan.existingIndex?.codeSymbols ?? 0
|
|
2082
|
+
);
|
|
2083
|
+
const curation = await this.tierManager.runCuration();
|
|
2084
|
+
const queued = findings.filter((finding) => finding.status === "queued").length;
|
|
2085
|
+
const blocked = findings.filter((finding) => finding.status === "blocked").length;
|
|
2086
|
+
const estimatedTokensCached = plan.estimatedTokensToCache + codeSymbolsIndexed * 5;
|
|
2087
|
+
this.telemetry?.recordIndexing({
|
|
2088
|
+
indexedFiles,
|
|
2089
|
+
skippedFiles,
|
|
2090
|
+
symbols: codeSymbolsIndexed,
|
|
2091
|
+
accepted: acceptedFindings.length,
|
|
2092
|
+
queued,
|
|
2093
|
+
rejected: blocked,
|
|
2094
|
+
tokensCached: estimatedTokensCached,
|
|
2095
|
+
tokensAvoidedFromRereads: estimatedTokensAvoidedFromRereads
|
|
2096
|
+
});
|
|
2097
|
+
for (const action of curation.actions.slice(-8)) {
|
|
2098
|
+
this.telemetry?.recordCurationAction(action);
|
|
2099
|
+
}
|
|
2100
|
+
return {
|
|
2101
|
+
projectId: plan.projectId,
|
|
2102
|
+
fingerprint: plan.fingerprint,
|
|
2103
|
+
existingIndex: plan.existingIndex,
|
|
2104
|
+
projectPath: plan.projectPath,
|
|
2105
|
+
sessionId: session.id,
|
|
2106
|
+
accepted: acceptedFindings.length,
|
|
2107
|
+
queued,
|
|
2108
|
+
blocked,
|
|
2109
|
+
applied,
|
|
2110
|
+
skippedDuplicateFacts,
|
|
2111
|
+
indexedFiles,
|
|
2112
|
+
skippedFiles,
|
|
2113
|
+
codeSymbolsIndexed,
|
|
2114
|
+
estimatedTokensCached,
|
|
2115
|
+
estimatedTokensAvoidedFromRereads,
|
|
2116
|
+
curationActions: curation.actions.length,
|
|
2117
|
+
findings
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
hasExistingFact(projectId, claim) {
|
|
2121
|
+
return this.substrate.listAll().some((episode) => {
|
|
2122
|
+
if (episode.projectId !== projectId) return false;
|
|
2123
|
+
return episode.content.kind === "summary" && episode.content.text === claim;
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
getExistingIndex(fingerprint, rootPath) {
|
|
2127
|
+
const row = this.substrate.raw().prepare(`SELECT * FROM project_index_projects WHERE fingerprint = ?`).get(fingerprint);
|
|
2128
|
+
if (!row) return null;
|
|
2129
|
+
return {
|
|
2130
|
+
projectId: row.projectId,
|
|
2131
|
+
fingerprint: row.fingerprint,
|
|
2132
|
+
rootPath: row.rootPath,
|
|
2133
|
+
packageName: row.packageName ?? void 0,
|
|
2134
|
+
gitRemote: row.gitRemote ?? void 0,
|
|
2135
|
+
indexedFiles: row.indexedFiles,
|
|
2136
|
+
codeSymbols: row.codeSymbols,
|
|
2137
|
+
lastIndexedAt: new Date(row.lastIndexedAt).toISOString(),
|
|
2138
|
+
sameRoot: path7.resolve(row.rootPath) === path7.resolve(rootPath)
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
recordProjectIndex(plan, indexedFiles, codeSymbols) {
|
|
2142
|
+
const now = Date.now();
|
|
2143
|
+
const existing = this.getExistingIndex(plan.fingerprint, plan.projectPath);
|
|
2144
|
+
this.substrate.raw().prepare(
|
|
2145
|
+
`INSERT INTO project_index_projects(
|
|
2146
|
+
projectId, fingerprint, rootPath, packageName, gitRemote, firstIndexedAt, lastIndexedAt, indexedFiles, codeSymbols
|
|
2147
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2148
|
+
ON CONFLICT(projectId) DO UPDATE SET
|
|
2149
|
+
fingerprint = excluded.fingerprint,
|
|
2150
|
+
rootPath = excluded.rootPath,
|
|
2151
|
+
packageName = excluded.packageName,
|
|
2152
|
+
gitRemote = excluded.gitRemote,
|
|
2153
|
+
lastIndexedAt = excluded.lastIndexedAt,
|
|
2154
|
+
indexedFiles = excluded.indexedFiles,
|
|
2155
|
+
codeSymbols = excluded.codeSymbols`
|
|
2156
|
+
).run(
|
|
2157
|
+
plan.projectId,
|
|
2158
|
+
plan.fingerprint,
|
|
2159
|
+
plan.projectPath,
|
|
2160
|
+
plan.identity.packageName ?? null,
|
|
2161
|
+
plan.identity.gitRemote ?? null,
|
|
2162
|
+
existing ? new Date(existing.lastIndexedAt).getTime() : now,
|
|
2163
|
+
now,
|
|
2164
|
+
indexedFiles,
|
|
2165
|
+
codeSymbols
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
};
|
|
2169
|
+
|
|
2170
|
+
// src/hooks/service.ts
|
|
2171
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
2172
|
+
var path8 = __toESM(require("path"), 1);
|
|
2173
|
+
var import_node_crypto3 = require("crypto");
|
|
2174
|
+
function projectIdFor3(rootPath) {
|
|
2175
|
+
return path8.basename(path8.resolve(rootPath)).replace(/[^A-Za-z0-9_.-]/g, "-") || "project";
|
|
2176
|
+
}
|
|
2177
|
+
function hashContent(content) {
|
|
2178
|
+
return (0, import_node_crypto3.createHash)("sha256").update(content).digest("hex");
|
|
2179
|
+
}
|
|
2180
|
+
function setupSignal(projectId, projectPath) {
|
|
2181
|
+
return [
|
|
2182
|
+
"<suture-setup>",
|
|
2183
|
+
`Project: ${projectId}`,
|
|
2184
|
+
`Path: ${path8.resolve(projectPath)}`,
|
|
2185
|
+
"Suture has no accepted project-map memory for this project yet.",
|
|
2186
|
+
"Use the Suture MCP discovery tools in this order:",
|
|
2187
|
+
"1. discovery_start with this project path",
|
|
2188
|
+
"2. discovery_scan for deterministic setup facts",
|
|
2189
|
+
"3. discovery_submit for structured, evidence-backed agent findings",
|
|
2190
|
+
"4. discovery_apply to persist accepted low-risk findings",
|
|
2191
|
+
"Do not submit secrets, tokens, .env values, private keys, or sensitive personal data.",
|
|
2192
|
+
"</suture-setup>"
|
|
2193
|
+
].join("\n");
|
|
2194
|
+
}
|
|
2195
|
+
var HookService = class {
|
|
2196
|
+
constructor(substrate, tierManager, memoryStore) {
|
|
2197
|
+
this.substrate = substrate;
|
|
2198
|
+
this.tierManager = tierManager;
|
|
2199
|
+
this.memoryStore = memoryStore;
|
|
2200
|
+
}
|
|
2201
|
+
substrate;
|
|
2202
|
+
tierManager;
|
|
2203
|
+
memoryStore;
|
|
2204
|
+
decideSessionStart(input2) {
|
|
2205
|
+
const projectId = projectIdFor3(input2.projectPath);
|
|
2206
|
+
const client = input2.client ?? "claude-code";
|
|
2207
|
+
const budget = input2.budget ?? 2048;
|
|
2208
|
+
const acceptedFindings = this.acceptedFindings(projectId);
|
|
2209
|
+
const coreBlock = this.tierManager.getCoreBlock();
|
|
2210
|
+
const force = this.hasReinjectFlag(projectId, input2.sessionId, client);
|
|
2211
|
+
let content = "";
|
|
2212
|
+
let kind = "skip_empty";
|
|
2213
|
+
let reason = "no core memory or accepted discovery findings";
|
|
2214
|
+
if (coreBlock || acceptedFindings.length > 0) {
|
|
2215
|
+
content = this.memoryBlock(projectId, coreBlock, acceptedFindings, budget);
|
|
2216
|
+
kind = "inject_memory";
|
|
2217
|
+
reason = force ? "forced by compaction reinjection flag" : "core memory or accepted discovery findings changed";
|
|
2218
|
+
} else {
|
|
2219
|
+
content = setupSignal(projectId, input2.projectPath).slice(0, budget);
|
|
2220
|
+
kind = "inject_setup_signal";
|
|
2221
|
+
reason = "first-run setup signal";
|
|
2222
|
+
}
|
|
2223
|
+
const hash = hashContent(content);
|
|
2224
|
+
const previous = this.lastHash(projectId, input2.sessionId, client, "SessionStart");
|
|
2225
|
+
if (!force && previous === hash) {
|
|
2226
|
+
return {
|
|
2227
|
+
kind: "skip_unchanged",
|
|
2228
|
+
projectId,
|
|
2229
|
+
sessionId: input2.sessionId,
|
|
2230
|
+
client,
|
|
2231
|
+
hash,
|
|
2232
|
+
content: "",
|
|
2233
|
+
reason: "unchanged hook context hash already injected for this session",
|
|
2234
|
+
forced: false
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
if (!content) {
|
|
2238
|
+
return { kind: "skip_empty", projectId, sessionId: input2.sessionId, client, hash, content, reason, forced: force };
|
|
2239
|
+
}
|
|
2240
|
+
this.recordInjection(projectId, input2.sessionId, client, "SessionStart", hash, kind, reason);
|
|
2241
|
+
if (force) this.consumeReinjectFlag(projectId, input2.sessionId, client);
|
|
2242
|
+
return { kind, projectId, sessionId: input2.sessionId, client, hash, content, reason, forced: force };
|
|
2243
|
+
}
|
|
2244
|
+
preCompact(input2) {
|
|
2245
|
+
const projectId = projectIdFor3(input2.projectPath);
|
|
2246
|
+
const client = input2.client ?? "claude-code";
|
|
2247
|
+
this.substrate.raw().prepare(
|
|
2248
|
+
`INSERT OR REPLACE INTO hook_reinject_flags(projectId, sessionId, client, source, reason, createdAt, consumedAt)
|
|
2249
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL)`
|
|
2250
|
+
).run(projectId, input2.sessionId, client, "hook:pre-compact", "force next SessionStart memory injection", Date.now());
|
|
2251
|
+
return { projectId, sessionId: input2.sessionId, client, flagged: true };
|
|
2252
|
+
}
|
|
2253
|
+
async postCompact(input2) {
|
|
2254
|
+
const projectId = projectIdFor3(input2.projectPath);
|
|
2255
|
+
const client = input2.client ?? "claude-code";
|
|
2256
|
+
const summary = input2.summary ?? (input2.summaryFile ? fs7.readFileSync(input2.summaryFile, "utf-8") : "");
|
|
2257
|
+
if (summary.trim()) {
|
|
2258
|
+
await this.memoryStore.remember({
|
|
2259
|
+
content: summary.trim(),
|
|
2260
|
+
kind: "summary",
|
|
2261
|
+
source: "hook:post-compact",
|
|
2262
|
+
projectId,
|
|
2263
|
+
tags: ["hook", "compact"],
|
|
2264
|
+
confidence: 0.75
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
this.preCompact({ ...input2, client });
|
|
2268
|
+
return { projectId, sessionId: input2.sessionId, client, stored: Boolean(summary.trim()) };
|
|
2269
|
+
}
|
|
2270
|
+
status(input2) {
|
|
2271
|
+
const projectId = projectIdFor3(input2.projectPath);
|
|
2272
|
+
const client = input2.client ?? "claude-code";
|
|
2273
|
+
return {
|
|
2274
|
+
projectId,
|
|
2275
|
+
sessionId: input2.sessionId,
|
|
2276
|
+
client,
|
|
2277
|
+
lastHash: this.lastHash(projectId, input2.sessionId, client, "SessionStart"),
|
|
2278
|
+
reinjectPending: this.hasReinjectFlag(projectId, input2.sessionId, client),
|
|
2279
|
+
acceptedFindings: this.acceptedFindings(projectId).length
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
formatDecision(decision, format = "claude-json") {
|
|
2283
|
+
if (format === "text") {
|
|
2284
|
+
return decision.content ? `${decision.content}
|
|
2285
|
+
` : "";
|
|
2286
|
+
}
|
|
2287
|
+
if (format === "json") {
|
|
2288
|
+
return JSON.stringify(decision);
|
|
2289
|
+
}
|
|
2290
|
+
const output2 = {
|
|
2291
|
+
hookSpecificOutput: {
|
|
2292
|
+
hookEventName: "SessionStart"
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
2295
|
+
if (decision.content) {
|
|
2296
|
+
output2.hookSpecificOutput.additionalContext = decision.content;
|
|
2297
|
+
}
|
|
2298
|
+
return JSON.stringify(output2);
|
|
2299
|
+
}
|
|
2300
|
+
memoryBlock(projectId, coreBlock, findings, budget) {
|
|
2301
|
+
const lines = ["<suture-memory>", `Project: ${projectId}`];
|
|
2302
|
+
if (coreBlock) {
|
|
2303
|
+
lines.push("", "Core memory:", coreBlock.trim());
|
|
2304
|
+
}
|
|
2305
|
+
if (findings.length > 0) {
|
|
2306
|
+
lines.push("", "Accepted project-map findings:");
|
|
2307
|
+
for (const finding of findings) lines.push(`- ${finding}`);
|
|
2308
|
+
}
|
|
2309
|
+
lines.push("</suture-memory>");
|
|
2310
|
+
const block = lines.join("\n");
|
|
2311
|
+
return block.length <= budget ? block : `${block.slice(0, Math.max(0, budget - 32))}
|
|
2312
|
+
</suture-memory>`;
|
|
2313
|
+
}
|
|
2314
|
+
acceptedFindings(projectId) {
|
|
2315
|
+
const rows = this.substrate.raw().prepare(
|
|
2316
|
+
`SELECT claim FROM discovery_findings WHERE projectId = ? AND status = 'auto_accepted' ORDER BY createdAt ASC LIMIT 20`
|
|
2317
|
+
).all(projectId);
|
|
2318
|
+
return rows.map((row) => row.claim);
|
|
2319
|
+
}
|
|
2320
|
+
lastHash(projectId, sessionId, client, event) {
|
|
2321
|
+
const row = this.substrate.raw().prepare(
|
|
2322
|
+
`SELECT lastHash FROM hook_injection_ledger WHERE projectId = ? AND sessionId = ? AND client = ? AND event = ?`
|
|
2323
|
+
).get(projectId, sessionId, client, event);
|
|
2324
|
+
return row?.lastHash ?? null;
|
|
2325
|
+
}
|
|
2326
|
+
recordInjection(projectId, sessionId, client, event, hash, source, reason) {
|
|
2327
|
+
this.substrate.raw().prepare(
|
|
2328
|
+
`INSERT OR REPLACE INTO hook_injection_ledger(
|
|
2329
|
+
projectId, sessionId, client, event, lastHash, lastInjectedAt, source, reason
|
|
2330
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2331
|
+
).run(projectId, sessionId, client, event, hash, Date.now(), source, reason);
|
|
2332
|
+
}
|
|
2333
|
+
hasReinjectFlag(projectId, sessionId, client) {
|
|
2334
|
+
const row = this.substrate.raw().prepare(
|
|
2335
|
+
`SELECT createdAt FROM hook_reinject_flags
|
|
2336
|
+
WHERE projectId = ? AND sessionId = ? AND client = ? AND consumedAt IS NULL`
|
|
2337
|
+
).get(projectId, sessionId, client);
|
|
2338
|
+
return Boolean(row);
|
|
2339
|
+
}
|
|
2340
|
+
consumeReinjectFlag(projectId, sessionId, client) {
|
|
2341
|
+
this.substrate.raw().prepare(
|
|
2342
|
+
`UPDATE hook_reinject_flags SET consumedAt = ? WHERE projectId = ? AND sessionId = ? AND client = ? AND consumedAt IS NULL`
|
|
2343
|
+
).run(Date.now(), projectId, sessionId, client);
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
|
|
2347
|
+
// src/cli/ascii.ts
|
|
2348
|
+
var DEFAULT_WIDTH = 78;
|
|
2349
|
+
function clampWidth(width) {
|
|
2350
|
+
return Math.max(48, Math.min(width ?? DEFAULT_WIDTH, 120));
|
|
2351
|
+
}
|
|
2352
|
+
function line(char, width) {
|
|
2353
|
+
return char.repeat(Math.max(0, width));
|
|
2354
|
+
}
|
|
2355
|
+
function pad(text, width) {
|
|
2356
|
+
const clipped = text.length > width ? text.slice(0, Math.max(0, width - 1)) + "." : text;
|
|
2357
|
+
return clipped + " ".repeat(Math.max(0, width - clipped.length));
|
|
2358
|
+
}
|
|
2359
|
+
function box(title, rows, width = DEFAULT_WIDTH) {
|
|
2360
|
+
const w = clampWidth(width);
|
|
2361
|
+
const inner = w - 4;
|
|
2362
|
+
const header = `+${line("-", w - 2)}+`;
|
|
2363
|
+
const titleRow = `| ${pad(title, inner)} |`;
|
|
2364
|
+
const body = rows.map((row) => `| ${pad(row, inner)} |`);
|
|
2365
|
+
return [header, titleRow, header, ...body, header].join("\n");
|
|
2366
|
+
}
|
|
2367
|
+
function progressBar(value, max, width = 24) {
|
|
2368
|
+
const normalizedMax = Math.max(1, max);
|
|
2369
|
+
const pct = Math.max(0, Math.min(1, value / normalizedMax));
|
|
2370
|
+
const filled = Math.round(pct * width);
|
|
2371
|
+
return `[${"#".repeat(filled)}${".".repeat(width - filled)}] ${(pct * 100).toFixed(0)}%`;
|
|
2372
|
+
}
|
|
2373
|
+
function sparkline(values, width = 24) {
|
|
2374
|
+
if (values.length === 0) return ".".repeat(width);
|
|
2375
|
+
const recent = values.slice(-width);
|
|
2376
|
+
const max = Math.max(...recent, 1);
|
|
2377
|
+
return recent.map((value) => {
|
|
2378
|
+
const pct = value / max;
|
|
2379
|
+
if (pct > 0.8) return "#";
|
|
2380
|
+
if (pct > 0.45) return "+";
|
|
2381
|
+
if (pct > 0.1) return "-";
|
|
2382
|
+
return ".";
|
|
2383
|
+
}).join("").padStart(width, ".");
|
|
2384
|
+
}
|
|
2385
|
+
function renderSetupWelcome(stateDir, projectPath, width = DEFAULT_WIDTH) {
|
|
2386
|
+
return box("Suture MCP setup", [
|
|
2387
|
+
"[ok] terminal-first local memory substrate",
|
|
2388
|
+
`[ok] state dir: ${stateDir}`,
|
|
2389
|
+
`[ok] project: ${projectPath}`,
|
|
2390
|
+
"[..] checking SQLite, FTS5, tree-sitter, and MCP command wiring"
|
|
2391
|
+
], width);
|
|
2392
|
+
}
|
|
2393
|
+
function renderIndexPlan(plan, width = DEFAULT_WIDTH) {
|
|
2394
|
+
const rows = [
|
|
2395
|
+
`Project id: ${plan.projectId}`,
|
|
2396
|
+
`Fingerprint: ${plan.fingerprint.slice(0, 12)}`,
|
|
2397
|
+
`Identity: ${plan.identity.basis.join(" + ")}`,
|
|
2398
|
+
...plan.existingIndex ? [
|
|
2399
|
+
"",
|
|
2400
|
+
"Existing substrate:",
|
|
2401
|
+
` ${plan.existingIndex.sameRoot ? "reuse same project" : "reuse matching fingerprint"}`,
|
|
2402
|
+
` ${plan.existingIndex.indexedFiles} files, ${plan.existingIndex.codeSymbols} symbols`,
|
|
2403
|
+
` last indexed: ${plan.existingIndex.lastIndexedAt}`
|
|
2404
|
+
] : ["Existing substrate: none found"],
|
|
2405
|
+
`Manifests: ${plan.manifestFiles.length}`,
|
|
2406
|
+
`Code files: ${plan.codeFiles.length}`,
|
|
2407
|
+
`Findings: ${plan.findings.length}`,
|
|
2408
|
+
`Ignored: ${plan.ignoredPaths.length}`,
|
|
2409
|
+
`Cache estimate: ${plan.estimatedTokensToCache.toLocaleString()} tokens`,
|
|
2410
|
+
"",
|
|
2411
|
+
"Scan scope:",
|
|
2412
|
+
...plan.scanPatterns.slice(0, 5).map((item) => ` + ${item}`),
|
|
2413
|
+
"",
|
|
2414
|
+
"Ignored examples:",
|
|
2415
|
+
...plan.ignoredPaths.length ? plan.ignoredPaths.slice(0, 5).map((item) => ` - ${item}`) : [" - none found"],
|
|
2416
|
+
"",
|
|
2417
|
+
"Findings preview:",
|
|
2418
|
+
...plan.findings.length ? plan.findings.slice(0, 8).map((finding) => ` [${finding.type}] ${finding.claim}`) : [" - none"]
|
|
2419
|
+
];
|
|
2420
|
+
return box("Index preview", rows, width);
|
|
2421
|
+
}
|
|
2422
|
+
function renderIndexResult(result, width = DEFAULT_WIDTH) {
|
|
2423
|
+
return box("Index applied", [
|
|
2424
|
+
`Project id: ${result.projectId}`,
|
|
2425
|
+
`Fingerprint: ${result.fingerprint.slice(0, 12)}`,
|
|
2426
|
+
result.existingIndex ? `Substrate: reused existing ${result.existingIndex.sameRoot ? "project" : "fingerprint"}` : "Substrate: created new project substrate",
|
|
2427
|
+
`Accepted facts: ${result.accepted} (${result.applied} new, ${result.skippedDuplicateFacts} duplicate)`,
|
|
2428
|
+
`Queued facts: ${result.queued}`,
|
|
2429
|
+
`Blocked facts: ${result.blocked}`,
|
|
2430
|
+
`Files indexed: ${result.indexedFiles}`,
|
|
2431
|
+
`Files skipped: ${result.skippedFiles}`,
|
|
2432
|
+
`Code symbols: ${result.codeSymbolsIndexed}`,
|
|
2433
|
+
`Tokens cached: ~${result.estimatedTokensCached.toLocaleString()}`,
|
|
2434
|
+
`Reread avoided: ~${result.estimatedTokensAvoidedFromRereads.toLocaleString()} tokens`,
|
|
2435
|
+
`Curation events: ${result.curationActions}`
|
|
2436
|
+
], width);
|
|
2437
|
+
}
|
|
2438
|
+
function renderStatsDashboard(stats, cost, width = DEFAULT_WIDTH) {
|
|
2439
|
+
const coreBudget = progressBar(stats.activeContextTokens, 2048);
|
|
2440
|
+
const rows = [
|
|
2441
|
+
`Estimated token savings: ${Math.round(stats.tokensSaved).toLocaleString()} (local estimate)`,
|
|
2442
|
+
`Savings sparkline: ${sparkline(stats.recentTokenSavings)}`,
|
|
2443
|
+
`Estimated cost saved: $${cost.estimatedUsd.toFixed(4)} (${cost.modelProfile})`,
|
|
2444
|
+
`Formula: ${cost.formula}`,
|
|
2445
|
+
"",
|
|
2446
|
+
`Core block budget: ${coreBudget} ${stats.activeContextTokens.toLocaleString()} / 2,048`,
|
|
2447
|
+
`Recall injections: ${stats.totalInjections}`,
|
|
2448
|
+
`Duplicate injections skipped: ${stats.skippedDuplicateInjections}`,
|
|
2449
|
+
`Search hits from substrate: ${stats.searchHitsServedFromSubstrate}`,
|
|
2450
|
+
"",
|
|
2451
|
+
`Indexed files: ${stats.indexedFiles}`,
|
|
2452
|
+
`Skipped unchanged files: ${stats.skippedUnchangedFiles}`,
|
|
2453
|
+
`Code symbols indexed: ${stats.codeSymbolsIndexed}`,
|
|
2454
|
+
`Accepted project facts: ${stats.projectFactsAccepted}`,
|
|
2455
|
+
`Queued project facts: ${stats.projectFactsQueued}`,
|
|
2456
|
+
`Rejected/blocked facts: ${stats.projectFactsRejected}`,
|
|
2457
|
+
`Avoided reread tokens: ~${stats.estimatedTokensAvoidedFromRereads.toLocaleString()}`,
|
|
2458
|
+
"",
|
|
2459
|
+
"Recent curation:",
|
|
2460
|
+
...stats.recentCurationActions.length ? stats.recentCurationActions.slice(-6).map((action) => ` ${action.action}: ${action.detail}`) : [" none yet"]
|
|
2461
|
+
];
|
|
2462
|
+
return box("Suture value dashboard", rows, width);
|
|
2463
|
+
}
|
|
2464
|
+
function renderSetupSuccess(result, stats, cost, width = DEFAULT_WIDTH, selfTest) {
|
|
2465
|
+
const selfTestRows = selfTest ? [
|
|
2466
|
+
selfTest.ok ? `[ok] MCP stdio self-test passed (${selfTest.tools} tools)` : `[!!] MCP stdio self-test failed: ${selfTest.error ?? "unknown error"}`,
|
|
2467
|
+
"[ok] MCP server config generated",
|
|
2468
|
+
"[ok] First project index complete",
|
|
2469
|
+
"[ok] First value dashboard ready"
|
|
2470
|
+
] : [
|
|
2471
|
+
"[ok] MCP server config generated",
|
|
2472
|
+
"[ok] First project index complete",
|
|
2473
|
+
"[ok] First value dashboard ready"
|
|
2474
|
+
];
|
|
2475
|
+
return [
|
|
2476
|
+
renderIndexResult(result, width),
|
|
2477
|
+
"",
|
|
2478
|
+
box("Install checks", selfTestRows, width),
|
|
2479
|
+
"",
|
|
2480
|
+
renderStatsDashboard(stats, cost, width),
|
|
2481
|
+
"",
|
|
2482
|
+
box("Next steps", [
|
|
2483
|
+
"1. Add the printed MCP config to your agent.",
|
|
2484
|
+
"2. Ask the agent to search Suture before rereading large files.",
|
|
2485
|
+
"3. Run `suture-mcp stats` to see value accumulate."
|
|
2486
|
+
], width)
|
|
2487
|
+
].join("\n");
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// src/cli/tui.ts
|
|
2491
|
+
var readline = __toESM(require("readline"), 1);
|
|
2492
|
+
function confidenceMarker(plan, index) {
|
|
2493
|
+
const finding = plan.findings[index];
|
|
2494
|
+
if (!finding) return " ";
|
|
2495
|
+
if (!finding.evidence?.length) return "!";
|
|
2496
|
+
if (finding.type === "convention" || (finding.confidence ?? 1) < 0.6) return "~";
|
|
2497
|
+
return "+";
|
|
2498
|
+
}
|
|
2499
|
+
function renderFindingPicker(plan, state, width = 78) {
|
|
2500
|
+
const selectedCount = state.selected.filter(Boolean).length;
|
|
2501
|
+
const start = Math.max(0, Math.min(state.cursor - 6, Math.max(0, plan.findings.length - 12)));
|
|
2502
|
+
const visible = plan.findings.slice(start, start + 12);
|
|
2503
|
+
const rows = [
|
|
2504
|
+
`Select durable project facts: ${selectedCount}/${plan.findings.length}`,
|
|
2505
|
+
"Keys: up/down move space toggle a all n none enter apply q cancel",
|
|
2506
|
+
"Confidence: + evidence-backed ~ review/queued ! no evidence",
|
|
2507
|
+
"",
|
|
2508
|
+
...visible.map((finding, offset) => {
|
|
2509
|
+
const index = start + offset;
|
|
2510
|
+
const cursor = index === state.cursor ? ">" : " ";
|
|
2511
|
+
const check = state.selected[index] ? "x" : " ";
|
|
2512
|
+
const confidence = confidenceMarker(plan, index);
|
|
2513
|
+
return `${cursor} [${check}] ${confidence} ${finding.type}: ${finding.claim}`;
|
|
2514
|
+
})
|
|
2515
|
+
];
|
|
2516
|
+
return box("Index curation", rows, width);
|
|
2517
|
+
}
|
|
2518
|
+
async function pickFindings(plan, width = 78) {
|
|
2519
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
|
|
2520
|
+
return plan.findings.map((_, index) => index);
|
|
2521
|
+
}
|
|
2522
|
+
const state = {
|
|
2523
|
+
cursor: 0,
|
|
2524
|
+
selected: plan.findings.map(() => true)
|
|
2525
|
+
};
|
|
2526
|
+
readline.emitKeypressEvents(process.stdin);
|
|
2527
|
+
process.stdin.setRawMode(true);
|
|
2528
|
+
process.stdin.resume();
|
|
2529
|
+
const render = () => {
|
|
2530
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2531
|
+
process.stdout.write(`${renderFindingPicker(plan, state, width)}
|
|
2532
|
+
`);
|
|
2533
|
+
};
|
|
2534
|
+
try {
|
|
2535
|
+
render();
|
|
2536
|
+
return await new Promise((resolve7) => {
|
|
2537
|
+
const cleanup = () => {
|
|
2538
|
+
process.stdin.off("keypress", onKeypress);
|
|
2539
|
+
process.stdin.setRawMode(false);
|
|
2540
|
+
};
|
|
2541
|
+
const finish = (value) => {
|
|
2542
|
+
cleanup();
|
|
2543
|
+
process.stdout.write("\n");
|
|
2544
|
+
resolve7(value);
|
|
2545
|
+
};
|
|
2546
|
+
const onKeypress = (_str, key) => {
|
|
2547
|
+
if (key.name === "up" || key.name === "k") {
|
|
2548
|
+
state.cursor = Math.max(0, state.cursor - 1);
|
|
2549
|
+
render();
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
if (key.name === "down" || key.name === "j") {
|
|
2553
|
+
state.cursor = Math.min(plan.findings.length - 1, state.cursor + 1);
|
|
2554
|
+
render();
|
|
2555
|
+
return;
|
|
2556
|
+
}
|
|
2557
|
+
if (key.name === "space") {
|
|
2558
|
+
state.selected[state.cursor] = !state.selected[state.cursor];
|
|
2559
|
+
render();
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (key.name === "a") {
|
|
2563
|
+
state.selected = state.selected.map(() => true);
|
|
2564
|
+
render();
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
if (key.name === "n") {
|
|
2568
|
+
state.selected = state.selected.map(() => false);
|
|
2569
|
+
render();
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
if (key.name === "return") {
|
|
2573
|
+
finish(state.selected.flatMap((selected, index) => selected ? [index] : []));
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
if (key.name === "escape" || key.name === "q" || key.ctrl && key.name === "c") {
|
|
2577
|
+
finish(null);
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
process.stdin.on("keypress", onKeypress);
|
|
2581
|
+
});
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
process.stdin.setRawMode(false);
|
|
2584
|
+
throw error;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// src/cli.ts
|
|
2589
|
+
function getFlag(args, flag) {
|
|
2590
|
+
const idx = args.indexOf(flag);
|
|
2591
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("--")) {
|
|
2592
|
+
return args[idx + 1];
|
|
2593
|
+
}
|
|
2594
|
+
return void 0;
|
|
2595
|
+
}
|
|
2596
|
+
function hasFlag(args, flag) {
|
|
2597
|
+
return args.includes(flag);
|
|
2598
|
+
}
|
|
2599
|
+
function positional(args) {
|
|
2600
|
+
const values = [];
|
|
2601
|
+
for (let i = 0; i < args.length; i++) {
|
|
2602
|
+
const arg = args[i];
|
|
2603
|
+
if (arg.startsWith("--")) {
|
|
2604
|
+
if (args[i + 1] && !args[i + 1].startsWith("--")) i++;
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
values.push(arg);
|
|
2608
|
+
}
|
|
2609
|
+
return values;
|
|
2610
|
+
}
|
|
2611
|
+
function terminalWidth() {
|
|
2612
|
+
return process.stdout.columns ? Math.min(process.stdout.columns, 100) : 78;
|
|
2613
|
+
}
|
|
2614
|
+
function ensureStateDir(dir) {
|
|
2615
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
2616
|
+
}
|
|
2617
|
+
function createRuntime(stateDir) {
|
|
2618
|
+
ensureStateDir(stateDir);
|
|
2619
|
+
const substrate = new SqliteSubstrate(path9.join(stateDir, "suture.db"));
|
|
2620
|
+
const db = substrate.raw();
|
|
2621
|
+
const memory = new MemoryGraph(db);
|
|
2622
|
+
const code = new CodeGraph(db);
|
|
2623
|
+
const fts = new Fts5Index(db);
|
|
2624
|
+
const indexes = new IndexManager(memory, code, fts);
|
|
2625
|
+
const router = new SearchRouter(memory, code, fts);
|
|
2626
|
+
const curator = new SemanticCurator();
|
|
2627
|
+
const tierManager = new TierManager(substrate, new RirScorer(), curator, indexes);
|
|
2628
|
+
const telemetry = new TelemetryService(path9.join(stateDir, "telemetry.json"));
|
|
2629
|
+
const memoryStore = new MemoryStore(substrate, indexes, router, curator, void 0, telemetry);
|
|
2630
|
+
return { substrate, telemetry, tierManager, memoryStore, code };
|
|
2631
|
+
}
|
|
2632
|
+
function printUsage(exitCode = 0) {
|
|
2633
|
+
const usage = [
|
|
2634
|
+
"Usage: suture-mcp <command> [options]",
|
|
2635
|
+
"",
|
|
2636
|
+
"Commands:",
|
|
2637
|
+
" setup [--project <path>] [--dir <state>] [--yes] [--json]",
|
|
2638
|
+
" init [--dir <state>]",
|
|
2639
|
+
" serve [--dir <state>]",
|
|
2640
|
+
" doctor [--dir <state>] [--json]",
|
|
2641
|
+
" index <project> [--dir <state>] [--apply-safe] [--json]",
|
|
2642
|
+
" hook session-start|pre-compact|post-compact|status --project <path> --session <id> [--client <name>]",
|
|
2643
|
+
" stats [--dir <state>] [--json] [--model-profile conservative|standard|premium]",
|
|
2644
|
+
" recall [--dir <state>]",
|
|
2645
|
+
"",
|
|
2646
|
+
"Beta defaults: local SQLite, no markdown memory files, safe opt-in indexing."
|
|
2647
|
+
].join("\n");
|
|
2648
|
+
process.stdout.write(`${usage}
|
|
2649
|
+
`);
|
|
2650
|
+
process.exit(exitCode);
|
|
2651
|
+
}
|
|
2652
|
+
function mcpConfig(command = "suture-mcp", stateDir) {
|
|
2653
|
+
return JSON.stringify({
|
|
2654
|
+
mcpServers: {
|
|
2655
|
+
suture: {
|
|
2656
|
+
command,
|
|
2657
|
+
args: ["serve", "--dir", stateDir]
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}, null, 2);
|
|
2661
|
+
}
|
|
2662
|
+
async function runMcpSelfTest(stateDir) {
|
|
2663
|
+
return await new Promise((resolve7) => {
|
|
2664
|
+
const clientInput = new import_node_stream2.PassThrough();
|
|
2665
|
+
const serverOutput = new import_node_stream2.PassThrough();
|
|
2666
|
+
let stdout = "";
|
|
2667
|
+
let settled = false;
|
|
2668
|
+
let initialized = false;
|
|
2669
|
+
let handle = null;
|
|
2670
|
+
const finish = (result) => {
|
|
2671
|
+
if (settled) return;
|
|
2672
|
+
settled = true;
|
|
2673
|
+
clearTimeout(timer);
|
|
2674
|
+
clientInput.destroy();
|
|
2675
|
+
serverOutput.destroy();
|
|
2676
|
+
void handle?.close();
|
|
2677
|
+
resolve7(result);
|
|
2678
|
+
};
|
|
2679
|
+
const send = (message) => {
|
|
2680
|
+
clientInput.write(`${JSON.stringify(message)}
|
|
2681
|
+
`);
|
|
2682
|
+
};
|
|
2683
|
+
const handleLine = (line2) => {
|
|
2684
|
+
if (!line2.trim()) return;
|
|
2685
|
+
const message = JSON.parse(line2);
|
|
2686
|
+
if (message.error) {
|
|
2687
|
+
finish({ ok: false, tools: 0, error: message.error.message ?? "MCP self-test returned an error" });
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
if (message.id === 1) {
|
|
2691
|
+
initialized = true;
|
|
2692
|
+
send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
2693
|
+
send({ jsonrpc: "2.0", method: "tools/list", id: 2 });
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
if (message.id === 2) {
|
|
2697
|
+
const tools = Array.isArray(message.result?.tools) ? message.result.tools.length : 0;
|
|
2698
|
+
finish({ ok: initialized && tools > 0, tools, error: tools > 0 ? void 0 : "No MCP tools were listed" });
|
|
2699
|
+
}
|
|
2700
|
+
};
|
|
2701
|
+
const timer = setTimeout(() => {
|
|
2702
|
+
finish({ ok: false, tools: 0, error: "MCP self-test timed out" });
|
|
2703
|
+
}, 1e4);
|
|
2704
|
+
serverOutput.on("data", (chunk) => {
|
|
2705
|
+
stdout += String(chunk);
|
|
2706
|
+
let newline = stdout.indexOf("\n");
|
|
2707
|
+
while (newline !== -1) {
|
|
2708
|
+
const line2 = stdout.slice(0, newline);
|
|
2709
|
+
stdout = stdout.slice(newline + 1);
|
|
2710
|
+
try {
|
|
2711
|
+
handleLine(line2);
|
|
2712
|
+
} catch (error) {
|
|
2713
|
+
finish({ ok: false, tools: 0, error: error instanceof Error ? error.message : String(error) });
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
newline = stdout.indexOf("\n");
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
startMcpServer(stateDir, { stdin: clientInput, stdout: serverOutput }).then((serverHandle) => {
|
|
2720
|
+
handle = serverHandle;
|
|
2721
|
+
send({
|
|
2722
|
+
jsonrpc: "2.0",
|
|
2723
|
+
method: "initialize",
|
|
2724
|
+
params: {
|
|
2725
|
+
protocolVersion: "2025-11-25",
|
|
2726
|
+
capabilities: {},
|
|
2727
|
+
clientInfo: { name: "suture-setup", version: "0.1.0" }
|
|
2728
|
+
},
|
|
2729
|
+
id: 1
|
|
2730
|
+
});
|
|
2731
|
+
}).catch((error) => {
|
|
2732
|
+
finish({ ok: false, tools: 0, error: error instanceof Error ? error.message : String(error) });
|
|
2733
|
+
});
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
async function promptFor(defaultValue, label) {
|
|
2737
|
+
const rl = readline2.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
2738
|
+
try {
|
|
2739
|
+
const answer = await rl.question(`${label} [${defaultValue}]: `);
|
|
2740
|
+
return answer.trim() || defaultValue;
|
|
2741
|
+
} finally {
|
|
2742
|
+
rl.close();
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
async function confirm(defaultNoPrompt) {
|
|
2746
|
+
const rl = readline2.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout });
|
|
2747
|
+
try {
|
|
2748
|
+
const answer = await rl.question(`${defaultNoPrompt} [y/N]: `);
|
|
2749
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
2750
|
+
} finally {
|
|
2751
|
+
rl.close();
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
async function runSetup(args, initialStateDir) {
|
|
2755
|
+
const json = hasFlag(args, "--json");
|
|
2756
|
+
const yes = hasFlag(args, "--yes");
|
|
2757
|
+
let stateDir = initialStateDir;
|
|
2758
|
+
let projectPath = path9.resolve(getFlag(args, "--project") ?? process.cwd());
|
|
2759
|
+
const interactive = !yes && !json && process.stdin.isTTY && process.stdout.isTTY;
|
|
2760
|
+
if (interactive) {
|
|
2761
|
+
process.stdout.write(`${renderSetupWelcome(stateDir, projectPath, terminalWidth())}
|
|
2762
|
+
|
|
2763
|
+
`);
|
|
2764
|
+
stateDir = resolveStateDir(await promptFor(stateDir, "State directory"));
|
|
2765
|
+
projectPath = path9.resolve(await promptFor(projectPath, "Project path"));
|
|
2766
|
+
}
|
|
2767
|
+
const runtime = createRuntime(stateDir);
|
|
2768
|
+
try {
|
|
2769
|
+
const indexer = new ProjectIndexer(
|
|
2770
|
+
runtime.substrate,
|
|
2771
|
+
runtime.memoryStore,
|
|
2772
|
+
runtime.code,
|
|
2773
|
+
runtime.tierManager,
|
|
2774
|
+
runtime.telemetry
|
|
2775
|
+
);
|
|
2776
|
+
const plan = indexer.preview(projectPath);
|
|
2777
|
+
if (json && !yes) {
|
|
2778
|
+
process.stdout.write(`${JSON.stringify({ stateDir, mcpConfig: JSON.parse(mcpConfig("suture-mcp", stateDir)), plan }, null, 2)}
|
|
2779
|
+
`);
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
if (!json) {
|
|
2783
|
+
process.stdout.write(`${renderIndexPlan(plan, terminalWidth())}
|
|
2784
|
+
|
|
2785
|
+
`);
|
|
2786
|
+
process.stdout.write(box("MCP config preview", mcpConfig("suture-mcp", stateDir).split("\n"), terminalWidth()) + "\n\n");
|
|
2787
|
+
}
|
|
2788
|
+
let enabledFindingIndexes;
|
|
2789
|
+
if (interactive) {
|
|
2790
|
+
enabledFindingIndexes = await pickFindings(plan, terminalWidth()) ?? void 0;
|
|
2791
|
+
if (!enabledFindingIndexes) {
|
|
2792
|
+
process.stdout.write("Setup stopped before persistence. No project facts were stored.\n");
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
} else if (!yes && !json && process.stdin.isTTY && !await confirm("Apply this safe project index now?")) {
|
|
2796
|
+
process.stdout.write("Setup stopped before persistence. No project facts were stored.\n");
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
if (!json) {
|
|
2800
|
+
process.stdout.write(box("Indexing", [
|
|
2801
|
+
`Applying ${enabledFindingIndexes?.length ?? plan.findings.length} selected findings`,
|
|
2802
|
+
"Indexing code symbols and updating local value metrics",
|
|
2803
|
+
"Running event-based curation after persistence"
|
|
2804
|
+
], terminalWidth()) + "\n\n");
|
|
2805
|
+
}
|
|
2806
|
+
const result = await indexer.apply(projectPath, { enabledFindingIndexes });
|
|
2807
|
+
const cost = runtime.telemetry.estimateCostSavings(getFlag(args, "--model-profile") ?? "standard");
|
|
2808
|
+
const selfTest = await runMcpSelfTest(stateDir);
|
|
2809
|
+
if (json) {
|
|
2810
|
+
process.stdout.write(`${JSON.stringify({
|
|
2811
|
+
stateDir,
|
|
2812
|
+
mcpConfig: JSON.parse(mcpConfig("suture-mcp", stateDir)),
|
|
2813
|
+
mcpSelfTest: selfTest,
|
|
2814
|
+
result,
|
|
2815
|
+
stats: runtime.telemetry.getStats(),
|
|
2816
|
+
cost
|
|
2817
|
+
}, null, 2)}
|
|
2818
|
+
`);
|
|
2819
|
+
return;
|
|
2820
|
+
}
|
|
2821
|
+
process.stdout.write(`${renderSetupSuccess(result, runtime.telemetry.getStats(), cost, terminalWidth(), selfTest)}
|
|
2822
|
+
`);
|
|
2823
|
+
} finally {
|
|
2824
|
+
runtime.substrate.close();
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
function runDoctor(args, stateDir) {
|
|
2828
|
+
const checks = [];
|
|
2829
|
+
let runtime = null;
|
|
2830
|
+
try {
|
|
2831
|
+
runtime = createRuntime(stateDir);
|
|
2832
|
+
checks.push({ name: "node", ok: Number(process.versions.node.split(".")[0]) >= 20, detail: process.versions.node });
|
|
2833
|
+
checks.push({ name: "state-dir", ok: fs8.existsSync(stateDir), detail: stateDir });
|
|
2834
|
+
const fts = runtime.substrate.raw().prepare(`SELECT sqlite_version() AS version`).get();
|
|
2835
|
+
checks.push({ name: "sqlite", ok: true, detail: fts.version });
|
|
2836
|
+
runtime.code.indexFile("doctor.ts", "export function doctorCheck(): boolean { return true; }");
|
|
2837
|
+
checks.push({ name: "tree-sitter", ok: runtime.code.countSymbolsForFile("doctor.ts") > 0, detail: "TypeScript parser loaded" });
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
checks.push({ name: "runtime", ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
2840
|
+
} finally {
|
|
2841
|
+
runtime?.substrate.close();
|
|
2842
|
+
}
|
|
2843
|
+
if (hasFlag(args, "--json")) {
|
|
2844
|
+
process.stdout.write(`${JSON.stringify({ checks }, null, 2)}
|
|
2845
|
+
`);
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
process.stdout.write(box("Suture doctor", checks.map((check) => {
|
|
2849
|
+
return `[${check.ok ? "ok" : "!!"}] ${check.name}: ${check.detail}`;
|
|
2850
|
+
}), terminalWidth()) + "\n");
|
|
2851
|
+
if (checks.some((check) => !check.ok)) process.exit(1);
|
|
2852
|
+
}
|
|
2853
|
+
async function runIndex(args, stateDir) {
|
|
2854
|
+
const projectPath = path9.resolve(positional(args)[0] ?? getFlag(args, "--project") ?? process.cwd());
|
|
2855
|
+
const runtime = createRuntime(stateDir);
|
|
2856
|
+
try {
|
|
2857
|
+
const indexer = new ProjectIndexer(
|
|
2858
|
+
runtime.substrate,
|
|
2859
|
+
runtime.memoryStore,
|
|
2860
|
+
runtime.code,
|
|
2861
|
+
runtime.tierManager,
|
|
2862
|
+
runtime.telemetry
|
|
2863
|
+
);
|
|
2864
|
+
if (!hasFlag(args, "--apply-safe")) {
|
|
2865
|
+
const plan = indexer.preview(projectPath);
|
|
2866
|
+
if (hasFlag(args, "--json")) {
|
|
2867
|
+
process.stdout.write(`${JSON.stringify(plan, null, 2)}
|
|
2868
|
+
`);
|
|
2869
|
+
} else {
|
|
2870
|
+
process.stdout.write(`${renderIndexPlan(plan, terminalWidth())}
|
|
2871
|
+
`);
|
|
2872
|
+
}
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
const result = await indexer.apply(projectPath);
|
|
2876
|
+
if (hasFlag(args, "--json")) {
|
|
2877
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
2878
|
+
`);
|
|
2879
|
+
} else {
|
|
2880
|
+
process.stdout.write(`${renderIndexResult(result, terminalWidth())}
|
|
2881
|
+
`);
|
|
2882
|
+
}
|
|
2883
|
+
} finally {
|
|
2884
|
+
runtime.substrate.close();
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
function runStats(args, stateDir) {
|
|
2888
|
+
const telemetry = new TelemetryService(path9.join(stateDir, "telemetry.json"));
|
|
2889
|
+
const modelProfile = getFlag(args, "--model-profile") ?? "standard";
|
|
2890
|
+
const explicitCost = getFlag(args, "--input-cost-per-million");
|
|
2891
|
+
const cost = telemetry.estimateCostSavings(
|
|
2892
|
+
modelProfile,
|
|
2893
|
+
explicitCost ? Number(explicitCost) : void 0
|
|
2894
|
+
);
|
|
2895
|
+
const stats = telemetry.getStats();
|
|
2896
|
+
if (hasFlag(args, "--json")) {
|
|
2897
|
+
process.stdout.write(`${JSON.stringify({ stats, cost }, null, 2)}
|
|
2898
|
+
`);
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
process.stdout.write(`${renderStatsDashboard(stats, cost, terminalWidth())}
|
|
2902
|
+
`);
|
|
2903
|
+
}
|
|
2904
|
+
async function runRecall(stateDir) {
|
|
2905
|
+
const runtime = createRuntime(stateDir);
|
|
2906
|
+
try {
|
|
2907
|
+
const block = runtime.tierManager.getCoreBlock();
|
|
2908
|
+
runtime.telemetry.recordToolUsage("recall");
|
|
2909
|
+
runtime.telemetry.recordInjection(block.length / 4);
|
|
2910
|
+
runtime.telemetry.recordSavings(block.length / 4);
|
|
2911
|
+
process.stdout.write(block + "\n");
|
|
2912
|
+
} finally {
|
|
2913
|
+
runtime.substrate.close();
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
function hookInput(args) {
|
|
2917
|
+
return {
|
|
2918
|
+
projectPath: path9.resolve(getFlag(args, "--project") ?? process.cwd()),
|
|
2919
|
+
sessionId: getFlag(args, "--session") ?? getFlag(args, "--session-id") ?? "default",
|
|
2920
|
+
client: getFlag(args, "--client"),
|
|
2921
|
+
format: getFlag(args, "--format") ?? (hasFlag(args, "--json") ? "json" : void 0),
|
|
2922
|
+
budget: getFlag(args, "--budget") ? Number(getFlag(args, "--budget")) : void 0
|
|
2923
|
+
};
|
|
2924
|
+
}
|
|
2925
|
+
async function runHook(args, stateDir) {
|
|
2926
|
+
const [event, ...eventArgs] = args;
|
|
2927
|
+
const runtime = createRuntime(stateDir);
|
|
2928
|
+
const hook = new HookService(runtime.substrate, runtime.tierManager, runtime.memoryStore);
|
|
2929
|
+
const input2 = hookInput(eventArgs);
|
|
2930
|
+
try {
|
|
2931
|
+
if (event === "session-start") {
|
|
2932
|
+
process.stdout.write(hook.formatDecision(
|
|
2933
|
+
hook.decideSessionStart(input2),
|
|
2934
|
+
input2.format ?? "claude-json"
|
|
2935
|
+
) + "\n");
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
if (event === "pre-compact") {
|
|
2939
|
+
const result = hook.preCompact(input2);
|
|
2940
|
+
process.stdout.write(hasFlag(eventArgs, "--json") ? `${JSON.stringify(result)}
|
|
2941
|
+
` : `Flagged next session-start reinjection for ${result.projectId}/${result.sessionId}
|
|
2942
|
+
`);
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2945
|
+
if (event === "post-compact") {
|
|
2946
|
+
const result = await hook.postCompact({
|
|
2947
|
+
...input2,
|
|
2948
|
+
summary: getFlag(eventArgs, "--summary"),
|
|
2949
|
+
summaryFile: getFlag(eventArgs, "--summary-file")
|
|
2950
|
+
});
|
|
2951
|
+
process.stdout.write(hasFlag(eventArgs, "--json") ? `${JSON.stringify(result)}
|
|
2952
|
+
` : `${result.stored ? "Stored compact summary and flagged" : "Flagged"} next session-start reinjection for ${result.projectId}/${result.sessionId}
|
|
2953
|
+
`);
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
if (event === "status") {
|
|
2957
|
+
const result = hook.status(input2);
|
|
2958
|
+
process.stdout.write(hasFlag(eventArgs, "--json") ? `${JSON.stringify(result)}
|
|
2959
|
+
` : box("Suture hook status", [
|
|
2960
|
+
`Project: ${result.projectId}`,
|
|
2961
|
+
`Session: ${result.sessionId}`,
|
|
2962
|
+
`Client: ${result.client}`,
|
|
2963
|
+
`Last hash: ${result.lastHash ?? "none"}`,
|
|
2964
|
+
`Reinject pending: ${result.reinjectPending ? "yes" : "no"}`,
|
|
2965
|
+
`Accepted findings: ${result.acceptedFindings}`
|
|
2966
|
+
], terminalWidth()) + "\n");
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
} finally {
|
|
2970
|
+
runtime.substrate.close();
|
|
2971
|
+
}
|
|
2972
|
+
process.stderr.write("Unknown hook event. Use: session-start, pre-compact, post-compact, or status.\n");
|
|
2973
|
+
process.exit(1);
|
|
2974
|
+
}
|
|
2975
|
+
async function main() {
|
|
2976
|
+
const [cmd, ...args] = process.argv.slice(2);
|
|
2977
|
+
const dir = resolveStateDir(getFlag(args, "--dir"));
|
|
2978
|
+
if (!cmd || cmd === "--help" || cmd === "-h") printUsage(0);
|
|
2979
|
+
if (cmd === "init") {
|
|
2980
|
+
const runtime = createRuntime(dir);
|
|
2981
|
+
runtime.substrate.close();
|
|
2982
|
+
process.stdout.write(`Initialized suture at ${dir}
|
|
2983
|
+
`);
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
if (cmd === "setup") {
|
|
2987
|
+
await runSetup(args, dir);
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
if (cmd === "serve") {
|
|
2991
|
+
const serverHandle = await startMcpServer(dir);
|
|
2992
|
+
await new Promise((resolve7) => {
|
|
2993
|
+
const keepAlive = setInterval(() => void 0, 2147483647);
|
|
2994
|
+
const stop = () => {
|
|
2995
|
+
clearInterval(keepAlive);
|
|
2996
|
+
void serverHandle.close();
|
|
2997
|
+
resolve7();
|
|
2998
|
+
};
|
|
2999
|
+
process.once("SIGTERM", stop);
|
|
3000
|
+
process.once("SIGINT", stop);
|
|
3001
|
+
});
|
|
3002
|
+
return;
|
|
3003
|
+
}
|
|
3004
|
+
if (cmd === "doctor") {
|
|
3005
|
+
runDoctor(args, dir);
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
if (cmd === "index" || cmd === "discover") {
|
|
3009
|
+
await runIndex(args, dir);
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
if (cmd === "hook") {
|
|
3013
|
+
await runHook(args, dir);
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
if (cmd === "stats") {
|
|
3017
|
+
runStats(args, dir);
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
if (cmd === "recall") {
|
|
3021
|
+
await runRecall(dir);
|
|
3022
|
+
return;
|
|
3023
|
+
}
|
|
3024
|
+
process.stderr.write(`Unknown command: ${cmd}
|
|
3025
|
+
|
|
3026
|
+
`);
|
|
3027
|
+
printUsage(1);
|
|
3028
|
+
}
|
|
3029
|
+
main().catch((err) => {
|
|
3030
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
3031
|
+
`);
|
|
3032
|
+
process.exit(1);
|
|
3033
|
+
});
|