@akira-tl/forgerelay 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +2 -2
  3. package/capabilities/artifacts-review/GUIDE.md +2 -4
  4. package/capabilities/managed-worktrees/GUIDE.md +12 -7
  5. package/capabilities/shell-processes/GUIDE.md +10 -8
  6. package/dist/artifact-tools.js +0 -128
  7. package/dist/capabilities.js +4 -7
  8. package/dist/hooks.js +12 -0
  9. package/dist/mcp/server-instructions.js +7 -13
  10. package/dist/review-checkpoints.js +4 -4
  11. package/dist/server.js +217 -365
  12. package/dist/ui/.vite/manifest.json +280 -280
  13. package/dist/ui/assets/{angular-html-CQmCUfkv.js → angular-html-BT2Z6jee.js} +1 -1
  14. package/dist/ui/assets/{angular-ts-BSbfxpMS.js → angular-ts-BwPE8YDt.js} +1 -1
  15. package/dist/ui/assets/{apl-CWCaH-E3.js → apl-CGbONVvU.js} +1 -1
  16. package/dist/ui/assets/{astro-R_p4G0xA.js → astro-DIAPy7cj.js} +1 -1
  17. package/dist/ui/assets/{blade-irHensDY.js → blade-Cgtd19Ky.js} +1 -1
  18. package/dist/ui/assets/{c-_XEQUoq6.js → c-acnvZ8tm.js} +1 -1
  19. package/dist/ui/assets/{cobol-Css6VQjJ.js → cobol-G-7HLVKW.js} +1 -1
  20. package/dist/ui/assets/{coffee-CpXUkyDm.js → coffee-DQFyKxEB.js} +1 -1
  21. package/dist/ui/assets/{cpp-BQHsPWwI.js → cpp-UyrmHXan.js} +1 -1
  22. package/dist/ui/assets/{crystal-Cbv8J9Fm.js → crystal-DuiWhEO3.js} +1 -1
  23. package/dist/ui/assets/{css-D8DkrS3U.js → css-D_WCbY6m.js} +1 -1
  24. package/dist/ui/assets/{edge-bBL_XDAG.js → edge-COkalhFi.js} +1 -1
  25. package/dist/ui/assets/{elixir-CQjsM9sA.js → elixir-cdlzbYR1.js} +1 -1
  26. package/dist/ui/assets/{elm-DLFl-jEZ.js → elm-ZwDYuYpb.js} +1 -1
  27. package/dist/ui/assets/{erb-BJjsdB_Q.js → erb-C8WG9TtC.js} +1 -1
  28. package/dist/ui/assets/{git-rebase-CqP5A2s0.js → git-rebase-CMlDVJBT.js} +1 -1
  29. package/dist/ui/assets/{glimmer-js-B5Hh_7xe.js → glimmer-js-DeLOQlol.js} +1 -1
  30. package/dist/ui/assets/{glimmer-ts-oaeSAC8s.js → glimmer-ts-DTEM04TK.js} +1 -1
  31. package/dist/ui/assets/{glsl-BylkOpcF.js → glsl-IJV4SB-M.js} +1 -1
  32. package/dist/ui/assets/{graphql-DsVRvP6S.js → graphql-PM-WDnoB.js} +1 -1
  33. package/dist/ui/assets/{hack-D4N5abT_.js → hack-C14sh6Lh.js} +1 -1
  34. package/dist/ui/assets/{haml-CIAJQIox.js → haml-B_5nS6Kx.js} +1 -1
  35. package/dist/ui/assets/{handlebars-DDEZyNXR.js → handlebars-eS-J0O-M.js} +1 -1
  36. package/dist/ui/assets/{heavy-payload-DafM8yjB.js → heavy-payload-BKB0QDOI.js} +1 -1
  37. package/dist/ui/assets/{html-zDy-3lvy.js → html-C3TPjrC5.js} +1 -1
  38. package/dist/ui/assets/{html-derivative-CZ91Y2d5.js → html-derivative-BlVZU2id.js} +1 -1
  39. package/dist/ui/assets/{http-ezaodZRf.js → http-BmbZzA5x.js} +1 -1
  40. package/dist/ui/assets/{hurl-BmL0o7TO.js → hurl-Dnd_YpOD.js} +1 -1
  41. package/dist/ui/assets/{java-Q2C0vwbH.js → java-ne9HdIVn.js} +1 -1
  42. package/dist/ui/assets/{javascript-UDhBkHE3.js → javascript-DpHT58rr.js} +1 -1
  43. package/dist/ui/assets/{jinja-BWRskcYS.js → jinja-DTvkZdHA.js} +1 -1
  44. package/dist/ui/assets/{jison-B18BY9Ds.js → jison-DUJG-tmh.js} +1 -1
  45. package/dist/ui/assets/{json-DNj7MpRr.js → json-DJmnBdxW.js} +1 -1
  46. package/dist/ui/assets/{jsx-CVpHJ6cr.js → jsx-BclJnZnN.js} +1 -1
  47. package/dist/ui/assets/{julia-CJBNBS9c.js → julia-P5B4pHFa.js} +1 -1
  48. package/dist/ui/assets/{just-Bmu2JCQT.js → just-DYaoh7C6.js} +1 -1
  49. package/dist/ui/assets/{latex-BB7F93KY.js → latex-CI5lnkWV.js} +1 -1
  50. package/dist/ui/assets/{liquid-Btnomc6E.js → liquid-D3LnAs_z.js} +1 -1
  51. package/dist/ui/assets/{lua-D0FYT9Vo.js → lua-dZC7ZQIY.js} +1 -1
  52. package/dist/ui/assets/{marko-MHLw0ZlK.js → marko-CRDrjqfS.js} +1 -1
  53. package/dist/ui/assets/{mdc-CSr-suGy.js → mdc-ZzwgmJVn.js} +1 -1
  54. package/dist/ui/assets/{nginx-ur1l6E-g.js → nginx-BAvFXGSV.js} +1 -1
  55. package/dist/ui/assets/{nim-B2k0IugA.js → nim-BHIFawe9.js} +1 -1
  56. package/dist/ui/assets/{perl-BqOHxFDn.js → perl-C_f5AnLa.js} +1 -1
  57. package/dist/ui/assets/{php-DzPK6O1_.js → php-DDqpegL5.js} +1 -1
  58. package/dist/ui/assets/{pug-D169OZkr.js → pug-CSaAZXyd.js} +1 -1
  59. package/dist/ui/assets/{qml-BtemJTC4.js → qml-CNHvRs_z.js} +1 -1
  60. package/dist/ui/assets/{r-BGVHu0bd.js → r-CAfoQND2.js} +1 -1
  61. package/dist/ui/assets/{razor-Diisjg1d.js → razor-BDfAPy7I.js} +1 -1
  62. package/dist/ui/assets/{regexp-B2F5vMZ6.js → regexp-Mug5jltQ.js} +1 -1
  63. package/dist/ui/assets/{review-payload-B-3I3Oj_.js → review-payload-D7GIdYDj.js} +1 -1
  64. package/dist/ui/assets/{rst-BxpeZwuT.js → rst-BzPP2gle.js} +1 -1
  65. package/dist/ui/assets/{ruby-DrCq4riN.js → ruby-CQjzokzX.js} +1 -1
  66. package/dist/ui/assets/{sas-CI_2W-1n.js → sas-Bp37KGr7.js} +1 -1
  67. package/dist/ui/assets/{scrollbar-DHsDF5w-.js → scrollbar-HUMBdLLK.js} +3 -3
  68. package/dist/ui/assets/{scss-BxtyHZ1p.js → scss-C_Z1DYnw.js} +1 -1
  69. package/dist/ui/assets/{shellscript-BY_YYrHT.js → shellscript-CLfJpn3I.js} +1 -1
  70. package/dist/ui/assets/{shellsession-C6uBrLmA.js → shellsession-CNc62UhY.js} +1 -1
  71. package/dist/ui/assets/{soy-BHM9gdPh.js → soy-CF44SpBo.js} +1 -1
  72. package/dist/ui/assets/{sql-XmhfEGz_.js → sql-CYnzfiQ1.js} +1 -1
  73. package/dist/ui/assets/{stata-BbHY7NEo.js → stata-DVeMA68W.js} +1 -1
  74. package/dist/ui/assets/{surrealql-CD49DZ3E.js → surrealql-52sF96Yv.js} +1 -1
  75. package/dist/ui/assets/{svelte-CbYsEYJY.js → svelte-ydwT7UbF.js} +1 -1
  76. package/dist/ui/assets/{templ-B5JoUxEB.js → templ-BXcoVoGh.js} +1 -1
  77. package/dist/ui/assets/{tex-DZcOfTZK.js → tex-COsYFp7O.js} +1 -1
  78. package/dist/ui/assets/{ts-tags-BeU-xqc0.js → ts-tags-CF--LE1s.js} +1 -1
  79. package/dist/ui/assets/{tsx-V7W1xGl3.js → tsx-DLBt2-2f.js} +1 -1
  80. package/dist/ui/assets/{twig-MIaJo5BU.js → twig-YoM-DiI5.js} +1 -1
  81. package/dist/ui/assets/{typescript-C0WESuVc.js → typescript-Ce9JGkNI.js} +1 -1
  82. package/dist/ui/assets/{vue-eJHwc9ra.js → vue-B5LDEy4l.js} +1 -1
  83. package/dist/ui/assets/{vue-html-CLmBujTU.js → vue-html-CPdGGk4T.js} +1 -1
  84. package/dist/ui/assets/{vue-vine-BP5kjIGb.js → vue-vine-B5bjgk0a.js} +1 -1
  85. package/dist/ui/assets/{workspace-app-C1qR5cAY.js → workspace-app-Cm9EX5x4.js} +3 -3
  86. package/dist/ui/assets/{xml-j7pmyP48.js → xml-Cun8xb0T.js} +1 -1
  87. package/dist/ui/assets/{xsl-5VFHbg6o.js → xsl-wls_CEK_.js} +1 -1
  88. package/dist/ui/assets/{yaml-DxFAt-KU.js → yaml-BxuXn9OL.js} +1 -1
  89. package/dist/ui/workspace-app.html +1 -1
  90. package/docs/artifact-exchange.md +4 -3
  91. package/docs/chatgpt-coding-workflow.md +31 -27
  92. package/docs/configuration.md +27 -20
  93. package/docs/debugging.md +3 -3
  94. package/docs/gotchas.md +9 -8
  95. package/docs/security.md +6 -5
  96. package/package.json +1 -1
  97. package/scripts/debug/accept.mjs +80 -25
