brainclaw 0.28.0 → 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.
- package/README.md +193 -170
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +683 -23
- package/dist/commands/accept.js +3 -0
- package/dist/commands/add-step.js +11 -26
- package/dist/commands/agent-board.js +70 -3
- package/dist/commands/audit.js +19 -0
- package/dist/commands/check-policy.js +54 -0
- package/dist/commands/check-security-mcp.js +145 -0
- package/dist/commands/check-security.js +106 -0
- package/dist/commands/claim-resource.js +1 -0
- package/dist/commands/codev.js +672 -0
- package/dist/commands/compact.js +74 -0
- package/dist/commands/complete-step.js +16 -26
- package/dist/commands/constraint.js +8 -20
- package/dist/commands/decision.js +9 -20
- package/dist/commands/delete-plan.js +10 -12
- package/dist/commands/delete-step.js +16 -0
- package/dist/commands/dispatch.js +163 -0
- package/dist/commands/doctor.js +1122 -49
- package/dist/commands/enable-agent.js +1 -0
- package/dist/commands/export.js +280 -22
- package/dist/commands/handoff.js +33 -0
- package/dist/commands/harvest.js +189 -0
- package/dist/commands/hooks.js +82 -25
- package/dist/commands/inbox.js +169 -0
- package/dist/commands/init.js +38 -31
- package/dist/commands/install-hooks.js +71 -44
- package/dist/commands/link.js +89 -0
- package/dist/commands/list-claims.js +48 -3
- package/dist/commands/list-plans.js +129 -25
- package/dist/commands/loops-handlers.js +409 -0
- package/dist/commands/mcp-read-handlers.js +1628 -0
- package/dist/commands/mcp-schemas.generated.js +74 -0
- package/dist/commands/mcp.js +4244 -1475
- package/dist/commands/plan-resource.js +64 -0
- package/dist/commands/plan.js +12 -26
- package/dist/commands/prune.js +37 -2
- package/dist/commands/reflect.js +20 -7
- package/dist/commands/release-claim.js +11 -6
- package/dist/commands/release-notes.js +170 -0
- package/dist/commands/repair.js +210 -0
- package/dist/commands/run-profile.js +57 -0
- package/dist/commands/sequence.js +113 -0
- package/dist/commands/session-end.js +423 -14
- package/dist/commands/session-start.js +214 -41
- package/dist/commands/setup-security.js +103 -0
- package/dist/commands/setup.js +42 -4
- package/dist/commands/stale.js +109 -0
- package/dist/commands/switch.js +131 -10
- package/dist/commands/trap.js +14 -31
- package/dist/commands/update-handoff.js +63 -4
- package/dist/commands/update-plan.js +21 -28
- package/dist/commands/update-step.js +37 -0
- package/dist/commands/upgrade.js +313 -6
- package/dist/commands/usage.js +102 -0
- package/dist/commands/version.js +20 -0
- package/dist/commands/who.js +124 -0
- package/dist/commands/worktree.js +105 -0
- package/dist/core/actions.js +315 -0
- package/dist/core/agent-capability.js +610 -17
- package/dist/core/agent-context.js +7 -1
- package/dist/core/agent-files.js +1169 -85
- package/dist/core/agent-integrations.js +160 -5
- package/dist/core/agent-inventory.js +2 -0
- package/dist/core/agent-profiles.js +93 -0
- package/dist/core/agent-registry.js +162 -30
- package/dist/core/agentrun-reconciler.js +345 -0
- package/dist/core/agentruns.js +424 -0
- package/dist/core/ai-agent-detection.js +31 -10
- package/dist/core/archival.js +77 -0
- package/dist/core/assignment-sweeper.js +82 -0
- package/dist/core/assignments.js +367 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/bootstrap.js +61 -10
- package/dist/core/brainclaw-version.js +94 -2
- package/dist/core/candidates.js +93 -2
- package/dist/core/claims.js +419 -0
- package/dist/core/codev-metrics.js +77 -0
- package/dist/core/codev-personas.js +31 -0
- package/dist/core/codev-plan-gen.js +35 -0
- package/dist/core/codev-prompts.js +74 -0
- package/dist/core/codev-responses.js +62 -0
- package/dist/core/codev-rounds.js +218 -0
- package/dist/core/config.js +4 -0
- package/dist/core/context.js +454 -34
- package/dist/core/coordination.js +201 -6
- package/dist/core/cross-project.js +230 -16
- package/dist/core/default-profiles/doctor.yaml +11 -0
- package/dist/core/default-profiles/janitor.yaml +11 -0
- package/dist/core/default-profiles/onboarder.yaml +11 -0
- package/dist/core/default-profiles/reviewer.yaml +13 -0
- package/dist/core/dispatcher.js +1189 -0
- package/dist/core/duplicates.js +2 -2
- package/dist/core/entity-operations.js +450 -0
- package/dist/core/entity-registry.js +344 -0
- package/dist/core/event-log.js +1 -0
- package/dist/core/events.js +106 -2
- package/dist/core/execution-adapters.js +154 -0
- package/dist/core/execution-context.js +63 -0
- package/dist/core/execution-profile.js +270 -0
- package/dist/core/execution.js +255 -0
- package/dist/core/facade-schema.js +81 -0
- package/dist/core/federation-cloud.js +99 -0
- package/dist/core/federation-message.js +52 -0
- package/dist/core/federation-transport.js +65 -0
- package/dist/core/gc-semantic.js +482 -0
- package/dist/core/governance.js +247 -0
- package/dist/core/guards.js +19 -0
- package/dist/core/ideation.js +72 -0
- package/dist/core/identity.js +252 -28
- package/dist/core/ids.js +6 -0
- package/dist/core/input-validation.js +2 -2
- package/dist/core/instruction-templates.js +344 -136
- package/dist/core/io.js +90 -11
- package/dist/core/lock.js +6 -2
- package/dist/core/loops/brief-assembly.js +213 -0
- package/dist/core/loops/facade-schema.js +148 -0
- package/dist/core/loops/index.js +7 -0
- package/dist/core/loops/iteration-engine.js +139 -0
- package/dist/core/loops/lock.js +385 -0
- package/dist/core/loops/store.js +201 -0
- package/dist/core/loops/types.js +403 -0
- package/dist/core/loops/verbs.js +534 -0
- package/dist/core/markdown.js +15 -3
- package/dist/core/memory-compactor.js +432 -0
- package/dist/core/memory-git.js +152 -8
- package/dist/core/messaging.js +278 -0
- package/dist/core/migration.js +32 -1
- package/dist/core/mutation-pipeline.js +4 -2
- package/dist/core/operations/memory-mutation.js +129 -0
- package/dist/core/operations/memory-write.js +78 -0
- package/dist/core/operations/plan.js +190 -0
- package/dist/core/policy.js +169 -0
- package/dist/core/repo-analysis.js +67 -0
- package/dist/core/reputation.js +9 -3
- package/dist/core/schema.js +546 -21
- package/dist/core/search.js +21 -2
- package/dist/core/security-cache.js +71 -0
- package/dist/core/security-guard.js +152 -0
- package/dist/core/security-scoring.js +86 -0
- package/dist/core/sequence.js +130 -0
- package/dist/core/socket-client.js +113 -0
- package/dist/core/staleness.js +246 -0
- package/dist/core/state.js +98 -22
- package/dist/core/store-resolution.js +54 -12
- package/dist/core/toml-writer.js +76 -0
- package/dist/core/upgrades/backup.js +232 -0
- package/dist/core/upgrades/health-check.js +169 -0
- package/dist/core/upgrades/patches/candidate-archive.js +145 -0
- package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
- package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
- package/dist/core/upgrades/schema-version.js +97 -0
- package/dist/core/worktree.js +606 -0
- package/dist/facts.js +114 -0
- package/dist/facts.json +111 -0
- package/docs/architecture/project-refs.md +5 -1
- package/docs/cli.md +690 -43
- package/docs/concepts/ideation-loop.md +317 -0
- package/docs/concepts/loop-engine.md +456 -0
- package/docs/concepts/mcp-governance.md +268 -0
- package/docs/concepts/memory-staleness.md +122 -0
- package/docs/concepts/multi-agent-workflows.md +166 -0
- package/docs/concepts/plans-and-claims.md +31 -6
- package/docs/concepts/project-md-convention.md +35 -0
- package/docs/concepts/troubleshooting.md +220 -0
- package/docs/concepts/upgrade-cli.md +202 -0
- package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
- package/docs/context-format-changelog.md +2 -2
- package/docs/context-format.md +2 -2
- package/docs/index.md +68 -0
- package/docs/integrations/agents.md +15 -16
- package/docs/integrations/cline.md +88 -0
- package/docs/integrations/codex.md +75 -23
- package/docs/integrations/continue.md +60 -0
- package/docs/integrations/copilot.md +67 -9
- package/docs/integrations/kilocode.md +72 -0
- package/docs/integrations/mcp.md +304 -21
- package/docs/integrations/mistral-vibe.md +122 -0
- package/docs/integrations/opencode.md +84 -0
- package/docs/integrations/overview.md +23 -8
- package/docs/integrations/roo.md +74 -0
- package/docs/integrations/windsurf.md +83 -0
- package/docs/mcp-schema-changelog.md +191 -1
- package/docs/playbooks/integration/index.md +121 -0
- package/docs/playbooks/productivity/index.md +102 -0
- package/docs/playbooks/team/index.md +122 -0
- package/docs/product/agent-first-model.md +184 -0
- package/docs/product/entity-model-audit.md +462 -0
- package/docs/quickstart-existing-project.md +135 -0
- package/docs/quickstart.md +124 -37
- package/docs/release-maintenance.md +79 -0
- package/docs/review.md +2 -0
- package/docs/server-operations.md +118 -0
- package/package.json +20 -12
- package/dist/commands/claude-desktop-extension.js +0 -18
- package/dist/commands/diff.js +0 -99
- package/dist/core/claude-desktop-extension.js +0 -224
package/dist/core/agent-files.js
CHANGED
|
@@ -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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
345
|
-
args: ['brainclaw', 'mcp'],
|
|
637
|
+
...brainclawMcpEntry('cline', mcpServers.brainclaw, cwd),
|
|
346
638
|
disabled: false,
|
|
347
|
-
autoApprove:
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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.
|
|
400
|
-
2.
|
|
401
|
-
3.
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
580
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
1106
|
+
const checkEventsCommand = `${bclawBin} check-events 2>/dev/null`;
|
|
593
1107
|
const postToolHooks = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
666
|
-
|
|
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
|
-
|
|
675
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
|
696
|
-
if (
|
|
697
|
-
|
|
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
|
|
719
|
-
if (
|
|
720
|
-
|
|
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: [
|
|
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
|
|
802
|
-
|
|
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
|
-
|
|
1820
|
+
if (result)
|
|
1821
|
+
results.push(result);
|
|
1822
|
+
return results;
|
|
818
1823
|
}
|
|
819
1824
|
case 'continue': {
|
|
820
1825
|
const results = [ensureContinueMcpConfig(cwd)];
|
|
821
|
-
const
|
|
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
|
|
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
|
|
859
|
-
|
|
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
|
|
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
|
|
881
|
-
|
|
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
|