@waishnav/devspace 1.0.1 → 1.0.3

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 (102) hide show
  1. package/README.md +140 -30
  2. package/dist/apply-patch.js +399 -0
  3. package/dist/cli.js +18 -2
  4. package/dist/config.js +11 -13
  5. package/dist/db/client.js +10 -3
  6. package/dist/db/migrations.js +123 -0
  7. package/dist/db/schema.js +24 -1
  8. package/dist/oauth-provider.js +35 -64
  9. package/dist/oauth-store.js +138 -0
  10. package/dist/process-platform.js +49 -0
  11. package/dist/process-sessions.js +329 -0
  12. package/dist/roots.js +5 -2
  13. package/dist/server.js +462 -203
  14. package/dist/skills.js +23 -3
  15. package/dist/ui/.vite/manifest.json +281 -281
  16. package/dist/ui/assets/{angular-html-B_77pPm_.js → angular-html-D6nc7PYw.js} +1 -1
  17. package/dist/ui/assets/{angular-ts-tGBi7sOr.js → angular-ts-BAIStLeI.js} +1 -1
  18. package/dist/ui/assets/{apl-CtVPpGgP.js → apl-CtEv5pop.js} +1 -1
  19. package/dist/ui/assets/{astro-CALPYvnG.js → astro-CVFIMXc6.js} +1 -1
  20. package/dist/ui/assets/{blade-Bh4eBXdk.js → blade-BgSlZ6jY.js} +1 -1
  21. package/dist/ui/assets/{c-BGkebjQE.js → c-bxLlCV26.js} +1 -1
  22. package/dist/ui/assets/{cobol-f7rP2PjR.js → cobol-BuZ-r51r.js} +1 -1
  23. package/dist/ui/assets/{coffee-E4OhMc7W.js → coffee-_JP0Joa7.js} +1 -1
  24. package/dist/ui/assets/{cpp-Cr_J6lB2.js → cpp-CP_eaz-H.js} +1 -1
  25. package/dist/ui/assets/{crystal-B_TFXhon.js → crystal-DZ-sEZd0.js} +1 -1
  26. package/dist/ui/assets/{css-DDMxq-4J.js → css-CZnyti9s.js} +1 -1
  27. package/dist/ui/assets/{edge-Bxqhgg5I.js → edge-DjsoDMTR.js} +1 -1
  28. package/dist/ui/assets/{elixir-WrSbgJJ1.js → elixir-C5b04AkY.js} +1 -1
  29. package/dist/ui/assets/{elm-BXBWJ4Kz.js → elm-Ddx-rgVY.js} +1 -1
  30. package/dist/ui/assets/{erb-D0iMw9rO.js → erb-cjF7oPcT.js} +1 -1
  31. package/dist/ui/assets/{git-rebase-BOrZ-_iR.js → git-rebase-DN5exBTl.js} +1 -1
  32. package/dist/ui/assets/{glimmer-js-PCWk96O8.js → glimmer-js-CEq0hNwa.js} +1 -1
  33. package/dist/ui/assets/{glimmer-ts-4pYwX8pC.js → glimmer-ts-4uYGFkon.js} +1 -1
  34. package/dist/ui/assets/{glsl-CDyKZvZX.js → glsl-DEJ9tTWm.js} +1 -1
  35. package/dist/ui/assets/{graphql-8sIw92YT.js → graphql-Bim5WuQh.js} +1 -1
  36. package/dist/ui/assets/{hack-Ci96Zcew.js → hack-BcyVIXhE.js} +1 -1
  37. package/dist/ui/assets/{haml-4uzh6End.js → haml-D9YaukDg.js} +1 -1
  38. package/dist/ui/assets/{handlebars-B6Eq7QV8.js → handlebars-CFaMw9zH.js} +1 -1
  39. package/dist/ui/assets/{heavy-payload--bwRtRpo.js → heavy-payload-B9aTqgfs.js} +1 -1
  40. package/dist/ui/assets/{html-C29wL7bm.js → html-CgO_zx4D.js} +1 -1
  41. package/dist/ui/assets/{html-derivative-Cmd90ilz.js → html-derivative-BxqkysJT.js} +1 -1
  42. package/dist/ui/assets/{http-Bdr_R0Kw.js → http-CO5dv0RA.js} +1 -1
  43. package/dist/ui/assets/{hurl-CiTPn-zH.js → hurl-DRHtrK78.js} +1 -1
  44. package/dist/ui/assets/{java-C03kBuJH.js → java-BvHC_DgR.js} +1 -1
  45. package/dist/ui/assets/{javascript-135g9Wwx.js → javascript-C-3payQx.js} +1 -1
  46. package/dist/ui/assets/{jinja-CPpiEHqV.js → jinja-CAOPvEOH.js} +1 -1
  47. package/dist/ui/assets/{jison-qabCQzwp.js → jison-totCB1L-.js} +1 -1
  48. package/dist/ui/assets/{json-CtRj9Trp.js → json-D0J9rEe1.js} +1 -1
  49. package/dist/ui/assets/{jsx-Ccuy5Wm3.js → jsx-na-XQD_B.js} +1 -1
  50. package/dist/ui/assets/{julia-Fwya-MwM.js → julia-CqrTtZ4-.js} +1 -1
  51. package/dist/ui/assets/{just-c9YEH7Gf.js → just-pmrVSkMc.js} +1 -1
  52. package/dist/ui/assets/{latex-D9xoD6UX.js → latex-Cd_geX3E.js} +1 -1
  53. package/dist/ui/assets/{liquid-BKZLZSHN.js → liquid-C6ky5sJ-.js} +1 -1
  54. package/dist/ui/assets/{lua-CBD2zNyC.js → lua-dgyBbeof.js} +1 -1
  55. package/dist/ui/assets/{marko-Dkgf9bj3.js → marko-Dy5mfXY9.js} +1 -1
  56. package/dist/ui/assets/{mdc-BUgUKiX3.js → mdc-7NUvxgI1.js} +1 -1
  57. package/dist/ui/assets/{nginx-CAaav4Zh.js → nginx-C-KiEXbI.js} +1 -1
  58. package/dist/ui/assets/{nim-CTjWWztL.js → nim-BBw8kfl9.js} +1 -1
  59. package/dist/ui/assets/{perl-DTM5zgH4.js → perl-CyxhJgbO.js} +1 -1
  60. package/dist/ui/assets/{php-dqfFDbWm.js → php-m4LAOpam.js} +1 -1
  61. package/dist/ui/assets/{pug-4f_clTm2.js → pug-Cpl0baYI.js} +1 -1
  62. package/dist/ui/assets/{qml-BFeWjFRL.js → qml-BoSGNgIb.js} +1 -1
  63. package/dist/ui/assets/{r-ei1iNvHm.js → r-C0JhqQd5.js} +1 -1
  64. package/dist/ui/assets/{razor-jj95jncJ.js → razor-CntsBNoh.js} +1 -1
  65. package/dist/ui/assets/{regexp-0joUf3Cn.js → regexp-BNZmOCRl.js} +1 -1
  66. package/dist/ui/assets/{review-payload-DxI2fTnP.js → review-payload-BgdWHWiv.js} +1 -1
  67. package/dist/ui/assets/{rst-BMbcN8--.js → rst-BZ2XHIxM.js} +1 -1
  68. package/dist/ui/assets/{ruby-BQx0xtOB.js → ruby-BIthfCTP.js} +1 -1
  69. package/dist/ui/assets/{sas-C_Fl1EyP.js → sas-5aT3fOla.js} +1 -1
  70. package/dist/ui/assets/{scss-5vaLJPJl.js → scss-DZPfdkDy.js} +1 -1
  71. package/dist/ui/assets/{shellscript-p_y7YCYP.js → shellscript-DbhKc5Xj.js} +1 -1
  72. package/dist/ui/assets/{shellsession-ChZkMp2K.js → shellsession-X9-bcyHh.js} +1 -1
  73. package/dist/ui/assets/{soy-im25cA8U.js → soy-Bd4tecrW.js} +1 -1
  74. package/dist/ui/assets/{sql-BzvG6ssm.js → sql-Chf5JrDn.js} +1 -1
  75. package/dist/ui/assets/{stata-CwSzNM20.js → stata-DvMMIc_T.js} +1 -1
  76. package/dist/ui/assets/{surrealql-BWzKhI6S.js → surrealql-D5FMzVF5.js} +1 -1
  77. package/dist/ui/assets/{svelte-NMlQHjaa.js → svelte-CTiVXZrv.js} +1 -1
  78. package/dist/ui/assets/{templ-1W11KGSL.js → templ-ldnfq-7w.js} +1 -1
  79. package/dist/ui/assets/{tex-CTxN8q2c.js → tex-QW0_7T2r.js} +1 -1
  80. package/dist/ui/assets/{ts-tags-B6prphu-.js → ts-tags-CAKiWIBx.js} +1 -1
  81. package/dist/ui/assets/{tsx-Daks24y9.js → tsx-Bu9xJJmq.js} +1 -1
  82. package/dist/ui/assets/{twig-C0KeCxch.js → twig-Dp1752_V.js} +1 -1
  83. package/dist/ui/assets/{typescript-37-dkCwp.js → typescript-RfO03Kmm.js} +1 -1
  84. package/dist/ui/assets/{useFileDiffInstance--R0_ywL_.js → useFileDiffInstance-DjqU4y1a.js} +3 -3
  85. package/dist/ui/assets/{vue-Ba-v02vP.js → vue-Dlasv57G.js} +1 -1
  86. package/dist/ui/assets/{vue-html-D2UmbuN7.js → vue-html-BHuJ7anu.js} +1 -1
  87. package/dist/ui/assets/{vue-vine-ZftUW_ID.js → vue-vine-CavE0PSk.js} +1 -1
  88. package/dist/ui/assets/{workspace-app-CsMiGtz9.js → workspace-app-DDV_qdjx.js} +6 -6
  89. package/dist/ui/assets/workspace-app-DGD9R89D.css +1 -0
  90. package/dist/ui/assets/{xml-DrhRYxTS.js → xml-Bbv64l4W.js} +1 -1
  91. package/dist/ui/assets/{xsl-DvbIvBcQ.js → xsl-Sro4gyQY.js} +1 -1
  92. package/dist/ui/assets/{yaml-oktUYAzQ.js → yaml-Du68c8sA.js} +1 -1
  93. package/dist/ui/workspace-app.html +2 -2
  94. package/dist/workspace-store.js +0 -50
  95. package/docs/chatgpt-coding-workflow.md +24 -4
  96. package/docs/codex-tool-mode-qa.md +73 -0
  97. package/docs/configuration.md +28 -5
  98. package/docs/gotchas.md +11 -5
  99. package/docs/setup.md +1 -1
  100. package/package.json +10 -5
  101. package/scripts/fix-node-pty-permissions.mjs +22 -0
  102. package/dist/ui/assets/workspace-app-8--oTbCd.css +0 -1