package/dist/server.js CHANGED
@@ -17,7 +17,7 @@ import { applyPatch } from "./apply-patch.js";
17
17
  import { buildCapabilityFingerprint } from "./capabilities.js";
18
18
  import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
19
19
  import { deletePath, renamePath } from "./file-mutations.js";
20
- import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform, registerArtifactTools, } from "./artifact-tools.js";
20
+ import { downloadIncomingArtifact, isArtifactDownloadSupportedPlatform, } from "./artifact-tools.js";
21
21
  import { ArtifactError } from "./artifact-error.js";
22
22
  import { loadConfig } from "./config.js";
23
23
  import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
@@ -25,7 +25,7 @@ import { checkHookConfiguration } from "./hook-cli.js";
25
25
  import { buildServerInstructions, buildShellMutationPolicy, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
26
26
  import { createOpenAIIncomingArtifactAdapter, IncomingArtifactAdapterRegistry, } from "./incoming-artifacts.js";
27
27
  import { logEvent, requestIp, requestPath, commandPreview, transportSessionIdPrefix, workspaceLogLabel, } from "./logger.js";
28
- import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, writeFileTool, } from "./pi-tools.js";
28
+ import { editFileTool, readFileTool, writeFileTool, } from "./pi-tools.js";
29
29
  import { SingleUserOAuthProvider } from "./oauth-provider.js";
