@launchchair/agent 0.4.11 → 0.5.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/local-bridge.mjs CHANGED
@@ -15,9 +15,10 @@ import os from "node:os";
15
15
  import path from "node:path";
16
16
  import process from "node:process";
17
17
  import { fileURLToPath } from "node:url";
18
+ import { resolveProjectWorkspace } from "./project-workspace.mjs";
18
19
  import { buildRunnerCommand, parseRunnerCommand } from "./runner-command.mjs";
19
20
 
20
- const VERSION = "0.4.11";
21
+ const VERSION = "0.5.0";
21
22
  const CLIENT_TYPES = ["codex", "chatgpt", "claude", "claude_code", "other"];
22
23
  const AGENT_SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
23
24
  const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
@@ -130,10 +131,10 @@ Optional env:
130
131
  AGENT_MOCK_RESPONSE If set, skips runner command and returns this payload
131
132
  OPENAI_API_KEY Enables built-in ChatGPT runner fallback
132
133
  ANTHROPIC_API_KEY Enables built-in Claude runner fallback
133
- LC_AGENT_CODEX_CWD Folder codex-runner should build in (default: bridge cwd)
134
+ LC_AGENT_CODEX_CWD Legacy search candidate; Git origin must match the project
134
135
  LC_AGENT_CODEX_SANDBOX read-only|workspace-write|danger-full-access (default: workspace-write)
135
136
  LC_AGENT_CODEX_MODEL Optional Codex model override
136
- LC_AGENT_CLAUDE_CODE_CWD Folder claude-code-runner should build in (default: bridge cwd)
137
+ LC_AGENT_CLAUDE_CODE_CWD Legacy search candidate; Git origin must match the project
137
138
  LC_AGENT_CLAUDE_CODE_MODEL Optional Claude Code model override
138
139
  LC_AGENT_CLAUDE_CODE_ALLOWED_TOOLS
139
140
  Optional Claude Code allowedTools list
@@ -193,6 +194,10 @@ const config = {
193
194
  gpt: normalizeTarget(env("LC_AGENT_ROUTER_GPT_TARGET", "codex"), "codex"),
194
195
  codex: normalizeTarget(env("LC_AGENT_ROUTER_CODEX_TARGET", "codex"), "codex"),
195
196
  },
