@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/AGENTS.md +35 -32
- package/README.md +63 -32
- package/launchchair-cli.mjs +253 -42
- package/local-bridge.mjs +34 -3
- package/mcp-bridge-server.mjs +113 -61
- package/mcp-capabilities.mjs +227 -0
- package/package.json +3 -1
- package/project-workspace.mjs +309 -0
- package/server.json +4 -4
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.
|
|
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
|
|
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
|
|
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
|
});
|
package/mcp-bridge-server.mjs
CHANGED
|
@@ -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.
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 ${
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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" :
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
557
|
-
if (!
|
|
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(
|
|
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
|
}
|