30
30
  import { McpTransportRegistry, } from "./mcp-sessions.js";
31
31
  import { ProcessManager, resolveProcessId, } from "./process-sessions.js";
@@ -67,7 +67,7 @@ function shouldAttachWidget(mode, kind) {
67
67
  case "off":
68
68
  return false;
69
69
  case "changes":
70
- return kind === "workspace" || kind === "show_changes" || kind === "capability";
70
+ return kind === "workspace" || kind === "capability";
71
71
  case "full":
72
72
  return true;
73
73
  }
@@ -461,7 +461,7 @@ function attachCompletedProcessNotices(processSessions, workspaceId, result) {
461
461
  }
462
462
  function processOutputSchema() {
463
463
  return resultOutputSchema({
464
- processId: z.number().int().positive().optional().describe("Canonical process handle for write_stdin."),
464
+ processId: z.number().int().positive().optional().describe("Canonical process handle for bash(action=\"process\") or the active command adapter."),
465
465
  sessionId: z.number().int().positive().optional().describe("Deprecated alias of processId for compatibility."),
466
466
  running: z.boolean(),
467
467
  exitCode: z.number().int().optional(),
@@ -613,6 +613,8 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
613
613
  });
614
614
  });
615
615
  }
616
+ if (config.toolMode !== "codex")
617
+ return;
616
618
  registerAppTool(server, "write_stdin", {
617
619
  title: "Write to process",
618
620
  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.",
@@ -709,7 +711,6 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
709
711
  files: review.files,
710
712
  },
711
713
  card: {
712
- tool: "show_changes",
713
714
  summary: review.summary,
714
715
  files: review.files,
715
716
  payload: { patch: review.patch },
@@ -1118,9 +1119,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1118
1119
  ...(execution.card
1119
1120
  ? {
1120
1121
  _meta: {
1121
- tool: execution.card.tool ?? toolNames.capability,
1122
+ tool: toolNames.capability,
1122
1123
  card: {
1123
1124
  workspaceId,
1125
+ capabilityName: name,
1124
1126
  summary: execution.card.summary ?? {},
1125
1127
  files: execution.card.files,
1126
1128
  payload: execution.card.payload ?? {},
@@ -1165,53 +1167,25 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1165
1167
  });
1166
1168
  });
1167
1169
  registerAppTool(server, toolNames.closeWorkspace, {
1168
- title: "Close logical workspace",
1169
- description: "Release one logical workspaceId after the user chooses cleanup. This does not delete checkout files. Use close_worktree to finalize and remove a managed worktree. Running or unconsumed processes prevent closure.",
1170
- inputSchema: {
1171
- workspaceId: z.string().describe("Logical workspace ID to release."),
1172
- },
1173
- outputSchema: resultOutputSchema({ workspaceId: z.string() }),
1174
- _meta: {},
1175
- annotations: WRITE_TOOL_ANNOTATIONS,
1176
- }, async ({ workspaceId }) => {
1177
- const workspace = workspaces.getWorkspace(workspaceId);
1178
- return runToolWithHooks(hooks, {
1179
- tool: toolNames.closeWorkspace,
1180
- invocation: workspaceHookInvocation(workspace),
1181
- payload: { workspaceId },
1182
- operation: async () => {
1183
- if (processSessions.activeWorkspaceIds().has(workspaceId)) {
1184
- throw new Error(`Workspace ${workspaceId} still owns a running process or an unconsumed process completion. Poll or consume it before closing this workspace.`);
1185
- }
1186
- workspaces.closeWorkspace(workspaceId);
1187
- const result = `Closed logical workspace ${workspaceId}. Physical project files were not removed.`;
1188
- return {
1189
- content: [textBlock(result)],
1190
- structuredContent: { result, workspaceId },
1191
- };
1192
- },
1193
- });
1194
- });
1195
- registerAppTool(server, toolNames.closeWorktree, {
1196
- title: "Close worktree",
1197
- description: "Finalize a managed worktree after its task is complete and verified. ForgeRelay may commit remaining changes, integrate the target branch when safe, and clean up the managed worktree. Read the managed-worktrees capability guide for advanced close, safety, and failure semantics.",
1170
+ title: "Close workspace",
1171
+ description: "Close one workspace after the user chooses cleanup. Checkout-backed workspaces release only the logical handle. Managed-worktree-backed workspaces finalize the existing safe worktree lifecycle, including hooks, commit/integration, and cleanup; provide commitMessage for that mode. Running or unconsumed processes prevent closure.",
1198
1172
  inputSchema: {
1199
- workspaceId: z
1200
- .string()
1201
- .describe("Managed worktree workspace identifier returned by open_workspace."),
1173
+ workspaceId: z.string().describe("Workspace identifier to close."),
1202
1174
  commitMessage: z
1203
1175
  .string()
1204
1176
  .min(1)
1205
- .describe("Concise Git commit message describing the completed worktree changes."),
1177
+ .optional()
1178
+ .describe("Required only for a managed-worktree-backed workspace; concise Git commit message for remaining worktree changes."),
1206
1179
  },
1207
1180
  outputSchema: resultOutputSchema({
1208
1181
  workspaceId: z.string(),
1209
- sourceRoot: z.string(),
1210
- branch: z.string(),
1211
- targetBranch: z.string(),
1212
- commitSha: z.string(),
1213
- mergedSha: z.string(),
1214
- committed: z.boolean(),
1182
+ mode: z.enum(["checkout", "worktree"]),
1183
+ sourceRoot: z.string().optional(),
1184
+ branch: z.string().optional(),
1185
+ targetBranch: z.string().optional(),
1186
+ commitSha: z.string().optional(),
1187
+ mergedSha: z.string().optional(),
1188
+ committed: z.boolean().optional(),
1215
1189
  cleanupWarning: z.string().optional(),
1216
1190
  }),
1217
1191
  _meta: {},
@@ -1219,49 +1193,70 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1219
1193
  }, async ({ workspaceId, commitMessage }, extra) => {
1220
1194
  const workspace = workspaces.getWorkspace(workspaceId);
1221
1195
  return runToolWithHooks(hooks, {
1222
- tool: toolNames.closeWorktree,
1196
+ tool: toolNames.closeWorkspace,
1223
1197
  invocation: workspaceHookInvocation(workspace),
1224
- payload: { commitMessage },
1225
- afterCwd: (response) => response.structuredContent.sourceRoot,
1198
+ payload: { workspaceId, commitMessage, mode: workspace.mode },
1199
+ afterCwd: (response) => "sourceRoot" in response.structuredContent &&
1200
+ typeof response.structuredContent.sourceRoot === "string"
1201
+ ? response.structuredContent.sourceRoot
1202
+ : undefined,
1226
1203
  operation: async () => {
1227
- const busyWorkspaceIds = workspaces
1228
- .workspaceIdsForPhysicalWorkspace(workspace)
1229
- .filter((id) => processSessions.activeWorkspaceIds().has(id));
1230
- if (busyWorkspaceIds.length > 0) {
1231
- throw new Error(`Cannot close this worktree while logical workspace processes are still running or awaiting completion delivery: ${busyWorkspaceIds.join(", ")}.`);
1204
+ if (workspace.mode === "worktree") {
1205
+ if (!commitMessage) {
1206
+ throw new Error(`Managed-worktree-backed workspace ${workspaceId} requires commitMessage when closing.`);
1207
+ }
1208
+ const busyWorkspaceIds = workspaces
1209
+ .workspaceIdsForPhysicalWorkspace(workspace)
1210
+ .filter((id) => processSessions.activeWorkspaceIds().has(id));
1211
+ if (busyWorkspaceIds.length > 0) {
1212
+ throw new Error(`Cannot close this worktree-backed workspace while logical workspace processes are still running or awaiting completion delivery: ${busyWorkspaceIds.join(", ")}.`);
1213
+ }
1214
+ const startedAt = performance.now();
1215
+ const closed = await workspaces.closeWorktree(workspaceId, commitMessage);
1216
+ const result = [
1217
+ `Closed managed-worktree-backed workspace ${workspaceId}.`,
1218
+ `Merged ${closed.branch} into ${closed.targetBranch} by fast-forward.`,
1219
+ `Source checkout: ${closed.sourceRoot}`,
1220
+ `Commit: ${closed.commitSha}`,
1221
+ closed.cleanupWarning
1222
+ ? `Cleanup warning: ${closed.cleanupWarning}`
1223
+ : "The managed worktree directory and branch were removed.",
1224
+ ].join("\n");
1225
+ logToolCall(config, {
1226
+ tool: toolNames.closeWorkspace,
1227
+ ...workspaceLogContext(workspace, extra.sessionId),
1228
+ path: closed.sourceRoot,
1229
+ success: true,
1230
+ durationMs: Math.round(performance.now() - startedAt),
1231
+ });
1232
+ return attachHookReports({
1233
+ content: [textBlock(result)],
1234
+ structuredContent: {
1235
+ result,
1236
+ workspaceId,
1237
+ mode: "worktree",
1238
+ sourceRoot: closed.sourceRoot,
1239
+ branch: closed.branch,
1240
+ targetBranch: closed.targetBranch,
1241
+ commitSha: closed.commitSha,
1242
+ mergedSha: closed.mergedSha,
1243
+ committed: closed.committed,
1244
+ cleanupWarning: closed.cleanupWarning,
1245
+ },
1246
+ }, closed.hookReports);
1232
1247
  }
1233
- const startedAt = performance.now();
1234
- const closed = await workspaces.closeWorktree(workspaceId, commitMessage);
1235
- const result = [
1236
- `Closed managed worktree ${workspaceId}.`,
1237
- `Merged ${closed.branch} into ${closed.targetBranch} by fast-forward.`,
1238
- `Source checkout: ${closed.sourceRoot}`,
1239
- `Commit: ${closed.commitSha}`,
1240
- closed.cleanupWarning
1241
- ? `Cleanup warning: ${closed.cleanupWarning}`
1242
- : "The worktree directory and managed branch were removed.",
1243
- ].join("\n");
1244
- logToolCall(config, {
1245
- tool: toolNames.closeWorktree,
1246
- ...workspaceLogContext(workspace, extra.sessionId),
1247
- path: closed.sourceRoot,
1248
- success: true,
1249
- durationMs: Math.round(performance.now() - startedAt),
1250
- });
1251
- return attachHookReports({
1252
- content: [{ type: "text", text: result }],
1253
- structuredContent: {
1254
- result,
1255
- workspaceId,
1256
- sourceRoot: closed.sourceRoot,
1257
- branch: closed.branch,
1258
- targetBranch: closed.targetBranch,
1259
- commitSha: closed.commitSha,
1260
- mergedSha: closed.mergedSha,
1261
- committed: closed.committed,
1262
- cleanupWarning: closed.cleanupWarning,
1263
- },
1264
- }, closed.hookReports);
1248
+ if (commitMessage !== undefined) {
1249
+ throw new Error("close_workspace commitMessage is only valid for managed-worktree-backed workspaces.");
1250
+ }
1251
+ if (processSessions.activeWorkspaceIds().has(workspaceId)) {
1252
+ throw new Error(`Workspace ${workspaceId} still owns a running process or an unconsumed process completion. Poll or consume it before closing this workspace.`);
1253
+ }
1254
+ workspaces.closeWorkspace(workspaceId);
1255
+ const result = `Closed checkout-backed workspace ${workspaceId}. Physical project files were not removed.`;
1256
+ return {
1257
+ content: [textBlock(result)],
1258
+ structuredContent: { result, workspaceId, mode: "checkout" },
1259
+ };
1265
1260
  },
1266
1261
  });
1267
1262
  });
@@ -1721,255 +1716,6 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1721
1716
  });
1722
1717
  });