package/dist/server.js CHANGED
@@ -12,10 +12,12 @@ 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";
@@ -63,7 +65,7 @@ function toolWidgetDescriptorMeta(config, kind) {
63
65
  };
64
66
  }
65
67
  function toolNamesFor(config) {
66
- return config.toolNaming === "short"
68
+ return config.toolNaming === "short" || config.toolMode === "codex"
67
69
  ? {
68
70
  openWorkspace: "open_workspace",
69
71
  read: "read",
@@ -86,7 +88,10 @@ function toolNamesFor(config) {
86
88
  };
87
89
  }
88
90
  function serverInstructions(config, toolNames) {
89
- const inspection = config.minimalTools
91
+ if (config.toolMode === "codex") {
92
+ 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.`;
93
+ }
94
+ const inspection = config.toolMode !== "full"
90
95
  ? `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
96
  : `Prefer ${toolNames.read}, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} for file inspection. `;
92
97
  const skills = config.skillsEnabled
@@ -292,7 +297,170 @@ async function assertWorkspaceAppAssets() {
292
297
  await access(candidate);
293
298
  }
294
299
  }
295
- function createMcpServer(config, workspaces, reviewCheckpoints) {
300
+ function processResult(snapshot) {
301
+ const status = snapshot.running
302
+ ? `Process running with session ID ${snapshot.sessionId}.`
303
+ : snapshot.signal
304
+ ? `Process exited after signal ${snapshot.signal}.`
305
+ : `Process exited with code ${snapshot.exitCode ?? "unknown"}.`;
306
+ return snapshot.output ? `${snapshot.output.replace(/\n$/, "")}\n${status}` : status;
307
+ }
308
+ function processOutputSchema() {
309
+ return resultOutputSchema({
310
+ sessionId: z.number().optional(),
311
+ running: z.boolean(),
312
+ exitCode: z.number().int().optional(),
313
+ signal: z.string().optional(),
314
+ wallTimeMs: z.number().nonnegative(),
315
+ outputTruncated: z.boolean(),
316
+ });
317
+ }
318
+ function processToolResponse(tool, workspaceId, snapshot, summary) {
319
+ const result = processResult(snapshot);
320
+ const content = [textBlock(result)];
321
+ const outputSummary = textSummary(snapshot.output ? [textBlock(snapshot.output)] : []);
322
+ return {
323
+ content,
324
+ _meta: {
325
+ tool,
326
+ card: {
327
+ workspaceId,
328
+ summary: { ...summary, ...outputSummary },
329
+ payload: { content },
330
+ },
331
+ },
332
+ structuredContent: {
333
+ result,
334
+ sessionId: snapshot.sessionId,
335
+ running: snapshot.running,
336
+ exitCode: snapshot.exitCode,
337
+ signal: snapshot.signal,
338
+ wallTimeMs: snapshot.wallTimeMs,
339
+ outputTruncated: snapshot.outputTruncated,
340
+ },
341
+ };
342
+ }
343
+ function registerCodexProcessTools(server, config, workspaces, processSessions) {
344
+ registerAppTool(server, "exec_command", {
345
+ title: "Execute command",
346
+ 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.",
347
+ inputSchema: {
348
+ workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
349
+ cmd: z.string().min(1).describe("Shell command to execute."),
350
+ tty: z
351
+ .boolean()
352
+ .optional()
353
+ .describe("Allocate a pseudo-terminal for interactive commands. Defaults to false."),
354
+ columns: z.number().int().min(1).max(1_000).optional().describe("Initial PTY width. Defaults to 80."),
355
+ rows: z.number().int().min(1).max(1_000).optional().describe("Initial PTY height. Defaults to 24."),
356
+ workingDirectory: z
357
+ .string()
358
+ .optional()
359
+ .describe("Working directory relative to the workspace root. Defaults to the workspace root."),
360
+ yieldTimeMs: z
361
+ .number()
362
+ .int()
363
+ .min(0)
364
+ .max(30_000)
365
+ .optional()
366
+ .describe("Milliseconds to wait before returning a running session. Defaults to 10000."),
367
+ maxOutputTokens: z
368
+ .number()
369
+ .int()
370
+ .positive()
371
+ .max(100_000)
372
+ .optional()
373
+ .describe("Approximate output token budget. Defaults to 10000."),
374
+ },
375
+ outputSchema: processOutputSchema(),
376
+ ...toolWidgetDescriptorMeta(config, "shell"),
377
+ annotations: SHELL_TOOL_ANNOTATIONS,
378
+ }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, maxOutputTokens }) => {
379
+ const startedAt = performance.now();
380
+ const workspace = workspaces.getWorkspace(workspaceId);
381
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
382
+ const snapshot = await processSessions.start({
383
+ workspaceId,
384
+ command: cmd,
385
+ cwd,
386
+ tty,
387
+ columns,
388
+ rows,
389
+ yieldTimeMs,
390
+ maxOutputTokens,
391
+ });
392
+ logToolCall(config, {
393
+ tool: "exec_command",
394
+ workspaceId,
395
+ workingDirectory: workingDirectory ?? ".",
396
+ command: cmd,
397
+ commandLength: cmd.length,
398
+ success: true,
399
+ durationMs: Math.round(performance.now() - startedAt),
400
+ });
401
+ return processToolResponse("exec_command", workspaceId, snapshot, {
402
+ command: cmd,
403
+ workingDirectory: workingDirectory ?? ".",
404
+ running: snapshot.running,
405
+ exitCode: snapshot.exitCode,
406
+ wallTimeMs: snapshot.wallTimeMs,
407
+ });
408
+ });
409
+ registerAppTool(server, "write_stdin", {
410
+ title: "Write to process",
411
+ 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.",
412
+ inputSchema: {
413
+ workspaceId: z.string().describe("Workspace identifier used to start the process."),
414
+ sessionId: z.number().describe("Process session identifier returned by exec_command."),
415
+ chars: z.string().optional().describe("Characters to write. Omit or pass an empty string to poll."),
416
+ columns: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this width."),
417
+ rows: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this height."),
418
+ yieldTimeMs: z
419
+ .number()
420
+ .int()
421
+ .min(0)
422
+ .max(30_000)
423
+ .optional()
424
+ .describe("Milliseconds to wait for process output or completion. Defaults to 10000."),
425
+ maxOutputTokens: z
426
+ .number()
427
+ .int()
428
+ .positive()
429
+ .max(100_000)
430
+ .optional()
431
+ .describe("Approximate output token budget. Defaults to 10000."),
432
+ },
433
+ outputSchema: processOutputSchema(),
434
+ ...toolWidgetDescriptorMeta(config, "shell"),
435
+ annotations: SHELL_TOOL_ANNOTATIONS,
436
+ }, async ({ workspaceId, sessionId, chars, columns, rows, yieldTimeMs, maxOutputTokens }) => {
437
+ const startedAt = performance.now();
438
+ workspaces.getWorkspace(workspaceId);
439
+ const snapshot = await processSessions.write({
440
+ workspaceId,
441
+ sessionId,
442
+ chars,
443
+ columns,
444
+ rows,
445
+ yieldTimeMs,
446
+ maxOutputTokens,
447
+ });
448
+ logToolCall(config, {
449
+ tool: "write_stdin",
450
+ workspaceId,
451
+ success: true,
452
+ durationMs: Math.round(performance.now() - startedAt),
453
+ });
454
+ return processToolResponse("write_stdin", workspaceId, snapshot, {
455
+ sessionId,
456
+ charactersWritten: chars?.length ?? 0,
457
+ running: snapshot.running,
458
+ exitCode: snapshot.exitCode,
459
+ wallTimeMs: snapshot.wallTimeMs,
460
+ });
461
+ });
462
+ }
463
+ function createMcpServer(config, workspaces, reviewCheckpoints, processSessions) {
296
464
  const toolNames = toolNamesFor(config);
297
465
  const server = new McpServer({
298
466
  name: "devspace",
@@ -530,144 +698,209 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
530
698
  },
531
699
  };
532
700
  });
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, {
701
+ if (config.toolMode !== "codex") {
702
+ registerAppTool(server, toolNames.write, {
703
+ title: "Write file",
704
+ 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.`,
705
+ inputSchema: {
706
+ workspaceId: z
707
+ .string()
708
+ .describe("Workspace identifier returned by open_workspace."),
709
+ path: z
710
+ .string()
711
+ .describe("File path to write, relative to the workspace root."),
712
+ content: z.string().describe("Complete new file content."),
713
+ },
714
+ outputSchema: resultOutputSchema(),
715
+ ...toolWidgetDescriptorMeta(config, "write"),
716
+ annotations: WRITE_TOOL_ANNOTATIONS,
717
+ }, async ({ workspaceId, ...input }) => {
718
+ const startedAt = performance.now();
719
+ const workspace = workspaces.getWorkspace(workspaceId);
720
+ workspaces.resolvePath(workspace, input.path);
721
+ const response = await writeFileTool(input, {
722
+ cwd: workspace.root,
723
+ root: workspace.root,
724
+ });
725
+ if (response.isError) {
726
+ logFailedToolResponse(config, {
727
+ tool: toolNames.write,
728
+ workspaceId,
729
+ path: input.path,
730
+ }, response.content, startedAt);
731
+ return response;
732
+ }
733
+ const patch = newFilePatch(input.path, input.content);
734
+ const stats = countDiffStats(patch);
735
+ const summary = {
736
+ ...stats,
737
+ lines: contentLineCount(input.content),
738
+ characters: input.content.length,
739
+ };
740
+ logToolCall(config, {
558
741
  tool: toolNames.write,
559
742
  workspaceId,
560
743
  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,
744
+ success: true,
745
+ durationMs: Math.round(performance.now() - startedAt),
746
+ });
747
+ return {
748
+ ...response,
749
+ _meta: {
750
+ tool: toolNames.write,
751
+ card: {
752
+ workspaceId,
753
+ path: input.path,
754
+ summary,
755
+ payload: {
756
+ content: response.content,
757
+ patch,
758
+ },
589
759
  },
590
760
  },
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,
761
+ structuredContent: {
762
+ result: contentText(response.content),
763
+ },
764
+ };
628
765
  });
629
- if (response.isError) {
630
- logFailedToolResponse(config, {
766
+ registerAppTool(server, toolNames.edit, {
767
+ title: "Edit file",
768
+ 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.`,
769
+ inputSchema: {
770
+ workspaceId: z
771
+ .string()
772
+ .describe("Workspace identifier returned by open_workspace."),
773
+ path: z
774
+ .string()
775
+ .describe("File path to edit, relative to the workspace root."),
776
+ edits: z
777
+ .array(z.object({
778
+ oldText: z
779
+ .string()
780
+ .describe("Exact text to replace. Must match uniquely in the original file."),
781
+ newText: z.string().describe("Replacement text."),
782
+ }))
783
+ .min(1),
784
+ },
785
+ outputSchema: resultOutputSchema({
786
+ status: z.literal("applied"),
787
+ }),
788
+ ...toolWidgetDescriptorMeta(config, "edit"),
789
+ annotations: EDIT_TOOL_ANNOTATIONS,
790
+ }, async ({ workspaceId, ...input }) => {
791
+ const startedAt = performance.now();
792
+ const workspace = workspaces.getWorkspace(workspaceId);
793
+ workspaces.resolvePath(workspace, input.path);
794
+ const response = await editFileTool(input, {
795
+ cwd: workspace.root,
796
+ root: workspace.root,
797
+ });
798
+ if (response.isError) {
799
+ logFailedToolResponse(config, {
800
+ tool: toolNames.edit,
801
+ workspaceId,
802
+ path: input.path,
803
+ }, response.content, startedAt);
804
+ return response;
805
+ }
806
+ const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
807
+ const summary = {
808
+ ...stats,
809
+ editCount: input.edits.length,
810
+ };
811
+ const editResultText = `Edited ${input.path} (+${stats.additions} -${stats.removals}).`;
812
+ const editContent = [textBlock(editResultText)];
813
+ logToolCall(config, {
631
814
  tool: toolNames.edit,
632
815
  workspaceId,
633
816
  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,
817
+ success: true,
818
+ durationMs: Math.round(performance.now() - startedAt),
819
+ });
820
+ return {
821
+ content: editContent,
822
+ _meta: {
823
+ tool: toolNames.edit,
824
+ card: {
825
+ workspaceId,
826
+ path: input.path,
827
+ summary,
828
+ payload: {
829
+ diff: response.details?.diff,
830
+ patch: response.details?.patch,
831
+ },
662
832
  },
663
833
  },
834
+ structuredContent: {
835
+ status: "applied",
836
+ result: contentText(editContent),
837
+ },
838
+ };
839
+ });
840
+ }
841
+ if (config.toolMode === "codex") {
842
+ registerAppTool(server, "apply_patch", {
843
+ title: "Apply patch",
844
+ description: "Apply one Codex-style patch inside an open workspace. Supports adding, overwriting, updating, deleting, and moving files. Earlier successful file changes remain if a later patch action fails. Use this for all file modifications. Paths must be relative to the workspace. Call open_workspace first and pass workspaceId.",
845
+ inputSchema: {
846
+ workspaceId: z
847
+ .string()
848
+ .describe("Workspace identifier returned by open_workspace."),
849
+ patch: z
850
+ .string()
851
+ .describe("Patch text enclosed by *** Begin Patch and *** End Patch markers."),
664
852
  },
665
- structuredContent: {
666
- status: "applied",
667
- result: contentText(editContent),
668
- },
669
- };
670
- });
853
+ outputSchema: resultOutputSchema({
854
+ additions: z.number(),
855
+ removals: z.number(),
856
+ files: z.array(z.object({
857
+ path: z.string(),
858
+ previousPath: z.string().optional(),
859
+ operation: z.enum(["add", "update", "delete", "move"]),
860
+ })),
861
+ }),
862
+ ...toolWidgetDescriptorMeta(config, "edit"),
863
+ annotations: EDIT_TOOL_ANNOTATIONS,
864
+ }, async ({ workspaceId, patch }) => {
865
+ const startedAt = performance.now();
866
+ const workspace = workspaces.getWorkspace(workspaceId);
867
+ const applied = await applyPatch(workspace.root, patch);
868
+ const paths = applied.files.map((file) => file.path).join(", ");
869
+ const result = `Applied patch to ${applied.files.length} file(s): ${paths}`;
870
+ const content = [textBlock(result)];
871
+ const displayPath = applied.files.length === 1
872
+ ? applied.files[0]?.path
873
+ : `${applied.files.length} files`;
874
+ logToolCall(config, {
875
+ tool: "apply_patch",
876
+ workspaceId,
877
+ success: true,
878
+ durationMs: Math.round(performance.now() - startedAt),
879
+ });
880
+ return {
881
+ content,
882
+ _meta: {
883
+ tool: "apply_patch",
884
+ card: {
885
+ workspaceId,
886
+ path: displayPath,
887
+ summary: {
888
+ files: applied.files.length,
889
+ additions: applied.additions,
890
+ removals: applied.removals,
891
+ },
892
+ payload: { patch: applied.patch },
893
+ },
894
+ },
895
+ structuredContent: {
896
+ result,
897
+ additions: applied.additions,
898
+ removals: applied.removals,
899
+ files: applied.files,
900
+ },
901
+ };
902
+ });
903
+ }
671
904
  if (config.widgets === "changes") {
672
905
  registerAppTool(server, "show_changes", {
673
906
  title: "Show changes",
@@ -723,7 +956,7 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
723
956
  };
724
957
  });
