@swarmvaultai/engine 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/chunk-EXD4RWT3.js +1131 -0
- package/dist/index.d.ts +12 -4
- package/dist/index.js +587 -74
- package/dist/registry-KLO5YIHP.js +12 -0
- package/dist/viewer/assets/index-C7PCTMog.js +330 -0
- package/dist/viewer/assets/index-DiMCbjBi.css +1 -0
- package/dist/viewer/index.html +2 -2
- package/package.json +2 -1
- package/dist/viewer/assets/index-4JhL4iIB.js +0 -330
- package/dist/viewer/assets/index-mRA-6D-Z.css +0 -1
package/dist/index.js
CHANGED
|
@@ -21,19 +21,35 @@ import {
|
|
|
21
21
|
uniqueBy,
|
|
22
22
|
writeFileIfChanged,
|
|
23
23
|
writeJsonFile
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-EXD4RWT3.js";
|
|
25
25
|
|
|
26
26
|
// src/agents.ts
|
|
27
|
+
import crypto from "crypto";
|
|
27
28
|
import fs from "fs/promises";
|
|
28
29
|
import path from "path";
|
|
30
|
+
import YAML from "yaml";
|
|
29
31
|
var managedStart = "<!-- swarmvault:managed:start -->";
|
|
30
32
|
var managedEnd = "<!-- swarmvault:managed:end -->";
|
|
31
33
|
var legacyManagedStart = "<!-- vault:managed:start -->";
|
|
32
34
|
var legacyManagedEnd = "<!-- vault:managed:end -->";
|
|
35
|
+
var claudeHookMatcher = "Glob|Grep";
|
|
36
|
+
var claudeHookCommand = "if [ -f wiki/graph/report.md ]; then echo 'swarmvault: Graph report exists. Read wiki/graph/report.md before broad raw-file searching.'; fi";
|
|
37
|
+
var geminiSessionMatcher = "startup";
|
|
38
|
+
var geminiSearchMatcher = "glob|grep|search|find";
|
|
39
|
+
var copilotHookVersion = 1;
|
|
40
|
+
var agentFileKinds = {
|
|
41
|
+
agents: "AGENTS.md",
|
|
42
|
+
claude: "CLAUDE.md",
|
|
43
|
+
gemini: "GEMINI.md",
|
|
44
|
+
cursor: ".cursor/rules/swarmvault.mdc",
|
|
45
|
+
aider: "CONVENTIONS.md",
|
|
46
|
+
copilot: ".github/copilot-instructions.md"
|
|
47
|
+
};
|
|
33
48
|
function buildManagedBlock(target) {
|
|
34
|
-
const
|
|
49
|
+
const heading = target === "aider" ? "# SwarmVault Conventions" : target === "copilot" ? "# SwarmVault Repository Instructions" : "# SwarmVault Rules";
|
|
50
|
+
return [
|
|
35
51
|
managedStart,
|
|
36
|
-
|
|
52
|
+
heading,
|
|
37
53
|
"",
|
|
38
54
|
"- Read `swarmvault.schema.md` before compile or query style work. It is the canonical schema path.",
|
|
39
55
|
"- Treat `raw/` as immutable source input.",
|
|
@@ -46,55 +62,75 @@ function buildManagedBlock(target) {
|
|
|
46
62
|
managedEnd,
|
|
47
63
|
""
|
|
48
64
|
].join("\n");
|
|
49
|
-
if (target === "cursor") {
|
|
50
|
-
return body;
|
|
51
|
-
}
|
|
52
|
-
return body;
|
|
53
65
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
async function installClaudeHook(rootDir) {
|
|
57
|
-
const settingsPath = path.join(rootDir, ".claude", "settings.json");
|
|
58
|
-
await ensureDir(path.dirname(settingsPath));
|
|
59
|
-
let settings = {};
|
|
60
|
-
if (await fileExists(settingsPath)) {
|
|
61
|
-
try {
|
|
62
|
-
settings = JSON.parse(await fs.readFile(settingsPath, "utf8"));
|
|
63
|
-
} catch {
|
|
64
|
-
settings = {};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const hooks = settings.hooks ?? {};
|
|
68
|
-
const preToolUse = hooks.PreToolUse ?? [];
|
|
69
|
-
const exists = preToolUse.some((entry) => entry.matcher === claudeHookMatcher && JSON.stringify(entry).includes("swarmvault:"));
|
|
70
|
-
if (!exists) {
|
|
71
|
-
preToolUse.push({
|
|
72
|
-
matcher: claudeHookMatcher,
|
|
73
|
-
hooks: [{ type: "command", command: claudeHookCommand }]
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
settings.hooks = { ...hooks, PreToolUse: preToolUse };
|
|
77
|
-
await fs.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
78
|
-
`, "utf8");
|
|
79
|
-
return settingsPath;
|
|
66
|
+
function supportsAgentHook(agent) {
|
|
67
|
+
return agent === "claude" || agent === "opencode" || agent === "gemini" || agent === "copilot";
|
|
80
68
|
}
|
|
81
|
-
function
|
|
69
|
+
function primaryTargetPathForAgent(rootDir, agent) {
|
|
82
70
|
switch (agent) {
|
|
83
71
|
case "codex":
|
|
84
72
|
case "goose":
|
|
85
73
|
case "pi":
|
|
86
74
|
case "opencode":
|
|
87
|
-
return path.join(rootDir,
|
|
75
|
+
return path.join(rootDir, agentFileKinds.agents);
|
|
88
76
|
case "claude":
|
|
89
|
-
return path.join(rootDir,
|
|
77
|
+
return path.join(rootDir, agentFileKinds.claude);
|
|
90
78
|
case "gemini":
|
|
91
|
-
return path.join(rootDir,
|
|
79
|
+
return path.join(rootDir, agentFileKinds.gemini);
|
|
92
80
|
case "cursor":
|
|
93
|
-
return path.join(rootDir,
|
|
81
|
+
return path.join(rootDir, agentFileKinds.cursor);
|
|
82
|
+
case "aider":
|
|
83
|
+
return path.join(rootDir, agentFileKinds.aider);
|
|
84
|
+
case "copilot":
|
|
85
|
+
return path.join(rootDir, agentFileKinds.copilot);
|
|
94
86
|
default:
|
|
95
87
|
throw new Error(`Unsupported agent ${String(agent)}`);
|
|
96
88
|
}
|
|
97
89
|
}
|
|
90
|
+
function hookScriptPathForAgent(rootDir, agent) {
|
|
91
|
+
switch (agent) {
|
|
92
|
+
case "opencode":
|
|
93
|
+
return path.join(rootDir, ".opencode", "plugins", "swarmvault-graph-first.js");
|
|
94
|
+
case "gemini":
|
|
95
|
+
return path.join(rootDir, ".gemini", "hooks", "swarmvault-graph-first.js");
|
|
96
|
+
case "copilot":
|
|
97
|
+
return path.join(rootDir, ".github", "hooks", "swarmvault-graph-first.js");
|
|
98
|
+
default:
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function hookConfigPathForAgent(rootDir, agent) {
|
|
103
|
+
switch (agent) {
|
|
104
|
+
case "claude":
|
|
105
|
+
return path.join(rootDir, ".claude", "settings.json");
|
|
106
|
+
case "gemini":
|
|
107
|
+
return path.join(rootDir, ".gemini", "settings.json");
|
|
108
|
+
case "copilot":
|
|
109
|
+
return path.join(rootDir, ".github", "hooks", "swarmvault-graph-first.json");
|
|
110
|
+
default:
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function targetsForAgent(rootDir, agent, options = {}) {
|
|
115
|
+
const targets = [primaryTargetPathForAgent(rootDir, agent)];
|
|
116
|
+
if (agent === "copilot") {
|
|
117
|
+
targets.push(path.join(rootDir, agentFileKinds.agents));
|
|
118
|
+
}
|
|
119
|
+
if (agent === "aider") {
|
|
120
|
+
targets.push(path.join(rootDir, ".aider.conf.yml"));
|
|
121
|
+
}
|
|
122
|
+
if (options.hook && supportsAgentHook(agent)) {
|
|
123
|
+
const configPath = hookConfigPathForAgent(rootDir, agent);
|
|
124
|
+
const scriptPath = hookScriptPathForAgent(rootDir, agent);
|
|
125
|
+
if (configPath) {
|
|
126
|
+
targets.push(configPath);
|
|
127
|
+
}
|
|
128
|
+
if (scriptPath) {
|
|
129
|
+
targets.push(scriptPath);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return [...new Set(targets)];
|
|
133
|
+
}
|
|
98
134
|
async function upsertManagedBlock(filePath, block) {
|
|
99
135
|
const existing = await fileExists(filePath) ? await fs.readFile(filePath, "utf8") : "";
|
|
100
136
|
if (!existing) {
|
|
@@ -115,51 +151,510 @@ async function upsertManagedBlock(filePath, block) {
|
|
|
115
151
|
${block}
|
|
116
152
|
`, "utf8");
|
|
117
153
|
}
|
|
154
|
+
async function writeOwnedFile(filePath, content, executable = false) {
|
|
155
|
+
await ensureDir(path.dirname(filePath));
|
|
156
|
+
await fs.writeFile(filePath, content, {
|
|
157
|
+
encoding: "utf8",
|
|
158
|
+
mode: executable ? 493 : 420
|
|
159
|
+
});
|
|
160
|
+
if (executable) {
|
|
161
|
+
await fs.chmod(filePath, 493);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function readJsonWithWarnings(filePath, fallback, label) {
|
|
165
|
+
if (!await fileExists(filePath)) {
|
|
166
|
+
return { data: fallback, warnings: [] };
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
170
|
+
return { data: parsed, warnings: [] };
|
|
171
|
+
} catch {
|
|
172
|
+
return {
|
|
173
|
+
data: fallback,
|
|
174
|
+
warnings: [`Could not parse ${label}. Left the existing file unchanged.`]
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function installClaudeHook(rootDir) {
|
|
179
|
+
const settingsPath = path.join(rootDir, ".claude", "settings.json");
|
|
180
|
+
await ensureDir(path.dirname(settingsPath));
|
|
181
|
+
const { data: settings, warnings } = await readJsonWithWarnings(settingsPath, {}, ".claude/settings.json");
|
|
182
|
+
if (warnings.length > 0 && await fileExists(settingsPath)) {
|
|
183
|
+
return { path: settingsPath, warnings };
|
|
184
|
+
}
|
|
185
|
+
const hooks = settings.hooks ?? {};
|
|
186
|
+
const preToolUse = hooks.PreToolUse ?? [];
|
|
187
|
+
const exists = preToolUse.some((entry) => entry.matcher === claudeHookMatcher && JSON.stringify(entry).includes("swarmvault:"));
|
|
188
|
+
if (!exists) {
|
|
189
|
+
preToolUse.push({
|
|
190
|
+
matcher: claudeHookMatcher,
|
|
191
|
+
hooks: [{ type: "command", command: claudeHookCommand }]
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
settings.hooks = { ...hooks, PreToolUse: preToolUse };
|
|
195
|
+
await fs.writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
196
|
+
`, "utf8");
|
|
197
|
+
return { path: settingsPath, warnings: [] };
|
|
198
|
+
}
|
|
199
|
+
function markerStateSnippet(agentKey) {
|
|
200
|
+
return `
|
|
201
|
+
function markerState(cwd) {
|
|
202
|
+
const hash = crypto.createHash("sha256").update(cwd).digest("hex");
|
|
203
|
+
const dir = path.join(os.tmpdir(), "swarmvault-agent-hooks", "${agentKey}", hash);
|
|
204
|
+
return {
|
|
205
|
+
dir,
|
|
206
|
+
markerPath: path.join(dir, "report-read")
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isReportPath(value, cwd) {
|
|
211
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const reportSuffix = path.join("wiki", "graph", "report.md");
|
|
215
|
+
const normalized = value.replaceAll("\\\\", "/");
|
|
216
|
+
const reportNormalized = reportSuffix.replaceAll("\\\\", "/");
|
|
217
|
+
if (normalized.endsWith(reportNormalized)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return path.resolve(cwd, value) === path.resolve(cwd, reportSuffix);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function collectCandidatePaths(node, acc = []) {
|
|
224
|
+
if (typeof node === "string") {
|
|
225
|
+
acc.push(node);
|
|
226
|
+
return acc;
|
|
227
|
+
}
|
|
228
|
+
if (!node || typeof node !== "object") {
|
|
229
|
+
return acc;
|
|
230
|
+
}
|
|
231
|
+
if (Array.isArray(node)) {
|
|
232
|
+
for (const item of node) {
|
|
233
|
+
collectCandidatePaths(item, acc);
|
|
234
|
+
}
|
|
235
|
+
return acc;
|
|
236
|
+
}
|
|
237
|
+
for (const [key, value] of Object.entries(node)) {
|
|
238
|
+
if (["path", "filePath", "file_path", "paths", "target", "targets"].includes(key)) {
|
|
239
|
+
collectCandidatePaths(value, acc);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return acc;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function resolveInputCwd(input) {
|
|
246
|
+
return path.resolve(
|
|
247
|
+
input?.cwd ??
|
|
248
|
+
input?.directory ??
|
|
249
|
+
input?.workspace?.cwd ??
|
|
250
|
+
input?.toolInput?.cwd ??
|
|
251
|
+
process.cwd()
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function resolveToolName(input) {
|
|
256
|
+
return String(input?.toolName ?? input?.tool_name ?? input?.tool?.name ?? input?.name ?? "");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function hasReport(cwd) {
|
|
260
|
+
try {
|
|
261
|
+
await fs.access(path.join(cwd, "wiki", "graph", "report.md"));
|
|
262
|
+
return true;
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function markReportRead(cwd) {
|
|
269
|
+
const state = markerState(cwd);
|
|
270
|
+
await fs.mkdir(state.dir, { recursive: true });
|
|
271
|
+
await fs.writeFile(state.markerPath, "seen\\n", "utf8");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function hasSeenReport(cwd) {
|
|
275
|
+
const state = markerState(cwd);
|
|
276
|
+
try {
|
|
277
|
+
await fs.access(state.markerPath);
|
|
278
|
+
return true;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function resetSession(cwd) {
|
|
285
|
+
const state = markerState(cwd);
|
|
286
|
+
await fs.rm(state.dir, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isBroadSearchTool(toolName) {
|
|
290
|
+
return /grep|glob|search|find/i.test(toolName);
|
|
291
|
+
}
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
function buildGeminiHookScript() {
|
|
295
|
+
return `#!/usr/bin/env node
|
|
296
|
+
import crypto from "node:crypto";
|
|
297
|
+
import fs from "node:fs/promises";
|
|
298
|
+
import os from "node:os";
|
|
299
|
+
import path from "node:path";
|
|
300
|
+
|
|
301
|
+
${markerStateSnippet("gemini").trim()}
|
|
302
|
+
|
|
303
|
+
async function readInput() {
|
|
304
|
+
let body = "";
|
|
305
|
+
for await (const chunk of process.stdin) {
|
|
306
|
+
body += chunk;
|
|
307
|
+
}
|
|
308
|
+
if (!body.trim()) {
|
|
309
|
+
return {};
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
return JSON.parse(body);
|
|
313
|
+
} catch {
|
|
314
|
+
return {};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function emit(value) {
|
|
319
|
+
process.stdout.write(\`\${JSON.stringify(value)}\\n\`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const mode = process.argv[2] ?? "";
|
|
323
|
+
const input = await readInput();
|
|
324
|
+
const cwd = resolveInputCwd(input);
|
|
325
|
+
const reportNote = "SwarmVault graph report exists at wiki/graph/report.md. Read it before broad grep/glob searching.";
|
|
326
|
+
|
|
327
|
+
if (!(await hasReport(cwd))) {
|
|
328
|
+
emit({});
|
|
329
|
+
process.exit(0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (mode === "session-start") {
|
|
333
|
+
await resetSession(cwd);
|
|
334
|
+
emit({
|
|
335
|
+
systemMessage: reportNote,
|
|
336
|
+
hookSpecificOutput: {
|
|
337
|
+
hookEventName: "SessionStart",
|
|
338
|
+
additionalContext: "SwarmVault graph report: wiki/graph/report.md"
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
process.exit(0);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const toolName = resolveToolName(input);
|
|
345
|
+
if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
|
|
346
|
+
await markReportRead(cwd);
|
|
347
|
+
emit({});
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (isBroadSearchTool(toolName) && !(await hasSeenReport(cwd))) {
|
|
352
|
+
emit({ systemMessage: reportNote });
|
|
353
|
+
process.exit(0);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
emit({});
|
|
357
|
+
`;
|
|
358
|
+
}
|
|
359
|
+
function buildCopilotHookScript() {
|
|
360
|
+
return `#!/usr/bin/env node
|
|
361
|
+
import crypto from "node:crypto";
|
|
362
|
+
import fs from "node:fs/promises";
|
|
363
|
+
import os from "node:os";
|
|
364
|
+
import path from "node:path";
|
|
365
|
+
|
|
366
|
+
${markerStateSnippet("copilot").trim()}
|
|
367
|
+
|
|
368
|
+
async function readInput() {
|
|
369
|
+
let body = "";
|
|
370
|
+
for await (const chunk of process.stdin) {
|
|
371
|
+
body += chunk;
|
|
372
|
+
}
|
|
373
|
+
if (!body.trim()) {
|
|
374
|
+
return {};
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
return JSON.parse(body);
|
|
378
|
+
} catch {
|
|
379
|
+
return {};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function emit(value) {
|
|
384
|
+
if (value !== undefined) {
|
|
385
|
+
process.stdout.write(\`\${JSON.stringify(value)}\\n\`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const mode = process.argv[2] ?? "";
|
|
390
|
+
const input = await readInput();
|
|
391
|
+
const cwd = resolveInputCwd(input);
|
|
392
|
+
const reportNote = "SwarmVault graph report exists at wiki/graph/report.md. Read it before broad grep/glob searching.";
|
|
393
|
+
|
|
394
|
+
if (!(await hasReport(cwd))) {
|
|
395
|
+
emit({});
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (mode === "session-start") {
|
|
400
|
+
await resetSession(cwd);
|
|
401
|
+
emit({});
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const toolName = resolveToolName(input);
|
|
406
|
+
if (collectCandidatePaths(input).some((value) => isReportPath(value, cwd))) {
|
|
407
|
+
await markReportRead(cwd);
|
|
408
|
+
emit({});
|
|
409
|
+
process.exit(0);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (isBroadSearchTool(toolName) && !(await hasSeenReport(cwd))) {
|
|
413
|
+
emit({
|
|
414
|
+
permissionDecision: "deny",
|
|
415
|
+
permissionDecisionReason: reportNote
|
|
416
|
+
});
|
|
417
|
+
process.exit(0);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
emit({});
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
function buildOpenCodePlugin() {
|
|
424
|
+
return `import path from "node:path";
|
|
425
|
+
|
|
426
|
+
const reportRelativePath = path.join("wiki", "graph", "report.md");
|
|
427
|
+
|
|
428
|
+
export const name = "swarmvault-graph-first";
|
|
429
|
+
|
|
430
|
+
export default async function swarmvaultGraphFirst({ client }) {
|
|
431
|
+
let reportSeen = false;
|
|
432
|
+
|
|
433
|
+
async function hasReport(cwd) {
|
|
434
|
+
try {
|
|
435
|
+
await Bun.file(path.join(cwd, reportRelativePath)).arrayBuffer();
|
|
436
|
+
return true;
|
|
437
|
+
} catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function note(message) {
|
|
443
|
+
if (client?.app?.log) {
|
|
444
|
+
await client.app.log({
|
|
445
|
+
level: "info",
|
|
446
|
+
message
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
async "session.created"(input) {
|
|
453
|
+
reportSeen = false;
|
|
454
|
+
const cwd = input?.session?.cwd ?? process.cwd();
|
|
455
|
+
if (await hasReport(cwd)) {
|
|
456
|
+
await note("SwarmVault graph report exists. Read wiki/graph/report.md before broad workspace searching.");
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
async "tool.execute.before"(input) {
|
|
460
|
+
const cwd = input?.session?.cwd ?? process.cwd();
|
|
461
|
+
if (!(await hasReport(cwd))) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const argsText = JSON.stringify(input?.args ?? {});
|
|
466
|
+
if (argsText.includes("wiki/graph/report.md")) {
|
|
467
|
+
reportSeen = true;
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!reportSeen && ["glob", "grep"].includes(String(input?.tool ?? ""))) {
|
|
472
|
+
await note("SwarmVault graph report exists. Read wiki/graph/report.md before broad workspace searching.");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
async function installGeminiHook(rootDir) {
|
|
480
|
+
const settingsPath = path.join(rootDir, ".gemini", "settings.json");
|
|
481
|
+
const scriptPath = path.join(rootDir, ".gemini", "hooks", "swarmvault-graph-first.js");
|
|
482
|
+
await writeOwnedFile(scriptPath, buildGeminiHookScript(), true);
|
|
483
|
+
const { data: settings, warnings } = await readJsonWithWarnings(settingsPath, {}, ".gemini/settings.json");
|
|
484
|
+
if (warnings.length > 0 && await fileExists(settingsPath)) {
|
|
485
|
+
return { paths: [settingsPath, scriptPath], warnings };
|
|
486
|
+
}
|
|
487
|
+
const hooks = settings.hooks ?? {};
|
|
488
|
+
const sessionStart = hooks.SessionStart ?? [];
|
|
489
|
+
const beforeTool = hooks.BeforeTool ?? [];
|
|
490
|
+
const sessionCommand = "node .gemini/hooks/swarmvault-graph-first.js session-start";
|
|
491
|
+
const beforeToolCommand = "node .gemini/hooks/swarmvault-graph-first.js before-tool";
|
|
492
|
+
if (!sessionStart.some((entry) => entry.matcher === geminiSessionMatcher && JSON.stringify(entry).includes("swarmvault-graph-first.js"))) {
|
|
493
|
+
sessionStart.push({
|
|
494
|
+
matcher: geminiSessionMatcher,
|
|
495
|
+
hooks: [{ name: "swarmvault-graph-first", type: "command", command: sessionCommand }]
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (!beforeTool.some((entry) => entry.matcher === geminiSearchMatcher && JSON.stringify(entry).includes("swarmvault-graph-first.js"))) {
|
|
499
|
+
beforeTool.push({
|
|
500
|
+
matcher: geminiSearchMatcher,
|
|
501
|
+
hooks: [{ name: "swarmvault-graph-first", type: "command", command: beforeToolCommand }]
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
settings.hooks = {
|
|
505
|
+
...hooks,
|
|
506
|
+
SessionStart: sessionStart,
|
|
507
|
+
BeforeTool: beforeTool
|
|
508
|
+
};
|
|
509
|
+
await writeOwnedFile(settingsPath, `${JSON.stringify(settings, null, 2)}
|
|
510
|
+
`);
|
|
511
|
+
return { paths: [settingsPath, scriptPath], warnings: [] };
|
|
512
|
+
}
|
|
513
|
+
async function mergeAiderConfig(rootDir) {
|
|
514
|
+
const configPath = path.join(rootDir, ".aider.conf.yml");
|
|
515
|
+
const readTarget = "CONVENTIONS.md";
|
|
516
|
+
if (!await fileExists(configPath)) {
|
|
517
|
+
const document = new YAML.Document();
|
|
518
|
+
document.set("read", [readTarget]);
|
|
519
|
+
await writeOwnedFile(configPath, `${document.toString()}`);
|
|
520
|
+
return { path: configPath, warnings: [] };
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const source = await fs.readFile(configPath, "utf8");
|
|
524
|
+
const document = YAML.parseDocument(source);
|
|
525
|
+
if (document.errors.length > 0) {
|
|
526
|
+
return {
|
|
527
|
+
path: configPath,
|
|
528
|
+
warnings: ["Could not parse .aider.conf.yml. Left the existing file unchanged; add `read: CONVENTIONS.md` manually."]
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const currentRead = document.get("read", true);
|
|
532
|
+
const values = typeof currentRead === "string" ? [currentRead] : Array.isArray(currentRead) ? currentRead.filter((item) => typeof item === "string") : [];
|
|
533
|
+
if (!values.includes(readTarget)) {
|
|
534
|
+
document.set("read", [...values, readTarget]);
|
|
535
|
+
await writeOwnedFile(configPath, `${document.toString()}`);
|
|
536
|
+
}
|
|
537
|
+
return { path: configPath, warnings: [] };
|
|
538
|
+
} catch {
|
|
539
|
+
return {
|
|
540
|
+
path: configPath,
|
|
541
|
+
warnings: ["Could not parse .aider.conf.yml. Left the existing file unchanged; add `read: CONVENTIONS.md` manually."]
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async function installCopilotHook(rootDir) {
|
|
546
|
+
const hooksDir = path.join(rootDir, ".github", "hooks");
|
|
547
|
+
const scriptPath = path.join(hooksDir, "swarmvault-graph-first.js");
|
|
548
|
+
const configPath = path.join(hooksDir, "swarmvault-graph-first.json");
|
|
549
|
+
await writeOwnedFile(scriptPath, buildCopilotHookScript(), true);
|
|
550
|
+
const config = {
|
|
551
|
+
version: copilotHookVersion,
|
|
552
|
+
hooks: {
|
|
553
|
+
sessionStart: [
|
|
554
|
+
{
|
|
555
|
+
type: "command",
|
|
556
|
+
bash: "node .github/hooks/swarmvault-graph-first.js session-start",
|
|
557
|
+
powershell: "node .github/hooks/swarmvault-graph-first.js session-start",
|
|
558
|
+
cwd: ".",
|
|
559
|
+
timeoutSec: 10
|
|
560
|
+
}
|
|
561
|
+
],
|
|
562
|
+
preToolUse: [
|
|
563
|
+
{
|
|
564
|
+
matcher: "glob|grep",
|
|
565
|
+
type: "command",
|
|
566
|
+
bash: "node .github/hooks/swarmvault-graph-first.js pre-tool-use",
|
|
567
|
+
powershell: "node .github/hooks/swarmvault-graph-first.js pre-tool-use",
|
|
568
|
+
cwd: ".",
|
|
569
|
+
timeoutSec: 10
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
await writeOwnedFile(configPath, `${JSON.stringify(config, null, 2)}
|
|
575
|
+
`);
|
|
576
|
+
return { paths: [configPath, scriptPath], warnings: [] };
|
|
577
|
+
}
|
|
578
|
+
async function installOpenCodeHook(rootDir) {
|
|
579
|
+
const pluginPath = path.join(rootDir, ".opencode", "plugins", "swarmvault-graph-first.js");
|
|
580
|
+
await writeOwnedFile(pluginPath, buildOpenCodePlugin());
|
|
581
|
+
return { paths: [pluginPath], warnings: [] };
|
|
582
|
+
}
|
|
583
|
+
function stableKeyForAgent(rootDir, agent) {
|
|
584
|
+
if (agent === "codex" || agent === "goose" || agent === "pi") {
|
|
585
|
+
return `shared:${path.join(rootDir, agentFileKinds.agents)}`;
|
|
586
|
+
}
|
|
587
|
+
return `${agent}:${crypto.createHash("sha1").update(targetsForAgent(rootDir, agent, { hook: supportsAgentHook(agent) }).join("\n")).digest("hex")}`;
|
|
588
|
+
}
|
|
118
589
|
async function installAgent(rootDir, agent, options = {}) {
|
|
119
590
|
await initWorkspace(rootDir);
|
|
120
|
-
const target =
|
|
591
|
+
const target = primaryTargetPathForAgent(rootDir, agent);
|
|
592
|
+
const warnings = [];
|
|
121
593
|
switch (agent) {
|
|
122
594
|
case "codex":
|
|
123
595
|
case "goose":
|
|
124
596
|
case "pi":
|
|
125
597
|
case "opencode":
|
|
126
|
-
await upsertManagedBlock(
|
|
127
|
-
|
|
128
|
-
case "claude":
|
|
598
|
+
await upsertManagedBlock(path.join(rootDir, agentFileKinds.agents), buildManagedBlock("agents"));
|
|
599
|
+
break;
|
|
600
|
+
case "claude":
|
|
129
601
|
await upsertManagedBlock(target, buildManagedBlock("claude"));
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
return target;
|
|
134
|
-
}
|
|
135
|
-
case "gemini": {
|
|
602
|
+
break;
|
|
603
|
+
case "gemini":
|
|
136
604
|
await upsertManagedBlock(target, buildManagedBlock("gemini"));
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
605
|
+
break;
|
|
606
|
+
case "cursor":
|
|
607
|
+
await writeOwnedFile(target, `${buildManagedBlock("cursor")}
|
|
608
|
+
`);
|
|
609
|
+
break;
|
|
610
|
+
case "aider":
|
|
611
|
+
await upsertManagedBlock(target, buildManagedBlock("aider"));
|
|
612
|
+
break;
|
|
613
|
+
case "copilot":
|
|
614
|
+
await upsertManagedBlock(path.join(rootDir, agentFileKinds.agents), buildManagedBlock("agents"));
|
|
615
|
+
await upsertManagedBlock(target, buildManagedBlock("copilot"));
|
|
616
|
+
break;
|
|
146
617
|
default:
|
|
147
618
|
throw new Error(`Unsupported agent ${String(agent)}`);
|
|
148
619
|
}
|
|
620
|
+
if (agent === "aider") {
|
|
621
|
+
const aiderResult = await mergeAiderConfig(rootDir);
|
|
622
|
+
warnings.push(...aiderResult.warnings);
|
|
623
|
+
}
|
|
624
|
+
if (options.hook && supportsAgentHook(agent)) {
|
|
625
|
+
if (agent === "claude") {
|
|
626
|
+
const result = await installClaudeHook(rootDir);
|
|
627
|
+
warnings.push(...result.warnings);
|
|
628
|
+
}
|
|
629
|
+
if (agent === "opencode") {
|
|
630
|
+
const result = await installOpenCodeHook(rootDir);
|
|
631
|
+
warnings.push(...result.warnings);
|
|
632
|
+
}
|
|
633
|
+
if (agent === "gemini") {
|
|
634
|
+
const result = await installGeminiHook(rootDir);
|
|
635
|
+
warnings.push(...result.warnings);
|
|
636
|
+
}
|
|
637
|
+
if (agent === "copilot") {
|
|
638
|
+
const result = await installCopilotHook(rootDir);
|
|
639
|
+
warnings.push(...result.warnings);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const targets = targetsForAgent(rootDir, agent, options);
|
|
643
|
+
return warnings.length > 0 ? { agent, target, targets, warnings } : { agent, target, targets };
|
|
149
644
|
}
|
|
150
645
|
async function installConfiguredAgents(rootDir) {
|
|
151
646
|
const { config } = await initWorkspace(rootDir);
|
|
152
|
-
const
|
|
647
|
+
const dedupedAgents = /* @__PURE__ */ new Map();
|
|
153
648
|
for (const agent of config.agents) {
|
|
154
|
-
const
|
|
155
|
-
if (!
|
|
156
|
-
|
|
649
|
+
const key = stableKeyForAgent(rootDir, agent);
|
|
650
|
+
if (!dedupedAgents.has(key)) {
|
|
651
|
+
dedupedAgents.set(key, agent);
|
|
157
652
|
}
|
|
158
653
|
}
|
|
159
654
|
return Promise.all(
|
|
160
|
-
[...
|
|
655
|
+
[...dedupedAgents.values()].map(
|
|
161
656
|
(agent) => installAgent(rootDir, agent, {
|
|
162
|
-
|
|
657
|
+
hook: supportsAgentHook(agent)
|
|
163
658
|
})
|
|
164
659
|
)
|
|
165
660
|
);
|
|
@@ -536,6 +1031,7 @@ async function exportGraphFormat(rootDir, format, outputPath) {
|
|
|
536
1031
|
// src/hooks.ts
|
|
537
1032
|
import fs3 from "fs/promises";
|
|
538
1033
|
import path3 from "path";
|
|
1034
|
+
import process2 from "process";
|
|
539
1035
|
var hookStart = "# >>> swarmvault hook >>>";
|
|
540
1036
|
var hookEnd = "# <<< swarmvault hook <<<";
|
|
541
1037
|
async function findNearestGitRoot(startPath) {
|
|
@@ -562,12 +1058,22 @@ async function findNearestGitRoot(startPath) {
|
|
|
562
1058
|
function shellQuote(value) {
|
|
563
1059
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
564
1060
|
}
|
|
1061
|
+
function resolveSwarmvaultExecutableCandidate() {
|
|
1062
|
+
const argvPath = process2.argv[1];
|
|
1063
|
+
if (typeof argvPath === "string" && argvPath.trim() && (argvPath.includes(`${path3.sep}@swarmvaultai${path3.sep}cli${path3.sep}`) || argvPath.includes(`${path3.sep}packages${path3.sep}cli${path3.sep}`))) {
|
|
1064
|
+
return path3.resolve(argvPath);
|
|
1065
|
+
}
|
|
1066
|
+
return "swarmvault";
|
|
1067
|
+
}
|
|
565
1068
|
function managedHookBlock(vaultRoot) {
|
|
1069
|
+
const resolvedExecutable = resolveSwarmvaultExecutableCandidate();
|
|
566
1070
|
return [
|
|
567
1071
|
hookStart,
|
|
568
1072
|
`cd ${shellQuote(vaultRoot)} || exit 0`,
|
|
569
|
-
|
|
570
|
-
"
|
|
1073
|
+
`swarmvault_bin=${shellQuote(resolvedExecutable)}`,
|
|
1074
|
+
'[ ! -x "$swarmvault_bin" ] && swarmvault_bin=$(command -v swarmvault 2>/dev/null || true)',
|
|
1075
|
+
'if [ -n "$swarmvault_bin" ] && [ -x "$swarmvault_bin" ]; then',
|
|
1076
|
+
` "$swarmvault_bin" watch --repo --once >/dev/null 2>&1 || printf '[swarmvault hook] refresh failed\\n' >&2`,
|
|
571
1077
|
"fi",
|
|
572
1078
|
hookEnd,
|
|
573
1079
|
""
|
|
@@ -663,6 +1169,7 @@ async function uninstallGitHooks(rootDir) {
|
|
|
663
1169
|
// src/ingest.ts
|
|
664
1170
|
import fs9 from "fs/promises";
|
|
665
1171
|
import path10 from "path";
|
|
1172
|
+
import { pathToFileURL } from "url";
|
|
666
1173
|
import { Readability } from "@mozilla/readability";
|
|
667
1174
|
import matter3 from "gray-matter";
|
|
668
1175
|
import ignore from "ignore";
|
|
@@ -3342,6 +3849,9 @@ function inferKind(mimeType, filePath) {
|
|
|
3342
3849
|
if (mimeType.includes("markdown")) {
|
|
3343
3850
|
return "markdown";
|
|
3344
3851
|
}
|
|
3852
|
+
if (mimeType.includes("html")) {
|
|
3853
|
+
return "html";
|
|
3854
|
+
}
|
|
3345
3855
|
if (mimeType.startsWith("text/")) {
|
|
3346
3856
|
return "text";
|
|
3347
3857
|
}
|
|
@@ -3351,9 +3861,6 @@ function inferKind(mimeType, filePath) {
|
|
|
3351
3861
|
if (mimeType.startsWith("image/")) {
|
|
3352
3862
|
return "image";
|
|
3353
3863
|
}
|
|
3354
|
-
if (mimeType.includes("html")) {
|
|
3355
|
-
return "html";
|
|
3356
|
-
}
|
|
3357
3864
|
return "binary";
|
|
3358
3865
|
}
|
|
3359
3866
|
function titleFromText(fallback, content) {
|
|
@@ -4429,6 +4936,12 @@ async function prepareFileInput(rootDir, absoluteInput, repoRoot, sourceClass) {
|
|
|
4429
4936
|
extractedText = payloadBytes.toString("utf8");
|
|
4430
4937
|
title = titleFromText(path10.basename(absoluteInput, path10.extname(absoluteInput)), extractedText);
|
|
4431
4938
|
extractionArtifact = createPlainTextExtractionArtifact(sourceKind, mimeType);
|
|
4939
|
+
} else if (sourceKind === "html") {
|
|
4940
|
+
const html = payloadBytes.toString("utf8");
|
|
4941
|
+
const converted = await convertHtmlToMarkdown(html, pathToFileURL(absoluteInput).toString());
|
|
4942
|
+
title = converted.title;
|
|
4943
|
+
extractedText = converted.markdown;
|
|
4944
|
+
extractionArtifact = createHtmlReadabilityExtractionArtifact(sourceKind, mimeType);
|
|
4432
4945
|
} else if (sourceKind === "pdf") {
|
|
4433
4946
|
title = path10.basename(absoluteInput, path10.extname(absoluteInput));
|
|
4434
4947
|
const extracted = await extractPdfText({ mimeType, bytes: payloadBytes });
|
|
@@ -5681,7 +6194,7 @@ function summarizeRoleQuestions(results) {
|
|
|
5681
6194
|
|
|
5682
6195
|
// src/web-search/registry.ts
|
|
5683
6196
|
import path14 from "path";
|
|
5684
|
-
import { pathToFileURL } from "url";
|
|
6197
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
5685
6198
|
import { z as z4 } from "zod";
|
|
5686
6199
|
|
|
5687
6200
|
// src/web-search/http-json.ts
|
|
@@ -5779,7 +6292,7 @@ async function createWebSearchAdapter(id, config, rootDir) {
|
|
|
5779
6292
|
throw new Error(`Web search provider ${id} is type "custom" but no module path was configured.`);
|
|
5780
6293
|
}
|
|
5781
6294
|
const resolvedModule = path14.isAbsolute(config.module) ? config.module : path14.resolve(rootDir, config.module);
|
|
5782
|
-
const loaded = await import(
|
|
6295
|
+
const loaded = await import(pathToFileURL2(resolvedModule).href);
|
|
5783
6296
|
const parsed = customWebSearchModuleSchema.parse(loaded);
|
|
5784
6297
|
return parsed.createAdapter(id, config, rootDir);
|
|
5785
6298
|
}
|
|
@@ -9200,7 +9713,7 @@ async function resolveImageGenerationProvider(rootDir) {
|
|
|
9200
9713
|
if (!providerConfig) {
|
|
9201
9714
|
throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
|
|
9202
9715
|
}
|
|
9203
|
-
const { createProvider: createProvider2 } = await import("./registry-
|
|
9716
|
+
const { createProvider: createProvider2 } = await import("./registry-KLO5YIHP.js");
|
|
9204
9717
|
return createProvider2(preferredProviderId, providerConfig, rootDir);
|
|
9205
9718
|
}
|
|
9206
9719
|
async function generateOutputArtifacts(rootDir, input) {
|
|
@@ -12475,7 +12988,7 @@ async function bootstrapDemo(rootDir, input) {
|
|
|
12475
12988
|
}
|
|
12476
12989
|
|
|
12477
12990
|
// src/mcp.ts
|
|
12478
|
-
var SERVER_VERSION = "0.1.
|
|
12991
|
+
var SERVER_VERSION = "0.1.28";
|
|
12479
12992
|
async function createMcpServer(rootDir) {
|
|
12480
12993
|
const server = new McpServer({
|
|
12481
12994
|
name: "swarmvault",
|
|
@@ -13133,7 +13646,7 @@ import mime2 from "mime-types";
|
|
|
13133
13646
|
|
|
13134
13647
|
// src/watch.ts
|
|
13135
13648
|
import path23 from "path";
|
|
13136
|
-
import
|
|
13649
|
+
import process3 from "process";
|
|
13137
13650
|
import chokidar from "chokidar";
|
|
13138
13651
|
var MAX_BACKOFF_MS = 3e4;
|
|
13139
13652
|
var BACKOFF_THRESHOLD = 3;
|
|
@@ -13398,7 +13911,7 @@ async function watchVault(rootDir, options = {}) {
|
|
|
13398
13911
|
consecutiveFailures++;
|
|
13399
13912
|
pending = true;
|
|
13400
13913
|
if (consecutiveFailures >= CRITICAL_THRESHOLD) {
|
|
13401
|
-
|
|
13914
|
+
process3.stderr.write(
|
|
13402
13915
|
`[swarmvault watch] ${consecutiveFailures} consecutive failures. Check vault state. Continuing at max backoff.
|
|
13403
13916
|
`
|
|
13404
13917
|
);
|