@waishnav/devspace 1.0.2 → 1.0.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.
Files changed (115) hide show
  1. package/README.md +43 -7
  2. package/dist/apply-patch.js +402 -0
  3. package/dist/cli.js +256 -4
  4. package/dist/config.js +17 -22
  5. package/dist/db/migrations.js +34 -0
  6. package/dist/db/schema.js +19 -0
  7. package/dist/local-agent-adapters.js +653 -0
  8. package/dist/local-agent-availability.js +119 -0
  9. package/dist/local-agent-path.js +24 -0
  10. package/dist/local-agent-profiles.js +135 -0
  11. package/dist/local-agent-runtime.js +48 -0
  12. package/dist/local-agent-store.js +145 -0
  13. package/dist/local-agent-targets.js +80 -0
  14. package/dist/process-platform.js +49 -0
  15. package/dist/process-sessions.js +337 -0
  16. package/dist/roots.js +5 -2
  17. package/dist/server.js +521 -243
  18. package/dist/skills.js +48 -4
  19. package/dist/ui/.vite/manifest.json +281 -281
  20. package/dist/ui/assets/{angular-html-Dj4LysSE.js → angular-html-CKeFzaPY.js} +1 -1
  21. package/dist/ui/assets/{angular-ts-CxuC_FFh.js → angular-ts-0wcM_Zxf.js} +1 -1
  22. package/dist/ui/assets/{apl-RwuvNFFo.js → apl-C7UdkNb2.js} +1 -1
  23. package/dist/ui/assets/{astro-ncoaUbsO.js → astro-DMqju4MJ.js} +1 -1
  24. package/dist/ui/assets/{blade-Dw8-Lzux.js → blade-DHanNgN5.js} +1 -1
  25. package/dist/ui/assets/{c-Dpbbo_f2.js → c-_iPKYPcx.js} +1 -1
  26. package/dist/ui/assets/{cobol-4ddX3THy.js → cobol-CagdLJsE.js} +1 -1
  27. package/dist/ui/assets/{coffee-Bn-a5KqK.js → coffee-CkNGriPi.js} +1 -1
  28. package/dist/ui/assets/{cpp-DoRrlM_T.js → cpp-CqmKOWIQ.js} +1 -1
  29. package/dist/ui/assets/{crystal-DiZfCxn-.js → crystal-DbAbDEO1.js} +1 -1
  30. package/dist/ui/assets/{css-DiQoSjDH.js → css-xwljL6Kw.js} +1 -1
  31. package/dist/ui/assets/{edge-wEhCUoEt.js → edge-CX3sYz15.js} +1 -1
  32. package/dist/ui/assets/{elixir-C3obhm0H.js → elixir-B3qL9hqd.js} +1 -1
  33. package/dist/ui/assets/{elm-DyTBfh1L.js → elm-CH4vrjBp.js} +1 -1
  34. package/dist/ui/assets/{erb-f4TnXRuY.js → erb-CupHqynN.js} +1 -1
  35. package/dist/ui/assets/{git-rebase-vhZbWNfp.js → git-rebase-CDxsU2LC.js} +1 -1
  36. package/dist/ui/assets/{glimmer-js-BG7puL6Y.js → glimmer-js-emkga3g1.js} +1 -1
  37. package/dist/ui/assets/{glimmer-ts-CNCfLLL1.js → glimmer-ts-D8kQDnUw.js} +1 -1
  38. package/dist/ui/assets/{glsl-DhT76AL5.js → glsl-BtfTaPhi.js} +1 -1
  39. package/dist/ui/assets/{graphql-CJX08w8y.js → graphql-DEus8iSO.js} +1 -1
  40. package/dist/ui/assets/{hack-DY4NGCCL.js → hack-DI8qpBqE.js} +1 -1
  41. package/dist/ui/assets/{haml-DwSkmVcG.js → haml-Br1i1gmi.js} +1 -1
  42. package/dist/ui/assets/{handlebars-CQ_1dEmi.js → handlebars-Ce23mu9x.js} +1 -1
  43. package/dist/ui/assets/{heavy-payload-2rGBiitA.js → heavy-payload-BcZ_nGfF.js} +1 -1
  44. package/dist/ui/assets/{html-B_e3Njif.js → html-RXVmfixf.js} +1 -1
  45. package/dist/ui/assets/{html-derivative-_nHZ_Bk-.js → html-derivative-D1aqeobb.js} +1 -1
  46. package/dist/ui/assets/{http-CfcvPIru.js → http-BebG00CE.js} +1 -1
  47. package/dist/ui/assets/{hurl-9vWDQIMO.js → hurl-kHMuvenL.js} +1 -1
  48. package/dist/ui/assets/{java-C4S5r6L5.js → java-DYc2V5RJ.js} +1 -1
  49. package/dist/ui/assets/{javascript-BRwqbMMC.js → javascript-BcfzTzr3.js} +1 -1
  50. package/dist/ui/assets/{jinja-DHxzMMc5.js → jinja-D2RhihUZ.js} +1 -1
  51. package/dist/ui/assets/{jison-B1DqeWXb.js → jison-BuoFEePN.js} +1 -1
  52. package/dist/ui/assets/{json-DcFTkEdd.js → json-BIDkCuJA.js} +1 -1
  53. package/dist/ui/assets/{jsx-DgfmYa9f.js → jsx-DHCL2HA0.js} +1 -1
  54. package/dist/ui/assets/{julia-3M8SJ8iF.js → julia-CvUQXscF.js} +1 -1
  55. package/dist/ui/assets/{just-UyH7_DNz.js → just-etc9R2qw.js} +1 -1
  56. package/dist/ui/assets/{latex-wUbha6EX.js → latex-BB3zRnxC.js} +1 -1
  57. package/dist/ui/assets/{liquid-DqNTB9kN.js → liquid-CIi5Mvpv.js} +1 -1
  58. package/dist/ui/assets/{lua-DrEQ1QTW.js → lua-Cox7XDiW.js} +1 -1
  59. package/dist/ui/assets/{marko-B3GJw9Md.js → marko-ruBIRV0n.js} +1 -1
  60. package/dist/ui/assets/{mdc-BnThk82m.js → mdc-CcbvB9U5.js} +1 -1
  61. package/dist/ui/assets/{nginx-B5IeSsOX.js → nginx-DtWz6ZYE.js} +1 -1
  62. package/dist/ui/assets/{nim-BJraatnS.js → nim-BV5BR4P7.js} +1 -1
  63. package/dist/ui/assets/{perl-BAhrR4gt.js → perl-BvzoSaJ4.js} +1 -1
  64. package/dist/ui/assets/{php-BKRCk_wX.js → php-C-clrAOy.js} +1 -1
  65. package/dist/ui/assets/{pug-B8gmSV7z.js → pug-E07_GXAr.js} +1 -1
  66. package/dist/ui/assets/{qml-DKrxIjHR.js → qml-dqt33dti.js} +1 -1
  67. package/dist/ui/assets/{r-YASBZvTH.js → r-BcVQxsAw.js} +1 -1
  68. package/dist/ui/assets/{razor-P-XFZqh6.js → razor-BW5T_UZ-.js} +1 -1
  69. package/dist/ui/assets/{regexp-CCpiH6nO.js → regexp-Ed_DEsuU.js} +1 -1
  70. package/dist/ui/assets/{review-payload-BLYwg6t8.js → review-payload-Dsmx33pY.js} +1 -1
  71. package/dist/ui/assets/{rst-CFAzckbn.js → rst-BkcGqLs8.js} +1 -1
  72. package/dist/ui/assets/{ruby-Dyp_Rx-S.js → ruby-Sf_X_YID.js} +1 -1
  73. package/dist/ui/assets/{sas-YkWZIXxT.js → sas-COsombsA.js} +1 -1
  74. package/dist/ui/assets/{scss-Cf89idGl.js → scss-K6Ylfa26.js} +1 -1
  75. package/dist/ui/assets/{shellscript-BFMb52q8.js → shellscript-D2EdqkgV.js} +1 -1
  76. package/dist/ui/assets/{shellsession-DuthW-5v.js → shellsession-jRXgXrnI.js} +1 -1
  77. package/dist/ui/assets/{soy-BohVVlJU.js → soy-BGspKQWW.js} +1 -1
  78. package/dist/ui/assets/{sql-CnK9R2Gt.js → sql-C4lAmYnM.js} +1 -1
  79. package/dist/ui/assets/{stata-D-sV4Vbr.js → stata-DQWvRqxp.js} +1 -1
  80. package/dist/ui/assets/{surrealql-ChAy0gHv.js → surrealql-OmKt9XJ_.js} +1 -1
  81. package/dist/ui/assets/{svelte-3HBVqG-H.js → svelte-D0rUmwDI.js} +1 -1
  82. package/dist/ui/assets/{templ-BixlV4Cp.js → templ-B-U0RMc0.js} +1 -1
  83. package/dist/ui/assets/{tex-DWoJKHvD.js → tex-BIHD-btS.js} +1 -1
  84. package/dist/ui/assets/{ts-tags-DLA8eH13.js → ts-tags-CGuDWfPC.js} +1 -1
  85. package/dist/ui/assets/{tsx-BYWa5JUz.js → tsx-B0-h_HlD.js} +1 -1
  86. package/dist/ui/assets/{twig-CnY65vAo.js → twig-BN2qDJWN.js} +1 -1
  87. package/dist/ui/assets/{typescript-ByInUsro.js → typescript-BRnwPzY3.js} +1 -1
  88. package/dist/ui/assets/{useFileDiffInstance-Dg0pWcJV.js → useFileDiffInstance-5ivdkUYO.js} +3 -3
  89. package/dist/ui/assets/{vue-Qngt2rkz.js → vue-DekNU1qt.js} +1 -1
  90. package/dist/ui/assets/{vue-html-DDue9upr.js → vue-html-Bjlv-uvH.js} +1 -1
  91. package/dist/ui/assets/{vue-vine-DBcAKsA6.js → vue-vine-C2Doytjq.js} +1 -1
  92. package/dist/ui/assets/{workspace-app-DGD9R89D.css → workspace-app-DASlX26z.css} +1 -1
  93. package/dist/ui/assets/{workspace-app-DuzpU4Hf.js → workspace-app-DYi7VJam.js} +5 -5
  94. package/dist/ui/assets/{xml-CRLR5jiL.js → xml-pXbxxgI3.js} +1 -1
  95. package/dist/ui/assets/{xsl-CmcwyXbm.js → xsl-BEAlgDI2.js} +1 -1
  96. package/dist/ui/assets/{yaml-Dpe6Eg-k.js → yaml-C1O-To71.js} +1 -1
  97. package/dist/ui/workspace-app.html +2 -2
  98. package/dist/user-config.js +21 -1
  99. package/dist/workspaces.js +3 -0
  100. package/docs/agent-profile-schema.md +165 -0
  101. package/docs/chatgpt-coding-workflow.md +46 -12
  102. package/docs/configuration.md +47 -12
  103. package/docs/gotchas.md +24 -5
  104. package/docs/setup.md +1 -1
  105. package/examples/agents/claude-implementer.md +30 -0
  106. package/examples/agents/codex-explorer.md +27 -0
  107. package/examples/agents/codex-qa-tester.md +29 -0
  108. package/examples/agents/codex-worker.md +25 -0
  109. package/examples/agents/copilot-reviewer.md +25 -0
  110. package/examples/agents/cursor-agent-worker.md +26 -0
  111. package/examples/agents/opencode-explorer.md +27 -0
  112. package/examples/agents/pi-reviewer.md +27 -0
  113. package/package.json +18 -5
  114. package/scripts/fix-node-pty-permissions.mjs +22 -0
  115. package/skills/subagent-delegation/SKILL.md +132 -0
