brainclaw 1.14.0 → 1.16.0

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 (63) hide show
  1. package/README.md +16 -263
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli/register-capture.js +209 -0
  4. package/dist/cli/register-code-map.js +19 -0
  5. package/dist/cli/register-coordination.js +472 -0
  6. package/dist/cli/register-federation.js +258 -0
  7. package/dist/cli/register-lifecycle.js +436 -0
  8. package/dist/cli/register-memory-context.js +502 -0
  9. package/dist/cli/register-planning.js +167 -0
  10. package/dist/cli/register-review.js +149 -0
  11. package/dist/cli/shared.js +5 -0
  12. package/dist/cli.js +212 -2015
  13. package/dist/commands/dispatch-watch.js +25 -2
  14. package/dist/commands/harvest.js +31 -6
  15. package/dist/commands/mcp-catalog.js +1438 -0
  16. package/dist/commands/mcp-contract.js +33 -0
  17. package/dist/commands/mcp-presentation.js +27 -0
  18. package/dist/commands/mcp-read-handlers.js +72 -36
  19. package/dist/commands/mcp-write-admin.js +328 -0
  20. package/dist/commands/mcp-write-claims.js +864 -0
  21. package/dist/commands/mcp-write-coordination.js +1825 -0
  22. package/dist/commands/mcp-write-entities.js +620 -0
  23. package/dist/commands/mcp-write-memory.js +451 -0
  24. package/dist/commands/mcp-write-sequences.js +116 -0
  25. package/dist/commands/mcp-write-support.js +367 -0
  26. package/dist/commands/mcp.js +261 -5570
  27. package/dist/commands/update-handoff.js +28 -42
  28. package/dist/core/agent-capability.js +31 -14
  29. package/dist/core/agent-files.js +1 -1
  30. package/dist/core/agent-registry.js +51 -3
  31. package/dist/core/claims.js +18 -0
  32. package/dist/core/coordination.js +5 -2
  33. package/dist/core/cross-project.js +35 -1
  34. package/dist/core/dispatcher.js +34 -20
  35. package/dist/core/entity-operations.js +335 -12
  36. package/dist/core/entity-registry.js +72 -9
  37. package/dist/core/execution.js +28 -4
  38. package/dist/core/facade-schema.js +30 -4
  39. package/dist/core/federation-cloud.js +142 -11
  40. package/dist/core/federation-outbox.js +292 -0
  41. package/dist/core/federation-signing.js +115 -0
  42. package/dist/core/handoff-review.js +35 -0
  43. package/dist/core/io.js +6 -0
  44. package/dist/core/protocol-tool-policy.js +113 -0
  45. package/dist/core/review-loop-close.js +115 -0
  46. package/dist/core/schema.js +25 -2
  47. package/dist/core/security-detectors.js +35 -6
  48. package/dist/core/security.js +32 -12
  49. package/dist/core/worktree.js +98 -9
  50. package/dist/facts.js +13 -11
  51. package/dist/facts.json +12 -10
  52. package/docs/PROTOCOL.md +7 -3
  53. package/docs/concepts/coordinator-runbook.md +3 -0
  54. package/docs/concepts/dispatch-lifecycle.md +4 -4
  55. package/docs/concepts/loop-engine.md +3 -1
  56. package/docs/concepts/troubleshooting.md +1 -1
  57. package/docs/integrations/codex.md +3 -3
  58. package/docs/integrations/overview.md +1 -1
  59. package/docs/mcp-schema-changelog.md +153 -2
  60. package/docs/playbooks/orchestration.md +1 -1
  61. package/docs/product/entity-model-audit.md +3 -2
  62. package/docs/security.md +22 -1
  63. package/package.json +3 -1
@@ -0,0 +1,33 @@
1
+ export const SCHEMA_VERSION = '1.0.0';
2
+ export const MCP_PROTOCOL_VERSIONS = ['2025-11-25', '2024-11-05'];
3
+ export const MCP_SERVER_NOT_INITIALIZED = -32002;
4
+ export function toolResponse(response, isError = false) {
5
+ const structuredContent = response.structuredContent
6
+ ? { ...response.structuredContent, schema_version: SCHEMA_VERSION }
7
+ : undefined;
8
+ return {
9
+ ...response,
10
+ structuredContent,
11
+ isError,
12
+ schema_version: SCHEMA_VERSION,
13
+ };
14
+ }
15
+ export function createToolErrorResponse(kind, message, details) {
16
+ return toolResponse({
17
+ content: [{ type: 'text', text: `Error: ${message}` }],
18
+ structuredContent: {
19
+ error: {
20
+ kind,
21
+ message,
22
+ ...(details !== undefined ? { details } : {}),
23
+ },
24
+ },
25
+ }, true);
26
+ }
27
+ export function normaliseFormat(value) {
28
+ if (value === 'json' || value === 'template') {
29
+ return value;
30
+ }
31
+ return 'markdown';
32
+ }
33
+ //# sourceMappingURL=mcp-contract.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * MCP presentation — renderers that turn built context objects into the
3
+ * strings served over the MCP surface.
4
+ *
5
+ * Extracted from mcp.ts (pln#622 PR1). Importing core/ here is the
6
+ * legitimate downward direction (commands → core); this module must not
7
+ * import mcp.js (assembly point) — enforced by
8
+ * tests/unit/mcp-dependency-direction.test.ts.
9
+ *
10
+ * @module
11
+ */
12
+ import { renderContextMarkdown, renderContextPromptTemplate, renderContextBriefing } from '../core/context.js';
13
+ export function renderContextForMcp(result, format, options) {
14
+ // Briefing profile always uses its own ultra-compact renderer
15
+ if (result.profile === 'briefing') {
16
+ return renderContextBriefing(result);
17
+ }
18
+ if (format === 'json') {
19
+ return JSON.stringify(result, null, 2);
20
+ }
21
+ if (format === 'template') {
22
+ const compact = options.compactTemplate || result.profile === 'openclaw';
23
+ return renderContextPromptTemplate(result, compact);
24
+ }
25
+ return renderContextMarkdown(result, options.explain);
26
+ }
27
+ //# sourceMappingURL=mcp-presentation.js.map
@@ -4,6 +4,10 @@
4
4
  * Extracted from mcp.ts to reduce file size. These handlers do not mutate
5
5
  * state — they build context, list items, search, and inspect.
6
6
  *
7
+ * pln#622 PR2: the per-call context (effective cwd, project routing, lazy
8
+ * config/state/agent-name loads) is resolved once at the entry point and
9
+ * shared by every handler via {@link ResolvedReadContext}.
10
+ *
7
11
  * @module
