@swarmvaultai/engine 0.1.27 → 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 +575 -69
- package/dist/registry-KLO5YIHP.js +12 -0
- package/package.json +8 -7
- package/LICENSE +0 -21
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
|
""
|
|
@@ -9207,7 +9713,7 @@ async function resolveImageGenerationProvider(rootDir) {
|
|
|
9207
9713
|
if (!providerConfig) {
|
|
9208
9714
|
throw new Error(`No provider configured with id "${preferredProviderId}" for task "imageProvider".`);
|
|
9209
9715
|
}
|
|
9210
|
-
const { createProvider: createProvider2 } = await import("./registry-
|
|
9716
|
+
const { createProvider: createProvider2 } = await import("./registry-KLO5YIHP.js");
|
|
9211
9717
|
return createProvider2(preferredProviderId, providerConfig, rootDir);
|
|
9212
9718
|
}
|
|
9213
9719
|
async function generateOutputArtifacts(rootDir, input) {
|
|
@@ -12482,7 +12988,7 @@ async function bootstrapDemo(rootDir, input) {
|
|
|
12482
12988
|
}
|
|
12483
12989
|
|
|
12484
12990
|
// src/mcp.ts
|
|
12485
|
-
var SERVER_VERSION = "0.1.
|
|
12991
|
+
var SERVER_VERSION = "0.1.28";
|
|
12486
12992
|
async function createMcpServer(rootDir) {
|
|
12487
12993
|
const server = new McpServer({
|
|
12488
12994
|
name: "swarmvault",
|
|
@@ -13140,7 +13646,7 @@ import mime2 from "mime-types";
|
|
|
13140
13646
|
|
|
13141
13647
|
// src/watch.ts
|
|
13142
13648
|
import path23 from "path";
|
|
13143
|
-
import
|
|
13649
|
+
import process3 from "process";
|
|
13144
13650
|
import chokidar from "chokidar";
|
|
13145
13651
|
var MAX_BACKOFF_MS = 3e4;
|
|
13146
13652
|
var BACKOFF_THRESHOLD = 3;
|
|
@@ -13405,7 +13911,7 @@ async function watchVault(rootDir, options = {}) {
|
|
|
13405
13911
|
consecutiveFailures++;
|
|
13406
13912
|
pending = true;
|
|
13407
13913
|
if (consecutiveFailures >= CRITICAL_THRESHOLD) {
|
|
13408
|
-
|
|
13914
|
+
process3.stderr.write(
|
|
13409
13915
|
`[swarmvault watch] ${consecutiveFailures} consecutive failures. Check vault state. Continuing at max backoff.
|
|
13410
13916
|
`
|
|
13411
13917
|
);
|