package/dist/server.js CHANGED
@@ -12,14 +12,18 @@ import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextpro
12
12
  import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
13
13
  import express from "express";
14
14
  import * as z from "zod/v4";
15
+ import { applyPatch } from "./apply-patch.js";
15
16
  import { loadConfig } from "./config.js";
16
17
  import { logEvent, requestIp, requestPath, commandPreview, sessionIdPrefix, } from "./logger.js";
17
18
  import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, runShellTool, writeFileTool, } from "./pi-tools.js";
18
19
  import { SingleUserOAuthProvider } from "./oauth-provider.js";
20
+ import { ProcessSessionManager } from "./process-sessions.js";
19
21
  import { createReviewCheckpointManager } from "./review-checkpoints.js";
20
22
  import { formatPathForPrompt } from "./skills.js";
21
23
  import { createWorkspaceStore } from "./workspace-store.js";
22
24
  import { formatAgentsPath, WorkspaceRegistry } from "./workspaces.js";
25
+ import { summarizeLocalAgentProfile } from "./local-agent-profiles.js";
26
+ import { formatLocalAgentProviderAvailabilitySummary, getLocalAgentProviderAvailabilitySnapshot, } from "./local-agent-availability.js";
23
27
  const WORKSPACE_APP_URI = "ui://devspace/workspace-app.html";
24
28
  const WORKSPACE_APP_MANIFEST_ENTRY = "workspace-app.html";
25
29
  const WRITE_TOOL_ANNOTATIONS = {
@@ -62,41 +66,42 @@ function toolWidgetDescriptorMeta(config, kind) {
62
66
  },
63
67
  };
64
68
  }
