@akira-tl/forgerelay 0.2.3 → 0.2.4
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/CHANGELOG.md +15 -0
- package/dist/hooks.js +18 -4
- package/dist/mcp/server-instructions.js +10 -8
- package/dist/pi-tools.js +1 -9
- package/dist/process-sessions.js +56 -8
- package/dist/server.js +217 -136
- package/dist/workspace-store.js +13 -0
- package/dist/workspaces.js +275 -25
- package/docs/chatgpt-coding-workflow.md +18 -1
- package/docs/configuration.md +20 -2
- package/docs/debugging.md +2 -2
- package/docs/security.md +10 -0
- package/package.json +1 -1
- package/scripts/debug/accept.mjs +12 -2
package/dist/server.js
CHANGED
|
@@ -21,10 +21,10 @@ import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
|
|
|
21
21
|
import { buildServerInstructions, buildShellMutationPolicy, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
|
|
22
22
|
import { createOpenAIIncomingArtifactAdapter, } from "./incoming-artifacts.js";
|
|
23
23
|
import { logEvent, requestIp, requestPath, commandPreview, sessionIdPrefix, workspaceLogLabel, } from "./logger.js";
|
|
24
|
-
import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool,
|
|
24
|
+
import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, writeFileTool, } from "./pi-tools.js";
|
|
25
25
|
import { SingleUserOAuthProvider } from "./oauth-provider.js";
|
|
26
26
|
import { McpSessionRegistry, } from "./mcp-sessions.js";
|
|
27
|
-
import { ProcessSessionManager } from "./process-sessions.js";
|
|
27
|
+
import { ProcessSessionManager, } from "./process-sessions.js";
|
|
28
28
|
import { createReviewCheckpointManager } from "./review-checkpoints.js";
|
|
29
29
|
import { openAiConversationScopeId } from "./request-meta.js";
|
|
30
30
|
import { shutdownHttpServer } from "./server-shutdown.js";
|
|
@@ -314,6 +314,45 @@ function processResult(snapshot) {
|
|
|
314
314
|
: `Process exited with code ${snapshot.exitCode ?? "unknown"}.`;
|
|
315
315
|
return snapshot.output ? `${snapshot.output.replace(/\n$/, "")}\n${status}` : status;
|
|
316
316
|
}
|
|
317
|
+
function completedProcessResult(snapshot) {
|
|
318
|
+
const status = snapshot.signal
|
|
319
|
+
? `Background process ${snapshot.sessionId} exited after signal ${snapshot.signal}.`
|
|
320
|
+
: `Background process ${snapshot.sessionId} exited with code ${snapshot.exitCode ?? "unknown"}.`;
|
|
321
|
+
const command = `Command: ${snapshot.command}`;
|
|
322
|
+
const output = snapshot.output ? `\n${snapshot.output.replace(/\n$/, "")}` : "";
|
|
323
|
+
return `${status}\n${command}${output}`;
|
|
324
|
+
}
|
|
325
|
+
function attachCompletedProcessNotices(processSessions, workspaceId, result) {
|
|
326
|
+
if (result instanceof Error) {
|
|
327
|
+
const completed = processSessions.takeCompleted(workspaceId);
|
|
328
|
+
if (completed.length > 0) {
|
|
329
|
+
result.message = [
|
|
330
|
+
result.message,
|
|
331
|
+
...completed.map((snapshot) => completedProcessResult(snapshot)),
|
|
332
|
+
].join("\n\n");
|
|
333
|
+
}
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
if (typeof result !== "object" || result === null)
|
|
337
|
+
return result;
|
|
338
|
+
const content = result.content;
|
|
339
|
+
if (!Array.isArray(content))
|
|
340
|
+
return result;
|
|
341
|
+
const structured = result.structuredContent;
|
|
342
|
+
const currentSessionId = structured?.running === true && typeof structured.sessionId === "number"
|
|
343
|
+
? structured.sessionId
|
|
344
|
+
: undefined;
|
|
345
|
+
const completed = processSessions.takeCompleted(workspaceId, undefined, currentSessionId);
|
|
346
|
+
if (completed.length === 0)
|
|
347
|
+
return result;
|
|
348
|
+
return {
|
|
349
|
+
...result,
|
|
350
|
+
content: [
|
|
351
|
+
...content,
|
|
352
|
+
...completed.map((snapshot) => textBlock(completedProcessResult(snapshot))),
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
}
|
|
317
356
|
function processOutputSchema() {
|
|
318
357
|
return resultOutputSchema({
|
|
319
358
|
sessionId: z.number().optional(),
|
|
@@ -367,86 +406,89 @@ function workspaceHookInvocation(workspace) {
|
|
|
367
406
|
function toolResultIsError(result) {
|
|
368
407
|
return typeof result === "object" && result !== null && result.isError === true;
|
|
369
408
|
}
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
outputSchema: processOutputSchema(),
|
|
403
|
-
...toolWidgetDescriptorMeta(config, "shell"),
|
|
404
|
-
annotations: SHELL_TOOL_ANNOTATIONS,
|
|
405
|
-
}, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, maxOutputTokens }, extra) => {
|
|
406
|
-
const workspace = workspaces.getWorkspace(workspaceId);
|
|
407
|
-
return runToolWithHooks(hooks, {
|
|
408
|
-
tool: "exec_command",
|
|
409
|
-
invocation: workspaceHookInvocation(workspace),
|
|
410
|
-
payload: { command: cmd, workingDirectory: workingDirectory ?? "." },
|
|
411
|
-
operation: async () => {
|
|
412
|
-
const startedAt = performance.now();
|
|
413
|
-
const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
|
|
414
|
-
const snapshot = await processSessions.start({
|
|
415
|
-
workspaceId,
|
|
416
|
-
command: cmd,
|
|
417
|
-
cwd,
|
|
418
|
-
workspaceRoot: workspace.root,
|
|
419
|
-
tty,
|
|
420
|
-
columns,
|
|
421
|
-
rows,
|
|
422
|
-
yieldTimeMs,
|
|
423
|
-
maxOutputTokens,
|
|
424
|
-
});
|
|
425
|
-
logToolCall(config, {
|
|
426
|
-
tool: "exec_command",
|
|
427
|
-
...workspaceLogContext(workspace, extra.sessionId),
|
|
428
|
-
workingDirectory: workingDirectory ?? ".",
|
|
429
|
-
command: cmd,
|
|
430
|
-
commandLength: cmd.length,
|
|
431
|
-
exitCode: snapshot.exitCode,
|
|
432
|
-
running: snapshot.running,
|
|
433
|
-
processSessionId: snapshot.sessionId,
|
|
434
|
-
success: snapshot.running || snapshot.exitCode === 0,
|
|
435
|
-
durationMs: Math.round(performance.now() - startedAt),
|
|
436
|
-
});
|
|
437
|
-
return processToolResponse("exec_command", workspaceId, snapshot, {
|
|
438
|
-
command: cmd,
|
|
439
|
-
workingDirectory: workingDirectory ?? ".",
|
|
440
|
-
running: snapshot.running,
|
|
441
|
-
exitCode: snapshot.exitCode,
|
|
442
|
-
wallTimeMs: snapshot.wallTimeMs,
|
|
443
|
-
});
|
|
409
|
+
function registerProcessTools(server, config, workspaces, processSessions, hooks) {
|
|
410
|
+
if (config.toolMode === "codex") {
|
|
411
|
+
registerAppTool(server, "exec_command", {
|
|
412
|
+
title: "Execute command",
|
|
413
|
+
description: `Run a command inside an open workspace. Returns its result when it exits during the yield window, otherwise returns a sessionId for write_stdin. Use this for file inspection, tests, builds, package scripts, generators, formatters, and long-running processes. ${buildShellMutationPolicy()} Call open_workspace first and pass workspaceId.`,
|
|
414
|
+
inputSchema: {
|
|
415
|
+
workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
|
|
416
|
+
cmd: z.string().min(1).describe("Shell command to execute."),
|
|
417
|
+
tty: z
|
|
418
|
+
.boolean()
|
|
419
|
+
.optional()
|
|
420
|
+
.describe("Allocate a pseudo-terminal for interactive commands. Defaults to false."),
|
|
421
|
+
columns: z.number().int().min(1).max(1_000).optional().describe("Initial PTY width. Defaults to 80."),
|
|
422
|
+
rows: z.number().int().min(1).max(1_000).optional().describe("Initial PTY height. Defaults to 24."),
|
|
423
|
+
workingDirectory: z
|
|
424
|
+
.string()
|
|
425
|
+
.optional()
|
|
426
|
+
.describe("Working directory relative to the workspace root. Defaults to the workspace root."),
|
|
427
|
+
yieldTimeMs: z
|
|
428
|
+
.number()
|
|
429
|
+
.int()
|
|
430
|
+
.min(0)
|
|
431
|
+
.max(30_000)
|
|
432
|
+
.optional()
|
|
433
|
+
.describe("Milliseconds to wait before returning a running session. Defaults to 10000."),
|
|
434
|
+
maxOutputTokens: z
|
|
435
|
+
.number()
|
|
436
|
+
.int()
|
|
437
|
+
.positive()
|
|
438
|
+
.max(100_000)
|
|
439
|
+
.optional()
|
|
440
|
+
.describe("Approximate output token budget. Defaults to 10000."),
|
|
444
441
|
},
|
|
442
|
+
outputSchema: processOutputSchema(),
|
|
443
|
+
...toolWidgetDescriptorMeta(config, "shell"),
|
|
444
|
+
annotations: SHELL_TOOL_ANNOTATIONS,
|
|
445
|
+
}, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, maxOutputTokens }, extra) => {
|
|
446
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
447
|
+
return runToolWithHooks(hooks, {
|
|
448
|
+
tool: "exec_command",
|
|
449
|
+
invocation: workspaceHookInvocation(workspace),
|
|
450
|
+
payload: { command: cmd, workingDirectory: workingDirectory ?? "." },
|
|
451
|
+
operation: async () => {
|
|
452
|
+
const startedAt = performance.now();
|
|
453
|
+
const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
|
|
454
|
+
const snapshot = await processSessions.start({
|
|
455
|
+
workspaceId,
|
|
456
|
+
command: cmd,
|
|
457
|
+
cwd,
|
|
458
|
+
workspaceRoot: workspace.root,
|
|
459
|
+
tty,
|
|
460
|
+
columns,
|
|
461
|
+
rows,
|
|
462
|
+
yieldTimeMs,
|
|
463
|
+
maxOutputTokens,
|
|
464
|
+
codexCi: true,
|
|
465
|
+
});
|
|
466
|
+
logToolCall(config, {
|
|
467
|
+
tool: "exec_command",
|
|
468
|
+
...workspaceLogContext(workspace, extra.sessionId),
|
|
469
|
+
workingDirectory: workingDirectory ?? ".",
|
|
470
|
+
command: cmd,
|
|
471
|
+
commandLength: cmd.length,
|
|
472
|
+
exitCode: snapshot.exitCode,
|
|
473
|
+
running: snapshot.running,
|
|
474
|
+
processSessionId: snapshot.sessionId,
|
|
475
|
+
success: snapshot.running || snapshot.exitCode === 0,
|
|
476
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
477
|
+
});
|
|
478
|
+
return processToolResponse("exec_command", workspaceId, snapshot, {
|
|
479
|
+
command: cmd,
|
|
480
|
+
workingDirectory: workingDirectory ?? ".",
|
|
481
|
+
running: snapshot.running,
|
|
482
|
+
exitCode: snapshot.exitCode,
|
|
483
|
+
wallTimeMs: snapshot.wallTimeMs,
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
});
|
|
445
487
|
});
|
|
446
|
-
}
|
|
488
|
+
}
|
|
447
489
|
registerAppTool(server, "write_stdin", {
|
|
448
490
|
title: "Write to process",
|
|
449
|
-
description: "Poll or write characters to a process returned by exec_command. Omit chars or pass an empty string to poll.
|
|
491
|
+
description: "Poll or write characters to a running process returned by bash or exec_command. Omit chars or pass an empty string to poll. Waiting never kills the process; pass \\u0003 to explicitly send Ctrl-C.",
|
|
450
492
|
inputSchema: {
|
|
451
493
|
workspaceId: z.string().describe("Workspace identifier used to start the process."),
|
|
452
494
|
sessionId: z.number().describe("Process session identifier returned by exec_command."),
|
|
@@ -457,9 +499,9 @@ function registerCodexProcessTools(server, config, workspaces, processSessions,
|
|
|
457
499
|
.number()
|
|
458
500
|
.int()
|
|
459
501
|
.min(0)
|
|
460
|
-
.max(
|
|
502
|
+
.max(300_000)
|
|
461
503
|
.optional()
|
|
462
|
-
.describe("Milliseconds to
|
|
504
|
+
.describe("Milliseconds to keep waiting before returning again, max 300000. Polling defaults to 5000; interaction defaults to 250."),
|
|
463
505
|
maxOutputTokens: z
|
|
464
506
|
.number()
|
|
465
507
|
.int()
|
|
@@ -515,7 +557,7 @@ function registerCodexProcessTools(server, config, workspaces, processSessions,
|
|
|
515
557
|
}
|
|
516
558
|
export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters) {
|
|
517
559
|
const toolDescriptions = buildToolDescriptions(config);
|
|
518
|
-
const hooks = new HookRunner(config.hooks, config.logging);
|
|
560
|
+
const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result));
|
|
519
561
|
const server = new McpServer({
|
|
520
562
|
name: "forgerelay",
|
|
521
563
|
title: "ForgeRelay",
|
|
@@ -552,11 +594,16 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
552
594
|
});
|
|
553
595
|
registerAppTool(server, "open_workspace", {
|
|
554
596
|
title: "Open workspace",
|
|
555
|
-
description: "Open a local
|
|
597
|
+
description: "Open or resume a local coding workspace. A conversation keeps a stable workspaceId for a project, while different conversations normally receive different logical workspaceIds that may point at the same physical checkout or worktree. Pass workspaceId to explicitly resume an existing logical workspace in this conversation. Default to checkout mode and only use mode=\"worktree\" when the user explicitly asks for isolated or parallel Git work. Workspaces idle for more than two days are reported for user-directed cleanup or resumption.",
|
|
556
598
|
inputSchema: {
|
|
557
599
|
path: z
|
|
558
600
|
.string()
|
|
559
|
-
.
|
|
601
|
+
.optional()
|
|
602
|
+
.describe("Project path to open. Required unless workspaceId is supplied. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
|
|
603
|
+
workspaceId: z
|
|
604
|
+
.string()
|
|
605
|
+
.optional()
|
|
606
|
+
.describe("Existing logical workspace ID to resume in this conversation. When supplied, ForgeRelay resumes that workspace rather than allocating another ID."),
|
|
560
607
|
mode: z
|
|
561
608
|
.enum(["checkout", "worktree"])
|
|
562
609
|
.optional()
|
|
@@ -568,7 +615,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
568
615
|
newWorktree: z
|
|
569
616
|
.boolean()
|
|
570
617
|
.optional()
|
|
571
|
-
.describe("When true, create another isolated managed worktree instead of reusing the existing worktree
|
|
618
|
+
.describe("When true, create another isolated managed Git worktree instead of reusing the existing physical worktree. Use only when the user explicitly requests separate Git isolation."),
|
|
619
|
+
newWorkspace: z
|
|
620
|
+
.boolean()
|
|
621
|
+
.optional()
|
|
622
|
+
.describe("When true, allocate a fresh logical workspaceId for the same physical checkout or worktree and bind this conversation to it. Use only after the user explicitly requests a new logical workspace."),
|
|
572
623
|
},
|
|
573
624
|
outputSchema: {
|
|
574
625
|
workspaceId: z.string(),
|
|
@@ -597,6 +648,16 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
597
648
|
managed: z.boolean(),
|
|
598
649
|
current: z.boolean(),
|
|
599
650
|
})),
|
|
651
|
+
staleWorkspaces: z.array(z.object({
|
|
652
|
+
workspaceId: z.string(),
|
|
653
|
+
root: z.string(),
|
|
654
|
+
mode: z.enum(["checkout", "worktree"]),
|
|
655
|
+
lastUsedAt: z.string(),
|
|
656
|
+
idleMs: z.number().nonnegative(),
|
|
657
|
+
branch: z.string().optional(),
|
|
658
|
+
targetBranch: z.string().optional(),
|
|
659
|
+
managed: z.boolean(),
|
|
660
|
+
})),
|
|
600
661
|
agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
|
|
601
662
|
availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema).optional(),
|
|
602
663
|
skills: z.array(workspaceSkillOutputSchema).optional(),
|
|
@@ -612,10 +673,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
612
673
|
idempotentHint: false,
|
|
613
674
|
openWorldHint: false,
|
|
614
675
|
},
|
|
615
|
-
}, async ({ path, mode, baseRef, newWorktree }, { _meta, sessionId }) => {
|
|
676
|
+
}, async ({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace }, { _meta, sessionId }) => {
|
|
616
677
|
const startedAt = performance.now();
|
|
617
|
-
const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, } = await workspaces.openWorkspace({ path, mode, baseRef, newWorktree }, {
|
|
678
|
+
const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace }, {
|
|
679
|
+
conversationScopeId: openAiConversationScopeId(_meta),
|
|
680
|
+
protectedWorkspaceIds: processSessions.activeWorkspaceIds(),
|
|
681
|
+
});
|
|
618
682
|
const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
|
|
683
|
+
const staleWorkspaces = await workspaces.listStaleWorkspaces(workspace);
|
|
619
684
|
if (config.widgets === "changes") {
|
|
620
685
|
await reviewCheckpoints.initializeWorkspace({
|
|
621
686
|
workspaceId: workspace.id,
|
|
@@ -701,6 +766,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
701
766
|
knownWorktrees.length > 0
|
|
702
767
|
? `Known worktrees: ${knownWorktrees.map((worktree) => `${worktree.path} [${worktree.workspaceId}]${worktree.branch ? ` branch=${worktree.branch}` : ""}${worktree.targetBranch ? ` target=${worktree.targetBranch}` : ""}${worktree.current ? " (current)" : ""}`).join(", ")}`
|
|
703
768
|
: undefined,
|
|
769
|
+
staleWorkspaces.length > 0
|
|
770
|
+
? `Idle logical workspaces for this same physical workspace (>2 days): ${staleWorkspaces.map((stale) => `${stale.workspaceId} last-used=${stale.lastUsedAt}`).join(", ")}. Tell the user these are available to resume or explicitly close; do not clean them up automatically.`
|
|
771
|
+
: undefined,
|
|
704
772
|
instruction,
|
|
705
773
|
].filter(Boolean).join("\n"),
|
|
706
774
|
},
|
|
@@ -712,7 +780,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
712
780
|
success: true,
|
|
713
781
|
durationMs: Math.round(performance.now() - startedAt),
|
|
714
782
|
});
|
|
715
|
-
return attachHookReports({
|
|
783
|
+
return hooks.decorateResult(workspace.id, attachHookReports({
|
|
716
784
|
content: resultContent,
|
|
717
785
|
_meta: {
|
|
718
786
|
tool: "open_workspace",
|
|
@@ -726,6 +794,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
726
794
|
sourceRoot: workspace.sourceRoot,
|
|
727
795
|
worktree: workspace.worktree,
|
|
728
796
|
worktrees: knownWorktrees,
|
|
797
|
+
staleWorkspaces,
|
|
729
798
|
agentsFiles: cardAgentsFiles,
|
|
730
799
|
availableAgentsFiles: cardAvailableAgentsFiles,
|
|
731
800
|
skills: cardSkills,
|
|
@@ -749,6 +818,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
749
818
|
sourceRoot: workspace.sourceRoot,
|
|
750
819
|
worktree: workspace.worktree,
|
|
751
820
|
worktrees: knownWorktrees,
|
|
821
|
+
staleWorkspaces,
|
|
752
822
|
...(includeBootstrapContext
|
|
753
823
|
? {
|
|
754
824
|
agentsFiles: loadedAgentsFiles,
|
|
@@ -761,7 +831,35 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
761
831
|
: {}),
|
|
762
832
|
instruction,
|
|
763
833
|
},
|
|
764
|
-
}, hookReports);
|
|
834
|
+
}, hookReports));
|
|
835
|
+
});
|
|
836
|
+
registerAppTool(server, toolNames.closeWorkspace, {
|
|
837
|
+
title: "Close logical workspace",
|
|
838
|
+
description: "Release one logical ForgeRelay workspaceId after the user explicitly chooses to clean it up. This never deletes checkout files. A worktree handle can be released only when another logical handle still anchors the same physical worktree; use close_worktree to finalize and remove the last managed worktree. Running or unconsumed background processes prevent closure.",
|
|
839
|
+
inputSchema: {
|
|
840
|
+
workspaceId: z.string().describe("Logical workspace ID to release."),
|
|
841
|
+
},
|
|
842
|
+
outputSchema: resultOutputSchema({ workspaceId: z.string() }),
|
|
843
|
+
_meta: {},
|
|
844
|
+
annotations: WRITE_TOOL_ANNOTATIONS,
|
|
845
|
+
}, async ({ workspaceId }) => {
|
|
846
|
+
const workspace = workspaces.getWorkspace(workspaceId);
|
|
847
|
+
return runToolWithHooks(hooks, {
|
|
848
|
+
tool: toolNames.closeWorkspace,
|
|
849
|
+
invocation: workspaceHookInvocation(workspace),
|
|
850
|
+
payload: { workspaceId },
|
|
851
|
+
operation: async () => {
|
|
852
|
+
if (processSessions.activeWorkspaceIds().has(workspaceId)) {
|
|
853
|
+
throw new Error(`Workspace ${workspaceId} still owns a running process or an unconsumed process completion. Poll or consume it before closing this workspace.`);
|
|
854
|
+
}
|
|
855
|
+
workspaces.closeWorkspace(workspaceId);
|
|
856
|
+
const result = `Closed logical workspace ${workspaceId}. Physical project files were not removed.`;
|
|
857
|
+
return {
|
|
858
|
+
content: [textBlock(result)],
|
|
859
|
+
structuredContent: { result, workspaceId },
|
|
860
|
+
};
|
|
861
|
+
},
|
|
862
|
+
});
|
|
765
863
|
});
|
|
766
864
|
registerAppTool(server, toolNames.closeWorktree, {
|
|
767
865
|
title: "Close worktree",
|
|
@@ -795,6 +893,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
795
893
|
payload: { commitMessage },
|
|
796
894
|
afterCwd: (response) => response.structuredContent.sourceRoot,
|
|
797
895
|
operation: async () => {
|
|
896
|
+
const busyWorkspaceIds = workspaces
|
|
897
|
+
.workspaceIdsForPhysicalWorkspace(workspace)
|
|
898
|
+
.filter((id) => processSessions.activeWorkspaceIds().has(id));
|
|
899
|
+
if (busyWorkspaceIds.length > 0) {
|
|
900
|
+
throw new Error(`Cannot close this worktree while logical workspace processes are still running or awaiting completion delivery: ${busyWorkspaceIds.join(", ")}.`);
|
|
901
|
+
}
|
|
798
902
|
const startedAt = performance.now();
|
|
799
903
|
const closed = await workspaces.closeWorktree(workspaceId, commitMessage);
|
|
800
904
|
const result = [
|
|
@@ -1554,80 +1658,57 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1554
1658
|
.string()
|
|
1555
1659
|
.optional()
|
|
1556
1660
|
.describe("Optional working directory relative to the workspace root. Defaults to the workspace root."),
|
|
1557
|
-
timeout: z
|
|
1558
|
-
.number()
|
|
1559
|
-
.positive()
|
|
1560
|
-
.max(300)
|
|
1561
|
-
.optional()
|
|
1562
|
-
.describe("Timeout in seconds. Defaults to 30, max 300."),
|
|
1563
1661
|
},
|
|
1564
|
-
outputSchema:
|
|
1662
|
+
outputSchema: processOutputSchema(),
|
|
1565
1663
|
...toolWidgetDescriptorMeta(config, "shell"),
|
|
1566
1664
|
annotations: SHELL_TOOL_ANNOTATIONS,
|
|
1567
|
-
}, async ({ workspaceId,
|
|
1665
|
+
}, async ({ workspaceId, command, workingDirectory }, extra) => {
|
|
1568
1666
|
const workspace = workspaces.getWorkspace(workspaceId);
|
|
1569
1667
|
return runToolWithHooks(hooks, {
|
|
1570
1668
|
tool: toolNames.shell,
|
|
1571
1669
|
invocation: workspaceHookInvocation(workspace),
|
|
1572
1670
|
payload: {
|
|
1573
|
-
command
|
|
1671
|
+
command,
|
|
1574
1672
|
workingDirectory: workingDirectory ?? ".",
|
|
1575
|
-
timeoutSeconds: input.timeout,
|
|
1576
1673
|
},
|
|
1577
1674
|
isFailure: toolResultIsError,
|
|
1578
1675
|
operation: async () => {
|
|
1579
1676
|
const startedAt = performance.now();
|
|
1580
1677
|
const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
|
|
1581
|
-
const
|
|
1678
|
+
const snapshot = await processSessions.start({
|
|
1679
|
+
workspaceId,
|
|
1680
|
+
command,
|
|
1582
1681
|
cwd,
|
|
1583
|
-
|
|
1682
|
+
workspaceRoot: workspace.root,
|
|
1683
|
+
yieldTimeMs: 300_000,
|
|
1584
1684
|
});
|
|
1585
|
-
if (response.isError) {
|
|
1586
|
-
logFailedToolResponse(config, {
|
|
1587
|
-
tool: toolNames.shell,
|
|
1588
|
-
...workspaceLogContext(workspace, extra.sessionId),
|
|
1589
|
-
workingDirectory: workingDirectory ?? ".",
|
|
1590
|
-
command: input.command,
|
|
1591
|
-
commandLength: input.command.length,
|
|
1592
|
-
}, response.content, startedAt);
|
|
1593
|
-
return response;
|
|
1594
|
-
}
|
|
1595
|
-
const summary = {
|
|
1596
|
-
command: input.command,
|
|
1597
|
-
workingDirectory: workingDirectory ?? ".",
|
|
1598
|
-
...textSummary(response.content),
|
|
1599
|
-
};
|
|
1600
1685
|
logToolCall(config, {
|
|
1601
1686
|
tool: toolNames.shell,
|
|
1602
1687
|
...workspaceLogContext(workspace, extra.sessionId),
|
|
1603
1688
|
workingDirectory: workingDirectory ?? ".",
|
|
1604
|
-
command
|
|
1605
|
-
commandLength:
|
|
1606
|
-
|
|
1689
|
+
command,
|
|
1690
|
+
commandLength: command.length,
|
|
1691
|
+
exitCode: snapshot.exitCode,
|
|
1692
|
+
running: snapshot.running,
|
|
1693
|
+
processSessionId: snapshot.sessionId,
|
|
1694
|
+
success: snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
|
|
1607
1695
|
durationMs: Math.round(performance.now() - startedAt),
|
|
1608
1696
|
});
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
},
|
|
1620
|
-
structuredContent: {
|
|
1621
|
-
result: contentText(response.content),
|
|
1622
|
-
},
|
|
1623
|
-
};
|
|
1697
|
+
const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
|
|
1698
|
+
command,
|
|
1699
|
+
workingDirectory: workingDirectory ?? ".",
|
|
1700
|
+
running: snapshot.running,
|
|
1701
|
+
exitCode: snapshot.exitCode,
|
|
1702
|
+
wallTimeMs: snapshot.wallTimeMs,
|
|
1703
|
+
});
|
|
1704
|
+
return !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
|
|
1705
|
+
? { ...response, isError: true }
|
|
1706
|
+
: response;
|
|
1624
1707
|
},
|
|
1625
1708
|
});
|
|
1626
1709
|
});
|
|
1627
1710
|
}
|
|
1628
|
-
|
|
1629
|
-
registerCodexProcessTools(server, config, workspaces, processSessions, hooks);
|
|
1630
|
-
}
|
|
1711
|
+
registerProcessTools(server, config, workspaces, processSessions, hooks);
|
|
1631
1712
|
if (config.artifactsEnabled && isArtifactDownloadSupportedPlatform()) {
|
|
1632
1713
|
registerArtifactTools(server, {
|
|
1633
1714
|
config,
|
package/dist/workspace-store.js
CHANGED
|
@@ -79,6 +79,19 @@ export class SqliteWorkspaceStore {
|
|
|
79
79
|
: query.where(and(...conditions)).all();
|
|
80
80
|
return rows.map(rowToWorkspaceSession);
|
|
81
81
|
}
|
|
82
|
+
deleteSession(id) {
|
|
83
|
+
this.database.db
|
|
84
|
+
.delete(workspaceSessions)
|
|
85
|
+
.where(eq(workspaceSessions.id, id))
|
|
86
|
+
.run();
|
|
87
|
+
}
|
|
88
|
+
listConversationBindings() {
|
|
89
|
+
return this.database.db
|
|
90
|
+
.select()
|
|
91
|
+
.from(workspaceConversationBindings)
|
|
92
|
+
.all()
|
|
93
|
+
.map(rowToWorkspaceConversationBinding);
|
|
94
|
+
}
|
|
82
95
|
getConversationBinding(conversationScopeId, targetKey) {
|
|
83
96
|
const row = this.database.db
|
|
84
97
|
.select()
|