@waishnav/devspace 1.0.3 → 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 (111) hide show
  1. package/dist/apply-patch.js +67 -64
  2. package/dist/cli.js +243 -4
  3. package/dist/config.js +6 -9
  4. package/dist/db/migrations.js +34 -0
  5. package/dist/db/schema.js +19 -0
  6. package/dist/local-agent-adapters.js +653 -0
  7. package/dist/local-agent-availability.js +119 -0
  8. package/dist/local-agent-path.js +24 -0
  9. package/dist/local-agent-profiles.js +135 -0
  10. package/dist/local-agent-runtime.js +48 -0
  11. package/dist/local-agent-store.js +145 -0
  12. package/dist/local-agent-targets.js +80 -0
  13. package/dist/process-sessions.js +11 -3
  14. package/dist/server.js +88 -50
  15. package/dist/skills.js +27 -3
  16. package/dist/ui/.vite/manifest.json +281 -281
  17. package/dist/ui/assets/{angular-html-D6nc7PYw.js → angular-html-CKeFzaPY.js} +1 -1
  18. package/dist/ui/assets/{angular-ts-BAIStLeI.js → angular-ts-0wcM_Zxf.js} +1 -1
  19. package/dist/ui/assets/{apl-CtEv5pop.js → apl-C7UdkNb2.js} +1 -1
  20. package/dist/ui/assets/{astro-CVFIMXc6.js → astro-DMqju4MJ.js} +1 -1
  21. package/dist/ui/assets/{blade-BgSlZ6jY.js → blade-DHanNgN5.js} +1 -1
  22. package/dist/ui/assets/{c-bxLlCV26.js → c-_iPKYPcx.js} +1 -1
  23. package/dist/ui/assets/{cobol-BuZ-r51r.js → cobol-CagdLJsE.js} +1 -1
  24. package/dist/ui/assets/{coffee-_JP0Joa7.js → coffee-CkNGriPi.js} +1 -1
  25. package/dist/ui/assets/{cpp-CP_eaz-H.js → cpp-CqmKOWIQ.js} +1 -1
  26. package/dist/ui/assets/{crystal-DZ-sEZd0.js → crystal-DbAbDEO1.js} +1 -1
  27. package/dist/ui/assets/{css-CZnyti9s.js → css-xwljL6Kw.js} +1 -1
  28. package/dist/ui/assets/{edge-DjsoDMTR.js → edge-CX3sYz15.js} +1 -1
  29. package/dist/ui/assets/{elixir-C5b04AkY.js → elixir-B3qL9hqd.js} +1 -1
  30. package/dist/ui/assets/{elm-Ddx-rgVY.js → elm-CH4vrjBp.js} +1 -1
  31. package/dist/ui/assets/{erb-cjF7oPcT.js → erb-CupHqynN.js} +1 -1
  32. package/dist/ui/assets/{git-rebase-DN5exBTl.js → git-rebase-CDxsU2LC.js} +1 -1
  33. package/dist/ui/assets/{glimmer-js-CEq0hNwa.js → glimmer-js-emkga3g1.js} +1 -1
  34. package/dist/ui/assets/{glimmer-ts-4uYGFkon.js → glimmer-ts-D8kQDnUw.js} +1 -1
  35. package/dist/ui/assets/{glsl-DEJ9tTWm.js → glsl-BtfTaPhi.js} +1 -1
  36. package/dist/ui/assets/{graphql-Bim5WuQh.js → graphql-DEus8iSO.js} +1 -1
  37. package/dist/ui/assets/{hack-BcyVIXhE.js → hack-DI8qpBqE.js} +1 -1
  38. package/dist/ui/assets/{haml-D9YaukDg.js → haml-Br1i1gmi.js} +1 -1
  39. package/dist/ui/assets/{handlebars-CFaMw9zH.js → handlebars-Ce23mu9x.js} +1 -1
  40. package/dist/ui/assets/{heavy-payload-B9aTqgfs.js → heavy-payload-BcZ_nGfF.js} +1 -1
  41. package/dist/ui/assets/{html-CgO_zx4D.js → html-RXVmfixf.js} +1 -1
  42. package/dist/ui/assets/{html-derivative-BxqkysJT.js → html-derivative-D1aqeobb.js} +1 -1
  43. package/dist/ui/assets/{http-CO5dv0RA.js → http-BebG00CE.js} +1 -1
  44. package/dist/ui/assets/{hurl-DRHtrK78.js → hurl-kHMuvenL.js} +1 -1
  45. package/dist/ui/assets/{java-BvHC_DgR.js → java-DYc2V5RJ.js} +1 -1
  46. package/dist/ui/assets/{javascript-C-3payQx.js → javascript-BcfzTzr3.js} +1 -1
  47. package/dist/ui/assets/{jinja-CAOPvEOH.js → jinja-D2RhihUZ.js} +1 -1
  48. package/dist/ui/assets/{jison-totCB1L-.js → jison-BuoFEePN.js} +1 -1
  49. package/dist/ui/assets/{json-D0J9rEe1.js → json-BIDkCuJA.js} +1 -1
  50. package/dist/ui/assets/{jsx-na-XQD_B.js → jsx-DHCL2HA0.js} +1 -1
  51. package/dist/ui/assets/{julia-CqrTtZ4-.js → julia-CvUQXscF.js} +1 -1
  52. package/dist/ui/assets/{just-pmrVSkMc.js → just-etc9R2qw.js} +1 -1
  53. package/dist/ui/assets/{latex-Cd_geX3E.js → latex-BB3zRnxC.js} +1 -1
  54. package/dist/ui/assets/{liquid-C6ky5sJ-.js → liquid-CIi5Mvpv.js} +1 -1
  55. package/dist/ui/assets/{lua-dgyBbeof.js → lua-Cox7XDiW.js} +1 -1
  56. package/dist/ui/assets/{marko-Dy5mfXY9.js → marko-ruBIRV0n.js} +1 -1
  57. package/dist/ui/assets/{mdc-7NUvxgI1.js → mdc-CcbvB9U5.js} +1 -1
  58. package/dist/ui/assets/{nginx-C-KiEXbI.js → nginx-DtWz6ZYE.js} +1 -1
  59. package/dist/ui/assets/{nim-BBw8kfl9.js → nim-BV5BR4P7.js} +1 -1
  60. package/dist/ui/assets/{perl-CyxhJgbO.js → perl-BvzoSaJ4.js} +1 -1
  61. package/dist/ui/assets/{php-m4LAOpam.js → php-C-clrAOy.js} +1 -1
  62. package/dist/ui/assets/{pug-Cpl0baYI.js → pug-E07_GXAr.js} +1 -1
  63. package/dist/ui/assets/{qml-BoSGNgIb.js → qml-dqt33dti.js} +1 -1
  64. package/dist/ui/assets/{r-C0JhqQd5.js → r-BcVQxsAw.js} +1 -1
  65. package/dist/ui/assets/{razor-CntsBNoh.js → razor-BW5T_UZ-.js} +1 -1
  66. package/dist/ui/assets/{regexp-BNZmOCRl.js → regexp-Ed_DEsuU.js} +1 -1
  67. package/dist/ui/assets/{review-payload-BgdWHWiv.js → review-payload-Dsmx33pY.js} +1 -1
  68. package/dist/ui/assets/{rst-BZ2XHIxM.js → rst-BkcGqLs8.js} +1 -1
  69. package/dist/ui/assets/{ruby-BIthfCTP.js → ruby-Sf_X_YID.js} +1 -1
  70. package/dist/ui/assets/{sas-5aT3fOla.js → sas-COsombsA.js} +1 -1
  71. package/dist/ui/assets/{scss-DZPfdkDy.js → scss-K6Ylfa26.js} +1 -1
  72. package/dist/ui/assets/{shellscript-DbhKc5Xj.js → shellscript-D2EdqkgV.js} +1 -1
  73. package/dist/ui/assets/{shellsession-X9-bcyHh.js → shellsession-jRXgXrnI.js} +1 -1
  74. package/dist/ui/assets/{soy-Bd4tecrW.js → soy-BGspKQWW.js} +1 -1
  75. package/dist/ui/assets/{sql-Chf5JrDn.js → sql-C4lAmYnM.js} +1 -1
  76. package/dist/ui/assets/{stata-DvMMIc_T.js → stata-DQWvRqxp.js} +1 -1
  77. package/dist/ui/assets/{surrealql-D5FMzVF5.js → surrealql-OmKt9XJ_.js} +1 -1
  78. package/dist/ui/assets/{svelte-CTiVXZrv.js → svelte-D0rUmwDI.js} +1 -1
  79. package/dist/ui/assets/{templ-ldnfq-7w.js → templ-B-U0RMc0.js} +1 -1
  80. package/dist/ui/assets/{tex-QW0_7T2r.js → tex-BIHD-btS.js} +1 -1
  81. package/dist/ui/assets/{ts-tags-CAKiWIBx.js → ts-tags-CGuDWfPC.js} +1 -1
  82. package/dist/ui/assets/{tsx-Bu9xJJmq.js → tsx-B0-h_HlD.js} +1 -1
  83. package/dist/ui/assets/{twig-Dp1752_V.js → twig-BN2qDJWN.js} +1 -1
  84. package/dist/ui/assets/{typescript-RfO03Kmm.js → typescript-BRnwPzY3.js} +1 -1
  85. package/dist/ui/assets/{useFileDiffInstance-DjqU4y1a.js → useFileDiffInstance-5ivdkUYO.js} +3 -3
  86. package/dist/ui/assets/{vue-Dlasv57G.js → vue-DekNU1qt.js} +1 -1
  87. package/dist/ui/assets/{vue-html-BHuJ7anu.js → vue-html-Bjlv-uvH.js} +1 -1
  88. package/dist/ui/assets/{vue-vine-CavE0PSk.js → vue-vine-C2Doytjq.js} +1 -1
  89. package/dist/ui/assets/{workspace-app-DGD9R89D.css → workspace-app-DASlX26z.css} +1 -1
  90. package/dist/ui/assets/{workspace-app-DDV_qdjx.js → workspace-app-DYi7VJam.js} +5 -5
  91. package/dist/ui/assets/{xml-Bbv64l4W.js → xml-pXbxxgI3.js} +1 -1
  92. package/dist/ui/assets/{xsl-Sro4gyQY.js → xsl-BEAlgDI2.js} +1 -1
  93. package/dist/ui/assets/{yaml-Du68c8sA.js → yaml-C1O-To71.js} +1 -1
  94. package/dist/ui/workspace-app.html +2 -2
  95. package/dist/user-config.js +21 -1
  96. package/dist/workspaces.js +3 -0
  97. package/docs/agent-profile-schema.md +165 -0
  98. package/docs/chatgpt-coding-workflow.md +24 -10
  99. package/docs/configuration.md +20 -8
  100. package/docs/gotchas.md +13 -0
  101. package/examples/agents/claude-implementer.md +30 -0
  102. package/examples/agents/codex-explorer.md +27 -0
  103. package/examples/agents/codex-qa-tester.md +29 -0
  104. package/examples/agents/codex-worker.md +25 -0
  105. package/examples/agents/copilot-reviewer.md +25 -0
  106. package/examples/agents/cursor-agent-worker.md +26 -0
  107. package/examples/agents/opencode-explorer.md +27 -0
  108. package/examples/agents/pi-reviewer.md +27 -0
  109. package/package.json +11 -3
  110. package/skills/subagent-delegation/SKILL.md +132 -0
  111. package/docs/codex-tool-mode-qa.md +0 -73
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
- import { access, mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { access, lstat, mkdir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, relative, resolve } from "node:path";
5
5
  import { TextDecoder } from "node:util";
6
+ import { createTwoFilesPatch, FILE_HEADERS_ONLY } from "diff";
6
7
  function patchError(message) {
7
8
  return new Error(`Invalid patch: ${message}`);
8
9
  }
@@ -249,23 +250,56 @@ export async function replaceFile(temporary, destination, destinationExists, pla
249
250
  }
250
251
  await rm(backup, { force: true });
251
252
  }