65
- function toolNamesFor(config) {
66
- return config.toolNaming === "short"
67
- ? {
68
- openWorkspace: "open_workspace",
69
- read: "read",
70
- write: "write",
71
- edit: "edit",
72
- grep: "grep",
73
- glob: "glob",
74
- ls: "ls",
75
- shell: "bash",
76
- }
77
- : {
78
- openWorkspace: "open_workspace",
79
- read: "read_file",
80
- write: "write_file",
81
- edit: "edit_file",
82
- grep: "grep_files",
83
- glob: "find_files",
84
- ls: "list_directory",
85
- shell: "run_shell",
86
- };
87
- }
88
- function serverInstructions(config, toolNames) {
89
- const inspection = config.minimalTools
69
+ const toolNames = {
70
+ openWorkspace: "open_workspace",
71
+ read: "read",
72
+ write: "write",
73
+ edit: "edit",
74
+ grep: "grep",
75
+ glob: "glob",
76
+ ls: "ls",
77
+ shell: "bash",
78
+ };
79
+ function serverInstructions(config) {
80
+ const showChangesInstruction = config.widgets === "changes"
81
+ ? " If the turn successfully modifies files by creating, editing, overwriting, deleting, moving, or applying patches, call show_changes exactly once for that workspace after the final related file change and before your final response so the user can inspect the aggregate diff for that turn. Do not call it after every individual file change; do not skip it because individual file-change tools already returned diffs."
82
+ : "";
83
+ if (config.toolMode === "codex") {
84
+ return `Use DevSpace as a local coding workspace. Call ${toolNames.openWorkspace} once per project folder or worktree and reuse its workspaceId. Use ${toolNames.read} for direct file reads, apply_patch for all file modifications, exec_command for inspection, tests, builds, and other commands, and write_stdin to poll or interact with running processes. Follow instructions returned by ${toolNames.openWorkspace}; read applicable instruction and skill files before working in their scope.${showChangesInstruction}`;
85
+ }
86
+ const inspection = config.toolMode !== "full"
90
87
  ? `In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled; use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection. `
91
88
  : `Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection. `;
92
89
  const skills = config.skillsEnabled
93
90
  ? `When ${toolNames.openWorkspace} returns available skills and a task matches a skill, use ${toolNames.read} to read that skill's path before proceeding. Skill paths may be outside the workspace, but ${toolNames.read} only permits advertised SKILL.md files and files under already-loaded skill directories. `
94
91
  : "";
95
92
  const agentsMd = `Follow instructions returned by ${toolNames.openWorkspace}. Before working under a path listed in availableAgentsFiles, use ${toolNames.read} to inspect that instruction file and follow it. `;
96
- const showChanges = config.widgets === "changes"
97
- ? " After creating, editing, or overwriting files, call show_changes once after the related file changes are complete so the user can see the aggregate diff."
93
+ return `Use DevSpace as a local coding workspace. Call ${toolNames.openWorkspace} once per project folder or worktree to obtain a workspaceId. Reuse that same workspaceId for all later file, search, edit, write, show-changes, and shell tools in that folder; do not call ${toolNames.openWorkspace} again unless switching folders/worktrees, changing checkout/worktree mode, the workspaceId is rejected as unknown, or the user explicitly asks to reopen. ${agentsMd}${skills}${inspection}Prefer ${toolNames.edit} for targeted modifications, ${toolNames.write} only for new files or complete rewrites, and ${toolNames.shell} for tests, builds, git inspection, package scripts, and commands that are better executed by the shell. Do not create or modify files with ${toolNames.shell}; avoid shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or any command whose purpose is to write project files.${showChangesInstruction}`;
94
+ }
95
+ function formatVisibleAgent(agent) {
96
+ const model = agent.model ? `, model ${agent.model}` : "";
97
+ const thinking = agent.thinking ? `, thinking ${agent.thinking}` : "";
98
+ const availability = agent.providerAvailable === false
99
+ ? `, unavailable: ${agent.providerUnavailableReason ?? "provider unavailable"}`
98
100
  : "";
99
- return `Use DevSpace as a local coding workspace. Call ${toolNames.openWorkspace} once per project folder or worktree to obtain a workspaceId. Reuse that same workspaceId for all later file, search, edit, write, show-changes, and shell tools in that folder; do not call ${toolNames.openWorkspace} again unless switching folders/worktrees, changing checkout/worktree mode, the workspaceId is rejected as unknown, or the user explicitly asks to reopen. ${agentsMd}${skills}${inspection}Prefer ${toolNames.edit} for targeted modifications, ${toolNames.write} only for new files or complete rewrites, and ${toolNames.shell} for tests, builds, git inspection, package scripts, and commands that are better executed by the shell. Do not create or modify files with ${toolNames.shell}; avoid shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or any command whose purpose is to write project files.${showChanges}`;
101
+ return `${agent.name} (${agent.provider}${model}${thinking}${availability})`;
102
+ }
103
+ function formatUnavailableAgentProvider(provider) {
104
+ return `${provider.name} (${provider.reason ?? "unavailable"})`;
100
105
  }
101
106
  function resultOutputSchema(extra = {}) {
102
107
  return {
@@ -115,6 +120,20 @@ const workspaceAgentsFileOutputSchema = z.object({
115
120
  path: z.string(),
116
121
  content: z.string(),
117
122
  });
123
+ const workspaceLocalAgentOutputSchema = z.object({
124
+ name: z.string(),
125
+ description: z.string(),
126
+ provider: z.string(),
127
+ model: z.string().optional(),
128
+ thinking: z.string().optional(),
129
+ providerAvailable: z.boolean().optional(),
130
+ providerUnavailableReason: z.string().optional(),
131
+ });
132
+ const workspaceLocalAgentProviderOutputSchema = z.object({
133
+ name: z.string(),
134
+ available: z.boolean(),
135
+ reason: z.string().optional(),
136
+ });
118
137
  const workspaceAvailableAgentsFileOutputSchema = z.object({
119
138
  path: z.string(),
120
139
  });
@@ -292,15 +311,178 @@ async function assertWorkspaceAppAssets() {
292
311
  await access(candidate);
293
312
  }
294
313
  }
295
- function createMcpServer(config, workspaces, reviewCheckpoints) {
296
- const toolNames = toolNamesFor(config);
314
+ function processResult(snapshot) {
315
+ const status = snapshot.running
316
+ ? `Process running with session ID ${snapshot.sessionId}.`
317
+ : snapshot.signal
318
+ ? `Process exited after signal ${snapshot.signal}.`
319
+ : `Process exited with code ${snapshot.exitCode ?? "unknown"}.`;
320
+ return snapshot.output ? `${snapshot.output.replace(/\n$/, "")}\n${status}` : status;
321
+ }
322
+ function processOutputSchema() {
323
+ return resultOutputSchema({
324
+ sessionId: z.number().optional(),
325
+ running: z.boolean(),
326
+ exitCode: z.number().int().optional(),
327
+ signal: z.string().optional(),
328
+ wallTimeMs: z.number().nonnegative(),
329
+ outputTruncated: z.boolean(),
330
+ });
331
+ }
332
+ function processToolResponse(tool, workspaceId, snapshot, summary) {
333
+ const result = processResult(snapshot);
334
+ const content = [textBlock(result)];
335
+ const outputSummary = textSummary(snapshot.output ? [textBlock(snapshot.output)] : []);
336
+ return {
337
+ content,
338
+ _meta: {
339
+ tool,
340
+ card: {
341
+ workspaceId,
342
+ summary: { ...summary, ...outputSummary },
343
+ payload: { content },
344
+ },
345
+ },
346
+ structuredContent: {
347
+ result,
348
+ sessionId: snapshot.sessionId,
349
+ running: snapshot.running,
350
+ exitCode: snapshot.exitCode,
351
+ signal: snapshot.signal,
352
+ wallTimeMs: snapshot.wallTimeMs,
353
+ outputTruncated: snapshot.outputTruncated,
354
+ },
355
+ };
356
+ }
357
+ function registerCodexProcessTools(server, config, workspaces, processSessions) {
358
+ registerAppTool(server, "exec_command", {
359
+ title: "Execute command",
360
+ 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, and long-running processes. Call open_workspace first and pass workspaceId.",
361
+ inputSchema: {
362
+ workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
363
+ cmd: z.string().min(1).describe("Shell command to execute."),
364
+ tty: z
365
+ .boolean()
366
+ .optional()
367
+ .describe("Allocate a pseudo-terminal for interactive commands. Defaults to false."),
368
+ columns: z.number().int().min(1).max(1_000).optional().describe("Initial PTY width. Defaults to 80."),
369
+ rows: z.number().int().min(1).max(1_000).optional().describe("Initial PTY height. Defaults to 24."),
370
+ workingDirectory: z
371
+ .string()
372
+ .optional()
373
+ .describe("Working directory relative to the workspace root. Defaults to the workspace root."),
374
+ yieldTimeMs: z
375
+ .number()
376
+ .int()
377
+ .min(0)
378
+ .max(30_000)
379
+ .optional()
380
+ .describe("Milliseconds to wait before returning a running session. Defaults to 10000."),
381
+ maxOutputTokens: z
382
+ .number()
383
+ .int()
384
+ .positive()
385
+ .max(100_000)
386
+ .optional()
387
+ .describe("Approximate output token budget. Defaults to 10000."),
388
+ },
389
+ outputSchema: processOutputSchema(),
390
+ ...toolWidgetDescriptorMeta(config, "shell"),
391
+ annotations: SHELL_TOOL_ANNOTATIONS,
392
+ }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, maxOutputTokens }) => {
393
+ const startedAt = performance.now();
394
+ const workspace = workspaces.getWorkspace(workspaceId);
395
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
396
+ const snapshot = await processSessions.start({
397
+ workspaceId,
398
+ command: cmd,
399
+ cwd,
400
+ workspaceRoot: workspace.root,
401
+ tty,
402
+ columns,
403
+ rows,
404
+ yieldTimeMs,
405
+ maxOutputTokens,
406
+ });
407
+ logToolCall(config, {
408
+ tool: "exec_command",
409
+ workspaceId,
410
+ workingDirectory: workingDirectory ?? ".",
411
+ command: cmd,
412
+ commandLength: cmd.length,
413
+ success: true,
414
+ durationMs: Math.round(performance.now() - startedAt),
415
+ });
416
+ return processToolResponse("exec_command", workspaceId, snapshot, {
417
+ command: cmd,
418
+ workingDirectory: workingDirectory ?? ".",
419
+ running: snapshot.running,
420
+ exitCode: snapshot.exitCode,
421
+ wallTimeMs: snapshot.wallTimeMs,
422
+ });
423
+ });
424
+ registerAppTool(server, "write_stdin", {
425
+ title: "Write to process",
426
+ description: "Poll or write characters to a process returned by exec_command. Omit chars or pass an empty string to poll. Pass \\u0003 to send Ctrl-C.",
427
+ inputSchema: {
428
+ workspaceId: z.string().describe("Workspace identifier used to start the process."),
429
+ sessionId: z.number().describe("Process session identifier returned by exec_command."),
430
+ chars: z.string().optional().describe("Characters to write. Omit or pass an empty string to poll."),
431
+ columns: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this width."),
432
+ rows: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this height."),
433
+ yieldTimeMs: z
434
+ .number()
435
+ .int()
436
+ .min(0)
437
+ .max(30_000)
438
+ .optional()
439
+ .describe("Milliseconds to wait for process output or completion. Defaults to 10000."),
440
+ maxOutputTokens: z
441
+ .number()
442
+ .int()
443
+ .positive()
444
+ .max(100_000)
445
+ .optional()
446
+ .describe("Approximate output token budget. Defaults to 10000."),
447
+ },
448
+ outputSchema: processOutputSchema(),
449
+ ...toolWidgetDescriptorMeta(config, "shell"),
450
+ annotations: SHELL_TOOL_ANNOTATIONS,
451
+ }, async ({ workspaceId, sessionId, chars, columns, rows, yieldTimeMs, maxOutputTokens }) => {
452
+ const startedAt = performance.now();
453
+ workspaces.getWorkspace(workspaceId);
454
+ const snapshot = await processSessions.write({
455
+ workspaceId,
456
+ sessionId,
457
+ chars,
458
+ columns,
459
+ rows,
460
+ yieldTimeMs,
461
+ maxOutputTokens,
462
+ });
463
+ logToolCall(config, {
464
+ tool: "write_stdin",
465
+ workspaceId,
466
+ success: true,
467
+ durationMs: Math.round(performance.now() - startedAt),
468
+ });
469
+ return processToolResponse("write_stdin", workspaceId, snapshot, {
470
+ sessionId,
471
+ charactersWritten: chars?.length ?? 0,
472
+ running: snapshot.running,
473
+ exitCode: snapshot.exitCode,
474
+ wallTimeMs: snapshot.wallTimeMs,
475
+ });
476
+ });
477
+ }
478
+ function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders) {
297
479
  const server = new McpServer({
298
480
  name: "devspace",
299
481
  title: "DevSpace",
300
482
  version: "0.1.0",
301
483
  description: "Secure local coding workspace for MCP clients. Provides workspace-scoped file, search, edit, write, and shell tools.",
302
484
  }, {
303
- instructions: serverInstructions(config, toolNames),
485
+ instructions: serverInstructions(config),
304
486
  });
305
487
  registerAppResource(server, "DevSpace Diff Card", WORKSPACE_APP_URI, {
306
488
  description: "Interactive card for viewing DevSpace file diffs.",
@@ -360,6 +542,8 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
360
542
  agentsFiles: z.array(workspaceAgentsFileOutputSchema),
361
543
  availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema),
362
544
  skills: z.array(workspaceSkillOutputSchema),
545
+ agentProviders: z.array(workspaceLocalAgentProviderOutputSchema),
546
+ agents: z.array(workspaceLocalAgentOutputSchema),
363
547
  skillDiagnostics: z.array(z.unknown()),
364
548
  instruction: z.string(),
365
549
  },
@@ -381,6 +565,16 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
381
565
  description: skill.description,
382
566
  path: formatPathForPrompt(skill.filePath),
383
567
  }));