725
958
  }
726
- if (!config.minimalTools) {
959
+ if (config.toolMode === "full") {
727
960
  registerAppTool(server, toolNames.grep, {
728
961
  title: config.toolNaming === "short" ? "Grep" : "Grep files",
729
962
  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.",
@@ -902,80 +1135,85 @@ function createMcpServer(config, workspaces, reviewCheckpoints) {
902
1135
  };
903
1136
  });
904
1137
  }
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, {
1138
+ if (config.toolMode !== "codex") {
1139
+ registerAppTool(server, toolNames.shell, {
1140
+ title: config.toolNaming === "short" ? "Bash" : "Run shell",
1141
+ description: config.toolMode !== "full"
1142
+ ? `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.`
1143
+ : `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.`,
1144
+ inputSchema: {
1145
+ workspaceId: z
1146
+ .string()
1147
+ .describe("Workspace identifier returned by open_workspace."),
1148
+ command: z
1149
+ .string()
1150
+ .describe(`Shell command to run. Must not create or modify project files; use ${toolNames.edit} or ${toolNames.write} for file changes.`),
1151
+ workingDirectory: z
1152
+ .string()
1153
+ .optional()
1154
+ .describe("Optional working directory relative to the workspace root. Defaults to the workspace root."),
1155
+ timeout: z
1156
+ .number()
1157
+ .positive()
1158
+ .max(300)
1159
+ .optional()
1160
+ .describe("Timeout in seconds. Defaults to 30, max 300."),
1161
+ },
1162
+ outputSchema: resultOutputSchema(),
1163
+ ...toolWidgetDescriptorMeta(config, "shell"),
1164
+ annotations: SHELL_TOOL_ANNOTATIONS,
1165
+ }, async ({ workspaceId, workingDirectory, ...input }) => {
1166
+ const startedAt = performance.now();
1167
+ const workspace = workspaces.getWorkspace(workspaceId);
1168
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
1169
+ const response = await runShellTool(input, {
1170
+ cwd,
1171
+ root: workspace.root,
1172
+ });
1173
+ if (response.isError) {
1174
+ logFailedToolResponse(config, {
1175
+ tool: toolNames.shell,
1176
+ workspaceId,
1177
+ workingDirectory: workingDirectory ?? ".",
1178
+ command: input.command,
1179
+ commandLength: input.command.length,
1180
+ }, response.content, startedAt);
1181
+ return response;
1182
+ }
1183
+ const summary = {
1184
+ command: input.command,
1185
+ workingDirectory: workingDirectory ?? ".",
1186
+ ...textSummary(response.content),
1187
+ };
1188
+ logToolCall(config, {
941
1189
  tool: toolNames.shell,
942
1190
  workspaceId,
943
1191
  workingDirectory: workingDirectory ?? ".",
944
1192
  command: input.command,
945
1193
  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 },
1194
+ success: true,
1195
+ durationMs: Math.round(performance.now() - startedAt),
1196
+ });
1197
+ return {
1198
+ ...response,
1199
+ _meta: {
1200
+ tool: toolNames.shell,
1201
+ card: {
1202
+ workspaceId,
1203
+ path: workingDirectory,
1204
+ summary,
1205
+ payload: { content: response.content },
1206
+ },
972
1207
  },
973
- },
974
- structuredContent: {
975
- result: contentText(response.content),
976
- },
977
- };
978
- });
1208
+ structuredContent: {
1209
+ result: contentText(response.content),
1210
+ },
1211
+ };
1212
+ });
1213
+ }
1214
+ if (config.toolMode === "codex") {
1215
+ registerCodexProcessTools(server, config, workspaces, processSessions);
1216
+ }
979
1217
  return server;