8
12
  */
9
13
  import { applyBootstrapImport, renderBootstrapInterview, renderBootstrapSummary, runBootstrapProfile, uninstallBootstrapImport } from '../core/bootstrap.js';
@@ -44,10 +48,11 @@ import { listAvailableProjectsForSession, switchProject } from './switch.js';
44
48
  import { resolveEffectiveCwdInfo } from '../core/store-resolution.js';
45
49
  import { resolveProjectCwd } from '../core/cross-project.js';
46
50
  import { readUnseenEvents, buildNotificationSummary } from '../core/event-log.js';
47
- import { boundListResult, DEFAULT_FIND_CHAR_BUDGET } from '../core/entity-operations.js';
51
+ import { boundListResult, DEFAULT_FIND_CHAR_BUDGET, projectAgentForRead } from '../core/entity-operations.js';
48
52
  import { handoffDiffPreviewNote } from '../core/handoff-snapshot.js';
49
53
  import { BootstrapInterviewAnswerSchema, AssignmentStatusSchema, AgentRunStatusSchema, AgentRunTransportSchema, ActionRequiredStatusSchema, ActionRequiredKindSchema } from '../core/schema.js';
50
- import { SCHEMA_VERSION, createToolErrorResponse, normaliseFormat, renderContextForMcp, } from './mcp.js';
54
+ import { SCHEMA_VERSION, createToolErrorResponse, normaliseFormat, } from './mcp-contract.js';
55
+ import { renderContextForMcp } from './mcp-presentation.js';
51
56
  function normalizeBootstrapInterviewAnswersArg(value) {
52
57
  if (!Array.isArray(value)) {
53
58
  return [];
@@ -124,25 +129,46 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
124
129
  const projectArg = args.project;
125
130
  const targetProjectArg = name === 'bclaw_switch' ? undefined : projectArg;
126
131
  let projectRoutingApplied = false;
132
+ let routedConfig;
127
133
  if (targetProjectArg) {
128
134
  cwd = resolveProjectCwd(targetProjectArg, cwd);
129
135
  activeSource = 'explicit';
130
136
  try {
131
- const config = loadConfig(cwd);
132
- resolvedProject = { path: cwd, name: config.project_name };
137
+ routedConfig = loadConfig(cwd);
138
+ resolvedProject = { path: cwd, name: routedConfig.project_name };
133
139
  }
134
140
  catch {
135
141
  resolvedProject = { path: cwd };
136
142
  }
137
143
  projectRoutingApplied = true;
138
144
  }
145
+ // Memoized lazy loads — seeded by the routing probe above and shared across
146
+ // the bclaw_context delegation, so one tool call never re-reads them.
147
+ let configCache = routedConfig;
148
+ let stateCache;
149
+ let agentNameCache;
150
+ return dispatchReadTool(name, args, {
151
+ cwd,
152
+ activeSource,
153
+ resolvedProject,
154
+ projectRoutingApplied,
155
+ connectionSessionId: context.connectionSessionId,
156
+ getConfig: () => (configCache ??= loadConfig(cwd)),
157
+ getState: () => (stateCache ??= loadState(cwd)),
158
+ getAgentName: () => (agentNameCache ??= resolveCurrentAgentName(cwd)),
159
+ });
160
+ }
161
+ function dispatchReadTool(name, args, ctx) {
162
+ const { cwd, activeSource, resolvedProject, projectRoutingApplied } = ctx;
139
163
  if (name === 'bclaw_get_context') {
140
164
  // pln#542: budget_tokens caps the relevance-ranked fill (~4 chars/token).
141
165
  // Explicit maxChars wins when both are given.
142
166
  const budgetTokens = typeof args.budget_tokens === 'number' && args.budget_tokens > 0 ? args.budget_tokens : undefined;
143
167
  const result = buildContext({
144
168
  target: args.path,
145
- project: targetProjectArg,
169
+ // Inside dispatch the name is never 'bclaw_switch', so the raw project
170
+ // arg is exactly the entry point's targetProjectArg.
171
+ project: args.project,
146
172
  agent: args.agent,
147
173
  host: args.host,
148
174
  allHosts: args.allHosts,
@@ -200,7 +226,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
200
226
  unseenEventCount = result.context_diff.unseen_event_count;
201
227
  }
202
228
  else {
203
- const agentName = args.agent ?? resolveCurrentAgentName(cwd);
229
+ const agentName = args.agent ?? ctx.getAgentName();
204
230
  const unseenEvents = readUnseenEvents(agentName, cwd);
205
231
  notifications = buildNotificationSummary(unseenEvents);
206
232
  unseenEventCount = unseenEvents.length;
@@ -228,7 +254,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
228
254
  if (!handoffId) {
229
255
  throw new Error('Missing required argument: id');
230
256
  }
231
- const state = loadState(cwd);
257
+ const state = ctx.getState();
232
258
  const handoff = state.open_handoffs.find((item) => item.id === handoffId || item.short_label === handoffId);
233
259
  if (!handoff) {
234
260
  return {
@@ -401,7 +427,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
401
427
  }
402
428
  if (name === 'bclaw_get_execution_context') {
403
429
  const executionContext = buildExecutionContext({ cwd });
404
- const config = loadConfig(cwd);
430
+ const config = ctx.getConfig();
405
431
  const installableUpdate = checkBrainclawInstallableUpdate(config, cwd, { useDefaultNpmSource: true });
406
432
  const installableUpdateNotice = renderBrainclawInstallableUpdateNotice(installableUpdate);
407
433
  const agentTooling = args.includeAgentTooling ? buildAgentToolingContext({ cwd }) : undefined;
@@ -425,7 +451,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
425
451
  };
426
452
  }
427
453
  if (name === 'bclaw_release_notes') {
428
- const config = loadConfig(cwd);
454
+ const config = ctx.getConfig();
429
455
  const updateCheck = checkBrainclawInstallableUpdate(config, cwd, { useDefaultNpmSource: true });
430
456
  const arn = updateCheck.agent_release_notes;
431
457
  const lines = [];
@@ -467,9 +493,9 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
467
493
  };
468
494
  }
469
495
  if (name === 'bclaw_get_agent_board_summary') {
470
- const config = loadConfig(cwd);
471
- const state = loadState(cwd);
472
- const agent = args.agent ?? resolveCurrentAgentName(cwd);
496
+ const config = ctx.getConfig();
497
+ const state = ctx.getState();
498
+ const agent = args.agent ?? ctx.getAgentName();
473
499
  const currentHost = resolveCurrentHostId();
474
500
  const activeClaims = listClaims(cwd).filter((c) => c.status === 'active');
475
501
  const pendingActions = listActionRequired(cwd).filter((a) => a.status === 'pending');
@@ -733,7 +759,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
733
759
  };
734
760
  }
735
761
  if (name === 'bclaw_list_plans') {
736
- let plans = loadState(cwd).plan_items;
762
+ let plans = ctx.getState().plan_items;
737
763
  // Direct lookup by ID
738
764
  if (args.id) {
739
765
  const plan = plans.find((p) => p.id === String(args.id) || p.short_label === String(args.id));
@@ -1200,25 +1226,32 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1200
1226
  const current = resolveCurrentAgentIdentity(cwd);
1201
1227
  const reputation = args.includeReputation ? buildReputationSnapshot(cwd) : undefined;
1202
1228
  const reputationById = new Map((reputation?.agents ?? []).map((agent) => [agent.agent_id ?? agent.key, toPublicReputationSummary(agent)]));
1203
- const structuredAgents = args.includeReputation
1204
- ? agents.map((agent) => ({
1205
- ...agent,
1206
- reputation: reputationById.get(agent.agent_id),
1207
- }))
1208
- : agents;
1229
+ // pln#625 Phase 2c (ideation loop lop_f8e8d18cb8c27ada) — redact through the
1230
+ // SAME projection as bclaw_find(entity=agent) so there is ONE source of truth
1231
+ // and no key material / invoke.env leaks here. This tool previously spread the
1232
+ // raw identity doc (identity_key.public_key + invoke.env in the clear).
1233
+ // Reputation stays an opt-in add-on.
1234
+ const structuredAgents = agents.map((agent) => {
1235
+ const projected = projectAgentForRead(agent);
1236
+ return args.includeReputation
1237
+ ? { ...projected, reputation: reputationById.get(agent.agent_id) }
1238
+ : projected;
1239
+ });
1209
1240
  const lines = structuredAgents.length === 0
1210
1241
  ? ['No registered agents.']
1211
1242
  : [
1212
1243
  `${structuredAgents.length} registered agent(s):`,
1213
1244
  ...structuredAgents.map((agent) => {
1214
1245
  const reputation = agent.reputation;
1215
- const currentLabel = current?.agent_id === agent.agent_id ? ' [current]' : '';
1216
- const capabilitiesLabel = agent.capabilities.length > 0 ? ` caps=${agent.capabilities.join(',')}` : '';
1217
- const fingerprintLabel = agent.identity_key ? ` fp=${agent.identity_key.fingerprint.slice(0, 12)}` : '';
1246
+ const capabilities = agent.capabilities ?? [];
1247
+ const fingerprint = agent.fingerprint;
1248
+ const currentLabel = current?.agent_id === agent.id ? ' [current]' : '';
1249
+ const capabilitiesLabel = capabilities.length > 0 ? ` caps=${capabilities.join(',')}` : '';
1250
+ const fingerprintLabel = fingerprint ? ` fp=${fingerprint.slice(0, 12)}` : '';
1218
1251
  const reputationLabel = reputation
1219
1252
  ? ` trust=${reputation.internal_trust} cq=${reputation.contribution_quality} rv=${reputation.review_reliability} ct=${reputation.continuity_hygiene}`
1220
1253
  : '';
1221
- return `- ${agent.agent_name} (${agent.agent_id}, kind=${agent.kind})${currentLabel}${reputationLabel}${capabilitiesLabel}${fingerprintLabel}`;
1254
+ return `- ${String(agent.name)} (${String(agent.id)}, kind=${String(agent.kind)})${currentLabel}${reputationLabel}${capabilitiesLabel}${fingerprintLabel}`;
1222
1255
  }),
1223
1256
  ];
1224
1257
  return {
@@ -1231,7 +1264,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1231
1264
  };
1232
1265
  }
1233
1266
  if (name === 'bclaw_list_instructions') {
1234
- const config = loadConfig(cwd);
1267
+ const config = ctx.getConfig();
1235
1268
  const project = args.project;
1236
1269
  const inferredProject = project ?? inferProjectFromTarget(args.path, config);
1237
1270
  const resolvedAgent = args.resolved ? resolveAgentScope(args.agent) : args.agent;
@@ -1438,7 +1471,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1438
1471
  if (name === 'bclaw_conflict_check') {
1439
1472
  const agentNameArg = args.agent;
1440
1473
  const agentIdArg = args.agentId;
1441
- const currentAgentName = agentNameArg ?? resolveCurrentAgentName(cwd);
1474
+ const currentAgentName = agentNameArg ?? ctx.getAgentName();
1442
1475
  const allClaimsForCheck = listClaims(cwd).filter((c) => c.status === 'active');
1443
1476
  const myClaimsForCheck = allClaimsForCheck.filter((c) => agentIdArg ? c.agent_id === agentIdArg : c.agent === currentAgentName);
1444
1477
  const otherClaimsForCheck = allClaimsForCheck.filter((c) => agentIdArg ? c.agent_id !== agentIdArg : c.agent !== currentAgentName);
@@ -1471,7 +1504,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1471
1504
  if (name === 'bclaw_switch') {
1472
1505
  if (args.list === true) {
1473
1506
  try {
1474
- const result = listAvailableProjectsForSession(cwd, context.connectionSessionId);
1507
+ const result = listAvailableProjectsForSession(cwd, ctx.connectionSessionId);
1475
1508
  const lines = result.projects.map(p => {
1476
1509
  const marker = p.active ? '→' : ' ';
1477
1510
  const label = p.name ? `${p.name} (${p.relative_path})` : p.relative_path;
@@ -1488,8 +1521,8 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1488
1521
  }
1489
1522
  if (args.clear === true) {
1490
1523
  try {
1491
- const session = context.connectionSessionId
1492
- ? loadSessionById(context.connectionSessionId, cwd)
1524
+ const session = ctx.connectionSessionId
1525
+ ? loadSessionById(ctx.connectionSessionId, cwd)
1493
1526
  : loadCurrentSession(cwd);
1494
1527
  if (session?.active_project) {
1495
1528
  const { active_project: _removed, ...rest } = session;
@@ -1509,7 +1542,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1509
1542
  return createToolErrorResponse('validation_error', 'Missing required argument: project (or use list=true / clear=true)');
1510
1543
  }
1511
1544
  try {
1512
- const result = switchProject(projectRef, { cwd, sessionOnly: true, sessionId: context.connectionSessionId });
1545
+ const result = switchProject(projectRef, { cwd, sessionOnly: true, sessionId: ctx.connectionSessionId });
1513
1546
  const text = `✔ Switched to ${result.name ? `"${result.name}"` : result.path} (${result.scope}-scoped)`;
1514
1547
  return {
1515
1548
  content: [{ type: 'text', text }],
@@ -1563,7 +1596,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1563
1596
  }
1564
1597
  const result = checkPolicy({
1565
1598
  scope,
1566
- agent: args.agent ?? resolveCurrentAgentName(cwd),
1599
+ agent: args.agent ?? ctx.getAgentName(),
1567
1600
  agentId: args.agentId,
1568
1601
  action: args.action,
1569
1602
  cwd,
@@ -1768,7 +1801,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1768
1801
  };
1769
1802
  }
1770
1803
  if (name === 'bclaw_read_inbox') {
1771
- const agentName = args.agent ?? resolveCurrentAgentName(cwd);
1804
+ const agentName = args.agent ?? ctx.getAgentName();
1772
1805
  const markAsRead = args.markAsRead === true; // default: false — reading doesn't imply processing
1773
1806
  const result = readInbox({
1774
1807
  agent: agentName,
@@ -1798,15 +1831,18 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1798
1831
  // Phase 3 slice 3c — unified dispatcher. See docs/concepts/mcp-governance.md
1799
1832
  // for the stability contract of the advanced tier.
1800
1833
  const kind = String(args.kind ?? '');
1834
+ // Delegation re-enters dispatchReadTool with the SAME resolved context —
1835
+ // store resolution and project routing already happened at the entry
1836
+ // point and must not be recomputed (pln#622 PR2).
1801
1837
  switch (kind) {
1802
1838
  case 'memory':
1803
- return handleMcpReadToolCall('bclaw_get_context', args, context);
1839
+ return dispatchReadTool('bclaw_get_context', args, ctx);
1804
1840
  case 'execution':
1805
- return handleMcpReadToolCall('bclaw_get_execution_context', args, context);
1841
+ return dispatchReadTool('bclaw_get_execution_context', args, ctx);
1806
1842
  case 'board':
1807
- return handleMcpReadToolCall('bclaw_get_agent_board', args, context);
1843
+ return dispatchReadTool('bclaw_get_agent_board', args, ctx);
1808
1844
  case 'board_summary':
1809
- return handleMcpReadToolCall('bclaw_get_agent_board_summary', args, context);
1845
+ return dispatchReadTool('bclaw_get_agent_board_summary', args, ctx);
1810
1846
  case 'cross_project': {
1811
1847
  // pln#558 step 3 — lightweight endpoint for the VS Code extension's
1812
1848
  // SYSTEM section: returns linked_projects + incoming_signals only,
@@ -1823,7 +1859,7 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
1823
1859
  if (typeof since !== 'string' || !since) {
1824
1860
  throw new Error('bclaw_context(kind="delta") requires `since` (session_id).');
1825
1861
  }
1826
- return handleMcpReadToolCall('bclaw_get_context', { ...args, since_session: since }, context);
1862
+ return dispatchReadTool('bclaw_get_context', { ...args, since_session: since }, ctx);
1827
1863
  }
1828
1864
  default:
1829
1865
  throw new Error(`bclaw_context: unknown kind '${kind}'. Expected memory | execution | board | board_summary | cross_project | delta.`);
@@ -0,0 +1,328 @@
1
+ /**
2
+ * MCP admin / provisioning write-tool handlers.
3
+ *
4
+ * Extracted from mcp.ts (pln#622 PR4) — mechanical move of the setup wizard,
5
+ * project init, and capability/tool registration write handlers. Behavior is
6
+ * unchanged; each handler receives the tool-call payload plus a
7
+ * {@link McpWriteAdminContext} carrying the model resolved once per write call.
8
+ *
9
+ * This module must never import ./mcp.js (dependency-direction guard,
10
+ * pln#622 PR1).
11
+ *
12
+ * @module
13
+ */
14
+ import fs from 'node:fs';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+ import { loadConfig } from '../core/config.js';
18
+ import { memoryExists } from '../core/io.js';
19
+ import { appendAuditEntry } from '../core/audit.js';
20
+ import { createCapability, createTool as createRegistryTool } from '../core/registries.js';
21
+ import { detectAiAgent } from '../core/ai-agent-detection.js';
22
+ import { checkGitPresence, scanGitRepos, parseRoots, parseRepoSelection, parseAgentSelection, getDetectedSetupAgentNames, getInstalledAgentNames, runGlobalInstall, initReposAndConfigureAgents, readSetupState, ALL_KNOWN_AGENTS, } from './setup.js';
23
+ import { buildAgentInventory } from '../core/agent-inventory.js';
24
+ import { probeForQuickSetup, buildQuickSetupProbeResponse, buildOnboardingPreview, resolveEmptyMemoryRecommendation } from '../core/setup-flow.js';
25
+ import { ensureUserStore, resolveHomeDir } from '../core/setup-state.js';
26
+ import { ensureTrust } from './mcp-write-support.js';
27
+ import { SCHEMA_VERSION, toolResponse, createToolErrorResponse, } from './mcp-contract.js';
28
+ export async function handleBclawSetup(payload, _ctx) {
29
+ const { args, cwd } = payload;
30
+ const step = args.step;
31
+ const choice = args.choice ?? '';
32
+ const rootsArg = args.roots;
33
+ const repoSelectionArg = args.repo_selection;
34
+ const modeArg = args.mode;
35
+ const env = process.env;
36
+ if (!checkGitPresence()) {
37
+ return { response: toolResponse({ content: [{ type: 'text', text: 'Git is not installed or not found in PATH. Install git from https://git-scm.com before running brainclaw setup.' }], structuredContent: { error: 'git_not_found' } }, true) };
38
+ }
39
+ // ─── Quick mode: probe current repo ──────────────────────────────
40
+ if (!step) {
41
+ // Auto-detect mode: if we're in a git repo, use quick mode unless batch is forced
42
+ const forceBatch = modeArg === 'batch';
43
+ if (!forceBatch) {
44
+ const probe = probeForQuickSetup(cwd);
45
+ if (probe.isGitRepo || probe.alreadyInitialized) {
46
+ const response = buildQuickSetupProbeResponse(probe);
47
+ return { response: toolResponse({ content: [{ type: 'text', text: response.text }], structuredContent: response.structured }) };
48
+ }
49
+ }
50
+ // Fall through to batch mode
51
+ const existingState = readSetupState(env);
52
+ const alreadyRun = existingState ? `Setup was previously run on ${new Date(existingState.completed_at).toLocaleDateString()}. You can re-run it.` : undefined;
53
+ return { response: toolResponse({ content: [{ type: 'text', text: [alreadyRun, "Where are the user's project directories? Please ask the user to provide one or more root paths where their git repositories are located (e.g. ~/Projects, C:\\Users\\user\\code)."].filter(Boolean).join('\n\n') }], structuredContent: { pending_question: 'project_roots', prompt: 'Please ask the user: "Where are your projects? Enter one or more root directories (comma-separated):"', ...(alreadyRun ? { already_run: alreadyRun } : {}) } }) };
54
+ }
55
+ // ─── Quick mode step: init with choices ──────────────────────────
56
+ if (step === 'quick_init') {
57
+ const projectType = args.project_type ?? 'standalone';
58
+ const topology = args.topology ?? 'embedded';
59
+ // Ensure user store exists
60
+ ensureUserStore(env);
61
+ // Map choices to init options
62
+ const projectMode = projectType === 'workspace' ? 'multi-project' : 'auto';
63
+ const topologyMode = topology === 'sidecar' ? 'sidecar' : 'embedded';
64
+ // Run init
65
+ try {
66
+ const { runInit } = await import('./init.js');
67
+ await runInit({
68
+ yes: true,
69
+ cwd,
70
+ skipAgentBootstrap: false,
71
+ projectMode,
72
+ topology: topologyMode,
73
+ });
74
+ }
75
+ catch (err) {
76
+ return { response: toolResponse({ content: [{ type: 'text', text: `Init failed: ${err instanceof Error ? err.message : String(err)}` }], structuredContent: { error: 'init_failed', details: err instanceof Error ? err.message : String(err) } }, true) };
77
+ }
78
+ // Detect agent and report
79
+ const detected = detectAiAgent(env);
80
+ const summary = [
81
+ `✔ Initialized ${cwd.split(/[\\/]/).pop() ?? cwd} (${projectType}, ${topology})`,
82
+ ];
83
+ if (detected) {
84
+ summary.push(`✔ Agent detected: ${detected.name}`);
85
+ }
86
+ summary.push('✔ Full brainclaw MCP catalog activates automatically; reload your agent session only if new tools do not appear.');
87
+ // Bootstrap route follows the shared empty-memory rule; the preview
88
+ // already embeds the same recommendation text when memory is empty.
89
+ const probe = probeForQuickSetup(cwd);
90
+ const bootstrapAvailable = probe.hasContent;
91
+ const emptyMemoryRec = resolveEmptyMemoryRecommendation(cwd);
92
+ const preview = buildOnboardingPreview(cwd);
93
+ return {
94
+ response: toolResponse({
95
+ content: [{ type: 'text', text: summary.join('\n') + '\n\n' + preview }],
96
+ structuredContent: {
97
+ setup_complete: true,
98
+ project_type: projectType,
99
+ topology,
100
+ detected_agent: detected?.name ?? null,
101
+ bootstrap_available: bootstrapAvailable,
102
+ bootstrap_route: emptyMemoryRec.route,
103
+ next_action: emptyMemoryRec.mcp_next_action,
104
+ preview,
105
+ summary,
106
+ },
107
+ }),
108
+ };
109
+ }
110
+ if (step === 'project_roots') {
111
+ const roots = parseRoots(choice, env);
112
+ if (roots.length === 0) {
113
+ return { response: toolResponse({ content: [{ type: 'text', text: 'No valid directories found from the provided paths. Please ask the user for valid root directories.' }], structuredContent: { error: 'no_valid_roots', provided: choice } }, true) };
114
+ }
115
+ const repos = scanGitRepos(roots);
116
+ const repoList = repos.map((r, i) => ` ${i + 1}) ${r.alreadyInitialised ? '[✔ init]' : '[ ]'} ${r.name} (${r.path})`).join('\n');
117
+ return { response: toolResponse({ content: [{ type: 'text', text: `Found ${repos.length} repository candidate(s):\n${repoList}\n\nAsk the user which repositories to initialise.` }], structuredContent: { pending_question: 'repo_selection', roots: roots.join(','), repos: repos.map((r) => ({ path: r.path, name: r.name, alreadyInitialised: r.alreadyInitialised })), prompt: 'Please ask the user: "Which repositories to initialise? Reply: (a)ll, (c)urrent, or numbers like 1,3"' } }) };
118
+ }
119
+ if (step === 'repo_selection') {
120
+ if (!rootsArg) {
121
+ return { response: toolResponse({ content: [{ type: 'text', text: 'Missing roots parameter. Pass the roots value from the previous step.' }], structuredContent: { error: 'missing_roots' } }, true) };
122
+ }
123
+ const roots = parseRoots(rootsArg, env);
124
+ const repos = scanGitRepos(roots);
125
+ const selectedRepos = parseRepoSelection(choice, repos, cwd);
126
+ const detected = detectAiAgent(env);
127
+ const installedAgents = getInstalledAgentNames(buildAgentInventory(resolveHomeDir(env) ?? os.homedir(), env));
128
+ const detectedSetupAgents = getDetectedSetupAgentNames(detected?.name, installedAgents);
129
+ const agentList = ALL_KNOWN_AGENTS.map((a, i) => {
130
+ const tag = a === detected?.name ? ' ← detected' : installedAgents.includes(a) ? ' ← installed' : '';
131
+ return ` ${i + 1}) ${a}${tag}`;
132
+ }).join('\n');
133
+ const detectedLine = detectedSetupAgents.length > 0 ? `\nDetected install set: ${detectedSetupAgents.join(', ')}\n` : '\n';
134
+ return { response: toolResponse({ content: [{ type: 'text', text: `Selected ${selectedRepos.length} repo(s). Detected AI agent: ${detected?.name ?? 'none'}.${detectedLine}\nAvailable agents:\n${agentList}\n\nAsk the user which agents to configure.` }], structuredContent: { pending_question: 'agent_selection', roots: rootsArg, repo_selection: choice, selected_repos: selectedRepos.map((r) => ({ path: r.path, name: r.name })), detected_agent: detected?.name ?? null, installed_agents: installedAgents, detected_setup_agents: detectedSetupAgents, all_agents: ALL_KNOWN_AGENTS, prompt: 'Please ask the user: "Which agents to configure? Reply: (d)etected installed, (a)ll, or agent names like claude-code,cursor"' } }) };
135
+ }
136
+ if (step === 'agent_selection') {
137
+ if (!rootsArg || !repoSelectionArg) {
138
+ return { response: toolResponse({ content: [{ type: 'text', text: 'Missing roots or repo_selection parameter from previous steps.' }], structuredContent: { error: 'missing_params' } }, true) };
139
+ }
140
+ const roots = parseRoots(rootsArg, env);
141
+ const repos = scanGitRepos(roots);
142
+ const selectedRepos = parseRepoSelection(repoSelectionArg, repos, cwd);
143
+ const detected = detectAiAgent(env);
144
+ const installedAgents = getInstalledAgentNames(buildAgentInventory(resolveHomeDir(env) ?? os.homedir(), env));
145
+ const selectedAgents = parseAgentSelection(choice, detected?.name, installedAgents);
146
+ const summary = [];
147
+ const written = runGlobalInstall(selectedAgents, env);
148
+ for (const f of written)
149
+ summary.push(`✔ Global config: ${f}`);
150
+ const { initialisedRepos, configActions } = await initReposAndConfigureAgents(selectedRepos, selectedAgents, env);
151
+ for (const p of initialisedRepos)
152
+ summary.push(`✔ Initialised repo: ${p}`);
153
+ for (const a of configActions)
154
+ summary.push(a);
155
+ let reloadMsg = '✔ Setup complete! Reload your AI agent session to activate brainclaw MCP tools.';
156
+ if (detected?.name === 'claude-code')
157
+ reloadMsg += '\n → In VS Code: Cmd/Ctrl+Shift+P → "Claude: Reload MCP Servers"';
158
+ else if (detected?.name === 'cursor')
159
+ reloadMsg += '\n → In Cursor: restart the editor';
160
+ else if (detected?.name === 'windsurf')
161
+ reloadMsg += '\n → In Windsurf: restart the editor';
162
+ return { response: toolResponse({ content: [{ type: 'text', text: [reloadMsg, '', ...summary].join('\n') }], structuredContent: { setup_complete: true, initialised_repos: initialisedRepos, global_configs_written: written, agent_configs_written: configActions, detected_agent: detected?.name ?? null, summary } }) };
163
+ }
164
+ return { response: toolResponse({ content: [{ type: 'text', text: `Unknown step: "${step}". Valid steps: project_roots, repo_selection, agent_selection.` }], structuredContent: { error: 'unknown_step', step } }, true) };
165
+ }
166
+ export async function handleBclawInitProject(payload, _ctx) {
167
+ const { args, cwd } = payload;
168
+ const rawPath = typeof args.path === 'string' ? args.path.trim() : '';
169
+ if (!rawPath) {
170
+ return { response: createToolErrorResponse('validation_error', 'path is required') };
171
+ }
172
+ const force = args.force === true;
173
+ const projectModeArg = typeof args.project_mode === 'string' ? args.project_mode : undefined;
174
+ const linkAs = typeof args.link_as === 'string' && args.link_as.trim().length > 0
175
+ ? args.link_as.trim()
176
+ : undefined;
177
+ const resolvedPath = path.isAbsolute(rawPath) ? rawPath : path.resolve(cwd, rawPath);
178
+ let wasAlreadyInitialized = false;
179
+ if (memoryExists(resolvedPath) && !force) {
180
+ wasAlreadyInitialized = true;
181
+ }
182
+ else {
183
+ if (!fs.existsSync(resolvedPath)) {
184
+ try {
185
+ fs.mkdirSync(resolvedPath, { recursive: true });
186
+ }
187
+ catch (err) {
188
+ return {
189
+ response: createToolErrorResponse('init_project_failed', `Failed to create target directory '${resolvedPath}': ${err instanceof Error ? err.message : String(err)}`),
190
+ };
191
+ }
192
+ }
193
+ try {
194
+ const { runInit } = await import('./init.js');
195
+ await runInit({
196
+ yes: true,
197
+ cwd: resolvedPath,
198
+ force,
199
+ ...(projectModeArg ? { projectMode: projectModeArg } : {}),
200
+ });
201
+ }
202
+ catch (err) {
203
+ return {
204
+ response: createToolErrorResponse('init_project_failed', `runInit failed for '${resolvedPath}': ${err instanceof Error ? err.message : String(err)}`),
205
+ };
206
+ }
207
+ }
208
+ let projectName;
209
+ try {
210
+ projectName = loadConfig(resolvedPath).project_name;
211
+ }
212
+ catch {
213
+ projectName = path.basename(resolvedPath);
214
+ }
215
+ let linkName;
216
+ try {
217
+ const { addCrossProjectLink } = await import('../core/cross-project.js');
218
+ const link = addCrossProjectLink({
219
+ path: resolvedPath,
220
+ name: linkAs ?? projectName,
221
+ cwd,
222
+ force,
223
+ });
224
+ linkName = link.name ?? path.basename(resolvedPath);
225
+ }
226
+ catch (err) {
227
+ const message = err instanceof Error ? err.message : String(err);
228
+ // Treat a duplicate link as idempotent success when the caller did
229
+ // not request --force; the project itself is initialised correctly
230
+ // and the existing link already points at it.
231
+ if (/already exists/i.test(message) && !force) {
232
+ try {
233
+ const { resolveCrossProjectLinks } = await import('../core/cross-project.js');
234
+ const existing = resolveCrossProjectLinks(cwd).find((l) => l.absolutePath === resolvedPath || l.path === rawPath);
235
+ linkName = existing?.name ?? linkAs ?? projectName;
236
+ }
237
+ catch {
238
+ linkName = linkAs ?? projectName;
239
+ }
240
+ }
241
+ else {
242
+ return {
243
+ response: createToolErrorResponse('init_project_failed', `Failed to register cross_project_link: ${message}`),
244
+ };
245
+ }
246
+ }
247
+ const summary = wasAlreadyInitialized
248
+ ? `✔ ${resolvedPath} already initialised; linked as '${linkName}'.`
249
+ : `✔ Initialised brainclaw at ${resolvedPath} and linked as '${linkName}'.`;
250
+ return {
251
+ response: toolResponse({
252
+ content: [{ type: 'text', text: summary }],
253
+ structuredContent: {
254
+ status: 'ok',
255
+ project_name: projectName,
256
+ path: resolvedPath,
257
+ link_id: linkName,
258
+ was_already_initialized: wasAlreadyInitialized,
259
+ },
260
+ }),
261
+ };
262
+ }
263
+ export function handleBclawAddCapability(payload, ctx) {
264
+ const { args, cwd, connectionSessionId } = payload;
265
+ const capName = String(args.name ?? '').trim();
266
+ const capDesc = String(args.description ?? '').trim();
267
+ if (!capName || !capDesc) {
268
+ return { response: createToolErrorResponse('validation_error', 'Missing required arguments: name and description') };
269
+ }
270
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
271
+ if (resolved.error) {
272
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
273
+ }
274
+ const resolvedIdentity = resolved.identity;
275
+ const extraTags = Array.isArray(args.tags) ? args.tags : [];
276
+ const cap = createCapability({
277
+ name: capName,
278
+ description: capDesc,
279
+ tags: extraTags,
280
+ author: resolvedIdentity.agent_name,
281
+ authorId: resolvedIdentity.agent_id,
282
+ model: ctx.currentModel,
283
+ }, cwd);
284
+ appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'create', item_id: cap.id, item_type: 'capability', reason: `capability: ${capName}` }, cwd);
285
+ return {
286
+ response: toolResponse({
287
+ content: [{ type: 'text', text: `✔ Capability registered: [${cap.id}] ${capName}` }],
288
+ id: cap.id,
289
+ name: capName,
290
+ schema_version: SCHEMA_VERSION,
291
+ }),
292
+ };
293
+ }
294
+ export function handleBclawAddTool(payload, ctx) {
295
+ const { args, cwd, connectionSessionId } = payload;
296
+ const toolName = String(args.name ?? '').trim();
297
+ const toolDesc = String(args.description ?? '').trim();
298
+ if (!toolName || !toolDesc) {
299
+ return { response: createToolErrorResponse('validation_error', 'Missing required arguments: name and description') };
300
+ }
301
+ const resolved = ensureTrust(args, { nameField: 'agent', idField: 'agentId' }, 'contributor', cwd, connectionSessionId);
302
+ if (resolved.error) {
303
+ return { response: createToolErrorResponse(resolved.error.kind, resolved.error.message, resolved.error.details) };
304
+ }
305
+ const resolvedIdentity = resolved.identity;
306
+ const toolType = String(args.type ?? 'utility');
307
+ const extraTags = Array.isArray(args.tags) ? args.tags : [];
308
+ const tool = createRegistryTool({
309
+ name: toolName,
310
+ description: toolDesc,
311
+ type: toolType,
312
+ tags: extraTags,
313
+ author: resolvedIdentity.agent_name,
314
+ authorId: resolvedIdentity.agent_id,
315
+ model: ctx.currentModel,
316
+ }, cwd);
317
+ appendAuditEntry({ actor: resolvedIdentity.agent_name, actor_id: resolvedIdentity.agent_id, action: 'create', item_id: tool.id, item_type: 'tool', reason: `tool: ${toolName}` }, cwd);
318
+ return {
319
+ response: toolResponse({
320
+ content: [{ type: 'text', text: `✔ Tool registered: [${tool.id}] ${toolName} (${toolType})` }],
321
+ id: tool.id,
322
+ name: toolName,
323
+ type: toolType,
324
+ schema_version: SCHEMA_VERSION,
325
+ }),
326
+ };
327
+ }
328
+ //# sourceMappingURL=mcp-write-admin.js.map