568
+ const visibleAgentProviders = config.subagents ? localAgentProviders : [];
569
+ const visibleAgents = workspace.agentProfiles.map((profile) => {
570
+ const summary = summarizeLocalAgentProfile(profile);
571
+ const availability = visibleAgentProviders.find((provider) => provider.name === summary.provider);
572
+ return {
573
+ ...summary,
574
+ providerAvailable: availability?.available,
575
+ providerUnavailableReason: availability?.reason,
576
+ };
577
+ });
384
578
  const loadedAgentsFiles = agentsFiles.map((file) => ({
385
579
  path: formatAgentsPath(file.path, workspace.root),
386
580
  content: file.content,
@@ -407,6 +601,15 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
407
601
  visibleSkills.length > 0
408
602
  ? `Available skills: ${visibleSkills.map((skill) => skill.name).join(", ")}`
409
603
  : undefined,
604
+ visibleAgentProviders.some((provider) => provider.available)
605
+ ? `Available subagent providers: ${visibleAgentProviders.filter((provider) => provider.available).map((provider) => provider.name).join(", ")}`
606
+ : undefined,
607
+ visibleAgentProviders.some((provider) => !provider.available)
608
+ ? `Unavailable subagent providers: ${visibleAgentProviders.filter((provider) => !provider.available).map(formatUnavailableAgentProvider).join(", ")}`
609
+ : undefined,
610
+ visibleAgents.length > 0
611
+ ? `Available subagent profiles: ${visibleAgents.map(formatVisibleAgent).join(", ")}`
612
+ : undefined,
410
613
  instruction,
411
614
  ].filter(Boolean).join("\n"),
412
615
  },
@@ -430,6 +633,8 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
430
633
  agentsFiles: loadedAgentsFiles.length,
431
634
  availableAgentsFiles: availableAgentsFileOutputs.length,
432
635
  skills: visibleSkills.length,
636
+ agentProviders: visibleAgentProviders.length,
637
+ agents: visibleAgents.length,
433
638
  skillDiagnostics: workspace.skillDiagnostics.length,
434
639
  },