197
+ legacyRepoPaths: {
198
+ codex: env("LC_AGENT_CODEX_CWD", savedConfigValue("codexCwd", process.cwd())),
199
+ claudeCode: env("LC_AGENT_CLAUDE_CODE_CWD", savedConfigValue("claudeCodeCwd", process.cwd())),
200
+ },
196
201
  runnerCommands: {
197
202
  generic: env("LC_AGENT_RUNNER_CMD"),
198
203
  gpt: env("LC_AGENT_RUNNER_CMD_GPT"),
@@ -347,6 +352,25 @@ async function claimNextRun() {
347
352
  return payload?.run ?? null;
348
353
  }
349
354
 
355
+ function resolveWorkspaceForRun(run) {
356
+ const target = getTargetForRun(run) === "claude_code" ? "claude_code" : "codex";
357
+ const legacyPath = target === "claude_code" ? config.legacyRepoPaths.claudeCode : config.legacyRepoPaths.codex;
358
+ const context = run?.runnerWorkspace && typeof run.runnerWorkspace === "object" && !Array.isArray(run.runnerWorkspace)
359
+ ? run.runnerWorkspace
360
+ : {};
361
+ return {
362
+ target,
363
+ workspace: resolveProjectWorkspace({
364
+ configDir: CONFIG_DIR,
365
+ configPath: CONFIG_PATH,
366
+ projectId: String(run?.projectId ?? context.projectId ?? "").trim(),
367
+ repository: context.repository,
368
+ requireRepository: context.requiresRepository === true,
369
+ legacyPaths: [legacyPath],
370
+ }),
371
+ };
372
+ }
373
+
350
374
  async function executeRunner(run) {
351
375
  if (config.mockResponse) {
352
376
  return {
@@ -367,6 +391,9 @@ async function executeRunner(run) {
367
391
  }
368
392
  const command = buildRunnerCommand(commandTemplate, run);
369
393
  const parsedCommand = parseRunnerCommand(command);
394
+ const resolved = resolveWorkspaceForRun(run);
395
+ const workspaceEnvName = resolved.target === "claude_code" ? "LC_AGENT_CLAUDE_CODE_CWD" : "LC_AGENT_CODEX_CWD";
396
+ log(`Workspace ${resolved.workspace.path} (${resolved.workspace.source})`);
370
397
 
371
398
  const startedAt = Date.now();
372
399
  return new Promise((resolve, reject) => {
@@ -375,6 +402,7 @@ async function executeRunner(run) {
375
402
  stdio: ["pipe", "pipe", "pipe"],
376
403
  env: {
377
404
  ...process.env,
405
+ ...(resolved.workspace.source === "isolated" && run?.runnerWorkspace?.requiresRepository !== true ? { LC_AGENT_CODEX_SKIP_GIT_CHECK: "1" } : {}),
378
406
  LC_AGENT_RUN_ID: String(run.id ?? ""),
379
407
  LC_AGENT_PROJECT_ID: String(run.projectId ?? ""),
380
408
  LC_AGENT_PHASE_KEY: String(run.phaseKey ?? ""),
@@ -382,6 +410,7 @@ async function executeRunner(run) {
382
410
  LC_AGENT_CARD_ID: String(run.cardId ?? ""),
383
411
  LC_AGENT_PROMPT_MODE: String(run.promptMode ?? ""),
384
412
  LC_AGENT_RUN_TYPE: String(run.runType ?? ""),
413
+ [workspaceEnvName]: resolved.workspace.path,
385
414
  },
386
415
  });
387
416
 
@@ -415,6 +444,7 @@ async function executeRunner(run) {
415
444
  durationMs,
416
445
  usedMock: false,
417
446
  target: getTargetForRun(run),
447
+ workspace: resolved.workspace,
418
448
  });
419
449
  });
420
450
 
@@ -480,6 +510,7 @@ async function runOnce() {
480
510
  runIntent: run.runIntent ?? "initial",
481
511
  attemptNumber: run.attemptNumber ?? 1,
482
512
  maxAttempts: run.maxAttempts ?? 1,
513
+ localWorkspaceSource: execution.workspace?.source ?? null,
483
514
  },
484
515
  error: null,
485
516
  });
@@ -17,9 +17,11 @@ import os from "node:os";
17
17
  import path from "node:path";
18
18
  import process from "node:process";
19
19
  import { fileURLToPath } from "node:url";
20
+ import { resolveProjectWorkspace } from "./project-workspace.mjs";
20
21
  import { buildRunnerCommand, parseRunnerCommand } from "./runner-command.mjs";
22
+ import { createMcpCapabilities, EXTRA_MCP_TOOLS, MCP_WORKFLOW_INSTRUCTIONS, VALIDATION_GATE_SCHEMA } from "./mcp-capabilities.mjs";
21
23
 
22
- const VERSION = "0.4.11";
24
+ const VERSION = "0.5.0";
23
25
  const SERVER_NAME = "launchchair-agent-mcp";
24
26
  const AGENT_SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
25
27
  const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
@@ -107,7 +109,7 @@ function builtInRunnerCommand(target) {
107
109
  }
108
110
 
109
111
  const config = {
110
- baseUrl: env("LAUNCHCHAIR_BASE_URL", savedConfigValue("baseUrl", "http://localhost:3000")).replace(/\/+$/, ""),
112
+ baseUrl: env("LAUNCHCHAIR_BASE_URL", savedConfigValue("baseUrl", "https://www.launchchair.io")).replace(/\/+$/, ""),
111
113
  token: env("LAUNCHCHAIR_AGENT_TOKEN", env("LAUNCHCHAIR_API_KEY", savedConfigValue("agentToken"))),
112
114
  projectId: env("LAUNCHCHAIR_PROJECT_ID", savedConfigValue("projectId")) || null,
113
115
  clientName: env("LC_AGENT_CLIENT_NAME", "MCP Bridge"),
@@ -128,76 +130,58 @@ const config = {
128
130
  codexTarget: env("LC_AGENT_RUNNER_CMD_CODEX", builtInRunnerCommand("codex")),
129
131
  claudeCode: env("LC_AGENT_RUNNER_CMD_CLAUDE_CODE", builtInRunnerCommand("claude_code")),
130
132
  },
131
- localRepoPaths: {
133
+ legacyRepoPaths: {
132
134
  codex: env("LC_AGENT_CODEX_CWD", savedConfigValue("codexCwd", process.cwd())),
133
135
  claudeCode: env("LC_AGENT_CLAUDE_CODE_CWD", savedConfigValue("claudeCodeCwd", process.cwd())),
134
136
  },
135
137
  };
136
138
 
137
- if (!config.token) {
138
- // eslint-disable-next-line no-console
139
- console.error(
140
- `Missing LaunchChair Agent API token. Run "launchchair login" or set LAUNCHCHAIR_AGENT_TOKEN. Checked ${CONFIG_PATH}.`,
141
- );
142
- process.exit(1);
143
- }
144
-
145
- if (
146
- !config.mockResponse &&
147
- !config.runnerCommands.generic &&
148
- !config.runnerCommands.gpt &&
149
- !config.runnerCommands.codex &&
150
- !config.runnerCommands.chatgpt &&
151
- !config.runnerCommands.claude &&
152
- !config.runnerCommands.codexTarget &&
153
- !config.runnerCommands.claudeCode
154
- ) {
155
- // eslint-disable-next-line no-console
156
- console.error(
157
- "No runner command configured for MCP bridge. Set LC_AGENT_RUNNER_CMD* or use the built-in Codex/Claude Code runners; ChatGPT/Claude API fallbacks require provider API keys.",
158
- );
159
- process.exit(1);
160
- }
139
+ let statusPromise = null;
140
+ let initialized = false;
141
+ const capabilities = createMcpCapabilities({ config, configPath: CONFIG_PATH, api, resolveProjectId, onAuthChange: () => { statusPromise = null; if (initialized) writeMessage({ jsonrpc: "2.0", method: "notifications/tools/list_changed" }); } });
161
142
 
162
143
  async function api(path, { method = "GET", body } = {}) {
144
+ const token = capabilities.currentToken();
145
+ if (!token) {
146
+ const error = new Error("Login required. Call launchchair_login and complete browser approval.");
147
+ error.status = 401;
148
+ error.payload = { code: "login_required", nextTool: "launchchair_login" };
149
+ throw error;
150
+ }
163
151
  const response = await fetch(`${config.baseUrl}${path}`, {
164
152
  method,
165
153
  headers: {
166
- Authorization: `Bearer ${config.token}`,
154
+ Authorization: `Bearer ${token}`,
167
155
  "Content-Type": "application/json",
168
156
  },
169
157
  body: body === undefined ? undefined : JSON.stringify(body),
158
+ signal: AbortSignal.timeout(30_000),
170
159
  });
171
160
 
172
- let payload = null;
173
- try {
174
- payload = await response.json();
175
- } catch (_error) {
176
- payload = null;
177
- }
161
+ const raw = await response.text();
162
+ let payload;
163
+ try { payload = raw ? JSON.parse(raw) : null; } catch { payload = { content: raw, contentType: response.headers.get("content-type") }; }
178
164
 
179
165
  if (!response.ok) {
180
166
  const err = new Error(`HTTP ${response.status}: ${String(payload?.error ?? response.statusText ?? "Request failed")}`);
181
167
  err.status = response.status;
182
- err.payload = payload;
168
+ err.payload = response.status === 401 ? { ...payload, code: "login_required", nextTool: "launchchair_login" } : payload;
183
169
  err.retryAfterSeconds = Number.parseInt(response.headers.get("retry-after") ?? "", 10) || null;
184
170
  throw err;
185
171
  }
186
172
  return payload;
187
173
  }
188
174
 
189
- let statusPromise = null;
190
-
191
175
  function loadStatus() {
192
176
  if (!statusPromise) {
193
- statusPromise = api("/api/agent/v1").catch(() => null);
177
+ statusPromise = api("/api/agent/v1").catch(() => { statusPromise = null; return null; });
194
178
  }
195
179
  return statusPromise;
196
180
  }
197
181
 
198
182
  function serverInstructions(status) {
199
183
  const featureInstructions = String(status?.capabilities?.mcpServerInstructions ?? "").trim();
200
- return featureInstructions ? `${BASE_SERVER_INSTRUCTIONS} ${featureInstructions}` : BASE_SERVER_INSTRUCTIONS;
184
+ return `${BASE_SERVER_INSTRUCTIONS} ${featureInstructions} ${MCP_WORKFLOW_INSTRUCTIONS}`;
201
185
  }
202
186
 
203
187
  function targetForRun(run) {
@@ -227,6 +211,32 @@ function commandForRun(run) {
227
211
  return config.runnerCommands.generic;
228
212
  }
229
213
 
214
+ function runnerTargetForRun(run) {
215
+ return targetForRun(run) === "claude_code" ? "claude_code" : "codex";
216
+ }
217
+
218
+ function resolveWorkspaceForRun(run, workspaceContext = run?.runnerWorkspace, options = {}) {
219
+ const target = runnerTargetForRun(run);
220
+ const context = workspaceContext && typeof workspaceContext === "object" && !Array.isArray(workspaceContext)
221
+ ? workspaceContext
222
+ : {};
223
+ const legacyPath = target === "claude_code" ? config.legacyRepoPaths.claudeCode : config.legacyRepoPaths.codex;
224
+ return resolveProjectWorkspace({
225
+ configDir: CONFIG_DIR,
226
+ configPath: CONFIG_PATH,
227
+ projectId: String(run?.projectId ?? context.projectId ?? "").trim(),
228
+ repository: context.repository,
229
+ requireRepository: context.requiresRepository === true,
230
+ preferRepository: options.preferRepository === true,
231
+ legacyPaths: [legacyPath],
232
+ });
233
+ }
234
+
235
+ async function getRunnerWorkspaceContext(projectId) {
236
+ const payload = await api(`/api/agent/v1/projects/${encodeURIComponent(projectId)}/runner-workspace`);
237
+ return payload?.workspace && typeof payload.workspace === "object" ? payload.workspace : { projectId };
238
+ }
239
+
230
240
  async function executeRunner(run) {
231
241
  if (config.mockResponse) {
232
242
  return {
@@ -245,6 +255,10 @@ async function executeRunner(run) {
245
255
  }
246
256
  const command = buildRunnerCommand(template, run);
247
257
  const parsedCommand = parseRunnerCommand(command);
258
+ const workspace = resolveWorkspaceForRun(run);
259
+ const workspaceEnvName = runnerTargetForRun(run) === "claude_code"
260
+ ? "LC_AGENT_CLAUDE_CODE_CWD"
261
+ : "LC_AGENT_CODEX_CWD";
248
262
  const startedAt = Date.now();
249
263
  return new Promise((resolve, reject) => {
250
264
  const child = spawn(parsedCommand.file, parsedCommand.args, {
@@ -259,6 +273,8 @@ async function executeRunner(run) {
259
273
  LC_AGENT_CARD_ID: String(run.cardId ?? ""),
260
274
  LC_AGENT_PROMPT_MODE: String(run.promptMode ?? ""),
261
275
  LC_AGENT_RUN_TYPE: String(run.runType ?? ""),
276
+ [workspaceEnvName]: workspace.path,
277
+ ...(workspace.source === "isolated" && run?.runnerWorkspace?.requiresRepository !== true ? { LC_AGENT_CODEX_SKIP_GIT_CHECK: "1" } : {}),
262
278
  },
263
279
  });
264
280
  let stdout = "";
@@ -286,6 +302,7 @@ async function executeRunner(run) {
286
302
  stderr,
287
303
  exitCode: code ?? 0,
288
304
  durationMs,
305
+ workspace,
289
306
  });
290
307
  });
291
308
  child.stdin.write(String(run.prompt ?? ""));
@@ -314,6 +331,7 @@ async function claimNextRun(args = {}) {
314
331
  method: "POST",
315
332
  body: {
316
333
  projectId,
334
+ ...(args.runId ? { runId: String(args.runId) } : {}),
317
335
  clientName: String(args.clientName ?? "").trim() || config.clientName,
318
336
  clientType: normalizeTarget(args.clientType ?? config.clientType, "other"),
319
337
  clientVersion: String(args.clientVersion ?? "").trim() || config.clientVersion,
@@ -366,6 +384,7 @@ async function runOnce(args = {}) {
366
384
  runIntent: run.runIntent ?? "initial",
367
385
  attemptNumber: run.attemptNumber ?? 1,
368
386
  maxAttempts: run.maxAttempts ?? 1,
387
+ localWorkspaceSource: execution.workspace?.source ?? null,
369
388
  },
370
389
  error: null,
371
390
  clientName: String(args.clientName ?? "").trim() || config.clientName,
@@ -520,22 +539,35 @@ async function continueProjectLoop(args = {}) {
520
539
  typeof args.ideaSnapshot === "object" && args.ideaSnapshot !== null && !Array.isArray(args.ideaSnapshot)
521
540
  ? args.ideaSnapshot
522
541
  : null;
523
- const runnerTarget = config.modeTargets.codex === "claude_code" ? "claude_code" : config.modeTargets.codex;
524
- const localRepoPath =
525
- runnerTarget === "claude_code" ? config.localRepoPaths.claudeCode : config.localRepoPaths.codex;
526
- const agentSetup = {
527
- localRepoPath,
528
- runnerTarget,
529
- clientType: config.clientType,
530
- ...(typeof args.agentSetup === "object" && args.agentSetup !== null && !Array.isArray(args.agentSetup)
531
- ? args.agentSetup
532
- : {}),
542
+ const runnerTarget = config.modeTargets.codex === "claude_code" ? "claude_code" : "codex";
543
+ let runnerWorkspaceContext = await getRunnerWorkspaceContext(projectId);
544
+ let agentSetup = null;
545
+ const refreshAgentSetup = async () => {
546
+ runnerWorkspaceContext = await getRunnerWorkspaceContext(projectId);
547
+ const resolvedAgentWorkspace = resolveWorkspaceForRun(
548
+ { projectId, promptMode: "codex", runnerWorkspace: runnerWorkspaceContext },
549
+ runnerWorkspaceContext,
550
+ { preferRepository: true },
551
+ );
552
+ agentSetup = {
553
+ ...(typeof args.agentSetup === "object" && args.agentSetup !== null && !Array.isArray(args.agentSetup)
554
+ ? args.agentSetup
555
+ : {}),
556
+ localRepoPath: resolvedAgentWorkspace.path,
557
+ runnerTarget,
558
+ clientType: config.clientType,
559
+ repositoryVerified: resolvedAgentWorkspace.repositoryVerified,
560
+ repositoryUrl: resolvedAgentWorkspace.repositoryUrl,
561
+ workspaceSource: resolvedAgentWorkspace.source,
562
+ };
533
563
  };
564
+ await refreshAgentSetup();
534
565
  let pendingChoiceSelections = normalizeChoiceSelections(args);
535
566
  const steps = [];
536
567
  let final = null;
537
568
 
538
569
  for (let index = 0; index < maxSteps; index += 1) {
570
+ if (index > 0) await refreshAgentSetup();
539
571
  const loop = await api(`/api/agent/v1/projects/${encodeURIComponent(projectId)}/loop/continue`, {
540
572
  method: "POST",
541
573
  body: {
@@ -553,13 +585,19 @@ async function continueProjectLoop(args = {}) {
553
585
  break;
554
586
  }
555
587
 
556
- const run = loop.run;
557
- if (!run?.id || !run?.prompt) {
558
- final = loop;
588
+ const claimed = loop.run?.id ? await claimNextRun({ ...args, projectId, runId: loop.run.id }) : null;
589
+ if (!claimed) {
590
+ final = { ...loop, run: loop.run?.id ? { id: loop.run.id } : null, message: "Run already claimed; do not execute it again." };
591
+ break;
592
+ }
593
+ const run = { ...claimed, runnerWorkspace: claimed.runnerWorkspace ?? loop.runnerWorkspace ?? runnerWorkspaceContext };
594
+ let execution;
595
+ try { execution = await executeRunner(run); }
596
+ catch (error) {
597
+ await completeRun({ runId: run.id, status: "failed", error: error.message, resultRaw: error.stdout || error.stderr || "" });
598
+ final = { status: "stopped_after_failed_step", projectId, runId: run.id, message: error.message };
559
599
  break;
560
600
  }
561
-
562
- const execution = await executeRunner(run);
563
601
  const stdout = String(execution.stdout ?? "").trim();
564
602
  const stderr = String(execution.stderr ?? "").trim();
565
603
  const output = stdout || stderr;
@@ -578,6 +616,7 @@ async function continueProjectLoop(args = {}) {
578
616
  attemptNumber: run.attemptNumber ?? 1,
579
617
  maxAttempts: run.maxAttempts ?? 1,
580
618
  projectLoopStep: index + 1,
619
+ localWorkspaceSource: execution.workspace?.source ?? null,
581
620
  },
582
621
  error: null,
583
622
  clientName: String(args.clientName ?? "").trim() || config.clientName,
@@ -645,6 +684,7 @@ async function createProject(args = {}) {
645
684
  delete createArgs.maxSteps;
646
685
  delete createArgs.fullLoop;
647
686
  delete createArgs.buildMaxSteps;
687
+ delete createArgs.executionMode;
648
688
  const body = {
649
689
  ...createArgs,
650
690
  name: String(args.name ?? args.projectName ?? "").trim(),
@@ -671,6 +711,8 @@ async function createProject(args = {}) {
671
711
  return payload;
672
712
  }
673
713
 
714
+ if (args.executionMode !== "local_runner") return { ...payload, next: { ...payload.next, tool: "launchchair_next_step", arguments: { projectId, stopPhaseKey: "build", completionTarget: fullLoop ? "phase_complete" : "build_board_ready" }, execution: "host", instructions: MCP_WORKFLOW_INSTRUCTIONS } };
715
+
674
716
  const planning = await continueProjectLoop({
675
717
  projectId,
676
718
  stopPhaseKey: "build",
@@ -949,8 +991,8 @@ function annotateTool(tool) {
949
991
  ...tool,
950
992
  annotations: {
951
993
  ...(tool.annotations ?? {}),
952
- readOnlyHint: READ_ONLY_TOOLS.has(tool.name),
953
- destructiveHint: false,
994
+ readOnlyHint: tool.annotations?.readOnlyHint ?? READ_ONLY_TOOLS.has(tool.name),
995
+ destructiveHint: tool.annotations?.destructiveHint ?? false,
954
996
  openWorldHint: true,
955
997
  },
956
998
  };
@@ -966,7 +1008,12 @@ function mergeToolOverrides(baseTools, overrides) {
966
1008
  merged.push(override);
967
1009
  }
968
1010
  }
969
- return merged.map(annotateTool);
1011
+ return merged.map((tool) => {
1012
+ if (["launchchair_create_project", "launchchair_create_feature_project"].includes(tool.name)) tool = { ...tool, inputSchema: { ...tool.inputSchema, properties: { ...tool.inputSchema.properties, idempotencyKey: { type: "string" }, ideaIntent: { type: "string", enum: ["preserve", "explore"], default: "preserve" }, validationGate: VALIDATION_GATE_SCHEMA, executionMode: { type: "string", enum: ["host", "local_runner"], default: "host" } } } };
1013
+ if (tool.name === "launchchair_claim_next_run") tool = { ...tool, inputSchema: { ...tool.inputSchema, properties: { ...tool.inputSchema.properties, runId: { type: "string" } } } };
1014
+ if (tool.name === "launchchair_continue_project") tool = { ...tool, description: "Advance one step in the current host agent by default. Only use executionMode=local_runner to spawn a separate local CLI.", inputSchema: { ...tool.inputSchema, properties: { ...tool.inputSchema.properties, executionMode: { type: "string", enum: ["host", "local_runner"], default: "host" } } } };
1015
+ return annotateTool(tool);
1016
+ });
970
1017
  }
971
1018
 
972
1019
  function writeMessage(obj) {
@@ -1009,7 +1056,7 @@ async function handleRpc(request) {
1009
1056
  writeResult(id, {
1010
1057
  protocolVersion: "2024-11-05",
1011
1058
  capabilities: {
1012
- tools: {},
1059
+ tools: { listChanged: true },
1013
1060
  },
1014
1061
  serverInfo: {
1015
1062
  name: SERVER_NAME,
@@ -1021,6 +1068,7 @@ async function handleRpc(request) {
1021
1068
  }
1022
1069
 
1023
1070
  if (method === "notifications/initialized") {
1071
+ initialized = true;
1024
1072
  return;
1025
1073
  }
1026
1074
 
@@ -1029,7 +1077,7 @@ async function handleRpc(request) {
1029
1077
  const overrides = Array.isArray(status?.capabilities?.mcpToolOverrides)
1030
1078
  ? status.capabilities.mcpToolOverrides
1031
1079
  : [];
1032
- const tools = mergeToolOverrides(MCP_TOOLS, overrides);
1080
+ const tools = mergeToolOverrides([...MCP_TOOLS, ...EXTRA_MCP_TOOLS], overrides);
1033
1081
  writeResult(id, { tools });
1034
1082
  return;
1035
1083
  }
@@ -1039,9 +1087,13 @@ async function handleRpc(request) {
1039
1087
  const toolName = String(params.name ?? "");
1040
1088
  const args = typeof params.arguments === "object" && params.arguments ? params.arguments : {};
1041
1089
  try {
1090
+ if (EXTRA_MCP_TOOLS.some((tool) => tool.name === toolName)) {
1091
+ writeResult(id, textResult(await capabilities.dispatch(toolName, args)));
1092
+ return;
1093
+ }
1042
1094
  if (toolName === "launchchair_status") {
1043
1095
  const status = await api("/api/agent/v1");
1044
- writeResult(id, textResult(status));
1096
+ writeResult(id, textResult({ ...status, authenticated: true }));
1045
1097
  return;
1046
1098
  }
1047
1099
  if (toolName === "launchchair_create_project") {
@@ -1090,7 +1142,7 @@ async function handleRpc(request) {
1090
1142
  return;
1091
1143
  }
1092
1144
  if (toolName === "launchchair_continue_project") {
1093
- const payload = await continueProjectLoop(args);
1145
+ const payload = args.executionMode === "local_runner" ? await continueProjectLoop(args) : await capabilities.dispatch("launchchair_next_step", { ...args, executionMode: undefined, maxSteps: undefined });
1094
1146
  writeResult(id, textResult(payload));
1095
1147
  return;
1096
1148
  }