brainclaw 0.29.2 → 1.5.3

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.
Files changed (195) hide show
  1. package/README.md +193 -170
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +673 -24
  4. package/dist/commands/accept.js +3 -0
  5. package/dist/commands/add-step.js +11 -26
  6. package/dist/commands/agent-board.js +70 -3
  7. package/dist/commands/audit.js +19 -0
  8. package/dist/commands/check-policy.js +54 -0
  9. package/dist/commands/check-security-mcp.js +145 -0
  10. package/dist/commands/check-security.js +106 -0
  11. package/dist/commands/claim-resource.js +1 -0
  12. package/dist/commands/codev.js +672 -0
  13. package/dist/commands/compact.js +74 -0
  14. package/dist/commands/complete-step.js +16 -26
  15. package/dist/commands/constraint.js +8 -20
  16. package/dist/commands/decision.js +9 -20
  17. package/dist/commands/delete-plan.js +10 -12
  18. package/dist/commands/delete-step.js +16 -0
  19. package/dist/commands/dispatch.js +163 -0
  20. package/dist/commands/doctor.js +1122 -49
  21. package/dist/commands/enable-agent.js +1 -0
  22. package/dist/commands/export.js +280 -22
  23. package/dist/commands/handoff.js +33 -0
  24. package/dist/commands/harvest.js +189 -0
  25. package/dist/commands/hooks.js +82 -25
  26. package/dist/commands/inbox.js +169 -0
  27. package/dist/commands/init.js +38 -31
  28. package/dist/commands/install-hooks.js +71 -44
  29. package/dist/commands/link.js +89 -0
  30. package/dist/commands/list-claims.js +48 -3
  31. package/dist/commands/list-plans.js +129 -25
  32. package/dist/commands/loops-handlers.js +409 -0
  33. package/dist/commands/mcp-read-handlers.js +1628 -0
  34. package/dist/commands/mcp-schemas.generated.js +74 -0
  35. package/dist/commands/mcp.js +4221 -1501
  36. package/dist/commands/plan-resource.js +64 -0
  37. package/dist/commands/plan.js +12 -26
  38. package/dist/commands/prune.js +37 -2
  39. package/dist/commands/reflect.js +20 -7
  40. package/dist/commands/release-claim.js +11 -6
  41. package/dist/commands/release-notes.js +170 -0
  42. package/dist/commands/repair.js +210 -0
  43. package/dist/commands/run-profile.js +57 -0
  44. package/dist/commands/sequence.js +113 -0
  45. package/dist/commands/session-end.js +423 -14
  46. package/dist/commands/session-start.js +214 -41
  47. package/dist/commands/setup-security.js +103 -0
  48. package/dist/commands/setup.js +42 -4
  49. package/dist/commands/stale.js +109 -0
  50. package/dist/commands/switch.js +100 -2
  51. package/dist/commands/trap.js +14 -31
  52. package/dist/commands/update-handoff.js +63 -4
  53. package/dist/commands/update-plan.js +21 -28
  54. package/dist/commands/update-step.js +37 -0
  55. package/dist/commands/upgrade.js +313 -6
  56. package/dist/commands/usage.js +102 -0
  57. package/dist/commands/version.js +20 -0
  58. package/dist/commands/who.js +33 -5
  59. package/dist/commands/worktree.js +105 -0
  60. package/dist/core/actions.js +315 -0
  61. package/dist/core/agent-capability.js +610 -17
  62. package/dist/core/agent-context.js +7 -1
  63. package/dist/core/agent-files.js +1169 -85
  64. package/dist/core/agent-integrations.js +160 -5
  65. package/dist/core/agent-inventory.js +2 -0
  66. package/dist/core/agent-profiles.js +93 -0
  67. package/dist/core/agent-registry.js +162 -30
  68. package/dist/core/agentrun-reconciler.js +345 -0
  69. package/dist/core/agentruns.js +424 -0
  70. package/dist/core/ai-agent-detection.js +31 -10
  71. package/dist/core/archival.js +77 -0
  72. package/dist/core/assignment-sweeper.js +82 -0
  73. package/dist/core/assignments.js +367 -0
  74. package/dist/core/audit.js +30 -0
  75. package/dist/core/brainclaw-version.js +94 -2
  76. package/dist/core/candidates.js +93 -2
  77. package/dist/core/claims.js +419 -0
  78. package/dist/core/codev-metrics.js +77 -0
  79. package/dist/core/codev-personas.js +31 -0
  80. package/dist/core/codev-plan-gen.js +35 -0
  81. package/dist/core/codev-prompts.js +74 -0
  82. package/dist/core/codev-responses.js +62 -0
  83. package/dist/core/codev-rounds.js +218 -0
  84. package/dist/core/config.js +4 -0
  85. package/dist/core/context.js +381 -34
  86. package/dist/core/coordination.js +201 -6
  87. package/dist/core/cross-project.js +230 -16
  88. package/dist/core/default-profiles/doctor.yaml +11 -0
  89. package/dist/core/default-profiles/janitor.yaml +11 -0
  90. package/dist/core/default-profiles/onboarder.yaml +11 -0
  91. package/dist/core/default-profiles/reviewer.yaml +13 -0
  92. package/dist/core/dispatcher.js +1189 -0
  93. package/dist/core/duplicates.js +2 -2
  94. package/dist/core/entity-operations.js +450 -0
  95. package/dist/core/entity-registry.js +344 -0
  96. package/dist/core/events.js +106 -2
  97. package/dist/core/execution-adapters.js +154 -0
  98. package/dist/core/execution-context.js +63 -0
  99. package/dist/core/execution-profile.js +270 -0
  100. package/dist/core/execution.js +255 -0
  101. package/dist/core/facade-schema.js +81 -0
  102. package/dist/core/federation-cloud.js +99 -0
  103. package/dist/core/federation-message.js +52 -0
  104. package/dist/core/federation-transport.js +65 -0
  105. package/dist/core/gc-semantic.js +482 -0
  106. package/dist/core/governance.js +247 -0
  107. package/dist/core/guards.js +19 -0
  108. package/dist/core/ideation.js +72 -0
  109. package/dist/core/identity.js +110 -25
  110. package/dist/core/ids.js +6 -0
  111. package/dist/core/input-validation.js +2 -2
  112. package/dist/core/instruction-templates.js +344 -136
  113. package/dist/core/io.js +90 -11
  114. package/dist/core/lock.js +6 -2
  115. package/dist/core/loops/brief-assembly.js +213 -0
  116. package/dist/core/loops/facade-schema.js +148 -0
  117. package/dist/core/loops/index.js +7 -0
  118. package/dist/core/loops/iteration-engine.js +139 -0
  119. package/dist/core/loops/lock.js +385 -0
  120. package/dist/core/loops/store.js +201 -0
  121. package/dist/core/loops/types.js +403 -0
  122. package/dist/core/loops/verbs.js +534 -0
  123. package/dist/core/markdown.js +15 -3
  124. package/dist/core/memory-compactor.js +432 -0
  125. package/dist/core/memory-git.js +152 -8
  126. package/dist/core/messaging.js +278 -0
  127. package/dist/core/migration.js +32 -1
  128. package/dist/core/mutation-pipeline.js +4 -2
  129. package/dist/core/operations/memory-mutation.js +129 -0
  130. package/dist/core/operations/memory-write.js +78 -0
  131. package/dist/core/operations/plan.js +190 -0
  132. package/dist/core/policy.js +169 -0
  133. package/dist/core/reputation.js +9 -3
  134. package/dist/core/schema.js +491 -6
  135. package/dist/core/search.js +21 -2
  136. package/dist/core/security-cache.js +71 -0
  137. package/dist/core/security-guard.js +152 -0
  138. package/dist/core/security-scoring.js +86 -0
  139. package/dist/core/sequence.js +130 -0
  140. package/dist/core/socket-client.js +113 -0
  141. package/dist/core/staleness.js +246 -0
  142. package/dist/core/state.js +98 -22
  143. package/dist/core/store-resolution.js +43 -11
  144. package/dist/core/toml-writer.js +76 -0
  145. package/dist/core/upgrades/backup.js +232 -0
  146. package/dist/core/upgrades/health-check.js +169 -0
  147. package/dist/core/upgrades/patches/candidate-archive.js +145 -0
  148. package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
  149. package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
  150. package/dist/core/upgrades/schema-version.js +97 -0
  151. package/dist/core/worktree.js +606 -0
  152. package/dist/facts.js +114 -0
  153. package/dist/facts.json +111 -0
  154. package/docs/architecture/project-refs.md +5 -1
  155. package/docs/cli.md +690 -43
  156. package/docs/concepts/ideation-loop.md +317 -0
  157. package/docs/concepts/loop-engine.md +456 -0
  158. package/docs/concepts/mcp-governance.md +268 -0
  159. package/docs/concepts/memory-staleness.md +122 -0
  160. package/docs/concepts/multi-agent-workflows.md +166 -0
  161. package/docs/concepts/plans-and-claims.md +31 -6
  162. package/docs/concepts/project-md-convention.md +35 -0
  163. package/docs/concepts/troubleshooting.md +220 -0
  164. package/docs/concepts/upgrade-cli.md +202 -0
  165. package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
  166. package/docs/context-format-changelog.md +2 -2
  167. package/docs/context-format.md +2 -2
  168. package/docs/index.md +68 -0
  169. package/docs/integrations/agents.md +15 -16
  170. package/docs/integrations/cline.md +88 -0
  171. package/docs/integrations/codex.md +75 -23
  172. package/docs/integrations/continue.md +60 -0
  173. package/docs/integrations/copilot.md +67 -9
  174. package/docs/integrations/kilocode.md +72 -0
  175. package/docs/integrations/mcp.md +304 -21
  176. package/docs/integrations/mistral-vibe.md +122 -0
  177. package/docs/integrations/opencode.md +84 -0
  178. package/docs/integrations/overview.md +23 -8
  179. package/docs/integrations/roo.md +74 -0
  180. package/docs/integrations/windsurf.md +83 -0
  181. package/docs/mcp-schema-changelog.md +191 -1
  182. package/docs/playbooks/integration/index.md +121 -0
  183. package/docs/playbooks/productivity/index.md +102 -0
  184. package/docs/playbooks/team/index.md +122 -0
  185. package/docs/product/agent-first-model.md +184 -0
  186. package/docs/product/entity-model-audit.md +462 -0
  187. package/docs/quickstart-existing-project.md +135 -0
  188. package/docs/quickstart.md +124 -37
  189. package/docs/release-maintenance.md +79 -0
  190. package/docs/review.md +2 -0
  191. package/docs/server-operations.md +118 -0
  192. package/package.json +20 -12
  193. package/dist/commands/claude-desktop-extension.js +0 -18
  194. package/dist/commands/diff.js +0 -99
  195. package/dist/core/claude-desktop-extension.js +0 -224
