@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/dist/index.js CHANGED
@@ -21,19 +21,35 @@ import {
21
21
  uniqueBy,
22
22
  writeFileIfChanged,
23
23
  writeJsonFile
24
- } from "./chunk-LEUV6TWJ.js";
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 body = [
49
+ const heading = target === "aider" ? "# SwarmVault Conventions" : target === "copilot" ? "# SwarmVault Repository Instructions" : "# SwarmVault Rules";
50
+ return [
35
51
  managedStart,
36
- "# SwarmVault Rules",
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
- var claudeHookMatcher = "Glob|Grep";
55
- 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";
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 targetPathForAgent(rootDir, agent) {
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, "AGENTS.md");
75
+ return path.join(rootDir, agentFileKinds.agents);
88
76
  case "claude":
89
- return path.join(rootDir, "CLAUDE.md");
77
+ return path.join(rootDir, agentFileKinds.claude);
90
78
  case "gemini":
91
- return path.join(rootDir, "GEMINI.md");
79
+ return path.join(rootDir, agentFileKinds.gemini);
92
80
  case "cursor":
93
- return path.join(rootDir, ".cursor", "rules", "swarmvault.mdc");
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 = targetPathForAgent(rootDir, agent);
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(target, buildManagedBlock("agents"));
127
- return target;
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
- if (options.claudeHook) {
131
- await installClaudeHook(rootDir);
132
- }
133
- return target;
134
- }
135
- case "gemini": {
602
+ break;
603
+ case "gemini":
136
604
  await upsertManagedBlock(target, buildManagedBlock("gemini"));
137
- return target;
138
- }
139
- case "cursor": {
140
- const rulesDir = path.dirname(target);
141
- await ensureDir(rulesDir);
142
- await fs.writeFile(target, `${buildManagedBlock("cursor")}
143
- `, "utf8");
144
- return target;
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 dedupedTargets = /* @__PURE__ */ new Map();
647
+ const dedupedAgents = /* @__PURE__ */ new Map();
153
648
  for (const agent of config.agents) {
154
- const target = targetPathForAgent(rootDir, agent);
155
- if (!dedupedTargets.has(target)) {
156
- dedupedTargets.set(target, agent);
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
- [...dedupedTargets.values()].map(
655
+ [...dedupedAgents.values()].map(
161
656
  (agent) => installAgent(rootDir, agent, {
162
- claudeHook: agent === "claude"
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
- "if command -v swarmvault >/dev/null 2>&1; then",
570
- " swarmvault watch --repo --once >/dev/null 2>&1 || printf '[swarmvault hook] refresh failed\\n' >&2",
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-YGVTLIZH.js");
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.27";
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 process2 from "process";
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
- process2.stderr.write(
13914
+ process3.stderr.write(
13409
13915
  `[swarmvault watch] ${consecutiveFailures} consecutive failures. Check vault state. Continuing at max backoff.
13410
13916
  `
13411
13917
  );