980
1218
  }
981
1219
  export function createServer(config = loadConfig()) {
@@ -989,7 +1227,7 @@ export function createServer(config = loadConfig()) {
989
1227
  const transports = new Map();
990
1228
  const mcpUrl = new URL("/mcp", config.publicBaseUrl);
991
1229
  const resourceServerUrl = resourceUrlFromServerUrl(mcpUrl);
992
- const oauthProvider = new SingleUserOAuthProvider(config.oauth, mcpUrl);
1230
+ const oauthProvider = new SingleUserOAuthProvider(config.oauth, mcpUrl, config.stateDir);
993
1231
  const bearerAuth = requireBearerAuth({
994
1232
  verifier: oauthProvider,
995
1233
  requiredScopes: [config.oauth.scopes[0] ?? "devspace"],
@@ -998,6 +1236,7 @@ export function createServer(config = loadConfig()) {
998
1236
  const workspaceStore = createWorkspaceStore(config.stateDir);
999
1237
  const workspaces = new WorkspaceRegistry(config, workspaceStore);
1000
1238
  const reviewCheckpoints = createReviewCheckpointManager();
1239
+ const processSessions = new ProcessSessionManager();
1001
1240
  if (config.logging.trustProxy) {
1002
1241
  app.set("trust proxy", true);
1003
1242
  }
@@ -1106,7 +1345,7 @@ export function createServer(config = loadConfig()) {
1106
1345
  });
1107
1346
  }
1108
1347
  };
1109
- const server = createMcpServer(config, workspaces, reviewCheckpoints);
1348
+ const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions);
1110
1349
  await server.connect(transport);
1111
1350
  }