@@ -1,6 +1,181 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import yaml from 'yaml';
7
+ import { MCP_HEADLESS_AUTO_TOOL_NAMES, REMOVED_IN_V1_TOOLS } from '../commands/mcp.js';
8
+ import { renderToml, tomlArrayTableHasEntry } from './toml-writer.js';
9
+ /**
10
+ * Resolve the brainclaw command for MCP configs.
11
+ * Returns `{ command: "<node>", args: ["<cli.js>", "mcp"] }` so the config
12
+ * works in non-login shells (VS Code Server, MCP subprocesses) on all OSes.
13
+ *
14
+ * Strategy:
15
+ * 1. Find the brainclaw bin via which/where
16
+ * 2. Trace from the bin/shim to the actual cli.js entry point
17
+ * 3. Pair it with the absolute node path
18
+ * Falls back to 'npx brainclaw mcp' if resolution fails.
19
+ */
20
+ function resolveBrainclawMcpCommand() {
21
+ const nodeBin = process.execPath;
22
+ // 1. Try to resolve the cli.js from the installed brainclaw binary
23
+ const cliJs = resolveBrainclawCliJs();
24
+ if (cliJs) {
25
+ return { command: nodeBin, args: [cliJs, 'mcp'] };
26
+ }
27
+ // 2. Fallback: npx (relies on PATH, may resolve wrong version)
28
+ return { command: 'npx', args: ['brainclaw', 'mcp'] };
29
+ }
30
+ /**
31
+ * Trace from the brainclaw bin/shim to the actual dist/cli.js file.
32
+ * Works on Windows (.cmd shim), macOS/Linux (symlink to bin stub).
33
+ */
34
+ function resolveBrainclawCliJs() {
35
+ // Strategy A: find via which/where and trace to cli.js
36
+ const whichCmd = os.platform() === 'win32' ? 'where' : 'which';
37
+ try {
38
+ const result = spawnSync(whichCmd, ['brainclaw'], { encoding: 'utf-8', timeout: 3000 });
39
+ if (result.status === 0) {
40
+ const resolved = result.stdout.trim().split(/\r?\n/)[0]?.trim();
41
+ if (resolved) {
42
+ const cliJs = traceToCliJs(resolved);
43
+ if (cliJs)
44
+ return cliJs;
45
+ }
46
+ }
47
+ }
48
+ catch {
49
+ // Non-fatal — try next strategy
50
+ }
51
+ // Strategy B: resolve from this file's own package (we ARE brainclaw)
52
+ try {
53
+ const ownCliJs = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'cli.js');
54
+ if (fs.existsSync(ownCliJs))
55
+ return ownCliJs;
56
+ }
57
+ catch {
58
+ // Non-fatal
59
+ }
60
+ return undefined;
61
+ }
62
+ /**
63
+ * Given a bin path (shim or symlink), trace to the dist/cli.js entry point.
64
+ *
65
+ * Windows: .cmd shim contains a line like `"%_prog%" "%dp0%\node_modules\brainclaw\dist\cli.js" %*`
66
+ * Unix: bin is a symlink → resolve to real path → go up to package root → dist/cli.js
67
+ */
68
+ function traceToCliJs(binPath) {
69
+ const isWindows = os.platform() === 'win32';
70
+ if (isWindows) {
71
+ // Read the .cmd shim and extract the cli.js path
72
+ const cmdPath = binPath.endsWith('.cmd') ? binPath : `${binPath}.cmd`;
73
+ try {
74
+ const content = fs.readFileSync(cmdPath, 'utf-8');
75
+ // Match patterns like: "%dp0%\node_modules\brainclaw\dist\cli.js"
76
+ const match = content.match(/%dp0%\\([^\s"]+cli\.js)/);
77
+ if (match) {
78
+ const shimDir = path.dirname(cmdPath);
79
+ const cliJs = path.resolve(shimDir, match[1]);
80
+ if (fs.existsSync(cliJs))
81
+ return cliJs;
82
+ }
83
+ }
84
+ catch {
85
+ // Fall through
86
+ }
87
+ }
88
+ else {
89
+ // Unix: follow symlink chain to the real bin, then find cli.js
90
+ try {
91
+ const realBin = fs.realpathSync(binPath);
92
+ // Typical layout: .../node_modules/.bin/brainclaw → ../brainclaw/dist/cli.js
93
+ // Or: .../node_modules/brainclaw/dist/cli.js (direct)
94
+ if (realBin.endsWith('cli.js') && fs.existsSync(realBin))
95
+ return realBin;
96
+ // The bin stub typically lives at node_modules/brainclaw/dist/cli.js
97
+ // or node_modules/.bin/brainclaw → ../brainclaw/dist/cli.js
98
+ const packageRoot = findPackageRoot(realBin);
99
+ if (packageRoot) {
100
+ const cliJs = path.join(packageRoot, 'dist', 'cli.js');
101
+ if (fs.existsSync(cliJs))
102
+ return cliJs;
103
+ }
104
+ }
105
+ catch {
106
+ // Fall through
107
+ }
108
+ }
109
+ return undefined;
110
+ }
111
+ /** Walk up from a file to find the nearest directory containing package.json with name "brainclaw". */
112
+ function findPackageRoot(from) {
113
+ let dir = path.dirname(from);
114
+ for (let i = 0; i < 10; i++) {
115
+ const pkgPath = path.join(dir, 'package.json');
116
+ try {
117
+ if (fs.existsSync(pkgPath)) {
118
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
119
+ if (pkg.name === 'brainclaw')
120
+ return dir;
121
+ }
122
+ }
123
+ catch { /* continue */ }
124
+ const parent = path.dirname(dir);
125
+ if (parent === dir)
126
+ break;
127
+ dir = parent;
128
+ }
129
+ return undefined;
130
+ }
131
+ /** Cached MCP command — resolved once per process. */
132
+ let cachedMcpCommand;
133
+ function getBrainclawMcpCommand() {
134
+ if (!cachedMcpCommand) {
135
+ cachedMcpCommand = resolveBrainclawMcpCommand();
136
+ }
137
+ return cachedMcpCommand;
138
+ }
139
+ /** Reset the cached MCP command so it gets re-resolved on next access. */
140
+ export function resetMcpCommandCache() {
141
+ cachedMcpCommand = undefined;
142
+ }
143
+ /** Module-level flag: when true, brainclawMcpEntry overwrites existing paths. */
144
+ let _forceResolve = false;
145
+ /**
146
+ * Build a complete MCP server entry with relay model env injection.
147
+ * Merges with the existing entry to preserve manual edits (e.g. custom command
148
+ * path, additional env vars, extra args). Only sets defaults for missing fields.
149
+ *
150
+ * When `workspacePath` is provided, injects BRAINCLAW_CWD into the env so
151
+ * the MCP server resolves the correct workspace root regardless of the IDE's
152
+ * process.cwd() at launch time.
153
+ */
154
+ function brainclawMcpEntry(agentName, existing, workspacePath) {
155
+ const defaults = getBrainclawMcpCommand();
156
+ const ex = isJsonObject(existing) ? existing : {};
157
+ const exEnv = isJsonObject(ex.env) ? ex.env : {};
158
+ // When _forceResolve is true (post-upgrade), always use newly resolved paths.
159
+ // Otherwise preserve existing command if it's an absolute path (manual edit).
160
+ // CRITICAL: once we decide to preserve the command, we MUST also preserve
161
+ // the args. Previously args was always overwritten, which silently clobbered
162
+ // manual customizations (--cwd, --debug, etc.) and broke setups on DGX.
163
+ // See trp#12 + pln#450.
164
+ const useExisting = !_forceResolve && typeof ex.command === 'string' && ex.command !== 'npx';
165
+ const existingArgs = Array.isArray(ex.args) ? ex.args : undefined;
166
+ return {
167
+ command: useExisting ? ex.command : defaults.command,
168
+ args: useExisting && existingArgs ? existingArgs : defaults.args,
169
+ // Merge env: preserve user-added vars, ensure BRAINCLAW_AGENT is set
170
+ env: {
171
+ ...exEnv,
172
+ BRAINCLAW_AGENT: agentName,
173
+ ...(workspacePath ? { BRAINCLAW_CWD: workspacePath } : {}),
174
+ },
175
+ // Preserve timeout if set
176
+ ...(typeof ex.timeout === 'number' ? { timeout: ex.timeout } : {}),
177
+ };
178
+ }
4
179
  export const BRAINCLAW_SECTION_START = '<!-- brainclaw:start -->';
5
180
  export const BRAINCLAW_SECTION_END = '<!-- brainclaw:end -->';
6
181
  export function buildBrainclawSection(storageDir) {
@@ -132,6 +307,8 @@ export function collectWorkspaceGitignoreEntries(cwd, results) {
132
307
  continue;
133
308
  if (result.relativePath === 'package.json')
134
309
  continue;
310
+ if (result.relativePath === VSCODE_EXTENSIONS_RELATIVE_PATH)
311
+ continue;
135
312
  const expectedWorkspacePath = path.resolve(workspaceRoot, result.relativePath);
136
313
  const actualPath = path.resolve(result.filePath);
137
314
  if (actualPath !== expectedWorkspacePath)
@@ -159,14 +336,31 @@ export const AGENT_EXPORT_REGISTRY = [
159
336
  { agentName: 'codex', format: 'agents-md', relativePath: 'AGENTS.md' },
160
337
  { agentName: 'continue', format: 'continue', relativePath: '.continue/rules/brainclaw.md' },
161
338
  { agentName: 'roo', format: 'roo', relativePath: '.roo/rules/brainclaw.md' },
339
+ { agentName: 'kilocode', format: 'kilocode', relativePath: '.kilo/rules/brainclaw.md' },
340
+ { agentName: 'mistral-vibe', format: 'agents-md', relativePath: 'AGENTS.md' },
162
341
  { agentName: 'opencode', format: 'agents-md', relativePath: 'AGENTS.md' },
163
342
  { agentName: 'antigravity', format: 'gemini-md', relativePath: 'GEMINI.md' },
343
+ { agentName: 'brainclaw', format: 'board-md', relativePath: 'BOARD.md' },
344
+ { agentName: 'openclaw', format: 'openclaw', relativePath: 'skills/openclaw/SKILL.md' },
345
+ { agentName: 'nanoclaw', format: 'nanoclaw', relativePath: 'skills/nanoclaw/SKILL.md' },
346
+ { agentName: 'nemoclaw', format: 'nemoclaw', relativePath: 'skills/nemoclaw/SKILL.md' },
347
+ { agentName: 'picoclaw', format: 'picoclaw', relativePath: 'skills/picoclaw/SKILL.md' },
348
+ { agentName: 'zeroclaw', format: 'zeroclaw', relativePath: 'skills/zeroclaw/SKILL.md' },
164
349
  ];
165
350
  export const FALLBACK_EXPORT_TARGET = {
166
351
  agentName: 'unknown',
167
352
  format: 'agents-md',
168
353
  relativePath: 'AGENTS.md',
169
354
  };
355
+ export const LIVE_COMPANION_EXPORT_REGISTRY = [
356
+ { agentName: 'cursor', relativePath: '.cursor/live.md' },
357
+ { agentName: 'cline', relativePath: '.clinerules/live.md' },
358
+ { agentName: 'windsurf', relativePath: '.windsurf/rules/live.md' },
359
+ { agentName: 'github-copilot', relativePath: '.github/copilot-instructions.live.md' },
360
+ { agentName: 'continue', relativePath: '.continue/live.md' },
361
+ { agentName: 'antigravity', relativePath: 'GEMINI.live.md' },
362
+ { agentName: 'mistral-vibe', relativePath: '.vibe/live.md' },
363
+ ];
170
364
  export function resolveExportTarget(agentName) {
171
365
  return AGENT_EXPORT_REGISTRY.find((t) => t.agentName === agentName) ?? FALLBACK_EXPORT_TARGET;
172
366
  }
@@ -188,38 +382,122 @@ export function writeExportFile(content, relativePath, cwd) {
188
382
  fs.writeFileSync(fullPath, next, 'utf-8');
189
383
  return { created: !existed, updated: existed, filePath: fullPath };
190
384
  }
191
- const ALL_BCLAW_TOOLS = [
192
- 'bclaw_get_context', 'bclaw_bootstrap', 'bclaw_get_execution_context',
193
- 'bclaw_read_handoff', 'bclaw_get_agent_board', 'bclaw_search', 'bclaw_estimation_report',
194
- 'bclaw_list_plans', 'bclaw_list_claims', 'bclaw_list_agents', 'bclaw_list_instructions', 'bclaw_list_candidates',
195
- 'bclaw_write_note', 'bclaw_create_candidate', 'bclaw_accept', 'bclaw_reject',
196
- 'bclaw_claim', 'bclaw_release_claim', 'bclaw_session_start', 'bclaw_session_end',
197
- 'bclaw_create_plan', 'bclaw_update_plan', 'bclaw_add_step', 'bclaw_complete_step',
198
- ];
385
+ function defaultLiveCompanionPath(stableRelativePath) {
386
+ const ext = path.extname(stableRelativePath);
387
+ if (!ext)
388
+ return `${stableRelativePath}.live`;
389
+ const base = stableRelativePath.slice(0, -ext.length);
390
+ return `${base}.live${ext}`;
391
+ }
392
+ export function resolveLiveCompanionPath(agentName, stableRelativePath) {
393
+ return LIVE_COMPANION_EXPORT_REGISTRY.find((target) => target.agentName === agentName)?.relativePath
394
+ ?? defaultLiveCompanionPath(stableRelativePath);
395
+ }
396
+ export function writeLiveCompanionFile(content, agentName, stableRelativePath, cwd) {
397
+ const relativePath = resolveLiveCompanionPath(agentName, stableRelativePath);
398
+ const fullPath = path.join(cwd, relativePath);
399
+ const dir = path.dirname(fullPath);
400
+ if (!fs.existsSync(dir))
401
+ fs.mkdirSync(dir, { recursive: true });
402
+ const existed = fs.existsSync(fullPath);
403
+ const existing = existed ? fs.readFileSync(fullPath, 'utf-8') : '';
404
+ if (existing === content) {
405
+ return { created: false, updated: false, filePath: fullPath, relativePath };
406
+ }
407
+ fs.writeFileSync(fullPath, content, 'utf-8');
408
+ return { created: !existed, updated: existed, filePath: fullPath, relativePath };
409
+ }
410
+ /**
411
+ * Returns the narrowed list of brainclaw MCP tool names that are safe for
412
+ * headless auto-approval. Sourced from MCP_HEADLESS_AUTO_TOOL_NAMES (the
413
+ * subset of ALL_TOOLS with headlessApproval === 'auto'), so the list
414
+ * auto-updates when tools are added/removed in src/commands/mcp.ts — no
415
+ * manual sync required.
416
+ *
417
+ * Excluded: dispatch, architectural gates (accept/reject), plan/sequence
418
+ * creation, setup, switch, bootstrap, release_notes, memory deletes, and
419
+ * other operations that warrant human review.
420
+ *
421
+ * IMPORTANT: Accessed lazily (not at module init) to avoid a circular-import
422
+ * TDZ error — this module ← commands/session-start ← commands/mcp cycles back
423
+ * into commands/mcp. Calling at runtime (inside writer functions) is always
424
+ * safe because by then all modules are fully initialized.
425
+ *
426
+ * Consumed by:
427
+ * - Cline `autoApprove` (.vscode/cline_mcp_settings.json)
428
+ * - Roo `alwaysAllow` (.roo/mcp.json)
429
+ * - Codex `[mcp_servers.brainclaw.tools.<name>] approval_mode = "approve"`
430
+ * (~/.codex/config.toml) — required for headless codex exec (non-interactive
431
+ * approval mode cancels MCP writes by default; explicit per-tool
432
+ * `approval_mode = "approve"` bypasses this).
433
+ */
434
+ function getHeadlessAutoApprovedToolNames() {
435
+ // Filter out tools removed at v1.0 — their handlers still exist as a
436
+ // migration escape hatch, but we don't want to advertise them in agent
437
+ // configs anymore. Otherwise Codex / Cline / Roo still discover names
438
+ // like `bclaw_get_context`, `bclaw_list_plans`, etc. and dispatch to
439
+ // deprecation warnings instead of the canonical grammar (pln#397 Codex
440
+ // post-alignment audit).
441
+ return MCP_HEADLESS_AUTO_TOOL_NAMES.filter((name) => !REMOVED_IN_V1_TOOLS.has(name));
442
+ }
199
443
  const CLINE_MCP_RELATIVE_PATH = '.vscode/cline_mcp_settings.json';
200
444
  const CURSOR_MDC_RELATIVE_PATH = '.cursor/rules/brainclaw-mcp-shim.mdc';
201
445
  const COPILOT_SKILL_RELATIVE_PATH = '.github/skills/brainclaw-context/SKILL.md';
446
+ const COPILOT_MCP_RELATIVE_PATH = '.vscode/settings.json';
447
+ const VSCODE_MCP_RELATIVE_PATH = '.vscode/mcp.json';
202
448
  const WINDSURF_MCP_RELATIVE_PATH = '.codeium/windsurf/mcp_config.json';
449
+ const WINDSURF_MODERN_RULES_RELATIVE_PATH = '.windsurf/rules/brainclaw.md';
203
450
  const CLAUDE_CODE_MCP_RELATIVE_PATH = '.mcp.json';
204
451
  const CLAUDE_CODE_COMMAND_RELATIVE_PATH = '.claude/commands/brainclaw.md';
205
452
  const CLAUDE_CODE_SETTINGS_RELATIVE_PATH = '.claude/settings.local.json';
206
453
  const CLAUDE_CODE_SESSION_MARKER_RELATIVE_PATH = '.claude/.bclaw-session';
207
454
  const CURSOR_MCP_RELATIVE_PATH = '.cursor/mcp.json';
208
455
  const ROO_MCP_RELATIVE_PATH = '.roo/mcp.json';
456
+ const KILOCODE_MCP_RELATIVE_PATH = '.kilo/mcp.json';
457
+ const KILOCODE_CONFIG_RELATIVE_PATH = 'kilo.jsonc';
458
+ const MISTRAL_VIBE_CONFIG_RELATIVE_PATH = '.vibe/config.toml';
209
459
  const CONTINUE_CONFIG_RELATIVE_PATH = '.continue/config.json';
460
+ const CONTINUE_PERMISSIONS_RELATIVE_PATH = '.continue/permissions.yaml';
210
461
  const OPENCODE_CONFIG_RELATIVE_PATH = 'opencode.json';
211
462
  const ANTIGRAVITY_MCP_RELATIVE_PATH = '.gemini/antigravity/mcp_config.json';
463
+ const ANTIGRAVITY_HOOKS_RELATIVE_PATH = '.gemini/antigravity/hooks.json';
464
+ const CURSOR_HOOKS_RELATIVE_PATH = '.cursor/hooks.json';
465
+ const COPILOT_HOOKS_RELATIVE_PATH = '.github/copilot/hooks.json';
466
+ const OPENCLAW_MCP_RELATIVE_PATH = '.openclaw/mcp.json';
467
+ const VSCODE_EXTENSIONS_RELATIVE_PATH = '.vscode/extensions.json';
468
+ const UNIVERSAL_SKILL_RELATIVE_PATH = '.agents/skills/brainclaw/SKILL.md';
469
+ /**
470
+ * Directories exclusively managed by brainclaw — safe to gitignore as a whole.
471
+ * Individual files in these directories don't need separate gitignore entries.
472
+ */
473
+ export const BRAINCLAW_EXCLUSIVE_DIRECTORIES = [
474
+ '.roo/',
475
+ '.kilo/',
476
+ '.continue/',
477
+ '.codeium/windsurf/',
478
+ '.gemini/antigravity/',
479
+ '.github/skills/brainclaw-context/',
480
+ ];
212
481
  export const LOCAL_ONLY_AGENT_WORKSPACE_FILES = [
213
482
  CLINE_MCP_RELATIVE_PATH,
214
483
  CURSOR_MDC_RELATIVE_PATH,
484
+ CURSOR_MCP_RELATIVE_PATH,
215
485
  COPILOT_SKILL_RELATIVE_PATH,
486
+ COPILOT_MCP_RELATIVE_PATH,
487
+ VSCODE_MCP_RELATIVE_PATH,
216
488
  CLAUDE_CODE_MCP_RELATIVE_PATH,
217
489
  CLAUDE_CODE_COMMAND_RELATIVE_PATH,
218
490
  CLAUDE_CODE_SETTINGS_RELATIVE_PATH,
219
491
  CLAUDE_CODE_SESSION_MARKER_RELATIVE_PATH,
220
492
  ROO_MCP_RELATIVE_PATH,
493
+ KILOCODE_MCP_RELATIVE_PATH,
494
+ KILOCODE_CONFIG_RELATIVE_PATH,
495
+ MISTRAL_VIBE_CONFIG_RELATIVE_PATH,
221
496
  CONTINUE_CONFIG_RELATIVE_PATH,
222
497
  OPENCODE_CONFIG_RELATIVE_PATH,
498
+ WINDSURF_MCP_RELATIVE_PATH,
499
+ WINDSURF_MODERN_RULES_RELATIVE_PATH,
500
+ ANTIGRAVITY_MCP_RELATIVE_PATH,
223
501
  ];
224
502
  function isJsonObject(value) {
225
503
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -236,6 +514,21 @@ function readJsonObject(filePath) {
236
514
  return {};
237
515
  }
238
516
  }
517
+ function readJsoncObject(filePath) {
518
+ if (!fs.existsSync(filePath)) {
519
+ return {};
520
+ }
521
+ try {
522
+ const raw = fs.readFileSync(filePath, 'utf-8');
523
+ const withoutBlockComments = raw.replace(/\/\*[\s\S]*?\*\//g, '');
524
+ const withoutLineComments = withoutBlockComments.replace(/^\s*\/\/.*$/gm, '');
525
+ const parsed = JSON.parse(withoutLineComments);
526
+ return isJsonObject(parsed) ? parsed : {};
527
+ }
528
+ catch {
529
+ return {};
530
+ }
531
+ }
239
532
  function writeTextFileIfChanged(filePath, content) {
240
533
  const existed = fs.existsSync(filePath);
241
534
  const current = existed ? fs.readFileSync(filePath, 'utf-8') : undefined;
@@ -341,10 +634,9 @@ export function ensureClineMcpConfig(cwd) {
341
634
  const existing = readJsonObject(filePath);
342
635
  const mcpServers = isJsonObject(existing.mcpServers) ? { ...existing.mcpServers } : {};
343
636
  mcpServers.brainclaw = {
344
- command: 'npx',
345
- args: ['brainclaw', 'mcp'],
637
+ ...brainclawMcpEntry('cline', mcpServers.brainclaw, cwd),
346
638
  disabled: false,
347
- autoApprove: ALL_BCLAW_TOOLS,
639
+ autoApprove: getHeadlessAutoApprovedToolNames(),
348
640
  };
349
641
  const { created, updated } = writeJsonFileIfChanged(filePath, {
350
642
  ...existing,
@@ -367,8 +659,8 @@ export function ensureWindsurfMcpConfig(homeDir) {
367
659
  const existing = readJsonObject(filePath);
368
660
  const mcpServers = isJsonObject(existing.mcpServers) ? { ...existing.mcpServers } : {};
369
661
  mcpServers.brainclaw = {
370
- command: 'npx',
371
- args: ['brainclaw', 'mcp'],
662
+ ...brainclawMcpEntry('windsurf', mcpServers.brainclaw),
663
+ alwaysAllow: getHeadlessAutoApprovedToolNames(),
372
664
  };
373
665
  const { created, updated } = writeJsonFileIfChanged(filePath, {
374
666
  ...existing,
@@ -383,6 +675,56 @@ export function ensureWindsurfMcpConfig(homeDir) {
383
675
  relativePath: WINDSURF_MCP_RELATIVE_PATH,
384
676
  };
385
677
  }
678
+ /**
679
+ * Writes `.windsurf/rules/brainclaw.md` — the modern Windsurf rules format
680
+ * (Wave 8+). Instructs Windsurf's Cascade to load brainclaw context and follow
681
+ * the coordination protocol before any significant code change.
682
+ *
683
+ * Unlike `.windsurfrules` (legacy), this file is workspace-scoped and supports
684
+ * the per-file rule activation model. Kept alongside `.windsurfrules` for
685
+ * backward compatibility.
686
+ */
687
+ export function ensureWindsurfModernRules(cwd) {
688
+ const filePath = path.join(cwd, WINDSURF_MODERN_RULES_RELATIVE_PATH);
689
+ const content = `# Brainclaw coordination rules
690
+
691
+ Brainclaw is the shared coordination layer. Use its MCP facades first — the CLI is only a fallback when MCP is unavailable.
692
+
693
+ ## Session start
694
+
695
+ Call \`bclaw_work(intent)\`. It loads memory (constraints, decisions, traps, plans, handoffs), resolves the claim, and starts a session in a single call.
696
+
697
+ - \`bclaw_work(intent: "resume")\` — continue existing work (auto-surfaces the context diff).
698
+ - \`bclaw_work(intent: "execute", scope: "<path>", task: "<text>")\` — start new work and claim the scope.
699
+ - \`bclaw_work(intent: "consult")\` — read-only context without claiming.
700
+
701
+ ## During work
702
+
703
+ - Mark plan steps done: \`bclaw_complete_step(planId, stepId)\`.
704
+ - Read the inbox: \`bclaw_read_inbox\`.
705
+ - Record notes, decisions, traps: \`bclaw_write_note\`, \`bclaw_create(entity, data)\`.
706
+
707
+ ## To coordinate with other agents
708
+
709
+ \`bclaw_coordinate(intent)\` — \`assign\`, \`consult\`, \`review\`, or \`reroute\`.
710
+
711
+ ## Before finishing
712
+
713
+ - Release your claims: \`bclaw_release_claim(id)\`.
714
+ - Close the session: \`bclaw_session_end\` (auto-releases remaining claims).
715
+
716
+ CLI fallback only when MCP is unavailable: \`brainclaw context\` / \`brainclaw session-end --auto-release\`.
717
+ `;
718
+ const { created, updated } = writeTextFileIfChanged(filePath, content);
719
+ return {
720
+ kind: 'rule',
721
+ label: 'Windsurf modern rules (.windsurf/rules/brainclaw.md)',
722
+ created,
723
+ updated,
724
+ filePath,
725
+ relativePath: WINDSURF_MODERN_RULES_RELATIVE_PATH,
726
+ };
727
+ }
386
728
  export function ensureCopilotSkill(cwd) {
387
729
  const filePath = path.join(cwd, '.github', 'skills', 'brainclaw-context', 'SKILL.md');
388
730
  const content = `---
@@ -392,13 +734,15 @@ description: "Use this skill when you need the latest Brainclaw context, active
392
734
 
393
735
  # Brainclaw Context
394
736
 
395
- Use this skill to fetch live project memory before significant edits or when asked about repository rules.
737
+ Fetch live project memory before significant edits. Prefer the Brainclaw MCP facade; use the CLI only as a fallback when MCP is unavailable.
396
738
 
397
739
  ## Steps
398
740
 
399
- 1. Run \`brainclaw context --json\`.
400
- 2. Read active plans, constraints, decisions, traps, and handoffs from the result.
401
- 3. Prefer Brainclaw state over stale assumptions from older instructions or prior sessions.
741
+ 1. Call \`bclaw_work(intent: "resume")\` to continue existing work, or \`bclaw_work(intent: "consult")\` for read-only context. The response contains active plans, constraints, decisions, traps, and handoffs.
742
+ 2. Prefer Brainclaw state over stale assumptions from older instructions or prior sessions.
743
+ 3. Coordinate with other agents via \`bclaw_coordinate(intent)\` (\`assign\`, \`consult\`, \`review\`).
744
+
745
+ CLI fallback: \`brainclaw context --json\` if the MCP server is not reachable.
402
746
  `;
403
747
  const { created, updated } = writeTextFileIfChanged(filePath, content);
404
748
  return {
@@ -410,6 +754,110 @@ Use this skill to fetch live project memory before significant edits or when ask
410
754
  relativePath: COPILOT_SKILL_RELATIVE_PATH,
411
755
  };
412
756
  }
757
+ /**
758
+ * Write .agents/skills/brainclaw/SKILL.md — universal cross-agent skill.
759
+ * Auto-discovered by Cursor, Copilot, Roo, OpenCode, Codex, Kilo, and Mistral
760
+ * via their shared .agents/skills/ path convention. Single writer, 7 agents,
761
+ * zero per-agent branching.
762
+ *
763
+ * Ref: surfaces_audit_2026_04_15.md (feedback_cross_agent_patterns rule #3).
764
+ */
765
+ export function ensureUniversalBrainclawSkill(cwd) {
766
+ const filePath = path.join(cwd, UNIVERSAL_SKILL_RELATIVE_PATH);
767
+ const content = `---
768
+ name: brainclaw
769
+ description: 'Load and act on Brainclaw project memory, active claims, plans, traps, and handoffs before code changes. Trigger: refresh brainclaw context, check active claims, load coordination state.'
770
+ allowed-tools: 'Read Bash(npx brainclaw:*)'
771
+ ---
772
+
773
+ # Brainclaw
774
+
775
+ Load the shared coordination state before any significant code change. Prefer the Brainclaw MCP facade; the CLI is a fallback when MCP is not reachable.
776
+
777
+ ## Steps
778
+
779
+ 1. Call \`bclaw_work(intent)\` — \`resume\` to continue existing work, \`execute\` to claim a new scope, or \`consult\` for read-only context. The response gives you memory, active claims, plans, traps, and handoffs.
780
+ 2. Respect active claims from other agents reported in the response; do not edit a claimed scope unless you own the claim.
781
+ 3. Use \`bclaw_coordinate(intent)\` to assign, consult, or review other agents when needed.
782
+ 4. When done, call \`bclaw_session_end\` (auto-releases your remaining claims).
783
+
784
+ CLI fallback only: \`brainclaw context --json\` / \`brainclaw claim create\` / \`brainclaw session-end --auto-release\` if the MCP server is unavailable.
785
+ `;
786
+ const { created, updated } = writeTextFileIfChanged(filePath, content);
787
+ return {
788
+ kind: 'skill',
789
+ label: 'Universal Brainclaw skill (.agents/skills/brainclaw/SKILL.md)',
790
+ created,
791
+ updated,
792
+ filePath,
793
+ relativePath: UNIVERSAL_SKILL_RELATIVE_PATH,
794
+ };
795
+ }
796
+ export function ensureCopilotMcpConfig(cwd) {
797
+ const filePath = path.join(cwd, '.vscode', 'settings.json');
798
+ const existing = readJsonObject(filePath);
799
+ const copilotMcpKey = 'github.copilot.chat.mcpServers';
800
+ const mcpServers = isJsonObject(existing[copilotMcpKey]) ? { ...existing[copilotMcpKey] } : {};
801
+ mcpServers.brainclaw = brainclawMcpEntry('github-copilot', mcpServers.brainclaw, cwd);
802
+ const { created, updated } = writeJsonFileIfChanged(filePath, {
803
+ ...existing,
804
+ [copilotMcpKey]: mcpServers,
805
+ });
806
+ return {
807
+ kind: 'mcp',
808
+ label: 'Copilot MCP settings (.vscode/settings.json)',
809
+ created,
810
+ updated,
811
+ filePath,
812
+ relativePath: COPILOT_MCP_RELATIVE_PATH,
813
+ };
814
+ }
815
+ /**
816
+ * Write .vscode/mcp.json — the VS Code-native universal MCP config.
817
+ * Works for Copilot, Claude Code (VS Code extension), and any MCP-consuming
818
+ * VS Code extension. Uses the { servers: { ... } } format.
819
+ */
820
+ export function ensureVscodeMcpConfig(cwd) {
821
+ const filePath = path.join(cwd, '.vscode', 'mcp.json');
822
+ const existing = readJsonObject(filePath);
823
+ const servers = isJsonObject(existing.servers) ? { ...existing.servers } : {};
824
+ servers.brainclaw = brainclawMcpEntry('github-copilot', servers.brainclaw, cwd);
825
+ const { created, updated } = writeJsonFileIfChanged(filePath, {
826
+ ...existing,
827
+ servers,
828
+ });
829
+ return {
830
+ kind: 'mcp',
831
+ label: 'VS Code MCP config (.vscode/mcp.json)',
832
+ created,
833
+ updated,
834
+ filePath,
835
+ relativePath: VSCODE_MCP_RELATIVE_PATH,
836
+ };
837
+ }
838
+ const BRAINCLAW_EXTENSION_ID = 'brainclaw.brainclaw-vscode';
839
+ export function ensureVscodeExtensionRecommendation(cwd) {
840
+ const filePath = path.join(cwd, '.vscode', 'extensions.json');
841
+ const existing = readJsonObject(filePath);
842
+ const recommendations = Array.isArray(existing.recommendations)
843
+ ? [...existing.recommendations]
844
+ : [];
845
+ if (!recommendations.includes(BRAINCLAW_EXTENSION_ID)) {
846
+ recommendations.push(BRAINCLAW_EXTENSION_ID);
847
+ }
848
+ const { created, updated } = writeJsonFileIfChanged(filePath, {
849
+ ...existing,
850
+ recommendations,
851
+ });
852
+ return {
853
+ kind: 'recommendation',
854
+ label: 'VS Code extension recommendation (.vscode/extensions.json)',
855
+ created,
856
+ updated,
857
+ filePath,
858
+ relativePath: VSCODE_EXTENSIONS_RELATIVE_PATH,
859
+ };
860
+ }
413
861
  export function ensureCursorMdc(cwd) {
414
862
  const filePath = path.join(cwd, '.cursor', 'rules', 'brainclaw-mcp-shim.mdc');
415
863
  const content = `---
@@ -418,12 +866,13 @@ globs: "**/*"
418
866
  alwaysApply: true
419
867
  ---
420
868
 
421
- Before significant edits or when asked about project rules, run:
422
- <run_command>
423
- brainclaw context --json
424
- </run_command>
869
+ Brainclaw is the shared coordination layer. Call the MCP facade first; the CLI is only a fallback when MCP is not reachable.
870
+
871
+ Before significant edits or when asked about project rules, call \`bclaw_work(intent: "consult")\` (or \`"resume"\` if continuing a task) via the Brainclaw MCP server. The response carries active claims, in-progress plans, constraints, decisions, traps, and handoffs.
425
872
 
426
- If Brainclaw reports active claims or in-progress plans, follow them before editing.
873
+ If the response lists active claims or in-progress plans, follow them before editing. Use \`bclaw_coordinate(intent)\` to dispatch, consult, or review other agents.
874
+
875
+ CLI fallback only when MCP is unavailable: \`brainclaw context --json\`.
427
876
  `;
428
877
  const { created, updated } = writeTextFileIfChanged(filePath, content);
429
878
  return {
@@ -452,6 +901,53 @@ function containsCommandHook(entries, command) {
452
901
  Array.isArray(entry.hooks) &&
453
902
  entry.hooks.some((h) => isJsonObject(h) && h.command === command));
454
903
  }
904
+ /**
905
+ * Replace a legacy command hook with a new one, or add the new one if neither exists.
906
+ * This enables clean upgrades: old hooks are swapped out, new hooks are added if fresh.
907
+ */
908
+ function replaceOrAddCommandHook(entries, newCommand, legacyCommand) {
909
+ if (containsCommandHook(entries, newCommand))
910
+ return;
911
+ // Find and replace legacy command
912
+ for (let i = 0; i < entries.length; i++) {
913
+ const entry = entries[i];
914
+ if (isJsonObject(entry) && Array.isArray(entry.hooks)) {
915
+ for (const h of entry.hooks) {
916
+ if (isJsonObject(h) && h.command === legacyCommand) {
917
+ h.command = newCommand;
918
+ return;
919
+ }
920
+ }
921
+ }
922
+ }
923
+ // Neither new nor legacy found — add fresh
924
+ entries.push(buildCommandHookEntry(newCommand));
925
+ }
926
+ /**
927
+ * Replace a hook matching any of the legacy patterns, or add fresh.
928
+ * Used for hooks where the command string changes across versions.
929
+ */
930
+ function replaceOrAddCommandHookByPattern(entries, newCommand, legacyPatterns) {
931
+ // Already present with the exact new command
932
+ if (entries.some(entry => isJsonObject(entry) && Array.isArray(entry.hooks) &&
933
+ entry.hooks.some(h => isJsonObject(h) && typeof h.command === 'string' && h.command === newCommand)))
934
+ return;
935
+ // Find and replace any entry containing a legacy pattern substring
936
+ for (const entry of entries) {
937
+ if (!isJsonObject(entry) || !Array.isArray(entry.hooks))
938
+ continue;
939
+ for (const h of entry.hooks) {
940
+ if (!isJsonObject(h) || typeof h.command !== 'string')
941
+ continue;
942
+ if (legacyPatterns.some(p => h.command.includes(p))) {
943
+ h.command = newCommand;
944
+ return;
945
+ }
946
+ }
947
+ }
948
+ // No match — add fresh
949
+ entries.push(buildCommandHookEntry(newCommand));
950
+ }
455
951
  export function ensureProjectDevDependency(cwd) {
456
952
  const filePath = path.join(cwd, 'package.json');
457
953
  if (!fs.existsSync(filePath))
@@ -485,10 +981,7 @@ export function ensureClaudeCodeMcpConfig(cwd) {
485
981
  const filePath = path.join(cwd, '.mcp.json');
486
982
  const existing = readJsonObject(filePath);
487
983
  const mcpServers = isJsonObject(existing.mcpServers) ? { ...existing.mcpServers } : {};
488
- mcpServers.brainclaw = {
489
- command: 'npx',
490
- args: ['brainclaw', 'mcp'],
491
- };
984
+ mcpServers.brainclaw = brainclawMcpEntry('claude-code', mcpServers.brainclaw, cwd);
492
985
  const { created, updated } = writeJsonFileIfChanged(filePath, {
493
986
  ...existing,
494
987
  mcpServers,
@@ -522,10 +1015,7 @@ export function ensureClaudeCodeUserSettings(homeDir, env = process.env) {
522
1015
  const existing = readJsonObject(filePath);
523
1016
  // MCP server
524
1017
  const mcpServers = isJsonObject(existing.mcpServers) ? { ...existing.mcpServers } : {};
525
- mcpServers.brainclaw = {
526
- command: 'npx',
527
- args: ['brainclaw', 'mcp'],
528
- };
1018
+ mcpServers.brainclaw = brainclawMcpEntry('claude-code', mcpServers.brainclaw);
529
1019
  // Permissions
530
1020
  const permissions = isJsonObject(existing.permissions) ? { ...existing.permissions } : {};
531
1021
  const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
@@ -574,25 +1064,54 @@ export function ensureClaudeCodeSettings(cwd) {
574
1064
  allow.push('mcp__brainclaw__*');
575
1065
  }
576
1066
  permissions.allow = allow;
577
- // Merge hooks UserPromptSubmit injects full context on first prompt, diff on subsequent
1067
+ // Ensure worktree base directories are in additionalDirectories
1068
+ // so dispatched sub-agents can Edit/Write files in their worktrees
1069
+ const additionalDirs = Array.isArray(permissions.additionalDirectories)
1070
+ ? [...permissions.additionalDirectories]
1071
+ : [];
1072
+ const worktreeDirs = [
1073
+ path.join(cwd, '.claude', 'worktrees'), // Claude Code Agent tool worktrees
1074
+ path.join(os.homedir(), '.brainclaw', 'worktrees'), // brainclaw claim worktrees
1075
+ ];
1076
+ for (const dir of worktreeDirs) {
1077
+ const normalized = dir.replace(/\\/g, '/');
1078
+ if (!additionalDirs.some(d => d.replace(/\\/g, '/') === normalized)) {
1079
+ additionalDirs.push(dir);
1080
+ }
1081
+ }
1082
+ permissions.additionalDirectories = additionalDirs;
1083
+ // Merge hooks — UserPromptSubmit opens a session on first prompt, diff on subsequent
578
1084
  const hooks = isJsonObject(existing.hooks) ? { ...existing.hooks } : {};
579
- const contextCommand = 'f=.claude/.bclaw-session; if [ ! -f "$f" ]; then touch "$f"; npx brainclaw context 2>/dev/null; else npx brainclaw context-diff 2>/dev/null; fi';
580
- const stopCommand = 'rm -f .claude/.bclaw-session; npx brainclaw session-end --auto-release --dry-run 2>/dev/null';
1085
+ const mcpCmd = getBrainclawMcpCommand();
1086
+ // For shell hooks, normalize Windows backslashes to forward slashes and quote if needed
1087
+ const bclawBin = mcpCmd.command === 'npx'
1088
+ ? 'npx brainclaw'
1089
+ : `"${mcpCmd.command.replace(/\\/g, '/')}"`;
1090
+ const sessionCommand = `f=.claude/.bclaw-session; if [ ! -f "$f" ]; then touch "$f"; ${bclawBin} session-start --include-context 2>/dev/null; else ${bclawBin} context-diff 2>/dev/null; fi`;
1091
+ const stopCommand = `rm -f .claude/.bclaw-session; ${bclawBin} session-end --auto-release --reflect --reflect-handoff --dispatch-review 2>/dev/null`;
1092
+ // Legacy commands to replace on upgrade (substring patterns to match old hooks)
1093
+ const legacyPatterns = [
1094
+ 'brainclaw context 2>/dev/null',
1095
+ 'brainclaw session-start --include-context 2>/dev/null',
1096
+ 'brainclaw session-end --auto-release',
1097
+ 'brainclaw context-diff 2>/dev/null',
1098
+ ];
581
1099
  const userPromptHooks = Array.isArray(hooks.UserPromptSubmit) ? [...hooks.UserPromptSubmit] : [];
582
- if (!containsCommandHook(userPromptHooks, contextCommand)) {
583
- userPromptHooks.push(buildCommandHookEntry(contextCommand));
584
- }
1100
+ replaceOrAddCommandHookByPattern(userPromptHooks, sessionCommand, legacyPatterns);
585
1101
  hooks.UserPromptSubmit = userPromptHooks;
586
1102
  const stopHooks = Array.isArray(hooks.Stop) ? [...hooks.Stop] : [];
587
- if (!containsCommandHook(stopHooks, stopCommand)) {
588
- stopHooks.push(buildCommandHookEntry(stopCommand));
589
- }
1103
+ replaceOrAddCommandHookByPattern(stopHooks, stopCommand, legacyPatterns);
590
1104
  hooks.Stop = stopHooks;
591
1105
  // PostToolUse — check for unseen events after any brainclaw MCP tool call
592
- const checkEventsCommand = 'npx brainclaw check-events 2>/dev/null';
1106
+ const checkEventsCommand = `${bclawBin} check-events 2>/dev/null`;
593
1107
  const postToolHooks = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
594
- if (!containsCommandHook(postToolHooks, checkEventsCommand)) {
595
- postToolHooks.push(buildMatchedCommandHookEntry('mcp__brainclaw__', checkEventsCommand));
1108
+ replaceOrAddCommandHookByPattern(postToolHooks, checkEventsCommand, ['npx brainclaw check-events']);
1109
+ // Preserve matcher for PostToolUse — only fire on brainclaw MCP tool calls
1110
+ for (const entry of postToolHooks) {
1111
+ if (isJsonObject(entry) && Array.isArray(entry.hooks) &&
1112
+ entry.hooks.some(h => isJsonObject(h) && typeof h.command === 'string' && h.command.includes('check-events'))) {
1113
+ entry.matcher = 'mcp__brainclaw__';
1114
+ }
596
1115
  }
597
1116
  hooks.PostToolUse = postToolHooks;
598
1117
  const { created, updated } = writeJsonFileIfChanged(filePath, {
@@ -616,10 +1135,7 @@ export function ensureCursorMcpConfig(homeDir) {
616
1135
  const filePath = path.join(homeDir, '.cursor', 'mcp.json');
617
1136
  const existing = readJsonObject(filePath);
618
1137
  const mcpServers = isJsonObject(existing.mcpServers) ? { ...existing.mcpServers } : {};
619
- mcpServers.brainclaw = {
620
- command: 'npx',
621
- args: ['brainclaw', 'mcp'],
622
- };
1138
+ mcpServers.brainclaw = brainclawMcpEntry('cursor', mcpServers.brainclaw);
623
1139
  const { created, updated } = writeJsonFileIfChanged(filePath, {
624
1140
  ...existing,
625
1141
  mcpServers,
@@ -638,9 +1154,8 @@ export function ensureRooMcpConfig(cwd) {
638
1154
  const existing = readJsonObject(filePath);
639
1155
  const mcpServers = isJsonObject(existing.mcpServers) ? { ...existing.mcpServers } : {};
640
1156
  mcpServers.brainclaw = {
641
- command: 'npx',
642
- args: ['brainclaw', 'mcp'],
643
- alwaysAllow: ALL_BCLAW_TOOLS,
1157
+ ...brainclawMcpEntry('roo', mcpServers.brainclaw, cwd),
1158
+ alwaysAllow: getHeadlessAutoApprovedToolNames(),
644
1159
  };
645
1160
  const { created, updated } = writeJsonFileIfChanged(filePath, {
646
1161
  ...existing,
@@ -655,46 +1170,324 @@ export function ensureRooMcpConfig(cwd) {
655
1170
  relativePath: ROO_MCP_RELATIVE_PATH,
656
1171
  };
657
1172
  }
1173
+ export function ensureKilocodeConfig(cwd) {
1174
+ const filePath = path.join(cwd, KILOCODE_CONFIG_RELATIVE_PATH);
1175
+ const existing = readJsoncObject(filePath);
1176
+ const permission = isJsonObject(existing.permission) ? { ...existing.permission } : {};
1177
+ permission.external_directory = 'deny';
1178
+ const { created, updated } = writeTextFileIfChanged(filePath, `${JSON.stringify({ ...existing, permission }, null, 2)}\n`);
1179
+ return {
1180
+ kind: 'permissions',
1181
+ label: 'Kilo Code permissions (kilo.jsonc)',
1182
+ created,
1183
+ updated,
1184
+ filePath,
1185
+ relativePath: KILOCODE_CONFIG_RELATIVE_PATH,
1186
+ };
1187
+ }
1188
+ export function ensureKilocodeMcpConfig(cwd) {
1189
+ const filePath = path.join(cwd, '.kilo', 'mcp.json');
1190
+ const existing = readJsonObject(filePath);
1191
+ const mcpServers = isJsonObject(existing.mcpServers) ? { ...existing.mcpServers } : {};
1192
+ mcpServers.brainclaw = {
1193
+ ...brainclawMcpEntry('kilocode', mcpServers.brainclaw, cwd),
1194
+ alwaysAllow: getHeadlessAutoApprovedToolNames(),
1195
+ };
1196
+ const { created, updated } = writeJsonFileIfChanged(filePath, {
1197
+ ...existing,
1198
+ mcpServers,
1199
+ });
1200
+ return {
1201
+ kind: 'mcp',
1202
+ label: 'Kilo Code MCP settings',
1203
+ created,
1204
+ updated,
1205
+ filePath,
1206
+ relativePath: KILOCODE_MCP_RELATIVE_PATH,
1207
+ };
1208
+ }
1209
+ /**
1210
+ * Mistral Vibe MCP config writer (pln#489). Mistral Vibe reads
1211
+ * `.vibe/config.toml` (project-level, prioritaire) or `~/.vibe/config.toml`
1212
+ * (user-level fallback). The MCP server registry uses TOML array-of-tables
1213
+ * `[[mcp_servers]]` with `name`, `transport`, `command`, `args`.
1214
+ *
1215
+ * Idempotent: if the file already declares a `[[mcp_servers]]` block whose
1216
+ * `name = "brainclaw"`, this function leaves it alone (no overwrite, preserves
1217
+ * any user-customized command/args/env). Otherwise it appends our block to
1218
+ * the end of the file. Other `[[mcp_servers]]` entries are preserved.
1219
+ *
1220
+ * Why a minimal TOML writer rather than a full parser/round-trip merge?
1221
+ * The MCP entry is append-only and our heuristic detection in
1222
+ * `tomlArrayTableHasEntry` covers the realistic file shapes Vibe writes (one
1223
+ * `name = "..."` field as the first key after each `[[mcp_servers]]` header).
1224
+ * If the user has hand-edited the file in unusual ways, they keep what they
1225
+ * wrote — this writer never deletes user content.
1226
+ */
1227
+ export function ensureMistralVibeMcpConfig(cwd) {
1228
+ const filePath = path.join(cwd, MISTRAL_VIBE_CONFIG_RELATIVE_PATH);
1229
+ const mcpCmd = getBrainclawMcpCommand();
1230
+ let existing = '';
1231
+ let existed = false;
1232
+ if (fs.existsSync(filePath)) {
1233
+ existing = fs.readFileSync(filePath, 'utf-8');
1234
+ existed = true;
1235
+ }
1236
+ if (existed && tomlArrayTableHasEntry(existing, 'mcp_servers', 'brainclaw')) {
1237
+ // Already wired. No-op.
1238
+ return {
1239
+ kind: 'mcp',
1240
+ label: 'Mistral Vibe MCP settings',
1241
+ created: false,
1242
+ updated: false,
1243
+ filePath,
1244
+ relativePath: MISTRAL_VIBE_CONFIG_RELATIVE_PATH,
1245
+ };
1246
+ }
1247
+ const brainclawBlock = renderToml({
1248
+ arrayTables: [{
1249
+ name: 'mcp_servers',
1250
+ entries: [{
1251
+ name: 'brainclaw',
1252
+ transport: 'stdio',
1253
+ command: mcpCmd.command,
1254
+ args: mcpCmd.args,
1255
+ }],
1256
+ }],
1257
+ });
1258
+ // Append (preserves any user-written content above) — separated by a blank
1259
+ // line if the file is non-empty and doesn't already end with one.
1260
+ const dir = path.dirname(filePath);
1261
+ if (!fs.existsSync(dir))
1262
+ fs.mkdirSync(dir, { recursive: true });
1263
+ let next;
1264
+ if (!existed || existing.length === 0) {
1265
+ next = brainclawBlock;
1266
+ }
1267
+ else {
1268
+ const sep = existing.endsWith('\n\n') ? '' : (existing.endsWith('\n') ? '\n' : '\n\n');
1269
+ next = existing + sep + brainclawBlock;
1270
+ }
1271
+ fs.writeFileSync(filePath, next, 'utf-8');
1272
+ return {
1273
+ kind: 'mcp',
1274
+ label: 'Mistral Vibe MCP settings',
1275
+ created: !existed,
1276
+ updated: existed,
1277
+ filePath,
1278
+ relativePath: MISTRAL_VIBE_CONFIG_RELATIVE_PATH,
1279
+ };
1280
+ }
658
1281
  export function ensureCodexMcpConfig(homeDir, env = process.env) {
659
1282
  const codexHome = env.CODEX_HOME?.trim() || (homeDir ? path.join(homeDir, '.codex') : null);
660
1283
  if (!codexHome)
661
1284
  return null;
662
1285
  const filePath = path.join(codexHome, 'config.toml');
1286
+ const mcpCmd = getBrainclawMcpCommand();
1287
+ // Normalize all paths to forward slashes so TOML backslash escapes don't
1288
+ // corrupt the file on Windows (e.g. \U would be an invalid unicode escape).
1289
+ const normalizedCommand = mcpCmd.command.replace(/\\/g, '/');
1290
+ const normalizedArgs = mcpCmd.args.map(a => a.replace(/\\/g, '/'));
663
1291
  const brainclawBlock = [
664
1292
  '\n[mcp_servers.brainclaw]',
665
- 'command = "npx"',
666
- 'args = ["brainclaw", "mcp"]',
1293
+ `command = "${normalizedCommand}"`,
1294
+ `args = [${normalizedArgs.map(a => `"${a}"`).join(', ')}]`,
1295
+ 'startup_timeout_ms = 20000',
1296
+ '',
1297
+ '[mcp_servers.brainclaw.env]',
1298
+ 'BRAINCLAW_AGENT = "codex"',
1299
+ '# BRAINCLAW_CWD is set per-workspace via brainclaw init; override here if needed',
667
1300
  ].join('\n');
1301
+ // Per-tool approval_mode blocks — required so codex exec in headless mode
1302
+ // auto-approves brainclaw MCP writes (e.g. bclaw_assignment_update). Without
1303
+ // these, codex falls back to the default "prompt" approval and cancels the
1304
+ // call because no human can answer in non-interactive mode.
1305
+ //
1306
+ // Only the headless-safe subset is written here (tools with headlessApproval='auto').
1307
+ // Sensitive tools (dispatch, accept, reject, create_plan, setup, switch, bootstrap,
1308
+ // memory deletes, etc.) are intentionally absent so codex must prompt before using them.
1309
+ const MACHINE_MANAGED_HEADER = '# ===========================================================\n' +
1310
+ '# MACHINE-MANAGED — DO NOT EDIT\n' +
1311
+ '# Generated by `brainclaw setup`. Changes will be overwritten\n' +
1312
+ '# on the next setup run. Only headless-safe tools are listed.\n' +
1313
+ '# Sensitive tools (dispatch, accept, reject, create_plan, etc.)\n' +
1314
+ '# are intentionally absent — codex will prompt before using them.\n' +
1315
+ '# ===========================================================';
1316
+ const toolsBlock = '\n' + MACHINE_MANAGED_HEADER + '\n' + getHeadlessAutoApprovedToolNames().map((tool) => `[mcp_servers.brainclaw.tools.${tool}]\napproval_mode = "approve"`).join('\n\n') + '\n';
668
1317
  let existing = '';
669
1318
  let fileExisted = false;
670
1319
  if (fs.existsSync(filePath)) {
671
1320
  existing = fs.readFileSync(filePath, 'utf-8');
672
1321
  fileExisted = true;
673
1322
  }
674
- if (existing.includes('[mcp_servers.brainclaw]')) {
675
- return { kind: 'mcp', label: 'Codex MCP config', created: false, updated: false, filePath };
1323
+ // Before writing: detect existing tool sections that are outside the catalog
1324
+ // or have a non-"approve" approval_mode, and warn the user.
1325
+ if (fileExisted && existing.length > 0) {
1326
+ const autoApprovedSet = new Set(getHeadlessAutoApprovedToolNames());
1327
+ const toolSectionRe = /^\[mcp_servers\.brainclaw\.tools\.([^\]]+)\]/gm;
1328
+ const approvalModeRe = /^\s*approval_mode\s*=\s*"([^"]+)"/m;
1329
+ let m;
1330
+ const warnings = [];
1331
+ // Split into sections to check each tool block
1332
+ const lines = existing.split('\n');
1333
+ let currentTool = null;
1334
+ let currentBlockLines = [];
1335
+ const checkBlock = (toolName, blockLines) => {
1336
+ const blockText = blockLines.join('\n');
1337
+ const approvalMatch = approvalModeRe.exec(blockText);
1338
+ const isInCatalog = autoApprovedSet.has(toolName);
1339
+ const approvalValue = approvalMatch ? approvalMatch[1] : null;
1340
+ if (!isInCatalog) {
1341
+ warnings.push(` • [mcp_servers.brainclaw.tools.${toolName}] — not in headless-auto catalog (will be removed)`);
1342
+ }
1343
+ else if (approvalValue && approvalValue !== 'approve') {
1344
+ warnings.push(` • [mcp_servers.brainclaw.tools.${toolName}] — approval_mode="${approvalValue}" (expected "approve", will be overwritten)`);
1345
+ }
1346
+ };
1347
+ for (const line of lines) {
1348
+ const headerMatch = /^\[mcp_servers\.brainclaw\.tools\.([^\]]+)\]/.exec(line);
1349
+ if (headerMatch) {
1350
+ if (currentTool !== null) {
1351
+ checkBlock(currentTool, currentBlockLines);
1352
+ }
1353
+ currentTool = headerMatch[1];
1354
+ currentBlockLines = [line];
1355
+ }
1356
+ else if (currentTool !== null) {
1357
+ // Stop collecting if we hit a new top-level section (not a sub-section of current tool)
1358
+ if (/^\[[^\]]+\]/.test(line) && !line.startsWith(`[mcp_servers.brainclaw.tools.${currentTool}`)) {
1359
+ checkBlock(currentTool, currentBlockLines);
1360
+ currentTool = null;
1361
+ currentBlockLines = [];
1362
+ }
1363
+ else {
1364
+ currentBlockLines.push(line);
1365
+ }
1366
+ }
1367
+ }
1368
+ if (currentTool !== null) {
1369
+ checkBlock(currentTool, currentBlockLines);
1370
+ }
1371
+ // Also detect tool sections not matched by the regex (toolSectionRe was already used inline above)
1372
+ void toolSectionRe; // suppress unused warning
1373
+ if (warnings.length > 0) {
1374
+ process.stdout.write(`[brainclaw] Warning: the following tool sections in ${filePath} will be overwritten:\n` +
1375
+ warnings.join('\n') + '\n');
1376
+ }
1377
+ }
1378
+ let content = existing;
1379
+ let changed = false;
1380
+ // Strip any pre-existing MACHINE_MANAGED_HEADER blocks before we emit a
1381
+ // fresh one. replaceTomlSection only touches `[section]` headers, so the
1382
+ // decorative comment block above the tool sections was preserved on each
1383
+ // run and accumulated (we observed three back-to-back copies in the wild).
1384
+ // Detection: a run of lines matching the header shape — a `# ==` divider,
1385
+ // a `# MACHINE-MANAGED — DO NOT EDIT` marker, then the rest up to the
1386
+ // closing `# ==` divider.
1387
+ const machineManagedBlockRe = /(?:\r?\n)?# =+\r?\n# MACHINE-MANAGED — DO NOT EDIT\r?\n(?:#[^\r\n]*\r?\n)+# =+\r?\n?/g;
1388
+ const strippedContent = content.replace(machineManagedBlockRe, '\n');
1389
+ if (strippedContent !== content) {
1390
+ content = strippedContent;
1391
+ changed = true;
1392
+ }
1393
+ // Main brainclaw block: create if missing, update only when force-resolving
1394
+ // (to preserve user customizations like `cwd` on the main section).
1395
+ if (!content.includes('[mcp_servers.brainclaw]')) {
1396
+ content = content + brainclawBlock + '\n';
1397
+ changed = true;
1398
+ }
1399
+ else if (_forceResolve) {
1400
+ const replaced = replaceTomlSection(content, 'mcp_servers.brainclaw', brainclawBlock.slice(1) + '\n');
1401
+ if (replaced !== content) {
1402
+ content = replaced;
1403
+ changed = true;
1404
+ }
1405
+ }
1406
+ // Per-tool approval blocks: ALWAYS sync to the current catalog, regardless
1407
+ // of _forceResolve. These sections are purely machine-managed (no user edits
1408
+ // expected) and must match the narrowed headless-auto catalog.
1409
+ const hasToolSections = /^\[mcp_servers\.brainclaw\.tools\./m.test(content);
1410
+ if (hasToolSections) {
1411
+ const replaced = replaceTomlSection(content, 'mcp_servers.brainclaw.tools', toolsBlock.slice(1));
1412
+ if (replaced !== content) {
1413
+ content = replaced;
1414
+ changed = true;
1415
+ }
676
1416
  }
677
- const newContent = existing + brainclawBlock + '\n';
678
- if (!fs.existsSync(path.dirname(filePath))) {
679
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
1417
+ else {
1418
+ content = content + toolsBlock;
1419
+ changed = true;
1420
+ }
1421
+ if (changed) {
1422
+ if (!fs.existsSync(path.dirname(filePath))) {
1423
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1424
+ }
1425
+ fs.writeFileSync(filePath, content, 'utf-8');
680
1426
  }
681
- fs.writeFileSync(filePath, newContent, 'utf-8');
682
1427
  return {
683
1428
  kind: 'mcp',
684
1429
  label: 'Codex MCP config',
685
1430
  created: !fileExisted,
686
- updated: fileExisted,
1431
+ updated: fileExisted && changed,
687
1432
  filePath,
688
1433
  };
689
1434
  }
1435
+ /**
1436
+ * Replace a TOML section (and all its sub-sections) with new content.
1437
+ *
1438
+ * Sections are identified by lines that start with `[` at column 0. We split
1439
+ * the file into chunks on those boundaries and replace any chunk whose header
1440
+ * matches `sectionName` or starts with `sectionName.` (sub-sections).
1441
+ * This avoids the pitfall of regex `[^\[]*` stopping at `[` characters that
1442
+ * appear inside TOML values such as arrays.
1443
+ */
1444
+ function replaceTomlSection(fileContent, sectionName, newBlock) {
1445
+ const lines = fileContent.split('\n');
1446
+ const sectionHeaderRe = /^\[([^\]]+)\]/;
1447
+ // Collect line-ranges for each top-level section start.
1448
+ // We only replace the section matching `sectionName` exactly and
1449
+ // sub-sections starting with `sectionName.` (e.g. mcp_servers.brainclaw.env).
1450
+ const result = [];
1451
+ let insideTarget = false;
1452
+ let replacementEmitted = false;
1453
+ for (let i = 0; i < lines.length; i++) {
1454
+ const line = lines[i];
1455
+ const m = sectionHeaderRe.exec(line);
1456
+ if (m) {
1457
+ const header = m[1];
1458
+ const isTarget = header === sectionName || header.startsWith(sectionName + '.');
1459
+ if (isTarget) {
1460
+ // Skip lines belonging to the target section
1461
+ insideTarget = true;
1462
+ if (!replacementEmitted) {
1463
+ // Emit the replacement block once (before the first matching section)
1464
+ result.push(newBlock);
1465
+ replacementEmitted = true;
1466
+ }
1467
+ continue;
1468
+ }
1469
+ else {
1470
+ insideTarget = false;
1471
+ }
1472
+ }
1473
+ if (!insideTarget) {
1474
+ result.push(line);
1475
+ }
1476
+ }
1477
+ return result.join('\n');
1478
+ }
690
1479
  export function ensureContinueMcpConfig(cwd) {
691
1480
  const filePath = path.join(cwd, '.continue', 'config.json');
692
1481
  const existing = readJsonObject(filePath);
693
1482
  // Continue uses an array for mcpServers, not a keyed object
694
1483
  const mcpServers = Array.isArray(existing.mcpServers) ? [...existing.mcpServers] : [];
695
- const alreadyPresent = mcpServers.some((entry) => isJsonObject(entry) && entry.name === 'brainclaw');
696
- if (!alreadyPresent) {
697
- mcpServers.push({ name: 'brainclaw', command: 'npx', args: ['brainclaw', 'mcp'] });
1484
+ const existingIdx = mcpServers.findIndex((entry) => isJsonObject(entry) && entry.name === 'brainclaw');
1485
+ if (existingIdx >= 0) {
1486
+ // Update existing entry, preserving manual edits
1487
+ mcpServers[existingIdx] = { name: 'brainclaw', ...brainclawMcpEntry('continue', mcpServers[existingIdx], cwd) };
1488
+ }
1489
+ else {
1490
+ mcpServers.push({ name: 'brainclaw', ...brainclawMcpEntry('continue', undefined, cwd) });
698
1491
  }
699
1492
  const { created, updated } = writeJsonFileIfChanged(filePath, {
700
1493
  ...existing,
@@ -715,9 +1508,13 @@ export function ensureContinueUserMcpConfig(homeDir) {
715
1508
  const filePath = path.join(homeDir, '.continue', 'config.json');
716
1509
  const existing = readJsonObject(filePath);
717
1510
  const mcpServers = Array.isArray(existing.mcpServers) ? [...existing.mcpServers] : [];
718
- const alreadyPresent = mcpServers.some((entry) => isJsonObject(entry) && entry.name === 'brainclaw');
719
- if (!alreadyPresent) {
720
- mcpServers.push({ name: 'brainclaw', command: 'npx', args: ['brainclaw', 'mcp'] });
1511
+ const existingIdx = mcpServers.findIndex((entry) => isJsonObject(entry) && entry.name === 'brainclaw');
1512
+ if (existingIdx >= 0) {
1513
+ // Update existing entry, preserving manual edits
1514
+ mcpServers[existingIdx] = { name: 'brainclaw', ...brainclawMcpEntry('continue', mcpServers[existingIdx]) };
1515
+ }
1516
+ else {
1517
+ mcpServers.push({ name: 'brainclaw', ...brainclawMcpEntry('continue') });
721
1518
  }
722
1519
  const { created, updated } = writeJsonFileIfChanged(filePath, {
723
1520
  ...existing,
@@ -731,13 +1528,65 @@ export function ensureContinueUserMcpConfig(homeDir) {
731
1528
  filePath,
732
1529
  };
733
1530
  }
1531
+ /**
1532
+ * Writes `~/.continue/permissions.yaml` with per-tool allow rules for
1533
+ * headless-auto-approved brainclaw MCP tools. Continue reads this file
1534
+ * to auto-approve tool calls without user confirmation.
1535
+ *
1536
+ * Format (best-effort per Continue docs):
1537
+ * ```yaml
1538
+ * # Managed by brainclaw — do not edit manually
1539
+ * tools:
1540
+ * bclaw_work:
1541
+ * allow: true
1542
+ * ...
1543
+ * ```
1544
+ */
1545
+ export function ensureContinueUserPermissions(homeDir) {
1546
+ if (!homeDir)
1547
+ return undefined;
1548
+ const filePath = path.join(homeDir, CONTINUE_PERMISSIONS_RELATIVE_PATH);
1549
+ let existing = {};
1550
+ if (fs.existsSync(filePath)) {
1551
+ try {
1552
+ const parsed = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
1553
+ existing = isJsonObject(parsed) ? { ...parsed } : {};
1554
+ }
1555
+ catch {
1556
+ existing = {};
1557
+ }
1558
+ }
1559
+ const toolsObj = isJsonObject(existing.tools) ? { ...existing.tools } : {};
1560
+ for (const name of getHeadlessAutoApprovedToolNames()) {
1561
+ const current = isJsonObject(toolsObj[name]) ? { ...toolsObj[name] } : {};
1562
+ toolsObj[name] = {
1563
+ ...current,
1564
+ allow: true,
1565
+ };
1566
+ }
1567
+ const content = `# Managed by brainclaw — do not edit manually\n${yaml.stringify({
1568
+ ...existing,
1569
+ tools: toolsObj,
1570
+ })}`;
1571
+ const { created, updated } = writeTextFileIfChanged(filePath, content);
1572
+ return {
1573
+ kind: 'permissions',
1574
+ label: 'Continue tool permissions',
1575
+ created,
1576
+ updated,
1577
+ filePath,
1578
+ };
1579
+ }
734
1580
  export function ensureOpenCodeMcpConfig(cwd) {
735
1581
  const filePath = path.join(cwd, 'opencode.json');
736
1582
  const existing = readJsonObject(filePath);
737
1583
  const mcp = isJsonObject(existing.mcp) ? { ...existing.mcp } : {};
1584
+ const mcpCmd = getBrainclawMcpCommand();
738
1585
  mcp.brainclaw = {
739
1586
  type: 'local',
740
- command: ['npx', 'brainclaw', 'mcp'],
1587
+ command: [mcpCmd.command, ...mcpCmd.args],
1588
+ env: { BRAINCLAW_AGENT: 'opencode', BRAINCLAW_CWD: cwd },
1589
+ permission: Object.fromEntries(getHeadlessAutoApprovedToolNames().map(t => [t, 'allow'])),
741
1590
  };
742
1591
  const { created, updated } = writeJsonFileIfChanged(filePath, {
743
1592
  ...existing,
@@ -759,10 +1608,7 @@ export function ensureAntigravityMcpConfig(homeDir) {
759
1608
  const filePath = path.join(homeDir, '.gemini', 'antigravity', 'mcp_config.json');
760
1609
  const existing = readJsonObject(filePath);
761
1610
  const mcpServers = isJsonObject(existing.mcpServers) ? { ...existing.mcpServers } : {};
762
- mcpServers.brainclaw = {
763
- command: 'npx',
764
- args: ['brainclaw', 'mcp'],
765
- };
1611
+ mcpServers.brainclaw = brainclawMcpEntry('antigravity', mcpServers.brainclaw);
766
1612
  const { created, updated } = writeJsonFileIfChanged(filePath, {
767
1613
  ...existing,
768
1614
  mcpServers,
@@ -776,6 +1622,154 @@ export function ensureAntigravityMcpConfig(homeDir) {
776
1622
  relativePath: ANTIGRAVITY_MCP_RELATIVE_PATH,
777
1623
  };
778
1624
  }
1625
+ function quoteShellArg(arg) {
1626
+ if (/^[A-Za-z0-9_./:=+-]+$/.test(arg))
1627
+ return arg;
1628
+ return `"${arg.replace(/"/g, '\\"')}"`;
1629
+ }
1630
+ /**
1631
+ * Resolve the brainclaw CLI invocation for hook configs.
1632
+ * Returns shell-safe parts like `["<node>", "<cli.js>"]` or `["npx", "brainclaw"]`.
1633
+ */
1634
+ function getBclawCliParts() {
1635
+ const mcpCmd = getBrainclawMcpCommand();
1636
+ if (mcpCmd.command === 'npx')
1637
+ return ['npx', 'brainclaw'];
1638
+ const argsWithoutMcp = [...mcpCmd.args];
1639
+ if (argsWithoutMcp[argsWithoutMcp.length - 1] === 'mcp') {
1640
+ argsWithoutMcp.pop();
1641
+ }
1642
+ return [
1643
+ mcpCmd.command.replace(/\\/g, '/'),
1644
+ ...argsWithoutMcp.map((arg) => arg.replace(/\\/g, '/')),
1645
+ ];
1646
+ }
1647
+ function buildHookCommand(args, shell = os.platform() === 'win32' ? 'powershell' : 'bash') {
1648
+ const rendered = [...getBclawCliParts(), ...args].map(quoteShellArg).join(' ');
1649
+ if (shell === 'powershell') {
1650
+ return `& ${rendered} 2>$null`;
1651
+ }
1652
+ return `${rendered} 2>/dev/null`;
1653
+ }
1654
+ /**
1655
+ * Writes `.cursor/hooks.json` — Cursor's native hooks config.
1656
+ * Events: sessionStart, beforeSubmitPrompt, stop (Cursor uses camelCase).
1657
+ * Format per https://cursor.com/docs/hooks: version 1, type "command".
1658
+ */
1659
+ export function ensureCursorHooks(cwd) {
1660
+ const filePath = path.join(cwd, CURSOR_HOOKS_RELATIVE_PATH);
1661
+ const existing = readJsonObject(filePath);
1662
+ const hooks = isJsonObject(existing.hooks) ? { ...existing.hooks } : {};
1663
+ const sessionStartCmd = buildHookCommand(['session-start', '--include-context']);
1664
+ const contextDiffCmd = buildHookCommand(['context-diff']);
1665
+ const sessionEndCmd = buildHookCommand(['session-end', '--auto-release', '--reflect', '--reflect-handoff', '--dispatch-review']);
1666
+ hooks.sessionStart = [{ type: 'command', command: sessionStartCmd }];
1667
+ hooks.beforeSubmitPrompt = [{ type: 'command', command: contextDiffCmd }];
1668
+ hooks.stop = [{ type: 'command', command: sessionEndCmd }];
1669
+ const { created, updated } = writeJsonFileIfChanged(filePath, {
1670
+ ...existing,
1671
+ version: 1,
1672
+ hooks,
1673
+ });
1674
+ return {
1675
+ kind: 'rule',
1676
+ label: 'Cursor session hooks',
1677
+ created,
1678
+ updated,
1679
+ filePath,
1680
+ relativePath: CURSOR_HOOKS_RELATIVE_PATH,
1681
+ };
1682
+ }
1683
+ /**
1684
+ * Writes `~/.gemini/antigravity/hooks.json` — Antigravity's native hooks config.
1685
+ * Events: SessionStart, UserPromptSubmit, Stop (PascalCase, top-level keys).
1686
+ */
1687
+ export function ensureAntigravityHooks(homeDir) {
1688
+ if (!homeDir)
1689
+ return undefined;
1690
+ const filePath = path.join(homeDir, ANTIGRAVITY_HOOKS_RELATIVE_PATH);
1691
+ const existing = readJsonObject(filePath);
1692
+ const sessionStartCmd = buildHookCommand(['session-start', '--include-context']);
1693
+ const contextDiffCmd = buildHookCommand(['context-diff']);
1694
+ const sessionEndCmd = buildHookCommand(['session-end', '--auto-release', '--reflect', '--reflect-handoff', '--dispatch-review']);
1695
+ const { created, updated } = writeJsonFileIfChanged(filePath, {
1696
+ ...existing,
1697
+ SessionStart: [{ command: sessionStartCmd }],
1698
+ UserPromptSubmit: [{ command: contextDiffCmd }],
1699
+ Stop: [{ command: sessionEndCmd }],
1700
+ });
1701
+ return {
1702
+ kind: 'rule',
1703
+ label: 'Antigravity session hooks',
1704
+ created,
1705
+ updated,
1706
+ filePath,
1707
+ relativePath: ANTIGRAVITY_HOOKS_RELATIVE_PATH,
1708
+ };
1709
+ }
1710
+ /**
1711
+ * Writes `.github/copilot/hooks.json` — GitHub Copilot's native hooks config.
1712
+ * Events: sessionStart, userPromptSubmitted, sessionEnd (camelCase).
1713
+ * Format per code.visualstudio.com/docs/copilot/customization/hooks:
1714
+ * version 1, type "command", uses bash/powershell fields, timeoutSec.
1715
+ */
1716
+ export function ensureCopilotHooks(cwd) {
1717
+ const filePath = path.join(cwd, COPILOT_HOOKS_RELATIVE_PATH);
1718
+ const existing = readJsonObject(filePath);
1719
+ const hooks = isJsonObject(existing.hooks) ? { ...existing.hooks } : {};
1720
+ hooks.sessionStart = [{
1721
+ type: 'command',
1722
+ bash: buildHookCommand(['session-start', '--include-context'], 'bash'),
1723
+ powershell: buildHookCommand(['session-start', '--include-context'], 'powershell'),
1724
+ timeoutSec: 30,
1725
+ }];
1726
+ hooks.userPromptSubmitted = [{
1727
+ type: 'command',
1728
+ bash: buildHookCommand(['context-diff'], 'bash'),
1729
+ powershell: buildHookCommand(['context-diff'], 'powershell'),
1730
+ timeoutSec: 10,
1731
+ }];
1732
+ hooks.sessionEnd = [{
1733
+ type: 'command',
1734
+ bash: buildHookCommand(['session-end', '--auto-release', '--reflect', '--reflect-handoff', '--dispatch-review'], 'bash'),
1735
+ powershell: buildHookCommand(['session-end', '--auto-release', '--reflect', '--reflect-handoff', '--dispatch-review'], 'powershell'),
1736
+ timeoutSec: 30,
1737
+ }];
1738
+ const { created, updated } = writeJsonFileIfChanged(filePath, {
1739
+ ...existing,
1740
+ version: 1,
1741
+ hooks,
1742
+ });
1743
+ return {
1744
+ kind: 'rule',
1745
+ label: 'Copilot session hooks',
1746
+ created,
1747
+ updated,
1748
+ filePath,
1749
+ relativePath: COPILOT_HOOKS_RELATIVE_PATH,
1750
+ };
1751
+ }
1752
+ export function ensureOpenClawMcpConfig(homeDir) {
1753
+ if (!homeDir) {
1754
+ return undefined;
1755
+ }
1756
+ const filePath = path.join(homeDir, '.openclaw', 'mcp.json');
1757
+ const existing = readJsonObject(filePath);
1758
+ const mcpServers = isJsonObject(existing.mcpServers) ? { ...existing.mcpServers } : {};
1759
+ mcpServers.brainclaw = brainclawMcpEntry('openclaw', mcpServers.brainclaw);
1760
+ const { created, updated } = writeJsonFileIfChanged(filePath, {
1761
+ ...existing,
1762
+ mcpServers,
1763
+ });
1764
+ return {
1765
+ kind: 'mcp',
1766
+ label: 'OpenClaw MCP config',
1767
+ created,
1768
+ updated,
1769
+ filePath,
1770
+ relativePath: OPENCLAW_MCP_RELATIVE_PATH,
1771
+ };
1772
+ }
779
1773
  export function writeDetectedAgentAutoConfig(agentName, cwd, env = process.env) {
780
1774
  switch (agentName) {
781
1775
  case 'claude-code': {
@@ -783,6 +1777,7 @@ export function writeDetectedAgentAutoConfig(agentName, cwd, env = process.env)
783
1777
  ensureClaudeCodeMcpConfig(cwd),
784
1778
  ensureClaudeCodeCommand(cwd),
785
1779
  ensureClaudeCodeSettings(cwd),
1780
+ ensureVscodeExtensionRecommendation(cwd),
786
1781
  ];
787
1782
  const userSettings = ensureClaudeCodeUserSettings(resolveHomeDir(env));
788
1783
  if (userSettings)
@@ -798,35 +1793,60 @@ export function writeDetectedAgentAutoConfig(agentName, cwd, env = process.env)
798
1793
  case 'cline':
799
1794
  return [ensureClineMcpConfig(cwd)];
800
1795
  case 'windsurf': {
801
- const result = ensureWindsurfMcpConfig(resolveHomeDir(env));
802
- return result ? [result] : [];
1796
+ const results = [ensureWindsurfModernRules(cwd)];
1797
+ const mcp = ensureWindsurfMcpConfig(resolveHomeDir(env));
1798
+ if (mcp)
1799
+ results.push(mcp);
1800
+ return results;
803
1801
  }
804
1802
  case 'github-copilot':
805
- return [ensureCopilotSkill(cwd)];
1803
+ return [ensureCopilotMcpConfig(cwd), ensureCopilotSkill(cwd), ensureCopilotHooks(cwd), ensureUniversalBrainclawSkill(cwd), ensureVscodeExtensionRecommendation(cwd)];
806
1804
  case 'cursor': {
807
- const results = [ensureCursorMdc(cwd)];
1805
+ const results = [ensureCursorMdc(cwd), ensureCursorHooks(cwd), ensureUniversalBrainclawSkill(cwd)];
808
1806
  const mcp = ensureCursorMcpConfig(resolveHomeDir(env));
809
1807
  if (mcp)
810
1808
  results.push(mcp);
811
1809
  return results;
812
1810
  }
813
1811
  case 'roo':
814
- return [ensureRooMcpConfig(cwd)];
1812
+ return [ensureRooMcpConfig(cwd), ensureUniversalBrainclawSkill(cwd)];
1813
+ case 'kilocode':
1814
+ return [ensureKilocodeMcpConfig(cwd), ensureKilocodeConfig(cwd), ensureUniversalBrainclawSkill(cwd)];
1815
+ case 'mistral-vibe':
1816
+ return [ensureMistralVibeMcpConfig(cwd), ensureUniversalBrainclawSkill(cwd)];
815
1817
  case 'codex': {
1818
+ const results = [ensureUniversalBrainclawSkill(cwd)];
816
1819
  const result = ensureCodexMcpConfig(resolveHomeDir(env), env);
817
- return result ? [result] : [];
1820
+ if (result)
1821
+ results.push(result);
1822
+ return results;
818
1823
  }
819
1824
  case 'continue': {
820
1825
  const results = [ensureContinueMcpConfig(cwd)];
821
- const userMcp = ensureContinueUserMcpConfig(resolveHomeDir(env));
1826
+ const homeDir = resolveHomeDir(env);
1827
+ const userMcp = ensureContinueUserMcpConfig(homeDir);
822
1828
  if (userMcp)
823
1829
  results.push(userMcp);
1830
+ const perms = ensureContinueUserPermissions(homeDir);
1831
+ if (perms)
1832
+ results.push(perms);
824
1833
  return results;
825
1834
  }
826
1835
  case 'opencode':
827
- return [ensureOpenCodeMcpConfig(cwd)];
1836
+ return [ensureOpenCodeMcpConfig(cwd), ensureUniversalBrainclawSkill(cwd)];
828
1837
  case 'antigravity': {
829
- const result = ensureAntigravityMcpConfig(resolveHomeDir(env));
1838
+ const homeDir = resolveHomeDir(env);
1839
+ const results = [];
1840
+ const mcp = ensureAntigravityMcpConfig(homeDir);
1841
+ if (mcp)
1842
+ results.push(mcp);
1843
+ const hooks = ensureAntigravityHooks(homeDir);
1844
+ if (hooks)
1845
+ results.push(hooks);
1846
+ return results;
1847
+ }
1848
+ case 'openclaw': {
1849
+ const result = ensureOpenClawMcpConfig(resolveHomeDir(env));
830
1850
  return result ? [result] : [];
831
1851
  }
832
1852
  default:
@@ -855,13 +1875,16 @@ export function writeExportCompanionFiles(format, cwd, env = process.env) {
855
1875
  case 'cline':
856
1876
  return [ensureClineMcpConfig(cwd)];
857
1877
  case 'windsurf': {
858
- const result = ensureWindsurfMcpConfig(resolveHomeDir(env));
859
- return result ? [result] : [];
1878
+ const results = [ensureWindsurfModernRules(cwd)];
1879
+ const mcp = ensureWindsurfMcpConfig(resolveHomeDir(env));
1880
+ if (mcp)
1881
+ results.push(mcp);
1882
+ return results;
860
1883
  }
861
1884
  case 'copilot-instructions':
862
- return [ensureCopilotSkill(cwd)];
1885
+ return [ensureVscodeMcpConfig(cwd), ensureCopilotMcpConfig(cwd), ensureCopilotSkill(cwd), ensureCopilotHooks(cwd)];
863
1886
  case 'cursor-rules': {
864
- const results = [ensureCursorMdc(cwd)];
1887
+ const results = [ensureCursorMdc(cwd), ensureCursorHooks(cwd)];
865
1888
  const mcp = ensureCursorMcpConfig(resolveHomeDir(env));
866
1889
  if (mcp)
867
1890
  results.push(mcp);
@@ -869,19 +1892,80 @@ export function writeExportCompanionFiles(format, cwd, env = process.env) {
869
1892
  }
870
1893
  case 'roo':
871
1894
  return [ensureRooMcpConfig(cwd)];
1895
+ case 'kilocode':
1896
+ return [ensureKilocodeMcpConfig(cwd), ensureKilocodeConfig(cwd), ensureUniversalBrainclawSkill(cwd)];
872
1897
  case 'continue': {
873
1898
  const results = [ensureContinueMcpConfig(cwd)];
874
- const userMcp = ensureContinueUserMcpConfig(resolveHomeDir(env));
1899
+ const homeDir = resolveHomeDir(env);
1900
+ const userMcp = ensureContinueUserMcpConfig(homeDir);
875
1901
  if (userMcp)
876
1902
  results.push(userMcp);
1903
+ const perms = ensureContinueUserPermissions(homeDir);
1904
+ if (perms)
1905
+ results.push(perms);
877
1906
  return results;
878
1907
  }
879
1908
  case 'gemini-md': {
880
- const result = ensureAntigravityMcpConfig(resolveHomeDir(env));
881
- return result ? [result] : [];
1909
+ const homeDir = resolveHomeDir(env);
1910
+ const results = [];
1911
+ const mcp = ensureAntigravityMcpConfig(homeDir);
1912
+ if (mcp)
1913
+ results.push(mcp);
1914
+ const hooks = ensureAntigravityHooks(homeDir);
1915
+ if (hooks)
1916
+ results.push(hooks);
1917
+ return results;
882
1918
  }
883
1919
  default:
884
1920
  return [];
885
1921
  }
886
1922
  }
1923
+ /**
1924
+ * Patch all MCP config files to use the currently resolved brainclaw binary.
1925
+ *
1926
+ * Called after upgrade / version --publish-local to fix stale paths.
1927
+ * Re-resolves the brainclaw command, then re-runs all ensure*McpConfig()
1928
+ * functions with forceResolve=true so existing absolute paths are overwritten.
1929
+ *
1930
+ * Returns the list of configs that were actually updated (not just created).
1931
+ */
1932
+ export function patchAllMcpConfigs(cwd, env = process.env) {
1933
+ // 1. Clear cached path so resolution picks up the new install location
1934
+ resetMcpCommandCache();
1935
+ // 2. Set force-resolve mode so brainclawMcpEntry overwrites existing paths
1936
+ _forceResolve = true;
1937
+ const results = [];
1938
+ const homeDir = resolveHomeDir(env);
1939
+ try {
1940
+ // Workspace-level configs
1941
+ results.push(ensureClaudeCodeMcpConfig(cwd));
1942
+ results.push(ensureVscodeMcpConfig(cwd));
1943
+ results.push(ensureVscodeExtensionRecommendation(cwd));
1944
+ results.push(ensureCopilotMcpConfig(cwd));
1945
+ results.push(ensureClineMcpConfig(cwd));
1946
+ results.push(ensureRooMcpConfig(cwd));
1947
+ results.push(ensureContinueMcpConfig(cwd));
1948
+ results.push(ensureOpenCodeMcpConfig(cwd));
1949
+ // Machine-level configs (in ~ or platform-specific)
1950
+ const userConfigs = [
1951
+ ensureClaudeCodeUserSettings(homeDir, env),
1952
+ ensureCursorMcpConfig(homeDir),
1953
+ ensureWindsurfMcpConfig(homeDir),
1954
+ ensureContinueUserMcpConfig(homeDir),
1955
+ ensureContinueUserPermissions(homeDir),
1956
+ ensureAntigravityMcpConfig(homeDir),
1957
+ ensureOpenClawMcpConfig(homeDir),
1958
+ ensureCodexMcpConfig(homeDir, env),
1959
+ ];
1960
+ for (const r of userConfigs) {
1961
+ if (r)
1962
+ results.push(r);
1963
+ }
1964
+ }
1965
+ finally {
1966
+ // Always reset force-resolve mode
1967
+ _forceResolve = false;
1968
+ }
1969
+ return results.filter(r => r.created || r.updated);
1970
+ }
887
1971
  //# sourceMappingURL=agent-files.js.map