brainclaw 1.5.4 → 1.6.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.
- package/README.md +52 -28
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +159 -12
- package/dist/commands/assignment-resource.js +182 -0
- package/dist/commands/bootstrap-loop.js +206 -0
- package/dist/commands/init.js +158 -22
- package/dist/commands/loop.js +156 -0
- package/dist/commands/loops-handlers.js +110 -55
- package/dist/commands/mcp-read-handlers.js +45 -4
- package/dist/commands/mcp.js +628 -205
- package/dist/commands/questions.js +180 -0
- package/dist/commands/reply.js +190 -0
- package/dist/commands/session-end.js +105 -3
- package/dist/commands/session-start.js +32 -53
- package/dist/commands/setup.js +87 -48
- package/dist/commands/switch.js +21 -1
- package/dist/core/agentrun-reconciler.js +65 -0
- package/dist/core/agentruns.js +10 -0
- package/dist/core/assignments.js +29 -10
- package/dist/core/claims.js +29 -0
- package/dist/core/context.js +1 -1
- package/dist/core/coordination.js +1 -1
- package/dist/core/dispatch-status.js +219 -0
- package/dist/core/entity-operations.js +166 -10
- package/dist/core/entity-registry.js +11 -10
- package/dist/core/execution-adapters.js +38 -2
- package/dist/core/facade-schema.js +55 -0
- package/dist/core/federation-cloud.js +27 -12
- package/dist/core/federation-materialize.js +57 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/loops/bootstrap-acquire.js +195 -0
- package/dist/core/loops/facade-schema.js +68 -1
- package/dist/core/loops/hooks/bootstrap-write.js +144 -0
- package/dist/core/loops/hooks/notify-operator.js +148 -0
- package/dist/core/loops/hooks/survey-source-reader.js +256 -0
- package/dist/core/loops/index.js +8 -2
- package/dist/core/loops/next-expected.js +63 -0
- package/dist/core/loops/presets/bootstrap.js +75 -0
- package/dist/core/loops/presets/index.js +16 -0
- package/dist/core/loops/store.js +224 -4
- package/dist/core/loops/types.js +346 -1
- package/dist/core/loops/verbs.js +739 -6
- package/dist/core/schema.js +31 -2
- package/dist/core/state.js +62 -0
- package/dist/core/store-resolution.js +26 -16
- package/dist/facts.js +7 -5
- package/dist/facts.json +6 -4
- package/docs/cli.md +115 -30
- package/docs/concepts/dispatch-lifecycle.md +228 -0
- package/docs/concepts/loop-engine.md +55 -0
- package/docs/concepts/multi-agent-workflows.md +167 -166
- package/docs/concepts/troubleshooting.md +10 -2
- package/docs/integrations/agents.md +14 -14
- package/docs/integrations/codex.md +15 -12
- package/docs/integrations/mcp.md +10 -4
- package/docs/integrations/overview.md +11 -0
- package/docs/playbooks/productivity/index.md +3 -3
- package/docs/quickstart-existing-project.md +48 -28
- package/docs/quickstart.md +42 -28
- package/package.json +1 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { ZodError } from 'zod';
|
|
2
2
|
import { listAgentRuns } from '../core/agentruns.js';
|
|
3
3
|
import { reconcileAgentRun } from '../core/agentrun-reconciler.js';
|
|
4
|
-
import { add_artifact, advance, closeLoop, complete_turn, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, resume, turn, VersionConflictError, withLoopLock, } from '../core/loops/index.js';
|
|
4
|
+
import { add_artifact, advance, AwaitingFileApplyApprovalError, closeLoop, complete_turn, computeNextExpected, getLoop, IdempotencyKeyReusedError, IdempotencyOwnerMismatchError, listLoopEvents, listLoops, LockLostError, LockTimeoutError, openLoop, pause, provideInput, requestInput, resume, sweepPauseTimeouts, turn, VersionConflictError, withLoopLock, } from '../core/loops/index.js';
|
|
5
5
|
import { BclawLoopRequestSchema, BCLAW_LOOP_INTENTS, } from '../core/loops/facade-schema.js';
|
|
6
|
+
// NextExpectedHint type now lives in src/core/loops/next-expected.ts
|
|
7
|
+
// (hoisted per can_e57c7782 follow-up so MCP facade + CLI share the
|
|
8
|
+
// same contract). Imported above.
|
|
6
9
|
function resolveActor(req, defaultActor) {
|
|
7
10
|
const agentId = req.agentId?.trim() || defaultActor;
|
|
8
11
|
const actor = req.agent?.trim() || req.agentId?.trim() || defaultActor;
|
|
@@ -128,68 +131,51 @@ function withLockedLoopMutation(req, agentId, cwd, work) {
|
|
|
128
131
|
// Best-effort fence at entry; see SLOT_BOUND_INTENTS / fence-check
|
|
129
132
|
// discipline comment above for why mid-verb re-checks are deferred.
|
|
130
133
|
fenceCheck();
|
|
134
|
+
// pln#508 step 3 follow-up (can_810ff9ec): run the timeout sweep
|
|
135
|
+
// INSIDE the loop lock for mutating intents. Previously the sweep
|
|
136
|
+
// ran at facade entry (before lock acquisition), which could race
|
|
137
|
+
// with concurrent writers and turn the caller's expected_version
|
|
138
|
+
// into a sweep-induced version_conflict. Now: sweep writes happen
|
|
139
|
+
// under the same lock as the caller's verb — single-writer
|
|
140
|
+
// serialization preserved. If the sweep bumps the version, the
|
|
141
|
+
// caller's verb sees the post-sweep state (e.g. their question
|
|
142
|
+
// already timed out → provide_input legitimately returns
|
|
143
|
+
// unknown_question, which is correct semantics).
|
|
144
|
+
trySweepLoopTimeouts(req.loop_id, cwd);
|
|
131
145
|
return work();
|
|
132
146
|
},
|
|
133
147
|
});
|
|
134
148
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
* status + slot states and pick the smallest correct action.
|
|
139
|
-
*/
|
|
140
|
-
function computeNextExpected(loop) {
|
|
141
|
-
if (loop.status === 'completed' || loop.status === 'cancelled' || loop.status === 'blocked') {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
if (loop.status === 'paused') {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
const currentPhaseSlots = loop.slots.filter((s) => (s.phase ?? loop.current_phase) === loop.current_phase);
|
|
148
|
-
const openSlots = currentPhaseSlots.filter((s) => s.status === 'open');
|
|
149
|
-
if (openSlots.length > 0) {
|
|
150
|
-
const first = openSlots[0];
|
|
151
|
-
return {
|
|
152
|
-
action: 'turn',
|
|
153
|
-
intent: 'bclaw_loop.turn',
|
|
154
|
-
phase: loop.current_phase,
|
|
155
|
-
slot_id: first.slot_id,
|
|
156
|
-
role: first.role,
|
|
157
|
-
blocking_on: openSlots.map((s) => s.slot_id),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
const assignedOrWorking = currentPhaseSlots.filter((s) => s.status === 'assigned' || s.status === 'working');
|
|
161
|
-
if (assignedOrWorking.length > 0) {
|
|
162
|
-
return {
|
|
163
|
-
action: 'complete_turn',
|
|
164
|
-
intent: 'bclaw_loop.complete_turn',
|
|
165
|
-
phase: loop.current_phase,
|
|
166
|
-
slot_id: assignedOrWorking[0].slot_id,
|
|
167
|
-
role: assignedOrWorking[0].role,
|
|
168
|
-
blocking_on: assignedOrWorking.map((s) => s.slot_id),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
const phaseNames = loop.phases.map((p) => p.name);
|
|
172
|
-
const currentIndex = phaseNames.indexOf(loop.current_phase);
|
|
173
|
-
if (currentIndex >= 0 && currentIndex + 1 < phaseNames.length) {
|
|
174
|
-
return {
|
|
175
|
-
action: 'advance',
|
|
176
|
-
intent: 'bclaw_loop.advance',
|
|
177
|
-
from_phase: loop.current_phase,
|
|
178
|
-
to_phase: phaseNames[currentIndex + 1],
|
|
179
|
-
blocking_on: [],
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
return {
|
|
183
|
-
action: 'close',
|
|
184
|
-
intent: 'bclaw_loop.close',
|
|
185
|
-
reason: 'terminal_phase_reached',
|
|
186
|
-
blocking_on: [],
|
|
187
|
-
};
|
|
188
|
-
}
|
|
149
|
+
// computeNextExpected lives in src/core/loops/next-expected.ts (hoisted
|
|
150
|
+
// per can_e57c7782 follow-up — same contract is now shared by both the
|
|
151
|
+
// MCP facade here and the CLI `brainclaw reply` command).
|
|
189
152
|
function summarizeLoop(loop, autoClosed) {
|
|
190
153
|
const suffix = autoClosed ? ' (auto-closed)' : '';
|
|
191
154
|
return `✔ loop ${loop.id} [${loop.kind}] phase=${loop.current_phase} status=${loop.status}${suffix}`;
|
|
192
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* pln#508 step 3 — lazy pause-timeout reconcile at facade entry.
|
|
158
|
+
*
|
|
159
|
+
* Phase 0 spec §6: any time the facade is invoked against a loop_id, sweep
|
|
160
|
+
* the target loop for timed-out operator_question artifacts BEFORE
|
|
161
|
+
* dispatching the intent. The downstream verb then sees the corrected state
|
|
162
|
+
* (e.g. a `cancel_loop` timeout already fired → mutating intents will get
|
|
163
|
+
* the natural `already cancelled` error from assertMutable; a `use_default`
|
|
164
|
+
* timeout already fired → open_questions reflects the synthesized answer).
|
|
165
|
+
*
|
|
166
|
+
* Best-effort: wrapped in try/catch so any reconcile error (corrupt loop on
|
|
167
|
+
* disk, fs hiccup, …) never blocks the facade. The handler proceeds with
|
|
168
|
+
* stale state in that case, which is no worse than the pre-step-3 behavior.
|
|
169
|
+
*
|
|
170
|
+
* Mirrors the lazy-reconcile pattern used by `agentrun-reconciler.ts` for
|
|
171
|
+
* agent_run silent completion (see entity-operations.ts loadAgentRunsWithReconciliation).
|
|
172
|
+
*/
|
|
173
|
+
function trySweepLoopTimeouts(loop_id, cwd) {
|
|
174
|
+
try {
|
|
175
|
+
sweepPauseTimeouts(loop_id, undefined, cwd);
|
|
176
|
+
}
|
|
177
|
+
catch { /* best-effort: never block facade on sweep errors */ }
|
|
178
|
+
}
|
|
193
179
|
export function handleBclawLoop(options) {
|
|
194
180
|
const startMs = Date.now();
|
|
195
181
|
const defaultActor = options.defaultActor ?? 'bclaw_loop';
|
|
@@ -204,6 +190,15 @@ export function handleBclawLoop(options) {
|
|
|
204
190
|
return errorResponse(req.intent, 'validation_error', semanticError, Date.now() - startMs);
|
|
205
191
|
}
|
|
206
192
|
const { actor, agentId } = resolveActor(req, defaultActor);
|
|
193
|
+
// pln#508 step 3 — lazy pause-timeout reconcile at facade entry. Only
|
|
194
|
+
// for the `get` read-only intent (no withLockedLoopMutation wrapper).
|
|
195
|
+
// Mutating intents sweep INSIDE withLockedLoopMutation to keep all
|
|
196
|
+
// writes under the same loop lock as the caller's verb — see
|
|
197
|
+
// can_810ff9ec follow-up. `open` has no existing loop_id; `list`
|
|
198
|
+
// enumerates many loops (unbounded fan-out, not in scope).
|
|
199
|
+
if (req.intent === 'get' && typeof req.loop_id === 'string') {
|
|
200
|
+
trySweepLoopTimeouts(req.loop_id, options.cwd);
|
|
201
|
+
}
|
|
207
202
|
try {
|
|
208
203
|
switch (req.intent) {
|
|
209
204
|
case 'open': {
|
|
@@ -309,6 +304,7 @@ export function handleBclawLoop(options) {
|
|
|
309
304
|
type: req.artifact.type,
|
|
310
305
|
body: req.artifact.body,
|
|
311
306
|
ref: req.artifact.ref,
|
|
307
|
+
addresses_critique: req.artifact.addresses_critique,
|
|
312
308
|
}
|
|
313
309
|
: undefined,
|
|
314
310
|
actor,
|
|
@@ -343,6 +339,7 @@ export function handleBclawLoop(options) {
|
|
|
343
339
|
body: req.artifact.body,
|
|
344
340
|
produced_by: req.artifact.produced_by,
|
|
345
341
|
ref: req.artifact.ref,
|
|
342
|
+
addresses_critique: req.artifact.addresses_critique,
|
|
346
343
|
},
|
|
347
344
|
actor,
|
|
348
345
|
}, options.cwd);
|
|
@@ -366,6 +363,54 @@ export function handleBclawLoop(options) {
|
|
|
366
363
|
return successResponse('resume', { loop, next_expected: computeNextExpected(loop) }, [loopArtifactEntry(loop.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', loop.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(loop));
|
|
367
364
|
});
|
|
368
365
|
}
|
|
366
|
+
case 'request_input': {
|
|
367
|
+
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
368
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
369
|
+
const result = requestInput({
|
|
370
|
+
loop_id: req.loop_id,
|
|
371
|
+
slot_id: req.slot_id,
|
|
372
|
+
phase: req.phase,
|
|
373
|
+
question_text: req.question_text,
|
|
374
|
+
evidence: req.evidence,
|
|
375
|
+
suggested_default: req.suggested_default,
|
|
376
|
+
options: req.options,
|
|
377
|
+
pause_scope: req.pause_scope,
|
|
378
|
+
on_timeout: req.on_timeout,
|
|
379
|
+
timeout_at: req.timeout_at,
|
|
380
|
+
actor,
|
|
381
|
+
}, options.cwd);
|
|
382
|
+
const newEvents = findNewLoopEvents(result.thread.id, beforeEvents, options.cwd);
|
|
383
|
+
return successResponse('request_input', {
|
|
384
|
+
loop: result.thread,
|
|
385
|
+
question_id: result.question_id,
|
|
386
|
+
artifact_id: result.artifact_id,
|
|
387
|
+
next_expected: computeNextExpected(result.thread),
|
|
388
|
+
}, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], [], Date.now() - startMs, summarizeLoop(result.thread));
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
case 'provide_input': {
|
|
392
|
+
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
393
|
+
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
394
|
+
const result = provideInput({
|
|
395
|
+
loop_id: req.loop_id,
|
|
396
|
+
replies_to: req.replies_to,
|
|
397
|
+
resolved_via: req.resolved_via,
|
|
398
|
+
answer_text: req.answer_text,
|
|
399
|
+
chosen_option_id: req.chosen_option_id,
|
|
400
|
+
by: req.by,
|
|
401
|
+
actor,
|
|
402
|
+
}, options.cwd);
|
|
403
|
+
const newEvents = findNewLoopEvents(result.thread.id, beforeEvents, options.cwd);
|
|
404
|
+
return successResponse('provide_input', {
|
|
405
|
+
loop: result.thread,
|
|
406
|
+
artifact_id: result.artifact_id,
|
|
407
|
+
duplicate: result.duplicate,
|
|
408
|
+
next_expected: computeNextExpected(result.thread),
|
|
409
|
+
}, [loopArtifactEntry(result.thread.id), ...loopEventArtifacts(newEvents)], [sideEffectUpdate('loop', result.thread.id), ...loopEventSideEffects(newEvents)], result.duplicate
|
|
410
|
+
? ['provide_input: idempotent replay — replies_to was already resolved; returning existing answer']
|
|
411
|
+
: [], Date.now() - startMs, summarizeLoop(result.thread));
|
|
412
|
+
});
|
|
413
|
+
}
|
|
369
414
|
case 'close': {
|
|
370
415
|
return withLockedLoopMutation(req, agentId, options.cwd, () => {
|
|
371
416
|
const beforeEvents = snapshotLoopEvents(req.loop_id, options.cwd);
|
|
@@ -395,6 +440,16 @@ export function handleBclawLoop(options) {
|
|
|
395
440
|
if (err instanceof LockLostError) {
|
|
396
441
|
return errorResponse(req.intent, 'lock_lost', err.message, Date.now() - startMs);
|
|
397
442
|
}
|
|
443
|
+
if (err instanceof AwaitingFileApplyApprovalError) {
|
|
444
|
+
// pln#512 phase 3 codex review fix #1: surface structurally so callers
|
|
445
|
+
// can branch on the code without parsing the message text.
|
|
446
|
+
return errorResponse(req.intent, 'awaiting_file_apply_approval', err.message, Date.now() - startMs, {
|
|
447
|
+
loop_id: err.loop_id,
|
|
448
|
+
question_id: err.question_id,
|
|
449
|
+
target_path: err.target_path,
|
|
450
|
+
diff_artifact_id: err.diff_artifact_id,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
398
453
|
const message = err instanceof Error ? err.message : String(err);
|
|
399
454
|
if (message.includes('unauthorized_slot_write')) {
|
|
400
455
|
return errorResponse(req.intent, 'unauthorized_slot_write', message, Date.now() - startMs);
|
|
@@ -13,6 +13,7 @@ import { listClaims, assessClaimLiveness } from '../core/claims.js';
|
|
|
13
13
|
import { listAssignments } from '../core/assignments.js';
|
|
14
14
|
import { listAgentRuns } from '../core/agentruns.js';
|
|
15
15
|
import { reconcileAgentRun } from '../core/agentrun-reconciler.js';
|
|
16
|
+
import { getDispatchStatus } from '../core/dispatch-status.js';
|
|
16
17
|
import { listActionRequired } from '../core/actions.js';
|
|
17
18
|
import { queryRuntimeEvents } from '../core/events.js';
|
|
18
19
|
import { listSequences, getActiveSequence } from '../core/sequence.js';
|
|
@@ -68,20 +69,24 @@ function getReviewAssignee(tags) {
|
|
|
68
69
|
return undefined;
|
|
69
70
|
}
|
|
70
71
|
export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
71
|
-
|
|
72
|
+
const baseCwd = context.cwd ?? process.cwd();
|
|
73
|
+
let cwd = name === 'bclaw_switch'
|
|
74
|
+
? baseCwd
|
|
75
|
+
: resolveEffectiveCwd({ baseCwd });
|
|
72
76
|
// If a project param is provided, resolve it to an actual cwd override.
|
|
73
77
|
// resolveProjectCwd unifies cross_project_links (siblings/peers) AND
|
|
74
78
|
// workspace store-chain children. Throws on unknown project — surfaces
|
|
75
79
|
// visibly as a tool error rather than silently falling back to the
|
|
76
80
|
// current project, which would mislead the caller.
|
|
77
81
|
const projectArg = args.project;
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
const targetProjectArg = name === 'bclaw_switch' ? undefined : projectArg;
|
|
83
|
+
if (targetProjectArg) {
|
|
84
|
+
cwd = resolveProjectCwd(targetProjectArg, cwd);
|
|
80
85
|
}
|
|
81
86
|
if (name === 'bclaw_get_context') {
|
|
82
87
|
const result = buildContext({
|
|
83
88
|
target: args.path,
|
|
84
|
-
project:
|
|
89
|
+
project: targetProjectArg,
|
|
85
90
|
agent: args.agent,
|
|
86
91
|
host: args.host,
|
|
87
92
|
allHosts: args.allHosts,
|
|
@@ -1623,6 +1628,42 @@ export function handleMcpReadToolCall(name, args = {}, context = {}) {
|
|
|
1623
1628
|
structuredContent: { thread_id: threadId, total: messages.length, messages, schema_version: SCHEMA_VERSION },
|
|
1624
1629
|
};
|
|
1625
1630
|
}
|
|
1631
|
+
if (name === 'bclaw_dispatch_status') {
|
|
1632
|
+
const targetId = String(args.target_id ?? '');
|
|
1633
|
+
if (!targetId) {
|
|
1634
|
+
throw new Error('Missing required argument: target_id (asgn_/clm_/lop_/run_)');
|
|
1635
|
+
}
|
|
1636
|
+
const tailLogLines = typeof args.tail_log_lines === 'number' ? args.tail_log_lines : undefined;
|
|
1637
|
+
const stallThresholdMs = typeof args.stall_threshold_ms === 'number' ? args.stall_threshold_ms : undefined;
|
|
1638
|
+
const status = getDispatchStatus({
|
|
1639
|
+
target_id: targetId,
|
|
1640
|
+
cwd,
|
|
1641
|
+
tail_log_lines: tailLogLines,
|
|
1642
|
+
stall_threshold_ms: stallThresholdMs,
|
|
1643
|
+
});
|
|
1644
|
+
// Text view: short, single-screen summary so an agent can decide what to do
|
|
1645
|
+
// without parsing the structured payload.
|
|
1646
|
+
const lines = [
|
|
1647
|
+
`Dispatch status for ${targetId} (resolved_from=${status.resolved_from})`,
|
|
1648
|
+
`Health: ${status.diagnosis.health}`,
|
|
1649
|
+
`Summary: ${status.diagnosis.summary}`,
|
|
1650
|
+
`Next action: ${status.diagnosis.recommended_next_action}`,
|
|
1651
|
+
'',
|
|
1652
|
+
'Entities:',
|
|
1653
|
+
` assignment=${status.entities.assignment_id ?? '-'} (status=${status.assignment?.status ?? '-'})`,
|
|
1654
|
+
` claim=${status.entities.claim_id ?? '-'} (status=${status.claim?.status ?? '-'})`,
|
|
1655
|
+
` loop=${status.entities.loop_id ?? '-'} (phase=${status.loop?.current_phase ?? '-'})`,
|
|
1656
|
+
` run=${status.entities.run_id ?? '-'} (status=${status.agent_run?.status ?? '-'})`,
|
|
1657
|
+
'',
|
|
1658
|
+
`Runtime: pid=${status.runtime.pid ?? '-'} alive=${status.runtime.pid_alive ?? 'unknown'} ack=${status.runtime.ack_file.exists}`,
|
|
1659
|
+
` stdout: ${status.runtime.log_files.stdout?.exists ? `${status.runtime.log_files.stdout.size_bytes}B` : 'absent'}`,
|
|
1660
|
+
` stderr: ${status.runtime.log_files.stderr?.exists ? `${status.runtime.log_files.stderr.size_bytes}B` : 'absent'}`,
|
|
1661
|
+
];
|
|
1662
|
+
return {
|
|
1663
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
1664
|
+
structuredContent: { ...status, schema_version: SCHEMA_VERSION },
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1626
1667
|
throw new Error(`Unknown read tool: ${name}`);
|
|
1627
1668
|
}
|
|
1628
1669
|
//# sourceMappingURL=mcp-read-handlers.js.map
|