253
+ export async function isSamePatchFile(source, destination, readIdentity = lstat) {
254
+ if (source === destination)
255
+ return true;
256
+ if (source.toLowerCase() !== destination.toLowerCase())
257
+ return false;
258
+ try {
259
+ const [sourceIdentity, destinationIdentity] = await Promise.all([
260
+ readIdentity(source),
261
+ readIdentity(destination),
262
+ ]);
263
+ return sourceIdentity.dev === destinationIdentity.dev && sourceIdentity.ino === destinationIdentity.ino;
264
+ }
265
+ catch (error) {
266
+ const code = error.code;
267
+ if (code === "ENOENT" || code === "ENOTDIR")
268
+ return false;
269
+ throw error;
270
+ }
271
+ }
252
272
  export async function applyPatch(root, patch) {
253
273
  const actions = parsePatch(patch);
254
274
  const results = [];
255
275
  const patches = [];
276
+ const staged = new Map();
277
+ const readStagedOptional = async (absolute, displayPath) => {
278
+ if (staged.has(absolute))
279
+ return staged.get(absolute) ?? null;
280
+ const file = await readOptionalTextFile(absolute, displayPath);
281
+ staged.set(absolute, file);
282
+ return file;
283
+ };
284
+ const readStagedRequired = async (absolute, displayPath) => {
285
+ const file = await readStagedOptional(absolute, displayPath);
286
+ if (!file)
287
+ throw patchError(`file does not exist: ${displayPath}`);
288
+ return file;
289
+ };
256
290
  for (const action of actions) {
257
291
  if (action.kind === "add") {
258
292
  const absolute = await resolveConfinedPath(root, action.path);
259
- const original = await readOptionalTextFile(absolute, action.path);
260
- await writeTextFile(absolute, action.content, original?.mode);
293
+ const original = await readStagedOptional(absolute, action.path);
294
+ staged.set(absolute, { content: action.content, mode: original?.mode });
261
295
  patches.push(unifiedFilePatch(action.path, action.path, original?.content ?? null, action.content));
262
296
  results.push({ path: action.path, operation: "add" });
263
297
  continue;
264
298
  }
265
299
  const absolute = await resolveConfinedPath(root, action.path);
266
- const file = await readRequiredTextFile(absolute, action.path);
300
+ const file = await readStagedRequired(absolute, action.path);
267
301
  if (action.kind === "delete") {
268
- await rm(absolute);
302
+ staged.set(absolute, null);
269
303
  patches.push(unifiedFilePatch(action.path, action.path, file.content, null));
270
304
  results.push({ path: action.path, operation: "delete" });
271
305
  continue;
@@ -273,32 +307,35 @@ export async function applyPatch(root, patch) {
273
307
  const updated = applyHunks(action.path, file.content, action.hunks);
274
308
  if (action.moveTo) {
275
309
  const destination = await resolveConfinedPath(root, action.moveTo);
276
- if (destination !== absolute)
277
- await readOptionalTextFile(destination, action.moveTo);
278
- await writeTextFile(destination, updated, file.mode);
279
- if (destination !== absolute)
280
- await rm(absolute);
310
+ const samePatchFile = await isSamePatchFile(absolute, destination);
311
+ if (!samePatchFile)
312
+ await readStagedOptional(destination, action.moveTo);
313
+ if (samePatchFile)
314
+ staged.delete(absolute);
315
+ staged.set(destination, { content: updated, mode: file.mode });
316
+ if (!samePatchFile)
317
+ staged.set(absolute, null);
281
318
  patches.push(unifiedFilePatch(action.path, action.moveTo, file.content, updated));
282
319
  results.push({ path: action.moveTo, previousPath: action.path, operation: "move" });
283
320
  }
284
321
  else {
285
- await writeTextFile(absolute, updated, file.mode);
322
+ staged.set(absolute, { content: updated, mode: file.mode });
286
323
  patches.push(unifiedFilePatch(action.path, action.path, file.content, updated));
287
324
  results.push({ path: action.path, operation: "update" });
288
325
  }
289
326
  }
327
+ for (const [absolute, file] of staged) {
328
+ if (file)
329
+ await writeTextFile(absolute, file.content, file.mode);
330
+ }
331
+ for (const [absolute, file] of staged) {
332
+ if (!file)
333
+ await rm(absolute, { force: true });
334
+ }
290
335
  const unifiedPatch = patches.filter(Boolean).join("\n");
291
336
  const stats = countPatchStats(unifiedPatch);
292
337
  return { files: results, patch: unifiedPatch, ...stats };
293
338
  }
294
- async function readRequiredTextFile(absolute, displayPath) {
295
- if (!(await fileExists(absolute)))
296
- throw patchError(`file does not exist: ${displayPath}`);
297
- const metadata = await stat(absolute);
298
- if (!metadata.isFile())
299
- throw patchError(`path is not a regular file: ${displayPath}`);
300
- return { content: await readUtf8Text(absolute, displayPath), mode: metadata.mode };
301
- }
302
339
  async function readOptionalTextFile(absolute, displayPath) {
303
340
  if (!(await fileExists(absolute)))
304
341
  return null;
@@ -332,60 +369,26 @@ async function writeTextFile(destination, content, mode) {
332
369
  throw error;
333
370
  }
334
371
  }
335
- function fileLines(content) {
336
- if (content.length === 0)
337
- return [];
338
- const normalized = content.replace(/\r\n/g, "\n");
339
- const lines = normalized.split("\n");
340
- if (normalized.endsWith("\n"))
341
- lines.pop();
342
- return lines;
343
- }
344
- function hunkRange(start, count) {
345
- return count === 0 ? "0,0" : `${start},${count}`;
346
- }
347
372
  function unifiedFilePatch(oldPath, newPath, oldContent, newContent) {
348
- const oldLines = fileLines(oldContent ?? "");
349
- const newLines = fileLines(newContent ?? "");
350
- let prefix = 0;
351
- while (prefix < oldLines.length &&
352
- prefix < newLines.length &&
353
- oldLines[prefix] === newLines[prefix]) {
354
- prefix += 1;
355
- }
356
- let suffix = 0;
357
- while (suffix < oldLines.length - prefix &&
358
- suffix < newLines.length - prefix &&
359
- oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]) {
360
- suffix += 1;
361
- }
362
- const contextBefore = Math.min(3, prefix);
363
- const contextAfter = Math.min(3, suffix);
364
- const oldChanged = oldLines.slice(prefix, oldLines.length - suffix);
365
- const newChanged = newLines.slice(prefix, newLines.length - suffix);
366
- const before = oldLines.slice(prefix - contextBefore, prefix);
367
- const after = oldLines.slice(oldLines.length - suffix, oldLines.length - suffix + contextAfter);
368
- const oldCount = contextBefore + oldChanged.length + contextAfter;
369
- const newCount = contextBefore + newChanged.length + contextAfter;
370
- const oldStart = oldContent === null ? 0 : prefix - contextBefore + 1;
371
- const newStart = newContent === null ? 0 : prefix - contextBefore + 1;
372
- const displayOld = oldContent === null ? "/dev/null" : `a/${oldPath}`;
373
- const displayNew = newContent === null ? "/dev/null" : `b/${newPath}`;
373
+ const oldFileName = oldContent === null ? "/dev/null" : `a/${oldPath}`;
374
+ const newFileName = newContent === null ? "/dev/null" : `b/${newPath}`;
375
+ const body = createTwoFilesPatch(oldFileName, newFileName, oldContent ?? "", newContent ?? "", "", "", { context: 3, headerOptions: FILE_HEADERS_ONLY });
374
376
  return [
375
377
  `diff --git a/${oldPath} b/${newPath}`,
376
378
  oldContent === null ? "new file mode 100644" : undefined,
377
379
  newContent === null ? "deleted file mode 100644" : undefined,
378
- `--- ${displayOld}`,
379
- `+++ ${displayNew}`,
380
- `@@ -${hunkRange(oldStart, oldCount)} +${hunkRange(newStart, newCount)} @@`,
381
- ...before.map((line) => ` ${line}`),
382
- ...oldChanged.map((line) => `-${line}`),
383
- ...newChanged.map((line) => `+${line}`),
384
- ...after.map((line) => ` ${line}`),
380
+ stripFinalNewline(body),
385
381
  ]
386
382
  .filter((line) => line !== undefined)
387
383
  .join("\n");
388
384
  }
385
+ function stripFinalNewline(value) {
386
+ if (value.endsWith("\r\n"))
387
+ return value.slice(0, -2);
388
+ if (value.endsWith("\n"))
389
+ return value.slice(0, -1);
390
+ return value;
391
+ }
389
392
  function countPatchStats(patch) {
390
393
  let additions = 0;
391
394
  let removals = 0;
package/dist/cli.js CHANGED
@@ -1,12 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { stdin as input, stdout as output } from "node:process";
4
- import { resolve } from "node:path";
4
+ import { spawn } from "node:child_process";
5
+ import { mkdtempSync, writeFileSync } from "node:fs";
6
+ import { readFile } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
5
10
  import * as prompts from "@clack/prompts";
6
11
  import { getShellConfig } from "@earendil-works/pi-coding-agent";
7
12
  import { satisfies } from "semver";
8
13
  import { loadConfig } from "./config.js";
9
- import { generateOwnerToken, loadDevspaceFiles, writeDevspaceAuth, writeDevspaceConfig, } from "./user-config.js";
14
+ import { runLocalAgentProvider } from "./local-agent-adapters.js";
15
+ import { isLocalAgentProvider, loadLocalAgentProfiles, } from "./local-agent-profiles.js";
16
+ import { assertLocalAgentProviderAvailable, formatLocalAgentProviderAvailabilitySummary, } from "./local-agent-availability.js";
17
+ import { formatAvailableLocalAgentTargets, parseLocalAgentRunArgs, resolveLocalAgentTarget, } from "./local-agent-targets.js";
18
+ import { createLocalAgentStore } from "./local-agent-store.js";
19
+ import { ensureDevspaceDefaultSkills, generateOwnerToken, loadDevspaceFiles, resolveSubagentsFlag, writeDevspaceAuth, writeDevspaceConfig, } from "./user-config.js";
10
20
  import { expandHomePath } from "./roots.js";
11
21
  const require = createRequire(import.meta.url);
12
22
  const SUPPORTED_NODE_RANGE = ">=20.12 <27";
@@ -28,6 +38,9 @@ async function main(argv) {
28
38
  case "config":
29
39
  runConfigCommand(args);
30
40
  return;
41
+ case "agents":
42
+ await runAgentsCommand(args);
43
+ return;
31
44
  case "help":
32
45
  printHelp();
33
46
  return;
@@ -39,7 +52,7 @@ async function main(argv) {
39
52
  function normalizeCommand(command) {
40
53
  if (!command || command === "serve" || command === "start")
41
54
  return "serve";
42
- if (command === "init" || command === "doctor" || command === "config")
55
+ if (command === "init" || command === "doctor" || command === "config" || command === "agents")
43
56
  return command;
44
57
  if (command === "help" || command === "--help" || command === "-h")
45
58
  return "help";
@@ -113,15 +126,18 @@ async function runInit({ force }) {
113
126
  port,
114
127
  allowedRoots,
115
128
  publicBaseUrl,
129
+ subagents: resolveSubagentsFlag(files.config),
116
130
  };
117
131
  const auth = {
118
132
  ownerToken: files.auth.ownerToken ?? generateOwnerToken(),
119
133
  };
120
134
  const configPath = writeDevspaceConfig(config);
121
135
  const authPath = writeDevspaceAuth(auth);
136
+ const seededSkillPaths = config.subagents ? ensureDevspaceDefaultSkills() : [];
122
137
  const lines = [
123
138
  `Config: ${configPath}`,
124
139
  `Auth: ${authPath}`,
140
+ ...seededSkillPaths.map((path) => `Default skill: ${path}`),
125
141
  `Local MCP URL: http://${config.host}:${config.port}/mcp`,
126
142
  ...(publicBaseUrl ? [`Public MCP URL: ${publicBaseUrl}/mcp`] : []),
127
143
  ];
@@ -154,7 +170,7 @@ async function serve() {
154
170
  }
155
171
  const { createServer } = await import("./server.js");
156
172
  const config = loadConfig();
157
- const { app, close } = createServer(config);
173
+ const { app, close, localAgentProviders } = createServer(config);
158
174
  const httpServer = app.listen(config.port, config.host, () => {
159
175
  console.log(`devspace listening on http://${config.host}:${config.port}/mcp`);
160
176
  console.log(`public base url: ${config.publicBaseUrl}`);
@@ -165,6 +181,9 @@ async function serve() {
165
181
  }
166
182
  console.log("auth: Owner password approval required");
167
183
  console.log(`logging: ${config.logging.level} ${config.logging.format}`);
184
+ if (config.subagents) {
185
+ console.log(`subagent providers: ${formatLocalAgentProviderAvailabilitySummary(localAgentProviders)}`);
186
+ }
168
187
  });
169
188
  const shutdown = () => {
170
189
  httpServer.close(() => {
@@ -231,12 +250,232 @@ function printHelp() {
231
250
  " devspace doctor Show config, runtime, and native dependency status",
232
251
  " devspace config get Print persisted config",
233
252
  " devspace config set publicBaseUrl <url|null>",
253
+ " devspace agents ls List subagent sessions",
254
+ " devspace agents run <profile-or-provider-or-id> [--model <model>] <prompt>",
255
+ " devspace agents show <id>",
234
256
  " devspace -v, --version Print the installed version",
235
257
  "",
236
258
  "For temporary tunnels:",
237
259
  " DEVSPACE_PUBLIC_BASE_URL=https://example.trycloudflare.com devspace serve",
238
260
  ].join("\n"));
239
261
  }
262
+ async function runAgentsCommand(args) {
263
+ const [subcommand, ...rest] = args;
264
+ switch (subcommand) {
265
+ case "ls":
266
+ case "list":
267
+ await runAgentsList();
268
+ return;
269
+ case "run":
270
+ await runAgentsRun(rest);
271
+ return;
272
+ case "show":
273
+ await runAgentsShow(rest);
274
+ return;
275
+ case "__worker":
276
+ await runAgentsWorker(rest);
277
+ return;
278
+ case undefined:
279
+ case "help":
280
+ case "--help":
281
+ case "-h":
282
+ printAgentsHelp();
283
+ return;
284
+ default:
285
+ throw new Error(`Unknown agents command: ${subcommand}`);
286
+ }
287
+ }
288
+ async function runAgentsList() {
289
+ const config = loadConfig();
290
+ const store = createLocalAgentStore(config);
291
+ const agents = store.list(resolveCurrentWorkspaceScope());
292
+ if (agents.length === 0) {
293
+ console.log("No subagent sessions found for this workspace.");
294
+ return;
295
+ }
296
+ for (const agent of agents) {
297
+ console.log(formatAgentLine(agent));
298
+ }
299
+ }
300
+ async function runAgentsRun(args) {
301
+ const parsed = parseLocalAgentRunArgs(args);
302
+ const config = loadConfig();
303
+ const workspaceRoot = resolveCurrentWorkspaceRoot();
304
+ const store = createLocalAgentStore(config);
305
+ const existing = store.get(parsed.target);
306
+ if (existing) {
307
+ if (!isLocalAgentProvider(existing.provider)) {
308
+ throw new Error(`Unknown subagent provider for existing session: ${existing.provider}`);
309
+ }
310
+ assertLocalAgentProviderAvailable(existing.provider);
311
+ const promptFile = writeAgentPromptFile(parsed.prompt);
312
+ store.update(existing.id, {
313
+ status: "starting",
314
+ model: parsed.model ?? existing.model,
315
+ thinking: parsed.thinking ?? existing.thinking,
316
+ latestResponse: undefined,
317
+ error: undefined,
318
+ });
319
+ spawnAgentWorker(existing.id, promptFile);
320
+ console.log(formatAgentLine({
321
+ ...existing,
322
+ status: "running",
323
+ model: parsed.model ?? existing.model,
324
+ thinking: parsed.thinking ?? existing.thinking,
325
+ }));
326
+ return;
327
+ }
328
+ const profiles = await loadLocalAgentProfiles(config, workspaceRoot);
329
+ const target = resolveLocalAgentTarget(parsed.target, profiles, parsed.model, parsed.thinking);
330
+ if (!target) {
331
+ throw new Error(`Unknown subagent profile, provider, or id: ${parsed.target}. Available ${formatAvailableLocalAgentTargets(profiles)}`);
332
+ }
333
+ assertLocalAgentProviderAvailable(target.provider);
334
+ const promptFile = writeAgentPromptFile(parsed.prompt);
335
+ const record = store.create({
336
+ workspaceId: process.env.DEVSPACE_WORKSPACE_ID,
337
+ workspaceRoot,
338
+ profileName: target.name,
339
+ provider: target.provider,
340
+ model: target.model,
341
+ thinking: target.thinking,
342
+ });
343
+ spawnAgentWorker(record.id, promptFile);
344
+ console.log(formatAgentLine({ ...record, status: "running" }));
345
+ }
346
+ async function runAgentsShow(args) {
347
+ const [id] = args;
348
+ if (!id)
349
+ throw new Error("Usage: devspace agents show <id>");
350
+ const config = loadConfig();
351
+ const store = createLocalAgentStore(config);
352
+ let record = store.get(id);
353
+ if (!record)
354
+ throw new Error(`Unknown subagent id: ${id}`);
355
+ const deadline = Date.now() + 15_000;
356
+ while ((record.status === "starting" || record.status === "running") && Date.now() < deadline) {
357
+ await sleep(500);
358
+ record = store.get(id) ?? record;
359
+ }
360
+ console.log(formatAgentLine(record));
361
+ if (record.latestResponse) {
362
+ console.log(record.latestResponse);
363
+ return;
364
+ }
365
+ if (record.error) {
366
+ console.log(record.error);
367
+ return;
368
+ }
369
+ if (record.status === "starting" || record.status === "running") {
370
+ console.log(`No final response yet. Call \`devspace agents show ${record.id}\` again later.`);
371
+ }
372
+ }
373
+ async function runAgentsWorker(args) {
374
+ const [id, promptFileFlag, promptFile] = args;
375
+ if (!id || promptFileFlag !== "--prompt-file" || !promptFile) {
376
+ throw new Error("Usage: devspace agents __worker <id> --prompt-file <path>");
377
+ }
378
+ const config = loadConfig();
379
+ const store = createLocalAgentStore(config);
380
+ const record = store.get(id);
381
+ if (!record)
382
+ throw new Error(`Unknown subagent id: ${id}`);
383
+ store.update(record.id, { status: "running", error: undefined });
384
+ try {
385
+ const profiles = await loadLocalAgentProfiles(config, record.workspaceRoot);
386
+ const profile = profiles.find((candidate) => candidate.name === record.profileName);
387
+ const prompt = await readFile(promptFile, "utf8");
388
+ const result = profile
389
+ ? await runLocalAgentProfile(profile, record, prompt)
390
+ : await runRawLocalAgentProvider(record, prompt);
391
+ store.update(record.id, {
392
+ providerSessionId: result.providerSessionId ?? undefined,
393
+ status: "idle",
394
+ latestResponse: result.finalResponse,
395
+ error: undefined,
396
+ });
397
+ }
398
+ catch (error) {
399
+ store.update(record.id, {
400
+ status: "error",
401
+ error: error instanceof Error ? error.message : String(error),
402
+ });
403
+ }
404
+ }
405
+ async function runLocalAgentProfile(profile, record, prompt) {
406
+ const body = profile.body.trim();
407
+ const fullPrompt = body ? `${body}\n\nTask:\n${prompt}` : prompt;
408
+ return runLocalAgentProvider(profile.provider, {
409
+ prompt: fullPrompt,
410
+ workspace: record.workspaceRoot,
411
+ providerSessionId: record.providerSessionId,
412
+ writeMode: "allowed",
413
+ model: record.model ?? profile.model,
414
+ thinking: record.thinking ?? profile.thinking,
415
+ });
416
+ }
417
+ async function runRawLocalAgentProvider(record, prompt) {
418
+ if (record.profileName !== record.provider || !isLocalAgentProvider(record.provider)) {
419
+ throw new Error(`Subagent profile not found: ${record.profileName}`);
420
+ }
421
+ return runLocalAgentProvider(record.provider, {
422
+ prompt,
423
+ workspace: record.workspaceRoot,
424
+ providerSessionId: record.providerSessionId,
425
+ writeMode: "allowed",
426
+ model: record.model,
427
+ thinking: record.thinking,
428
+ });
429
+ }
430
+ function spawnAgentWorker(agentId, promptFile) {
431
+ const child = spawn(process.execPath, [
432
+ ...process.execArgv,
433
+ fileURLToPath(import.meta.url),
434
+ "agents",
435
+ "__worker",
436
+ agentId,
437
+ "--prompt-file",
438
+ promptFile,
439
+ ], {
440
+ detached: true,
441
+ stdio: "ignore",
442
+ env: process.env,
443
+ });
444
+ child.unref();
445
+ }
446
+ function writeAgentPromptFile(prompt) {
447
+ const directory = mkdtempSync(join(tmpdir(), "devspace-agent-prompt-"));
448
+ const filePath = join(directory, "prompt.txt");
449
+ writeFileSync(filePath, prompt, { mode: 0o600 });
450
+ return filePath;
451
+ }
452
+ function resolveCurrentWorkspaceRoot() {
453
+ return resolve(process.env.DEVSPACE_WORKSPACE_ROOT || process.cwd());
454
+ }
455
+ function resolveCurrentWorkspaceScope() {
456
+ return {
457
+ workspaceId: process.env.DEVSPACE_WORKSPACE_ID,
458
+ workspaceRoot: resolveCurrentWorkspaceRoot(),
459
+ };
460
+ }
461
+ function formatAgentLine(agent) {
462
+ const model = agent.model ? ` ${agent.model}` : "";
463
+ const thinking = agent.thinking ? ` thinking=${agent.thinking}` : "";
464
+ return `${agent.id} ${agent.status} ${agent.profileName} ${agent.provider}${model}${thinking}`;
465
+ }
466
+ function sleep(ms) {
467
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
468
+ }
469
+ function printAgentsHelp() {
470
+ console.log([
471
+ "DevSpace agents",
472
+ "",
473
+ "Usage:",
474
+ " devspace agents ls",
475
+ " devspace agents run <profile-or-provider-or-id> [--model <model>] [--thinking <level>] <prompt>",
476
+ " devspace agents show <id>",
477
+ ].join("\n"));
478
+ }
240
479
  function printVersion() {
241
480
  const packageJson = require("../package.json");
242
481
  if (typeof packageJson.version !== "string") {
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join, resolve } from "node:path";
3
3
  import { expandHomePath } from "./roots.js";
4
- import { loadDevspaceFiles } from "./user-config.js";
4
+ import { devspaceAgentsDir, devspaceSkillsDir, loadDevspaceFiles } from "./user-config.js";
5
5
  const DEFAULT_OAUTH_ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
6
6
  const DEFAULT_OAUTH_REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
7
7
  function parsePort(value) {
@@ -91,13 +91,6 @@ function parsePositiveInteger(value, fallback, name) {
91
91
  }
92
92
  return parsed;
93
93
  }
94
- function parseToolNaming(value) {
95
- if (!value || value === "short")
96
- return "short";
97
- if (value === "legacy")
98
- return "legacy";
99
- throw new Error(`Invalid DEVSPACE_TOOL_NAMING: ${value}`);
100
- }
101
94
  function parseLoggingConfig(env) {
102
95
  return {
103
96
  level: parseLogLevel(env.DEVSPACE_LOG_LEVEL),
@@ -169,12 +162,16 @@ export function loadConfig(env = process.env) {
169
162
  allowedHosts: parseAllowedHosts(env.DEVSPACE_ALLOWED_HOSTS, derivedAllowedHosts),
170
163
  publicBaseUrl,
171
164
  toolMode: parseToolMode(env),
172
- toolNaming: parseToolNaming(env.DEVSPACE_TOOL_NAMING),
173
165
  widgets: parseWidgetMode(env.DEVSPACE_WIDGETS),
174
166
  stateDir: resolve(expandHomePath(env.DEVSPACE_STATE_DIR ?? files.config.stateDir ?? defaultStateDir())),
175
167
  worktreeRoot: resolve(expandHomePath(env.DEVSPACE_WORKTREE_ROOT ?? files.config.worktreeRoot ?? defaultWorktreeRoot())),
176
168
  skillsEnabled: env.DEVSPACE_SKILLS === undefined ? true : parseBoolean(env.DEVSPACE_SKILLS),
177
169
  skillPaths: parsePathList(env.DEVSPACE_SKILL_PATHS),
170
+ devspaceSkillsDir: devspaceSkillsDir(env),
171
+ devspaceAgentsDir: devspaceAgentsDir(env),
172
+ subagents: env.DEVSPACE_SUBAGENTS === undefined
173
+ ? files.config.subagents === true
174
+ : parseBoolean(env.DEVSPACE_SUBAGENTS),
178
175
  agentDir: resolve(expandHomePath(env.DEVSPACE_AGENT_DIR ?? files.config.agentDir ?? defaultAgentDir())),
179
176
  logging: parseLoggingConfig(env),
180
177
  };
@@ -9,6 +9,11 @@ const migrations = [
9
9
  name: "oauth-state",
10
10
  up: migrateOAuthState,
11
11
  },
12
+ {
13
+ version: 3,
14
+ name: "local-agent-sessions",
15
+ up: migrateLocalAgentSessions,
16
+ },
12
17
  ];
13
18
  export function migrateDatabase(sqlite) {
14
19
  const migrate = sqlite.transaction(() => {
@@ -115,6 +120,35 @@ function migrateOAuthState(sqlite) {
115
120
  on oauth_refresh_tokens(expires_at);
116
121
  `);
117
122
  }
123
+ function migrateLocalAgentSessions(sqlite) {
124
+ sqlite.exec(`
125
+ create table if not exists local_agent_sessions (
126
+ id text primary key,
127
+ workspace_id text,
128
+ workspace_root text not null,
129
+ profile_name text not null,
130
+ provider text not null,
131
+ model text,
132
+ thinking text,
133
+ provider_session_id text,
134
+ status text not null,
135
+ latest_response text,
136
+ error text,
137
+ created_at text not null,
138
+ updated_at text not null
139
+ );
140
+
141
+ create index if not exists local_agent_sessions_workspace_id_idx
142
+ on local_agent_sessions(workspace_id, updated_at desc);
143
+
144
+ create index if not exists local_agent_sessions_workspace_root_idx
145
+ on local_agent_sessions(workspace_root, updated_at desc);
146
+
147
+ create index if not exists local_agent_sessions_provider_session_id_idx
148
+ on local_agent_sessions(provider_session_id);
149
+ `);
150
+ addColumnIfMissing(sqlite, "local_agent_sessions", "thinking", "text");
151
+ }
118
152
  function addColumnIfMissing(sqlite, table, column, definition) {
119
153
  const columns = sqlite.prepare(`pragma table_info(${table})`).all();
120
154
  if (columns.some((existingColumn) => existingColumn.name === column))
package/dist/db/schema.js CHANGED
@@ -50,3 +50,22 @@ export const oauthRefreshTokens = sqliteTable("oauth_refresh_tokens", {
50
50
  expiresAt: integer("expires_at").notNull(),
51
51
  resource: text("resource"),
52
52
  });
53
+ export const localAgentSessions = sqliteTable("local_agent_sessions", {
54
+ id: text("id").primaryKey(),
55
+ workspaceId: text("workspace_id"),
56
+ workspaceRoot: text("workspace_root").notNull(),
57
+ profileName: text("profile_name").notNull(),
58
+ provider: text("provider").notNull(),
59
+ model: text("model"),
60
+ thinking: text("thinking"),
61
+ providerSessionId: text("provider_session_id"),
62
+ status: text("status").notNull(),
63
+ latestResponse: text("latest_response"),
64
+ error: text("error"),
65
+ createdAt: text("created_at").notNull(),
66
+ updatedAt: text("updated_at").notNull(),
67
+ }, (table) => [
68
+ index("local_agent_sessions_workspace_id_idx").on(table.workspaceId, table.updatedAt),
69
+ index("local_agent_sessions_workspace_root_idx").on(table.workspaceRoot, table.updatedAt),
70
+ index("local_agent_sessions_provider_session_id_idx").on(table.providerSessionId),
71
+ ]);