@swarmvaultai/engine 3.16.1 → 3.17.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/dist/chunk-2CH2WWS4.js +1359 -0
- package/dist/chunk-2PN46RDI.js +26846 -0
- package/dist/chunk-333AMRSV.js +1056 -0
- package/dist/chunk-3GVEUYQZ.js +1641 -0
- package/dist/chunk-4MSSM2GH.js +1476 -0
- package/dist/chunk-563TZ4TZ.js +26573 -0
- package/dist/chunk-5GEPTIZE.js +26010 -0
- package/dist/chunk-5HNZ2WQI.js +1341 -0
- package/dist/chunk-5Q4IV4O3.js +1336 -0
- package/dist/chunk-65IRGGXX.js +27576 -0
- package/dist/chunk-6MO57J5C.js +988 -0
- package/dist/chunk-6UPHDGEB.js +1073 -0
- package/dist/chunk-75BU5TQ6.js +1690 -0
- package/dist/chunk-7O2HJSWQ.js +1686 -0
- package/dist/chunk-7QHDATCQ.js +1673 -0
- package/dist/chunk-B3FC4J3P.js +1214 -0
- package/dist/chunk-BTWPJEP2.js +1421 -0
- package/dist/chunk-CG67P2HB.js +1420 -0
- package/dist/chunk-CSPDMCON.js +26846 -0
- package/dist/chunk-CVFY54CF.js +24893 -0
- package/dist/chunk-CWLDFLH2.js +1163 -0
- package/dist/chunk-DAJAZPPO.js +26865 -0
- package/dist/chunk-EEWB4WGH.js +1056 -0
- package/dist/chunk-EXD4RWT3.js +1131 -0
- package/dist/chunk-F7HZZ3VM.js +931 -0
- package/dist/chunk-FD3LJQ4T.js +1216 -0
- package/dist/chunk-G2TH6ZTA.js +1468 -0
- package/dist/chunk-H3CDZYRE.js +1701 -0
- package/dist/chunk-HFU5S5NO.js +838 -0
- package/dist/chunk-HKU2T5JX.js +25213 -0
- package/dist/chunk-HOJ7NSYC.js +937 -0
- package/dist/chunk-HORJDLXV.js +27614 -0
- package/dist/chunk-HRRPWXRZ.js +1335 -0
- package/dist/chunk-HW72C7O2.js +1690 -0
- package/dist/chunk-IAEYFTUS.js +1159 -0
- package/dist/chunk-IHMJCCXR.js +1146 -0
- package/dist/chunk-JEWLYIHN.js +27619 -0
- package/dist/chunk-JJDJF2P3.js +27012 -0
- package/dist/chunk-JTRE7C7P.js +26062 -0
- package/dist/chunk-L7DKPPV4.js +27339 -0
- package/dist/chunk-LEUV6TWJ.js +1131 -0
- package/dist/chunk-MB7HPUTR.js +1364 -0
- package/dist/chunk-MZSUYTSL.js +998 -0
- package/dist/chunk-N56FAH4N.js +1404 -0
- package/dist/chunk-NCSZ4AKP.js +1057 -0
- package/dist/chunk-NECZ4MUE.js +1416 -0
- package/dist/chunk-NHGS4LOI.js +1346 -0
- package/dist/chunk-NUWZUYE7.js +1701 -0
- package/dist/chunk-OK5752AP.js +1325 -0
- package/dist/chunk-QMW7OISM.js +1063 -0
- package/dist/chunk-RN56HUXA.js +26972 -0
- package/dist/chunk-RSQRF4FV.js +1424 -0
- package/dist/chunk-S2E65WRI.js +26062 -0
- package/dist/chunk-SRHM3HP4.js +944 -0
- package/dist/chunk-U7JO257M.js +25017 -0
- package/dist/chunk-UQCF65BN.js +1623 -0
- package/dist/chunk-USSP4GVB.js +25064 -0
- package/dist/chunk-V7KX3AQD.js +26010 -0
- package/dist/chunk-VSDBQVSE.js +27584 -0
- package/dist/chunk-WOA5LSNB.js +26559 -0
- package/dist/chunk-WWP3VPEJ.js +26080 -0
- package/dist/chunk-YFKWMXJ6.js +26066 -0
- package/dist/chunk-Z552HHPV.js +26846 -0
- package/dist/chunk-ZQ5T64AR.js +1365 -0
- package/dist/hooks/claude.js +236 -19
- package/dist/hooks/codex.js +134 -6
- package/dist/hooks/copilot.js +95 -3
- package/dist/hooks/gemini.js +153 -5
- package/dist/hooks/opencode.js +4 -2
- package/dist/index.d.ts +19 -1
- package/dist/index.js +237 -3
- package/dist/memory-A4VPLUBA.js +32 -0
- package/dist/memory-DNSQCDHC.js +32 -0
- package/dist/memory-ECS3TSGC.js +32 -0
- package/dist/memory-FVIBFROA.js +32 -0
- package/dist/memory-G6I3DBW4.js +32 -0
- package/dist/memory-GFOW2QWQ.js +32 -0
- package/dist/memory-GSCQ6F53.js +32 -0
- package/dist/memory-HE6VWUPV.js +32 -0
- package/dist/memory-HEA7XNKB.js +32 -0
- package/dist/memory-HMP3Y4PQ.js +32 -0
- package/dist/memory-JRYTVHNH.js +32 -0
- package/dist/memory-K3NL5E3K.js +32 -0
- package/dist/memory-KANI73CX.js +32 -0
- package/dist/memory-KI5G2A4C.js +32 -0
- package/dist/memory-PK55JUKG.js +32 -0
- package/dist/memory-PK5JJNAG.js +32 -0
- package/dist/memory-PQWSJ4RR.js +32 -0
- package/dist/memory-QCVKS3H4.js +32 -0
- package/dist/memory-SAQPBIB4.js +32 -0
- package/dist/memory-SVGRP5KS.js +32 -0
- package/dist/memory-TQ46BGCI.js +32 -0
- package/dist/memory-YKQWWIVY.js +32 -0
- package/dist/memory-Z7BP5OSC.js +32 -0
- package/dist/registry-2QC3VN7M.js +12 -0
- package/dist/registry-2REAPKPO.js +12 -0
- package/dist/registry-2XHXZDGH.js +12 -0
- package/dist/registry-4C55ZCPL.js +12 -0
- package/dist/registry-4QRMVAHX.js +12 -0
- package/dist/registry-5SYH3Y3U.js +12 -0
- package/dist/registry-6KZMA3XM.js +12 -0
- package/dist/registry-7QACDJQQ.js +12 -0
- package/dist/registry-B7UXRBW3.js +12 -0
- package/dist/registry-FKEREVDO.js +12 -0
- package/dist/registry-FLSGGY2R.js +12 -0
- package/dist/registry-G7NSRYCO.js +12 -0
- package/dist/registry-GH4O3A7H.js +12 -0
- package/dist/registry-IBH6K2KK.js +12 -0
- package/dist/registry-ILDEBNCW.js +12 -0
- package/dist/registry-JFEW5RUP.js +12 -0
- package/dist/registry-JQYQOZYN.js +12 -0
- package/dist/registry-JR5WY22P.js +12 -0
- package/dist/registry-KLO5YIHP.js +12 -0
- package/dist/registry-KVJAO5DF.js +12 -0
- package/dist/registry-MYJX6AEE.js +12 -0
- package/dist/registry-NBLIJHZT.js +12 -0
- package/dist/registry-NLRWSN5J.js +12 -0
- package/dist/registry-NMXDBYIZ.js +12 -0
- package/dist/registry-OUB6W3LM.js +12 -0
- package/dist/registry-P5KRT66L.js +12 -0
- package/dist/registry-PGZWRXMD.js +12 -0
- package/dist/registry-QAG2ZYH3.js +12 -0
- package/dist/registry-SUXWCWB4.js +12 -0
- package/dist/registry-SYCRRA65.js +12 -0
- package/dist/registry-TYROWPR5.js +12 -0
- package/dist/registry-U23ML76I.js +12 -0
- package/dist/registry-U76DBOV3.js +12 -0
- package/dist/registry-UA42LQUQ.js +12 -0
- package/dist/registry-W6ZFRI73.js +12 -0
- package/dist/registry-X5PMZTZY.js +12 -0
- package/dist/registry-XIL5F33J.js +12 -0
- package/dist/registry-XOPLQNZY.js +12 -0
- package/dist/registry-YDXVCE4Q.js +12 -0
- package/dist/registry-YGVTLIZH.js +12 -0
- package/dist/registry-ZNW3FDED.js +12 -0
- package/dist/viewer/assets/{index-Cq5HAlrV.js → index-BZE-2FtS.js} +37 -37
- package/dist/viewer/index.html +1 -1
- package/dist/viewer/lib.js +29 -1
- package/package.json +1 -1
package/dist/hooks/claude.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/hooks/claude.ts
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
|
|
3
6
|
// src/hooks/marker-state.ts
|
|
4
7
|
import crypto from "crypto";
|
|
5
8
|
import fs from "fs/promises";
|
|
@@ -13,6 +16,23 @@ function markerState(cwd, agentKey) {
|
|
|
13
16
|
markerPath: path.join(dir, "report-read")
|
|
14
17
|
};
|
|
15
18
|
}
|
|
19
|
+
function flagPath(cwd, agentKey, name) {
|
|
20
|
+
const safeName = name.replaceAll(/[^a-z0-9-]/gi, "-");
|
|
21
|
+
return path.join(markerState(cwd, agentKey).dir, safeName);
|
|
22
|
+
}
|
|
23
|
+
async function markFlag(cwd, agentKey, name) {
|
|
24
|
+
const target = flagPath(cwd, agentKey, name);
|
|
25
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
26
|
+
await fs.writeFile(target, "seen\n", "utf8");
|
|
27
|
+
}
|
|
28
|
+
async function hasFlag(cwd, agentKey, name) {
|
|
29
|
+
try {
|
|
30
|
+
await fs.access(flagPath(cwd, agentKey, name));
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
16
36
|
function isReportPath(value, cwd) {
|
|
17
37
|
if (typeof value !== "string" || value.length === 0) {
|
|
18
38
|
return false;
|
|
@@ -57,6 +77,10 @@ function resolveToolName(input) {
|
|
|
57
77
|
const shaped = input ?? {};
|
|
58
78
|
return String(shaped.toolName ?? shaped.tool_name ?? shaped.tool?.name ?? shaped.name ?? "");
|
|
59
79
|
}
|
|
80
|
+
function resolveToolInput(input) {
|
|
81
|
+
const shaped = input ?? {};
|
|
82
|
+
return shaped.toolInput ?? shaped.tool_input ?? {};
|
|
83
|
+
}
|
|
60
84
|
async function hasReport(cwd) {
|
|
61
85
|
try {
|
|
62
86
|
await fs.access(reportPath(cwd));
|
|
@@ -135,6 +159,100 @@ function isBroadSearchInput(input) {
|
|
|
135
159
|
}
|
|
136
160
|
return collectCommandCandidates(input).some(commandLooksLikeBroadSearch);
|
|
137
161
|
}
|
|
162
|
+
var VAULT_ARTIFACT_SEGMENTS = ["wiki", "raw", "state", "agent", "inbox"];
|
|
163
|
+
function isVaultArtifactSearch(input, cwd) {
|
|
164
|
+
const artifactRoot = artifactRootDir(cwd);
|
|
165
|
+
const candidates = [...collectCandidatePaths(input), ...collectCommandCandidates(input)];
|
|
166
|
+
return candidates.some((candidate) => {
|
|
167
|
+
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const normalized = candidate.replaceAll("\\", "/");
|
|
171
|
+
if (VAULT_ARTIFACT_SEGMENTS.some(
|
|
172
|
+
(segment) => normalized.includes(`${segment}/`) && normalized.match(new RegExp(`(^|[\\s'"=/])${segment}/`))
|
|
173
|
+
)) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
const resolved = path.resolve(cwd, candidate);
|
|
177
|
+
return VAULT_ARTIFACT_SEGMENTS.some(
|
|
178
|
+
(segment) => resolved.startsWith(path.join(artifactRoot, segment) + path.sep) || resolved === path.join(artifactRoot, segment)
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async function isNarrowSearch(input) {
|
|
183
|
+
const toolInput = resolveToolInput(input);
|
|
184
|
+
const candidate = toolInput?.path;
|
|
185
|
+
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const stats = await fs.stat(candidate);
|
|
190
|
+
return stats.isFile();
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function resolveGraphFirstMode(cwd) {
|
|
196
|
+
const fromEnv = process.env.SWARMVAULT_GRAPH_FIRST?.trim().toLowerCase();
|
|
197
|
+
if (fromEnv === "deny" || fromEnv === "context" || fromEnv === "off") {
|
|
198
|
+
return fromEnv;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const raw = await fs.readFile(path.join(cwd, "swarmvault.config.json"), "utf8");
|
|
202
|
+
const parsed = JSON.parse(raw);
|
|
203
|
+
const fromConfig = typeof parsed?.hooks?.graphFirst === "string" ? parsed.hooks.graphFirst.toLowerCase() : "";
|
|
204
|
+
if (fromConfig === "deny" || fromConfig === "context" || fromConfig === "off") {
|
|
205
|
+
return fromConfig;
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
return "deny";
|
|
210
|
+
}
|
|
211
|
+
async function readWatchStaleness(cwd) {
|
|
212
|
+
const watchDir = path.join(artifactRootDir(cwd), "state", "watch");
|
|
213
|
+
let lastRunAt;
|
|
214
|
+
let lastRunSuccess;
|
|
215
|
+
let pendingCount = 0;
|
|
216
|
+
let found = false;
|
|
217
|
+
try {
|
|
218
|
+
const raw = await fs.readFile(path.join(watchDir, "status.json"), "utf8");
|
|
219
|
+
const parsed = JSON.parse(raw);
|
|
220
|
+
lastRunAt = typeof parsed?.lastRun?.finishedAt === "string" ? parsed.lastRun.finishedAt : void 0;
|
|
221
|
+
lastRunSuccess = typeof parsed?.lastRun?.success === "boolean" ? parsed.lastRun.success : void 0;
|
|
222
|
+
found = true;
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const raw = await fs.readFile(path.join(watchDir, "pending-semantic-refresh.json"), "utf8");
|
|
227
|
+
const parsed = JSON.parse(raw);
|
|
228
|
+
if (Array.isArray(parsed)) {
|
|
229
|
+
pendingCount = parsed.length;
|
|
230
|
+
found = true;
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
if (!found) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return { lastRunAt, lastRunSuccess, pendingSemanticRefreshCount: pendingCount };
|
|
238
|
+
}
|
|
239
|
+
function collectEditedFilePaths(input, cwd) {
|
|
240
|
+
const toolInput = resolveToolInput(input);
|
|
241
|
+
const candidates = [];
|
|
242
|
+
for (const key of ["file_path", "filePath", "path", "notebook_path", "notebookPath"]) {
|
|
243
|
+
const value = toolInput?.[key];
|
|
244
|
+
if (typeof value === "string" && value.length > 0) {
|
|
245
|
+
candidates.push(value);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const artifactRoot = artifactRootDir(cwd);
|
|
249
|
+
const resolved = candidates.map((candidate) => path.resolve(cwd, candidate)).filter(
|
|
250
|
+
(absolutePath) => !VAULT_ARTIFACT_SEGMENTS.some(
|
|
251
|
+
(segment) => absolutePath === path.join(artifactRoot, segment) || absolutePath.startsWith(path.join(artifactRoot, segment) + path.sep)
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
return [...new Set(resolved)];
|
|
255
|
+
}
|
|
138
256
|
async function readHookInput() {
|
|
139
257
|
let body = "";
|
|
140
258
|
for await (const chunk of process.stdin) {
|
|
@@ -149,7 +267,52 @@ async function readHookInput() {
|
|
|
149
267
|
return {};
|
|
150
268
|
}
|
|
151
269
|
}
|
|
152
|
-
var
|
|
270
|
+
var GRAPH_FIRST_COMMANDS = [
|
|
271
|
+
'- `swarmvault graph query "<seed>"` \u2014 top matches with page paths plus an inline excerpt of the best page; usually answers where-is/what-calls in one command',
|
|
272
|
+
'- `swarmvault graph explain "<node>"` \u2014 compact node summary with neighbors and its wiki page',
|
|
273
|
+
"- `swarmvault graph blast <target>` \u2014 reverse-import impact analysis for change-impact questions",
|
|
274
|
+
"- `wiki/graph/report.md` \u2014 orientation report (architecture, communities, key nodes)",
|
|
275
|
+
"Do not add `--json` to these \u2014 the plain output is far smaller and already structured.",
|
|
276
|
+
"Trust the graph/wiki answer for orientation questions; verify in source only when you are about to edit or the evidence conflicts. Answer directly in chat \u2014 do not write answer files unless asked for a durable artifact."
|
|
277
|
+
];
|
|
278
|
+
function buildGraphFirstNote(staleness) {
|
|
279
|
+
const lines = [
|
|
280
|
+
"This repo has a SwarmVault code graph. To save tokens, answer code-understanding questions (where is X, what calls Y, how is Z structured, impact of changing W) from the graph instead of reading or grepping source files:",
|
|
281
|
+
...GRAPH_FIRST_COMMANDS,
|
|
282
|
+
"Read source files directly only when you are about to edit them, or when the graph lacks the detail you need.",
|
|
283
|
+
"After your edits the SwarmVault hook refreshes the graph automatically."
|
|
284
|
+
];
|
|
285
|
+
if (staleness?.pendingSemanticRefreshCount) {
|
|
286
|
+
lines.push(
|
|
287
|
+
`Note: ${staleness.pendingSemanticRefreshCount} non-code change(s) await semantic refresh \u2014 run \`swarmvault compile\` when convenient.`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (staleness?.lastRunSuccess === false) {
|
|
291
|
+
lines.push(
|
|
292
|
+
"Note: the last graph refresh failed \u2014 run `swarmvault graph status` then `swarmvault graph update` before relying on the graph."
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return lines.join("\n");
|
|
296
|
+
}
|
|
297
|
+
function extractSearchTerm(input) {
|
|
298
|
+
const toolInput = resolveToolInput(input);
|
|
299
|
+
for (const key of ["pattern", "query", "regex"]) {
|
|
300
|
+
const value = toolInput?.[key];
|
|
301
|
+
if (typeof value === "string" && value.length > 0) {
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return "<your term>";
|
|
306
|
+
}
|
|
307
|
+
function buildDenyReason(toolName, input) {
|
|
308
|
+
const term = extractSearchTerm(input).slice(0, 120);
|
|
309
|
+
return [
|
|
310
|
+
`SwarmVault graph-first: this repo has a compiled code graph that answers structure questions in far fewer tokens than ${toolName || "broad search"}.`,
|
|
311
|
+
`Run: swarmvault graph query "${term}" \u2014 it prints the top matches with page paths plus an inline excerpt of the best page, which usually answers the question without reading source. Add --context calls for caller/impact questions. Do not add --json (much larger output).`,
|
|
312
|
+
"Trust that answer for orientation questions instead of re-verifying in source files.",
|
|
313
|
+
"If the graph does not answer, repeat this exact search \u2014 it will be allowed for the rest of the session."
|
|
314
|
+
].join(" ");
|
|
315
|
+
}
|
|
153
316
|
|
|
154
317
|
// src/hooks/claude.ts
|
|
155
318
|
var AGENT_KEY = "claude";
|
|
@@ -157,38 +320,92 @@ function emit(value) {
|
|
|
157
320
|
process.stdout.write(`${JSON.stringify(value)}
|
|
158
321
|
`);
|
|
159
322
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
323
|
+
function denyFlagName(toolName) {
|
|
324
|
+
return `deny-search-${(toolName || "unknown").toLowerCase()}`;
|
|
325
|
+
}
|
|
326
|
+
async function handleSessionStart(cwd) {
|
|
327
|
+
await resetSession(cwd, AGENT_KEY);
|
|
328
|
+
const staleness = await readWatchStaleness(cwd);
|
|
329
|
+
emit({
|
|
330
|
+
hookSpecificOutput: {
|
|
331
|
+
hookEventName: "SessionStart",
|
|
332
|
+
additionalContext: buildGraphFirstNote(staleness)
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async function handlePostEdit(cwd, input) {
|
|
337
|
+
const editedPaths = collectEditedFilePaths(input, cwd);
|
|
338
|
+
if (editedPaths.length > 0) {
|
|
339
|
+
try {
|
|
340
|
+
const child = spawn("swarmvault", ["graph", "update", ...editedPaths.flatMap((p) => ["--file", p]), "--json"], {
|
|
341
|
+
cwd,
|
|
342
|
+
detached: true,
|
|
343
|
+
stdio: "ignore"
|
|
344
|
+
});
|
|
345
|
+
child.unref();
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
emit({});
|
|
350
|
+
}
|
|
351
|
+
async function handlePreToolUse(cwd, input) {
|
|
352
|
+
if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
|
|
353
|
+
await markReportRead(cwd, AGENT_KEY);
|
|
165
354
|
emit({});
|
|
166
|
-
|
|
355
|
+
return;
|
|
167
356
|
}
|
|
168
|
-
|
|
169
|
-
|
|
357
|
+
const mode = await resolveGraphFirstMode(cwd);
|
|
358
|
+
if (mode === "off" || !isBroadSearchInput(input)) {
|
|
359
|
+
emit({});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (isVaultArtifactSearch(input, cwd) || await isNarrowSearch(input)) {
|
|
363
|
+
emit({});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const toolName = resolveToolName(input);
|
|
367
|
+
const flag = denyFlagName(toolName);
|
|
368
|
+
if (mode === "deny" && !await hasFlag(cwd, AGENT_KEY, flag)) {
|
|
369
|
+
await markFlag(cwd, AGENT_KEY, flag);
|
|
170
370
|
emit({
|
|
171
371
|
hookSpecificOutput: {
|
|
172
|
-
hookEventName: "
|
|
173
|
-
|
|
372
|
+
hookEventName: "PreToolUse",
|
|
373
|
+
permissionDecision: "deny",
|
|
374
|
+
permissionDecisionReason: buildDenyReason(toolName, input)
|
|
174
375
|
}
|
|
175
376
|
});
|
|
176
|
-
|
|
377
|
+
return;
|
|
177
378
|
}
|
|
178
|
-
if (
|
|
379
|
+
if (!await hasSeenReport(cwd, AGENT_KEY)) {
|
|
179
380
|
await markReportRead(cwd, AGENT_KEY);
|
|
180
|
-
emit({});
|
|
181
|
-
process.exit(0);
|
|
182
|
-
}
|
|
183
|
-
if (isBroadSearchInput(input) && !await hasSeenReport(cwd, AGENT_KEY)) {
|
|
184
381
|
emit({
|
|
185
382
|
hookSpecificOutput: {
|
|
186
383
|
hookEventName: "PreToolUse",
|
|
187
|
-
|
|
384
|
+
permissionDecision: "allow",
|
|
385
|
+
additionalContext: buildDenyReason(toolName, input)
|
|
188
386
|
}
|
|
189
387
|
});
|
|
190
|
-
|
|
388
|
+
return;
|
|
191
389
|
}
|
|
192
390
|
emit({});
|
|
193
391
|
}
|
|
392
|
+
async function main() {
|
|
393
|
+
const mode = process.argv[2] ?? "";
|
|
394
|
+
const input = await readHookInput();
|
|
395
|
+
const cwd = resolveInputCwd(input);
|
|
396
|
+
if (!await hasReport(cwd) || await resolveGraphFirstMode(cwd) === "off") {
|
|
397
|
+
emit({});
|
|
398
|
+
process.exit(0);
|
|
399
|
+
}
|
|
400
|
+
if (mode === "session-start") {
|
|
401
|
+
await handleSessionStart(cwd);
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}
|
|
404
|
+
if (mode === "post-edit") {
|
|
405
|
+
await handlePostEdit(cwd, input);
|
|
406
|
+
process.exit(0);
|
|
407
|
+
}
|
|
408
|
+
await handlePreToolUse(cwd, input);
|
|
409
|
+
process.exit(0);
|
|
410
|
+
}
|
|
194
411
|
await main();
|
package/dist/hooks/codex.js
CHANGED
|
@@ -57,6 +57,10 @@ function resolveToolName(input) {
|
|
|
57
57
|
const shaped = input ?? {};
|
|
58
58
|
return String(shaped.toolName ?? shaped.tool_name ?? shaped.tool?.name ?? shaped.name ?? "");
|
|
59
59
|
}
|
|
60
|
+
function resolveToolInput(input) {
|
|
61
|
+
const shaped = input ?? {};
|
|
62
|
+
return shaped.toolInput ?? shaped.tool_input ?? {};
|
|
63
|
+
}
|
|
60
64
|
async function hasReport(cwd) {
|
|
61
65
|
try {
|
|
62
66
|
await fs.access(reportPath(cwd));
|
|
@@ -135,6 +139,83 @@ function isBroadSearchInput(input) {
|
|
|
135
139
|
}
|
|
136
140
|
return collectCommandCandidates(input).some(commandLooksLikeBroadSearch);
|
|
137
141
|
}
|
|
142
|
+
var VAULT_ARTIFACT_SEGMENTS = ["wiki", "raw", "state", "agent", "inbox"];
|
|
143
|
+
function isVaultArtifactSearch(input, cwd) {
|
|
144
|
+
const artifactRoot = artifactRootDir(cwd);
|
|
145
|
+
const candidates = [...collectCandidatePaths(input), ...collectCommandCandidates(input)];
|
|
146
|
+
return candidates.some((candidate) => {
|
|
147
|
+
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const normalized = candidate.replaceAll("\\", "/");
|
|
151
|
+
if (VAULT_ARTIFACT_SEGMENTS.some(
|
|
152
|
+
(segment) => normalized.includes(`${segment}/`) && normalized.match(new RegExp(`(^|[\\s'"=/])${segment}/`))
|
|
153
|
+
)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
const resolved = path.resolve(cwd, candidate);
|
|
157
|
+
return VAULT_ARTIFACT_SEGMENTS.some(
|
|
158
|
+
(segment) => resolved.startsWith(path.join(artifactRoot, segment) + path.sep) || resolved === path.join(artifactRoot, segment)
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async function isNarrowSearch(input) {
|
|
163
|
+
const toolInput = resolveToolInput(input);
|
|
164
|
+
const candidate = toolInput?.path;
|
|
165
|
+
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const stats = await fs.stat(candidate);
|
|
170
|
+
return stats.isFile();
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function resolveGraphFirstMode(cwd) {
|
|
176
|
+
const fromEnv = process.env.SWARMVAULT_GRAPH_FIRST?.trim().toLowerCase();
|
|
177
|
+
if (fromEnv === "deny" || fromEnv === "context" || fromEnv === "off") {
|
|
178
|
+
return fromEnv;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const raw = await fs.readFile(path.join(cwd, "swarmvault.config.json"), "utf8");
|
|
182
|
+
const parsed = JSON.parse(raw);
|
|
183
|
+
const fromConfig = typeof parsed?.hooks?.graphFirst === "string" ? parsed.hooks.graphFirst.toLowerCase() : "";
|
|
184
|
+
if (fromConfig === "deny" || fromConfig === "context" || fromConfig === "off") {
|
|
185
|
+
return fromConfig;
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
return "deny";
|
|
190
|
+
}
|
|
191
|
+
async function readWatchStaleness(cwd) {
|
|
192
|
+
const watchDir = path.join(artifactRootDir(cwd), "state", "watch");
|
|
193
|
+
let lastRunAt;
|
|
194
|
+
let lastRunSuccess;
|
|
195
|
+
let pendingCount = 0;
|
|
196
|
+
let found = false;
|
|
197
|
+
try {
|
|
198
|
+
const raw = await fs.readFile(path.join(watchDir, "status.json"), "utf8");
|
|
199
|
+
const parsed = JSON.parse(raw);
|
|
200
|
+
lastRunAt = typeof parsed?.lastRun?.finishedAt === "string" ? parsed.lastRun.finishedAt : void 0;
|
|
201
|
+
lastRunSuccess = typeof parsed?.lastRun?.success === "boolean" ? parsed.lastRun.success : void 0;
|
|
202
|
+
found = true;
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const raw = await fs.readFile(path.join(watchDir, "pending-semantic-refresh.json"), "utf8");
|
|
207
|
+
const parsed = JSON.parse(raw);
|
|
208
|
+
if (Array.isArray(parsed)) {
|
|
209
|
+
pendingCount = parsed.length;
|
|
210
|
+
found = true;
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
if (!found) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
return { lastRunAt, lastRunSuccess, pendingSemanticRefreshCount: pendingCount };
|
|
218
|
+
}
|
|
138
219
|
async function readHookInput() {
|
|
139
220
|
let body = "";
|
|
140
221
|
for await (const chunk of process.stdin) {
|
|
@@ -149,7 +230,52 @@ async function readHookInput() {
|
|
|
149
230
|
return {};
|
|
150
231
|
}
|
|
151
232
|
}
|
|
152
|
-
var
|
|
233
|
+
var GRAPH_FIRST_COMMANDS = [
|
|
234
|
+
'- `swarmvault graph query "<seed>"` \u2014 top matches with page paths plus an inline excerpt of the best page; usually answers where-is/what-calls in one command',
|
|
235
|
+
'- `swarmvault graph explain "<node>"` \u2014 compact node summary with neighbors and its wiki page',
|
|
236
|
+
"- `swarmvault graph blast <target>` \u2014 reverse-import impact analysis for change-impact questions",
|
|
237
|
+
"- `wiki/graph/report.md` \u2014 orientation report (architecture, communities, key nodes)",
|
|
238
|
+
"Do not add `--json` to these \u2014 the plain output is far smaller and already structured.",
|
|
239
|
+
"Trust the graph/wiki answer for orientation questions; verify in source only when you are about to edit or the evidence conflicts. Answer directly in chat \u2014 do not write answer files unless asked for a durable artifact."
|
|
240
|
+
];
|
|
241
|
+
function buildGraphFirstNote(staleness) {
|
|
242
|
+
const lines = [
|
|
243
|
+
"This repo has a SwarmVault code graph. To save tokens, answer code-understanding questions (where is X, what calls Y, how is Z structured, impact of changing W) from the graph instead of reading or grepping source files:",
|
|
244
|
+
...GRAPH_FIRST_COMMANDS,
|
|
245
|
+
"Read source files directly only when you are about to edit them, or when the graph lacks the detail you need.",
|
|
246
|
+
"After your edits the SwarmVault hook refreshes the graph automatically."
|
|
247
|
+
];
|
|
248
|
+
if (staleness?.pendingSemanticRefreshCount) {
|
|
249
|
+
lines.push(
|
|
250
|
+
`Note: ${staleness.pendingSemanticRefreshCount} non-code change(s) await semantic refresh \u2014 run \`swarmvault compile\` when convenient.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
if (staleness?.lastRunSuccess === false) {
|
|
254
|
+
lines.push(
|
|
255
|
+
"Note: the last graph refresh failed \u2014 run `swarmvault graph status` then `swarmvault graph update` before relying on the graph."
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return lines.join("\n");
|
|
259
|
+
}
|
|
260
|
+
function extractSearchTerm(input) {
|
|
261
|
+
const toolInput = resolveToolInput(input);
|
|
262
|
+
for (const key of ["pattern", "query", "regex"]) {
|
|
263
|
+
const value = toolInput?.[key];
|
|
264
|
+
if (typeof value === "string" && value.length > 0) {
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return "<your term>";
|
|
269
|
+
}
|
|
270
|
+
function buildDenyReason(toolName, input) {
|
|
271
|
+
const term = extractSearchTerm(input).slice(0, 120);
|
|
272
|
+
return [
|
|
273
|
+
`SwarmVault graph-first: this repo has a compiled code graph that answers structure questions in far fewer tokens than ${toolName || "broad search"}.`,
|
|
274
|
+
`Run: swarmvault graph query "${term}" \u2014 it prints the top matches with page paths plus an inline excerpt of the best page, which usually answers the question without reading source. Add --context calls for caller/impact questions. Do not add --json (much larger output).`,
|
|
275
|
+
"Trust that answer for orientation questions instead of re-verifying in source files.",
|
|
276
|
+
"If the graph does not answer, repeat this exact search \u2014 it will be allowed for the rest of the session."
|
|
277
|
+
].join(" ");
|
|
278
|
+
}
|
|
153
279
|
|
|
154
280
|
// src/hooks/codex.ts
|
|
155
281
|
var AGENT_KEY = "codex";
|
|
@@ -157,10 +283,10 @@ function emit(value) {
|
|
|
157
283
|
process.stdout.write(`${JSON.stringify(value)}
|
|
158
284
|
`);
|
|
159
285
|
}
|
|
160
|
-
function note() {
|
|
286
|
+
function note(message) {
|
|
161
287
|
return {
|
|
162
288
|
priority: "IMPORTANT",
|
|
163
|
-
message
|
|
289
|
+
message
|
|
164
290
|
};
|
|
165
291
|
}
|
|
166
292
|
async function main() {
|
|
@@ -173,7 +299,7 @@ async function main() {
|
|
|
173
299
|
}
|
|
174
300
|
if (mode === "session-start") {
|
|
175
301
|
await resetSession(cwd, AGENT_KEY);
|
|
176
|
-
emit(note());
|
|
302
|
+
emit(note(buildGraphFirstNote(await readWatchStaleness(cwd))));
|
|
177
303
|
process.exit(0);
|
|
178
304
|
}
|
|
179
305
|
if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
|
|
@@ -181,8 +307,10 @@ async function main() {
|
|
|
181
307
|
emit({});
|
|
182
308
|
process.exit(0);
|
|
183
309
|
}
|
|
184
|
-
|
|
185
|
-
|
|
310
|
+
const graphFirstMode = await resolveGraphFirstMode(cwd);
|
|
311
|
+
if (graphFirstMode !== "off" && isBroadSearchInput(input) && !isVaultArtifactSearch(input, cwd) && !await isNarrowSearch(input) && !await hasSeenReport(cwd, AGENT_KEY)) {
|
|
312
|
+
await markReportRead(cwd, AGENT_KEY);
|
|
313
|
+
emit(note(buildDenyReason(resolveToolName(input), input)));
|
|
186
314
|
process.exit(0);
|
|
187
315
|
}
|
|
188
316
|
emit({});
|
package/dist/hooks/copilot.js
CHANGED
|
@@ -57,6 +57,10 @@ function resolveToolName(input) {
|
|
|
57
57
|
const shaped = input ?? {};
|
|
58
58
|
return String(shaped.toolName ?? shaped.tool_name ?? shaped.tool?.name ?? shaped.name ?? "");
|
|
59
59
|
}
|
|
60
|
+
function resolveToolInput(input) {
|
|
61
|
+
const shaped = input ?? {};
|
|
62
|
+
return shaped.toolInput ?? shaped.tool_input ?? {};
|
|
63
|
+
}
|
|
60
64
|
async function hasReport(cwd) {
|
|
61
65
|
try {
|
|
62
66
|
await fs.access(reportPath(cwd));
|
|
@@ -96,6 +100,74 @@ async function resetSession(cwd, agentKey) {
|
|
|
96
100
|
function isBroadSearchTool(toolName) {
|
|
97
101
|
return /grep|glob|search|find/i.test(toolName);
|
|
98
102
|
}
|
|
103
|
+
function collectCommandCandidates(node, acc = []) {
|
|
104
|
+
if (!node || typeof node !== "object") {
|
|
105
|
+
return acc;
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(node)) {
|
|
108
|
+
for (const item of node) {
|
|
109
|
+
collectCommandCandidates(item, acc);
|
|
110
|
+
}
|
|
111
|
+
return acc;
|
|
112
|
+
}
|
|
113
|
+
for (const [key, value] of Object.entries(node)) {
|
|
114
|
+
if (["command", "cmd", "script", "bash", "shell"].includes(key) && typeof value === "string") {
|
|
115
|
+
acc.push(value);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
collectCommandCandidates(value, acc);
|
|
119
|
+
}
|
|
120
|
+
return acc;
|
|
121
|
+
}
|
|
122
|
+
var VAULT_ARTIFACT_SEGMENTS = ["wiki", "raw", "state", "agent", "inbox"];
|
|
123
|
+
function isVaultArtifactSearch(input, cwd) {
|
|
124
|
+
const artifactRoot = artifactRootDir(cwd);
|
|
125
|
+
const candidates = [...collectCandidatePaths(input), ...collectCommandCandidates(input)];
|
|
126
|
+
return candidates.some((candidate) => {
|
|
127
|
+
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const normalized = candidate.replaceAll("\\", "/");
|
|
131
|
+
if (VAULT_ARTIFACT_SEGMENTS.some(
|
|
132
|
+
(segment) => normalized.includes(`${segment}/`) && normalized.match(new RegExp(`(^|[\\s'"=/])${segment}/`))
|
|
133
|
+
)) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
const resolved = path.resolve(cwd, candidate);
|
|
137
|
+
return VAULT_ARTIFACT_SEGMENTS.some(
|
|
138
|
+
(segment) => resolved.startsWith(path.join(artifactRoot, segment) + path.sep) || resolved === path.join(artifactRoot, segment)
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function isNarrowSearch(input) {
|
|
143
|
+
const toolInput = resolveToolInput(input);
|
|
144
|
+
const candidate = toolInput?.path;
|
|
145
|
+
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const stats = await fs.stat(candidate);
|
|
150
|
+
return stats.isFile();
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function resolveGraphFirstMode(cwd) {
|
|
156
|
+
const fromEnv = process.env.SWARMVAULT_GRAPH_FIRST?.trim().toLowerCase();
|
|
157
|
+
if (fromEnv === "deny" || fromEnv === "context" || fromEnv === "off") {
|
|
158
|
+
return fromEnv;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const raw = await fs.readFile(path.join(cwd, "swarmvault.config.json"), "utf8");
|
|
162
|
+
const parsed = JSON.parse(raw);
|
|
163
|
+
const fromConfig = typeof parsed?.hooks?.graphFirst === "string" ? parsed.hooks.graphFirst.toLowerCase() : "";
|
|
164
|
+
if (fromConfig === "deny" || fromConfig === "context" || fromConfig === "off") {
|
|
165
|
+
return fromConfig;
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
return "deny";
|
|
170
|
+
}
|
|
99
171
|
async function readHookInput() {
|
|
100
172
|
let body = "";
|
|
101
173
|
for await (const chunk of process.stdin) {
|
|
@@ -110,7 +182,25 @@ async function readHookInput() {
|
|
|
110
182
|
return {};
|
|
111
183
|
}
|
|
112
184
|
}
|
|
113
|
-
|
|
185
|
+
function extractSearchTerm(input) {
|
|
186
|
+
const toolInput = resolveToolInput(input);
|
|
187
|
+
for (const key of ["pattern", "query", "regex"]) {
|
|
188
|
+
const value = toolInput?.[key];
|
|
189
|
+
if (typeof value === "string" && value.length > 0) {
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return "<your term>";
|
|
194
|
+
}
|
|
195
|
+
function buildDenyReason(toolName, input) {
|
|
196
|
+
const term = extractSearchTerm(input).slice(0, 120);
|
|
197
|
+
return [
|
|
198
|
+
`SwarmVault graph-first: this repo has a compiled code graph that answers structure questions in far fewer tokens than ${toolName || "broad search"}.`,
|
|
199
|
+
`Run: swarmvault graph query "${term}" \u2014 it prints the top matches with page paths plus an inline excerpt of the best page, which usually answers the question without reading source. Add --context calls for caller/impact questions. Do not add --json (much larger output).`,
|
|
200
|
+
"Trust that answer for orientation questions instead of re-verifying in source files.",
|
|
201
|
+
"If the graph does not answer, repeat this exact search \u2014 it will be allowed for the rest of the session."
|
|
202
|
+
].join(" ");
|
|
203
|
+
}
|
|
114
204
|
|
|
115
205
|
// src/hooks/copilot.ts
|
|
116
206
|
var AGENT_KEY = "copilot";
|
|
@@ -139,10 +229,12 @@ async function main() {
|
|
|
139
229
|
emit({});
|
|
140
230
|
process.exit(0);
|
|
141
231
|
}
|
|
142
|
-
|
|
232
|
+
const graphFirstMode = await resolveGraphFirstMode(cwd);
|
|
233
|
+
if (graphFirstMode === "deny" && isBroadSearchTool(toolName) && !isVaultArtifactSearch(input, cwd) && !await isNarrowSearch(input) && !await hasSeenReport(cwd, AGENT_KEY)) {
|
|
234
|
+
await markReportRead(cwd, AGENT_KEY);
|
|
143
235
|
emit({
|
|
144
236
|
permissionDecision: "deny",
|
|
145
|
-
permissionDecisionReason:
|
|
237
|
+
permissionDecisionReason: buildDenyReason(toolName, input)
|
|
146
238
|
});
|
|
147
239
|
process.exit(0);
|
|
148
240
|
}
|