435
640
  },
@@ -443,6 +648,8 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
443
648
  agentsFiles: loadedAgentsFiles,
444
649
  availableAgentsFiles: availableAgentsFileOutputs,
445
650
  skills: visibleSkills,
651
+ agentProviders: visibleAgentProviders,
652
+ agents: visibleAgents,
446
653
  skillDiagnostics: workspace.skillDiagnostics,
447
654
  instruction,
448
655
  },
@@ -530,172 +737,229 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
530
737
  },
531
738
  };
532
739
  });
533
- registerAppTool(server, toolNames.write, {
534
- title: "Write file",
535
- description: `Create or completely overwrite a file inside an open workspace. Prefer ${toolNames.edit} for targeted changes to existing files. Call open_workspace first and pass workspaceId.`,
536
- inputSchema: {
537
- workspaceId: z
538
- .string()
539
- .describe("Workspace identifier returned by open_workspace."),
540
- path: z
541
- .string()
542
- .describe("File path to write, relative to the workspace root."),
543
- content: z.string().describe("Complete new file content."),
544
- },
545
- outputSchema: resultOutputSchema(),
546
- ...toolWidgetDescriptorMeta(config, "write"),
547
- annotations: WRITE_TOOL_ANNOTATIONS,
548
- }, async ({ workspaceId, ...input }) => {
549
- const startedAt = performance.now();
550
- const workspace = workspaces.getWorkspace(workspaceId);
551
- workspaces.resolvePath(workspace, input.path);
552
- const response = await writeFileTool(input, {
553
- cwd: workspace.root,
554
- root: workspace.root,
555
- });
556
- if (response.isError) {
557
- logFailedToolResponse(config, {
740
+ if (config.toolMode !== "codex") {
741
+ registerAppTool(server, toolNames.write, {
742
+ title: "Write file",
743
+ description: `Create or completely overwrite a file inside an open workspace. Prefer ${toolNames.edit} for targeted changes to existing files. Call open_workspace first and pass workspaceId.`,
744
+ inputSchema: {
745
+ workspaceId: z
746
+ .string()
747
+ .describe("Workspace identifier returned by open_workspace."),
748
+ path: z
749
+ .string()
750
+ .describe("File path to write, relative to the workspace root."),
751
+ content: z.string().describe("Complete new file content."),
752
+ },
753
+ outputSchema: resultOutputSchema(),
754
+ ...toolWidgetDescriptorMeta(config, "write"),
755
+ annotations: WRITE_TOOL_ANNOTATIONS,
756
+ }, async ({ workspaceId, ...input }) => {
757
+ const startedAt = performance.now();
758
+ const workspace = workspaces.getWorkspace(workspaceId);
759
+ workspaces.resolvePath(workspace, input.path);
760
+ const response = await writeFileTool(input, {
761
+ cwd: workspace.root,
762
+ root: workspace.root,
763
+ });
764
+ if (response.isError) {
765
+ logFailedToolResponse(config, {
766
+ tool: toolNames.write,
767
+ workspaceId,
768
+ path: input.path,
769
+ }, response.content, startedAt);
770
+ return response;
771
+ }
772
+ const patch = newFilePatch(input.path, input.content);
773
+ const stats = countDiffStats(patch);
774
+ const summary = {
775
+ ...stats,
776
+ lines: contentLineCount(input.content),
777
+ characters: input.content.length,
778
+ };
779
+ logToolCall(config, {
558
780
  tool: toolNames.write,
559
781
  workspaceId,
560
782
  path: input.path,
561
- }, response.content, startedAt);
562
- return response;
563
- }
564
- const patch = newFilePatch(input.path, input.content);
565
- const stats = countDiffStats(patch);
566
- const summary = {
567
- ...stats,
568
- lines: contentLineCount(input.content),
569
- characters: input.content.length,
570
- };
571
- logToolCall(config, {
572
- tool: toolNames.write,
573
- workspaceId,
574
- path: input.path,
575
- success: true,
576
- durationMs: Math.round(performance.now() - startedAt),
577
- });
578
- return {
579
- ...response,
580
- _meta: {
581
- tool: toolNames.write,
582
- card: {
583
- workspaceId,
584
- path: input.path,
585
- summary,
586
- payload: {
587
- content: response.content,
588
- patch,
783
+ success: true,
784
+ durationMs: Math.round(performance.now() - startedAt),
785
+ });
786
+ return {
787
+ ...response,
788
+ _meta: {
789
+ tool: toolNames.write,
790
+ card: {
791
+ workspaceId,
792
+ path: input.path,
793
+ summary,
794
+ payload: {
795
+ content: response.content,
796
+ patch,
797
+ },
589
798
  },
590
799
  },
591
- },
592
- structuredContent: {
593
- result: contentText(response.content),
594
- },
595
- };
596
- });
597
- registerAppTool(server, toolNames.edit, {
598
- title: "Edit file",
599
- description: `Edit one file inside an open workspace by replacing exact text blocks. Prefer this over ${toolNames.write} for targeted changes. Each oldText must match a unique, non-overlapping region of the original file; merge nearby changes into one edit and keep oldText as small as possible while still unique. Call open_workspace first and pass workspaceId.`,
600
- inputSchema: {
601
- workspaceId: z
602
- .string()
603
- .describe("Workspace identifier returned by open_workspace."),
604
- path: z
605
- .string()
606
- .describe("File path to edit, relative to the workspace root."),
607
- edits: z
608
- .array(z.object({
609
- oldText: z
610
- .string()
611
- .describe("Exact text to replace. Must match uniquely in the original file."),
612
- newText: z.string().describe("Replacement text."),
613
- }))
614
- .min(1),
615
- },
616
- outputSchema: resultOutputSchema({
617
- status: z.literal("applied"),
618
- }),
619
- ...toolWidgetDescriptorMeta(config, "edit"),
620
- annotations: EDIT_TOOL_ANNOTATIONS,
621
- }, async ({ workspaceId, ...input }) => {
622
- const startedAt = performance.now();
623
- const workspace = workspaces.getWorkspace(workspaceId);
624
- workspaces.resolvePath(workspace, input.path);
625
- const response = await editFileTool(input, {
626
- cwd: workspace.root,
627
- root: workspace.root,
800
+ structuredContent: {
801
+ result: contentText(response.content),
802
+ },
803
+ };
628
804
  });
629
- if (response.isError) {
630
- logFailedToolResponse(config, {
805
+ registerAppTool(server, toolNames.edit, {
806
+ title: "Edit file",
807
+ description: `Edit one file inside an open workspace by replacing exact text blocks. Prefer this over ${toolNames.write} for targeted changes. Each oldText must match a unique, non-overlapping region of the original file; merge nearby changes into one edit and keep oldText as small as possible while still unique. Call open_workspace first and pass workspaceId.`,
808
+ inputSchema: {
809
+ workspaceId: z
810
+ .string()
811
+ .describe("Workspace identifier returned by open_workspace."),
812
+ path: z
813
+ .string()
814
+ .describe("File path to edit, relative to the workspace root."),
815
+ edits: z
816
+ .array(z.object({
817
+ oldText: z
818
+ .string()
819
+ .describe("Exact text to replace. Must match uniquely in the original file."),
820
+ newText: z.string().describe("Replacement text."),
821
+ }))
822
+ .min(1),
823
+ },
824
+ outputSchema: resultOutputSchema({
825
+ status: z.literal("applied"),
826
+ }),
827
+ ...toolWidgetDescriptorMeta(config, "edit"),
828
+ annotations: EDIT_TOOL_ANNOTATIONS,
829
+ }, async ({ workspaceId, ...input }) => {
830
+ const startedAt = performance.now();
831
+ const workspace = workspaces.getWorkspace(workspaceId);
832
+ workspaces.resolvePath(workspace, input.path);
833
+ const response = await editFileTool(input, {
834
+ cwd: workspace.root,
835
+ root: workspace.root,
836
+ });
837
+ if (response.isError) {
838
+ logFailedToolResponse(config, {
839
+ tool: toolNames.edit,
840
+ workspaceId,
841
+ path: input.path,
842
+ }, response.content, startedAt);
843
+ return response;
844
+ }
845
+ const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
846
+ const summary = {
847
+ ...stats,
848
+ editCount: input.edits.length,
849
+ };
850
+ const editResultText = `Edited ${input.path} (+${stats.additions} -${stats.removals}).`;
851
+ const editContent = [textBlock(editResultText)];
852
+ logToolCall(config, {
631
853
  tool: toolNames.edit,
632
854
  workspaceId,
633
855
  path: input.path,
634
- }, response.content, startedAt);
635
- return response;
636
- }
637
- const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
638
- const summary = {
639
- ...stats,
640
- editCount: input.edits.length,
641
- };
642
- const editResultText = `Edited ${input.path} (+${stats.additions} -${stats.removals}).`;
643
- const editContent = [textBlock(editResultText)];
644
- logToolCall(config, {
645
- tool: toolNames.edit,
646
- workspaceId,
647
- path: input.path,
648
- success: true,
649
- durationMs: Math.round(performance.now() - startedAt),
650
- });
651
- return {
652
- content: editContent,
653
- _meta: {
654
- tool: toolNames.edit,
655
- card: {
656
- workspaceId,
657
- path: input.path,
658
- summary,
659
- payload: {
660
- diff: response.details?.diff,
661
- patch: response.details?.patch,
856
+ success: true,
857
+ durationMs: Math.round(performance.now() - startedAt),
858
+ });
859
+ return {
860
+ content: editContent,
861
+ _meta: {
862
+ tool: toolNames.edit,
863
+ card: {
864
+ workspaceId,
865
+ path: input.path,
866
+ summary,
867
+ payload: {
868
+ diff: response.details?.diff,
869
+ patch: response.details?.patch,
870
+ },
662
871
  },
663
872
  },
873
+ structuredContent: {
874
+ status: "applied",
875
+ result: contentText(editContent),
876
+ },
877
+ };
878
+ });
879
+ }
880
+ if (config.toolMode === "codex") {
881
+ registerAppTool(server, "apply_patch", {
882
+ title: "Apply patch",
883
+ description: "Apply one Codex-style patch inside an open workspace. Supports adding, overwriting, updating, deleting, and moving files. Use this for all file modifications. Paths must be relative to the workspace. Call open_workspace first and pass workspaceId.",
884
+ inputSchema: {
885
+ workspaceId: z
886
+ .string()
887
+ .describe("Workspace identifier returned by open_workspace."),
888
+ patch: z
889
+ .string()
890
+ .describe("Patch text enclosed by *** Begin Patch and *** End Patch markers."),
664
891
  },
665
- structuredContent: {
666
- status: "applied",
667
- result: contentText(editContent),
668
- },
669
- };
670
- });
892
+ outputSchema: resultOutputSchema({
893
+ additions: z.number(),
894
+ removals: z.number(),
895
+ files: z.array(z.object({
896
+ path: z.string(),
897
+ previousPath: z.string().optional(),
898
+ operation: z.enum(["add", "update", "delete", "move"]),
899
+ })),
900
+ }),
901
+ ...toolWidgetDescriptorMeta(config, "edit"),
902
+ annotations: EDIT_TOOL_ANNOTATIONS,
903
+ }, async ({ workspaceId, patch }) => {
904
+ const startedAt = performance.now();
905
+ const workspace = workspaces.getWorkspace(workspaceId);
906
+ const applied = await applyPatch(workspace.root, patch);
907
+ const paths = applied.files.map((file) => file.path).join(", ");
908
+ const result = `Applied patch to ${applied.files.length} file(s): ${paths}`;
909
+ const content = [textBlock(result)];
910
+ const displayPath = applied.files.length === 1
911
+ ? applied.files[0]?.path
912
+ : `${applied.files.length} files`;
913
+ logToolCall(config, {
914
+ tool: "apply_patch",
915
+ workspaceId,
916
+ success: true,
917
+ durationMs: Math.round(performance.now() - startedAt),
918
+ });
919
+ return {
920
+ content,
921
+ _meta: {
922
+ tool: "apply_patch",
923
+ card: {
924
+ workspaceId,
925
+ path: displayPath,
926
+ summary: {
927
+ files: applied.files.length,
928
+ additions: applied.additions,
929
+ removals: applied.removals,
930
+ },
931
+ payload: { patch: applied.patch },
932
+ },
933
+ },
934
+ structuredContent: {
935
+ result,
936
+ additions: applied.additions,
937
+ removals: applied.removals,
938
+ files: applied.files,
939
+ },
940
+ };
941
+ });
942
+ }
671
943
  if (config.widgets === "changes") {
672
944
  registerAppTool(server, "show_changes", {
673
945
  title: "Show changes",
674
- description: "Show aggregate file changes in an open workspace since the last shown checkpoint or since the workspace was opened. After you create, edit, or overwrite files, call this once when the related file changes are complete so the user can inspect the combined diff.",
946
+ description: "Show aggregate file changes for an open workspace. If the current turn successfully modified files, call this exactly once after the final related file change and before your final response so the user can inspect the combined diff for the turn. Do not call it after every individual file change, and do not skip it because prior file-change tools already displayed per-tool diffs.",
675
947
  inputSchema: {
676
948
  workspaceId: z
677
949
  .string()
678
950
  .describe("Workspace identifier returned by open_workspace."),
679
- since: z
680
- .enum(["last_shown", "workspace_open"])
681
- .optional()
682
- .describe("Defaults to last_shown. Use workspace_open to compare against the initial open_workspace checkpoint."),
683
- markReviewed: z
684
- .boolean()
685
- .optional()
686
- .describe("Defaults to true. When true, advances the last shown checkpoint to the current workspace state."),
687
951
  },
688
952
  outputSchema: resultOutputSchema(),
689
953
  ...toolWidgetDescriptorMeta(config, "show_changes"),
690
954
  annotations: { readOnlyHint: true },
691
- }, async ({ workspaceId, since, markReviewed }) => {
955
+ }, async ({ workspaceId }) => {
692
956
  const startedAt = performance.now();
693
957
  const workspace = workspaces.getWorkspace(workspaceId);
694
958
  const review = await reviewCheckpoints.reviewChanges({
695
959
  workspaceId,
696
960
  root: workspace.root,
697
- since: since ?? "last_shown",
698
- markReviewed: markReviewed ?? true,
961
+ since: "last_shown",
962
+ markReviewed: true,
699
963
  });
700
964
  const content = [textBlock(review.result)];
701
965
  logToolCall(config, {
@@ -723,9 +987,9 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
723
987
  };
724
988
  });
725
989
  }
726
- if (!config.minimalTools) {
990
+ if (config.toolMode === "full") {
727
991
  registerAppTool(server, toolNames.grep, {
728
- title: config.toolNaming === "short" ? "Grep" : "Grep files",
992
+ title: "Grep",
729
993
  description: "Search file contents inside an open workspace. Use this before broad reads when looking for symbols, text, or usage sites. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
730
994
  inputSchema: {
731
995
  workspaceId: z
@@ -787,7 +1051,7 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
787
1051
  };
788
1052
  });
789
1053
  registerAppTool(server, toolNames.glob, {
790
- title: config.toolNaming === "short" ? "Glob" : "Find files",
1054
+ title: "Glob",
791
1055
  description: "Find files by glob pattern inside an open workspace. Use this to discover filenames or narrow file sets before reading. Respects project ignore rules. Call open_workspace first and pass workspaceId.",
792
1056
  inputSchema: {
793
1057
  workspaceId: z
@@ -848,7 +1112,7 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
848
1112
  };
849
1113
  });
850
1114
  registerAppTool(server, toolNames.ls, {
851
- title: config.toolNaming === "short" ? "Ls" : "List directory",
1115
+ title: "Ls",
852
1116
  description: "List a directory inside an open workspace. Use this for directory inspection before reading files. Call open_workspace first and pass workspaceId.",
853
1117
  inputSchema: {
854
1118
  workspaceId: z
@@ -902,80 +1166,85 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
902
1166
  };
903
1167
  });
904
1168
  }
905
- registerAppTool(server, toolNames.shell, {
906
- title: config.toolNaming === "short" ? "Bash" : "Run shell",
907
- description: config.minimalTools
908
- ? `Run a shell command inside an open workspace. Use only for tests, builds, git inspection, package scripts, search, file discovery, and directory inspection. In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled; use command-line tools such as grep, rg, find, ls, and tree for those read-only inspection actions. Do not use ${toolNames.shell} to create or modify files. Do not use shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or generated scripts to write project files; use ${toolNames.edit} for targeted changes and ${toolNames.write} for new files or full rewrites. Prefer ${toolNames.read} for direct file reads. Call open_workspace first and pass workspaceId. This is powerful local execution and should only be exposed behind strong authentication.`
909
- : `Run a shell command inside an open workspace. Use only for tests, builds, git inspection, package scripts, and commands that are better executed by the shell. Do not use ${toolNames.shell} to create or modify files. Do not use shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or generated scripts to write project files; use ${toolNames.edit} for targeted changes and ${toolNames.write} for new files or full rewrites. Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection. Call open_workspace first and pass workspaceId. This is powerful local execution and should only be exposed behind strong authentication.`,
910
- inputSchema: {
911
- workspaceId: z
912
- .string()
913
- .describe("Workspace identifier returned by open_workspace."),
914
- command: z
915
- .string()
916
- .describe(`Shell command to run. Must not create or modify project files; use ${toolNames.edit} or ${toolNames.write} for file changes.`),
917
- workingDirectory: z
918
- .string()
919
- .optional()
920
- .describe("Optional working directory relative to the workspace root. Defaults to the workspace root."),
921
- timeout: z
922
- .number()
923
- .positive()
924
- .max(300)
925
- .optional()
926
- .describe("Timeout in seconds. Defaults to 30, max 300."),
927
- },
928
- outputSchema: resultOutputSchema(),
929
- ...toolWidgetDescriptorMeta(config, "shell"),
930
- annotations: SHELL_TOOL_ANNOTATIONS,
931
- }, async ({ workspaceId, workingDirectory, ...input }) => {
932
- const startedAt = performance.now();
933
- const workspace = workspaces.getWorkspace(workspaceId);
934
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
935
- const response = await runShellTool(input, {
936
- cwd,
937
- root: workspace.root,
938
- });
939
- if (response.isError) {
940
- logFailedToolResponse(config, {
1169
+ if (config.toolMode !== "codex") {
1170
+ registerAppTool(server, toolNames.shell, {
1171
+ title: "Bash",
1172
+ description: config.toolMode !== "full"
1173
+ ? `Run a shell command inside an open workspace. Use only for tests, builds, git inspection, package scripts, search, file discovery, and directory inspection. In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled; use command-line tools such as grep, rg, find, ls, and tree for those read-only inspection actions. Do not use ${toolNames.shell} to create or modify files. Do not use shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or generated scripts to write project files; use ${toolNames.edit} for targeted changes and ${toolNames.write} for new files or full rewrites. Prefer ${toolNames.read} for direct file reads. Call open_workspace first and pass workspaceId. This is powerful local execution and should only be exposed behind strong authentication.`
1174
+ : `Run a shell command inside an open workspace. Use only for tests, builds, git inspection, package scripts, and commands that are better executed by the shell. Do not use ${toolNames.shell} to create or modify files. Do not use shell redirection, heredocs, tee, sed -i, perl -i, node/python/ruby scripts, or generated scripts to write project files; use ${toolNames.edit} for targeted changes and ${toolNames.write} for new files or full rewrites. Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection. Call open_workspace first and pass workspaceId. This is powerful local execution and should only be exposed behind strong authentication.`,
1175
+ inputSchema: {
1176
+ workspaceId: z
1177
+ .string()
1178
+ .describe("Workspace identifier returned by open_workspace."),
1179
+ command: z
1180
+ .string()
1181
+ .describe(`Shell command to run. Must not create or modify project files; use ${toolNames.edit} or ${toolNames.write} for file changes.`),
1182
+ workingDirectory: z
1183
+ .string()
1184
+ .optional()
1185
+ .describe("Optional working directory relative to the workspace root. Defaults to the workspace root."),
1186
+ timeout: z
1187
+ .number()
1188
+ .positive()
1189
+ .max(300)
1190
+ .optional()
1191
+ .describe("Timeout in seconds. Defaults to 30, max 300."),
1192
+ },
1193
+ outputSchema: resultOutputSchema(),
1194
+ ...toolWidgetDescriptorMeta(config, "shell"),
1195
+ annotations: SHELL_TOOL_ANNOTATIONS,
1196
+ }, async ({ workspaceId, workingDirectory, ...input }) => {
1197
+ const startedAt = performance.now();
1198
+ const workspace = workspaces.getWorkspace(workspaceId);
1199
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
1200
+ const response = await runShellTool(input, {
1201
+ cwd,
1202
+ root: workspace.root,
1203
+ });
1204
+ if (response.isError) {
1205
+ logFailedToolResponse(config, {
1206
+ tool: toolNames.shell,
1207
+ workspaceId,
1208
+ workingDirectory: workingDirectory ?? ".",
1209
+ command: input.command,
1210
+ commandLength: input.command.length,
1211
+ }, response.content, startedAt);
1212
+ return response;
1213
+ }
1214
+ const summary = {
1215
+ command: input.command,
1216
+ workingDirectory: workingDirectory ?? ".",
1217
+ ...textSummary(response.content),
1218
+ };
1219
+ logToolCall(config, {
941
1220
  tool: toolNames.shell,
942
1221
  workspaceId,
943
1222
  workingDirectory: workingDirectory ?? ".",
944
1223
  command: input.command,
945
1224
  commandLength: input.command.length,
946
- }, response.content, startedAt);
947
- return response;
948
- }
949
- const summary = {
950
- command: input.command,
951
- workingDirectory: workingDirectory ?? ".",
952
- ...textSummary(response.content),
953
- };
954
- logToolCall(config, {
955
- tool: toolNames.shell,
956
- workspaceId,
957
- workingDirectory: workingDirectory ?? ".",
958
- command: input.command,
959
- commandLength: input.command.length,
960
- success: true,
961
- durationMs: Math.round(performance.now() - startedAt),
962
- });
963
- return {
964
- ...response,
965
- _meta: {
966
- tool: toolNames.shell,
967
- card: {
968
- workspaceId,
969
- path: workingDirectory,
970
- summary,
971
- payload: { content: response.content },
1225
+ success: true,
1226
+ durationMs: Math.round(performance.now() - startedAt),
1227
+ });
1228
+ return {
1229
+ ...response,
1230
+ _meta: {
1231
+ tool: toolNames.shell,
1232
+ card: {
1233
+ workspaceId,
1234
+ path: workingDirectory,
1235
+ summary,
1236
+ payload: { content: response.content },
1237
+ },
972
1238
  },
973
- },
974
- structuredContent: {
975
- result: contentText(response.content),
976
- },
977
- };
978
- });
1239
+ structuredContent: {
1240
+ result: contentText(response.content),
1241
+ },
1242
+ };
1243
+ });
1244
+ }
1245
+ if (config.toolMode === "codex") {
1246
+ registerCodexProcessTools(server, config, workspaces, processSessions);
1247
+ }
979
1248
  return server;
980
1249
  }
981
1250
  export function createServer(config = loadConfig()) {
@@ -998,6 +1267,10 @@ export function createServer(config = loadConfig()) {
998
1267
  const workspaceStore = createWorkspaceStore(config.stateDir);
999
1268
  const workspaces = new WorkspaceRegistry(config, workspaceStore);
1000
1269
  const reviewCheckpoints = createReviewCheckpointManager();
1270
+ const processSessions = new ProcessSessionManager();
1271
+ const localAgentProviders = config.subagents
1272
+ ? getLocalAgentProviderAvailabilitySnapshot()
1273
+ : [];
1001
1274
  if (config.logging.trustProxy) {
1002
1275
  app.set("trust proxy", true);
1003
1276
  }
@@ -1106,7 +1379,7 @@ export function createServer(config = loadConfig()) {
1106
1379
  });
1107
1380
  }
1108
1381
  };
1109
- const server = createMcpServer(config, workspaces, reviewCheckpoints);
1382
+ const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders);
1110
1383
  await server.connect(transport);
1111
1384
  }
1112
1385
  else {
@@ -1129,10 +1402,12 @@ export function createServer(config = loadConfig()) {
1129
1402
  return {
1130
1403
  app,
1131
1404
  config,
1405
+ localAgentProviders,
1132
1406
  close: () => {
1133
1407
  if (closed)
1134
1408
  return;
1135
1409
  closed = true;
1410
+ processSessions.shutdown();
1136
1411
  oauthProvider.close();
1137
1412
  workspaceStore.close?.();
1138
1413
  },
@@ -1146,7 +1421,7 @@ async function isMainModule() {
1146
1421
  return modulePath === entrypointPath;
1147
1422
  }
1148
1423
  if (await isMainModule()) {
1149
- const { app, config, close } = createServer();
1424
+ const { app, config, close, localAgentProviders } = createServer();
1150
1425
  const httpServer = app.listen(config.port, config.host, () => {
1151
1426
  console.log(`devspace listening on http://${config.host}:${config.port}/mcp`);
1152
1427
  console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
@@ -1155,6 +1430,9 @@ if (await isMainModule()) {
1155
1430
  console.log(`request logging: ${config.logging.requests ? "enabled" : "disabled"}`);
1156
1431
  console.log(`asset logging: ${config.logging.assets ? "enabled" : "disabled"}`);
1157
1432
  console.log(`trust proxy: ${config.logging.trustProxy ? "enabled" : "disabled"}`);
1433
+ if (config.subagents) {
1434
+ console.log(`subagent providers: ${formatLocalAgentProviderAvailabilitySummary(localAgentProviders)}`);
1435
+ }
1158
1436
  });
1159
1437
  const shutdown = () => {
1160
1438
  httpServer.close(() => {