1112
1351
  else {
@@ -1125,7 +1364,19 @@ export function createServer(config = loadConfig()) {
1125
1364
  }
1126
1365
  }
1127
1366
  });
1128
- return { app, config };
1367
+ let closed = false;
1368
+ return {
1369
+ app,
1370
+ config,
1371
+ close: () => {
1372
+ if (closed)
1373
+ return;
1374
+ closed = true;
1375
+ processSessions.shutdown();
1376
+ oauthProvider.close();
1377
+ workspaceStore.close?.();
1378
+ },
1379
+ };
1129
1380
  }
1130
1381
  async function isMainModule() {
1131
1382
  if (!process.argv[1])
@@ -1135,8 +1386,8 @@ async function isMainModule() {
1135
1386
  return modulePath === entrypointPath;
1136
1387
  }
1137
1388
  if (await isMainModule()) {
1138
- const { app, config } = createServer();
1139
- app.listen(config.port, config.host, () => {
1389
+ const { app, config, close } = createServer();
1390
+ const httpServer = app.listen(config.port, config.host, () => {
1140
1391
  console.log(`devspace listening on http://${config.host}:${config.port}/mcp`);
1141
1392
  console.log(`allowed roots: ${config.allowedRoots.join(", ")}`);
1142
1393
  console.log("auth: oauth owner-token flow required");
@@ -1145,4 +1396,12 @@ if (await isMainModule()) {
1145
1396
  console.log(`asset logging: ${config.logging.assets ? "enabled" : "disabled"}`);
1146
1397
  console.log(`trust proxy: ${config.logging.trustProxy ? "enabled" : "disabled"}`);
1147
1398
  });
1399
+ const shutdown = () => {
1400
+ httpServer.close(() => {
1401
+ close();
1402
+ process.exit(0);
1403
+ });
1404
+ };
1405
+ process.once("SIGINT", shutdown);
1406
+ process.once("SIGTERM", shutdown);
1148
1407
  }