1723
1718
  }
1724
- if (config.widgets === "changes") {
1725
- registerAppTool(server, "show_changes", {
1726
- title: "Show changes",
1727
- description: "Show the changes made in this turn for an open workspace. Call this once after the final related file change and before your final response so the user can review the combined diff. Do not call it after each individual file change.",
1728
- inputSchema: {
1729
- workspaceId: z
1730
- .string()
1731
- .describe("Workspace identifier returned by open_workspace."),
1732
- },
1733
- outputSchema: resultOutputSchema(),
1734
- ...toolWidgetDescriptorMeta(config, "show_changes"),
1735
- annotations: { readOnlyHint: true },
1736
- }, async ({ workspaceId }, extra) => {
1737
- const workspace = workspaces.getWorkspace(workspaceId);
1738
- return runToolWithHooks(hooks, {
1739
- tool: "show_changes",
1740
- invocation: workspaceHookInvocation(workspace),
1741
- operation: async () => {
1742
- const startedAt = performance.now();
1743
- const review = await reviewWorkspaceChanges(reviewCheckpoints, workspace);
1744
- const content = [textBlock(review.result)];
1745
- logToolCall(config, {
1746
- tool: "show_changes",
1747
- ...workspaceLogContext(workspace, extra.sessionId),
1748
- success: true,
1749
- durationMs: Math.round(performance.now() - startedAt),
1750
- });
1751
- return {
1752
- content,
1753
- _meta: {
1754
- tool: "show_changes",
1755
- card: {
1756
- workspaceId,
1757
- summary: review.summary,
1758
- files: review.files,
1759
- payload: {
1760
- patch: review.patch,
1761
- },
1762
- },
1763
- },
1764
- structuredContent: {
1765
- result: contentText(content),
1766
- },
1767
- };
1768
- },
1769
- });
1770
- });
1771
- }
1772
- if (config.toolMode === "full") {
1773
- registerAppTool(server, toolNames.grep, {
1774
- title: "Grep",
1775
- description: "Search file contents inside an open workspace or the OS temp directory. Use this before broad reads when looking for symbols, text, or usage sites. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
1776
- inputSchema: {
1777
- workspaceId: z
1778
- .string()
1779
- .describe("Workspace identifier returned by open_workspace."),
1780
- pattern: z.string().describe("Search pattern."),
1781
- path: z
1782
- .string()
1783
- .optional()
1784
- .describe("Optional path or glob scope relative to the workspace root, or an absolute path inside the OS temp directory."),
1785
- include: z.string().optional().describe("Optional include glob."),
1786
- },
1787
- outputSchema: resultOutputSchema(),
1788
- ...toolWidgetDescriptorMeta(config, "search"),
1789
- annotations: { readOnlyHint: true },
1790
- }, async ({ workspaceId, ...input }, extra) => {
1791
- const workspace = workspaces.getWorkspace(workspaceId);
1792
- return runToolWithHooks(hooks, {
1793
- tool: toolNames.grep,
1794
- invocation: workspaceHookInvocation(workspace),
1795
- payload: { pattern: input.pattern, path: input.path, include: input.include },
1796
- isFailure: toolResultIsError,
1797
- operation: async () => {
1798
- const startedAt = performance.now();
1799
- const response = await grepFilesTool(input, {
1800
- cwd: workspace.root,
1801
- root: workspace.root,
1802
- fileRoots: workspaces.fileToolRoots(workspace),
1803
- });
1804
- if (response.isError) {
1805
- logFailedToolResponse(config, {
1806
- tool: toolNames.grep,
1807
- ...workspaceLogContext(workspace, extra.sessionId),
1808
- path: input.path,
1809
- }, response.content, startedAt);
1810
- return response;
1811
- }
1812
- const summary = {
1813
- pattern: input.pattern,
1814
- scope: input.path ?? ".",
1815
- ...textSummary(response.content),
1816
- };
1817
- logToolCall(config, {
1818
- tool: toolNames.grep,
1819
- ...workspaceLogContext(workspace, extra.sessionId),
1820
- path: input.path,
1821
- success: true,
1822
- durationMs: Math.round(performance.now() - startedAt),
1823
- });
1824
- return {
1825
- ...response,
1826
- _meta: {
1827
- tool: toolNames.grep,
1828
- card: {
1829
- workspaceId,
1830
- path: input.path,
1831
- summary,
1832
- payload: { content: response.content },
1833
- },
1834
- },
1835
- structuredContent: {
1836
- result: contentText(response.content),
1837
- },
1838
- };
1839
- },
1840
- });
1841
- });
1842
- registerAppTool(server, toolNames.glob, {
1843
- title: "Glob",
1844
- description: "Find files by glob pattern inside an open workspace or the OS temp directory. Use this to discover filenames or narrow file sets before reading. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
1845
- inputSchema: {
1846
- workspaceId: z
1847
- .string()
1848
- .describe("Workspace identifier returned by open_workspace."),
1849
- pattern: z.string().describe("File glob pattern."),
1850
- path: z
1851
- .string()
1852
- .optional()
1853
- .describe("Optional path scope relative to the workspace root, or an absolute path inside the OS temp directory."),
1854
- },
1855
- outputSchema: resultOutputSchema(),
1856
- ...toolWidgetDescriptorMeta(config, "search"),
1857
- annotations: { readOnlyHint: true },
1858
- }, async ({ workspaceId, ...input }, extra) => {
1859
- const workspace = workspaces.getWorkspace(workspaceId);
1860
- return runToolWithHooks(hooks, {
1861
- tool: toolNames.glob,
1862
- invocation: workspaceHookInvocation(workspace),
1863
- payload: { pattern: input.pattern, path: input.path },
1864
- isFailure: toolResultIsError,
1865
- operation: async () => {
1866
- const startedAt = performance.now();
1867
- const response = await findFilesTool(input, {
1868
- cwd: workspace.root,
1869
- root: workspace.root,
1870
- fileRoots: workspaces.fileToolRoots(workspace),
1871
- });
1872
- if (response.isError) {
1873
- logFailedToolResponse(config, {
1874
- tool: toolNames.glob,
1875
- ...workspaceLogContext(workspace, extra.sessionId),
1876
- path: input.path,
1877
- }, response.content, startedAt);
1878
- return response;
1879
- }
1880
- const summary = {
1881
- pattern: input.pattern,
1882
- scope: input.path ?? ".",
1883
- ...textSummary(response.content),
1884
- };
1885
- logToolCall(config, {
1886
- tool: toolNames.glob,
1887
- ...workspaceLogContext(workspace, extra.sessionId),
1888
- path: input.path,
1889
- success: true,
1890
- durationMs: Math.round(performance.now() - startedAt),
1891
- });
1892
- return {
1893
- ...response,
1894
- _meta: {
1895
- tool: toolNames.glob,
1896
- card: {
1897
- workspaceId,
1898
- path: input.path,
1899
- summary,
1900
- payload: { content: response.content },
1901
- },
1902
- },
1903
- structuredContent: {
1904
- result: contentText(response.content),
1905
- },
1906
- };
1907
- },
1908
- });
1909
- });
1910
- registerAppTool(server, toolNames.ls, {
1911
- title: "Ls",
1912
- description: "List a directory inside an open workspace or the OS temp directory. Use this for directory inspection before reading files. Call open_workspace first and pass workspaceId.",
1913
- inputSchema: {
1914
- workspaceId: z
1915
- .string()
1916
- .describe("Workspace identifier returned by open_workspace."),
1917
- path: z
1918
- .string()
1919
- .describe("Directory path to list, relative to the workspace root or absolute inside the OS temp directory."),
1920
- },
1921
- outputSchema: resultOutputSchema(),
1922
- ...toolWidgetDescriptorMeta(config, "directory"),
1923
- annotations: { readOnlyHint: true },
1924
- }, async ({ workspaceId, ...input }, extra) => {
1925
- const workspace = workspaces.getWorkspace(workspaceId);
1926
- return runToolWithHooks(hooks, {
1927
- tool: toolNames.ls,
1928
- invocation: workspaceHookInvocation(workspace),
1929
- payload: { path: input.path },
1930
- isFailure: toolResultIsError,
1931
- operation: async () => {
1932
- const startedAt = performance.now();
1933
- const response = await listDirectoryTool(input, {
1934
- cwd: workspace.root,
1935
- root: workspace.root,
1936
- fileRoots: workspaces.fileToolRoots(workspace),
1937
- });
1938
- if (response.isError) {
1939
- logFailedToolResponse(config, {
1940
- tool: toolNames.ls,
1941
- ...workspaceLogContext(workspace, extra.sessionId),
1942
- path: input.path,
1943
- }, response.content, startedAt);
1944
- return response;
1945
- }
1946
- const summary = textSummary(response.content);
1947
- logToolCall(config, {
1948
- tool: toolNames.ls,
1949
- ...workspaceLogContext(workspace, extra.sessionId),
1950
- path: input.path,
1951
- success: true,
1952
- durationMs: Math.round(performance.now() - startedAt),
1953
- });
1954
- return {
1955
- ...response,
1956
- _meta: {
1957
- tool: toolNames.ls,
1958
- card: {
1959
- workspaceId,
1960
- path: input.path,
1961
- summary,
1962
- payload: { content: response.content },
1963
- },
1964
- },
1965
- structuredContent: {
1966
- result: contentText(response.content),
1967
- },
1968
- };
1969
- },
1970
- });
1971
- });
1972
- }
1973
1719
  if (config.toolMode !== "codex") {
1974
1720
  registerAppTool(server, toolNames.shell, {
1975
1721
  title: "Bash",
@@ -1978,73 +1724,179 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1978
1724
  workspaceId: z
1979
1725
  .string()
1980
1726
  .describe("Workspace identifier returned by open_workspace."),
1727
+ action: z
1728
+ .enum(["run", "process"])
1729
+ .optional()
1730
+ .describe("Defaults to run. Use process with a returned processId to poll, interact, resize, or interrupt a running command."),
1981
1731
  command: z
1982
1732
  .string()
1983
- .describe(toolDescriptions.shellCommand),
1733
+ .optional()
1734
+ .describe(`${toolDescriptions.shellCommand} Required for action=run.`),
1735
+ processId: z
1736
+ .number()
1737
+ .int()
1738
+ .positive()
1739
+ .optional()
1740
+ .describe("Process identifier returned by a previous bash action=run call. Required for action=process."),
1741
+ input: z
1742
+ .string()
1743
+ .optional()
1744
+ .describe("Characters to write for action=process. Omit to poll/wait without input."),
1745
+ interrupt: z
1746
+ .boolean()
1747
+ .optional()
1748
+ .describe("For action=process, send SIGINT to the process. Cannot be combined with input."),
1749
+ tty: z
1750
+ .boolean()
1751
+ .optional()
1752
+ .describe("For action=run, allocate a pseudo-terminal for interactive commands. Defaults to false."),
1753
+ columns: z
1754
+ .number()
1755
+ .int()
1756
+ .min(1)
1757
+ .max(1_000)
1758
+ .optional()
1759
+ .describe("Initial PTY width for action=run, or resize width for action=process."),
1760
+ rows: z
1761
+ .number()
1762
+ .int()
1763
+ .min(1)
1764
+ .max(1_000)
1765
+ .optional()
1766
+ .describe("Initial PTY height for action=run, or resize height for action=process."),
1984
1767
  workingDirectory: z
1985
1768
  .string()
1986
1769
  .optional()
1987
- .describe("Optional working directory relative to the workspace root. Defaults to the workspace root."),
1770
+ .describe("For action=run, working directory relative to the workspace root. Defaults to the workspace root."),
1771
+ yieldTimeMs: z
1772
+ .number()
1773
+ .int()
1774
+ .min(0)
1775
+ .max(300_000)
1776
+ .optional()
1777
+ .describe("Milliseconds to wait before returning. Run preserves the existing 300000ms default; process polling defaults to 5000ms and interaction to 250ms."),
1778
+ maxOutputTokens: z
1779
+ .number()
1780
+ .int()
1781
+ .positive()
1782
+ .max(100_000)
1783
+ .optional()
1784
+ .describe("Approximate output token budget. Defaults to 10000."),
1988
1785
  },
1989
1786
  outputSchema: processOutputSchema(),
1990
1787
  ...toolWidgetDescriptorMeta(config, "shell"),
1991
1788
  annotations: SHELL_TOOL_ANNOTATIONS,
1992
- }, async ({ workspaceId, command, workingDirectory }, extra) => {
1789
+ }, async ({ workspaceId, action = "run", command, processId, input, interrupt, tty, columns, rows, workingDirectory, yieldTimeMs, maxOutputTokens, }, extra) => {
1993
1790
  const workspace = workspaces.getWorkspace(workspaceId);
1791
+ if (action === "run") {
1792
+ if (!command)
1793
+ throw new Error("bash action=run requires command.");
1794
+ if (processId !== undefined || input !== undefined || interrupt !== undefined) {
1795
+ throw new Error("bash action=run does not accept processId, input, or interrupt.");
1796
+ }
1797
+ return runToolWithHooks(hooks, {
1798
+ tool: toolNames.shell,
1799
+ invocation: workspaceHookInvocation(workspace),
1800
+ payload: {
1801
+ action,
1802
+ command,
1803
+ workingDirectory: workingDirectory ?? ".",
1804
+ },
1805
+ isFailure: toolResultIsError,
1806
+ operation: async () => {
1807
+ const startedAt = performance.now();
1808
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
1809
+ const snapshot = await processSessions.start({
1810
+ workspaceId,
1811
+ command,
1812
+ cwd,
1813
+ workspaceRoot: workspace.root,
1814
+ tty,
1815
+ columns,
1816
+ rows,
1817
+ yieldTimeMs: yieldTimeMs ?? 300_000,
1818
+ maxOutputTokens,
1819
+ });
1820
+ logToolCall(config, {
1821
+ tool: toolNames.shell,
1822
+ ...workspaceLogContext(workspace, extra.sessionId),
1823
+ workingDirectory: workingDirectory ?? ".",
1824
+ command,
1825
+ commandLength: command.length,
1826
+ exitCode: snapshot.exitCode,
1827
+ running: snapshot.running,
1828
+ processId: snapshot.processId,
1829
+ success: snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
1830
+ durationMs: Math.round(performance.now() - startedAt),
1831
+ });
1832
+ const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
1833
+ action,
1834
+ command,
1835
+ workingDirectory: workingDirectory ?? ".",
1836
+ running: snapshot.running,
1837
+ exitCode: snapshot.exitCode,
1838
+ wallTimeMs: snapshot.wallTimeMs,
1839
+ });
1840
+ return !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
1841
+ ? { ...response, isError: true }
1842
+ : response;
1843
+ },
1844
+ });
1845
+ }
1846
+ if (command !== undefined || workingDirectory !== undefined || tty !== undefined) {
1847
+ throw new Error("bash action=process does not accept command, workingDirectory, or tty.");
1848
+ }
1849
+ if (processId === undefined)
1850
+ throw new Error("bash action=process requires processId.");
1851
+ if (interrupt && input !== undefined) {
1852
+ throw new Error("bash action=process cannot combine interrupt with input.");
1853
+ }
1994
1854
  return runToolWithHooks(hooks, {
1995
1855
  tool: toolNames.shell,
1996
1856
  invocation: workspaceHookInvocation(workspace),
1997
1857
  payload: {
1998
- command,
1999
- workingDirectory: workingDirectory ?? ".",
1858
+ action,
1859
+ processId,
1860
+ inputLength: input?.length ?? 0,
1861
+ interrupt: interrupt ?? false,
1862
+ columns,
1863
+ rows,
2000
1864
  },
2001
1865
  isFailure: toolResultIsError,
2002
1866
  operation: async () => {
2003
1867
  const startedAt = performance.now();
2004
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
2005
- const snapshot = await processSessions.start({
1868
+ const snapshot = await processSessions.write({
2006
1869
  workspaceId,
2007
- command,
2008
- cwd,
2009
- workspaceRoot: workspace.root,
2010
- yieldTimeMs: 300_000,
1870
+ processId,
1871
+ chars: interrupt ? "\u0003" : input,
1872
+ columns,
1873
+ rows,
1874
+ yieldTimeMs,
1875
+ maxOutputTokens,
2011
1876
  });
2012
1877
  logToolCall(config, {
2013
1878
  tool: toolNames.shell,
2014
1879
  ...workspaceLogContext(workspace, extra.sessionId),
2015
- workingDirectory: workingDirectory ?? ".",
2016
- command,
2017
- commandLength: command.length,
2018
1880
  exitCode: snapshot.exitCode,
2019
1881
  running: snapshot.running,
2020
1882
  processId: snapshot.processId,
2021
- success: snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
1883
+ success: snapshot.running || snapshot.exitCode === 0,
2022
1884
  durationMs: Math.round(performance.now() - startedAt),
2023
1885
  });
2024
- const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
2025
- command,
2026
- workingDirectory: workingDirectory ?? ".",
1886
+ return processToolResponse(toolNames.shell, workspaceId, snapshot, {
1887
+ action,
1888
+ processId,
1889
+ inputLength: input?.length ?? 0,
1890
+ interrupt: interrupt ?? false,
2027
1891
  running: snapshot.running,
2028
1892
  exitCode: snapshot.exitCode,
2029
1893
  wallTimeMs: snapshot.wallTimeMs,
2030
1894
  });
2031
- return !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
2032
- ? { ...response, isError: true }
2033
- : response;
2034
1895
  },
2035
1896
  });
2036
1897
  });
2037
1898
  }
2038
1899
  registerProcessTools(server, config, workspaces, processSessions, hooks);
2039
- if (config.artifactsEnabled && isArtifactDownloadSupportedPlatform()) {
2040
- registerArtifactTools(server, {
2041
- config,
2042
- workspaces,
2043
- hooks,
2044
- incomingArtifactAdapters,
2045
- incomingArtifactRegistry,
2046
- });
2047
- }
2048
1900
  return server;
2049
1901
  }
2050
1902
  export function createServer(config = loadConfig(), options = {}) {