@agimon-ai/doompi-runner 0.0.1-alpha.87 → 0.0.1-alpha.89

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 (128) hide show
  1. package/dist/{bashRunService-_5_HI6BZ.mjs → bashRunService-DVCoTO2o.mjs} +8 -4
  2. package/dist/bashRunService-DVCoTO2o.mjs.map +1 -0
  3. package/dist/{bashRunService-B6PmOl6j.cjs → bashRunService-DbUkhN8y.cjs} +8 -4
  4. package/dist/bashRunService-DbUkhN8y.cjs.map +1 -0
  5. package/dist/bashRunService.cjs +1 -1
  6. package/dist/bashRunService.d.mts +3 -1
  7. package/dist/bashRunService.d.mts.map +1 -1
  8. package/dist/bashRunService.mjs +1 -1
  9. package/dist/{bashTool-DSLtCIZs.mjs → bashTool-BX4UF_cF.mjs} +4 -3
  10. package/dist/{bashTool-DSLtCIZs.mjs.map → bashTool-BX4UF_cF.mjs.map} +1 -1
  11. package/dist/{bashTool-C2jX5J1U.cjs → bashTool-DzucJQKE.cjs} +4 -3
  12. package/dist/{bashTool-C2jX5J1U.cjs.map → bashTool-DzucJQKE.cjs.map} +1 -1
  13. package/dist/bashTool.cjs +1 -1
  14. package/dist/bashTool.mjs +1 -1
  15. package/dist/bin/cli.cjs +1 -1
  16. package/dist/bin/cli.mjs +1 -1
  17. package/dist/extensions/mcp-ui.mjs +23 -0
  18. package/dist/extensions/mcp-ui.mjs.map +1 -0
  19. package/dist/extensions/mcp.cjs +8 -1
  20. package/dist/extensions/mcp.cjs.map +1 -1
  21. package/dist/extensions/mcp.d.mts.map +1 -1
  22. package/dist/extensions/mcp.mjs +8 -1
  23. package/dist/extensions/mcp.mjs.map +1 -1
  24. package/dist/extensions/pi.cjs +24 -19
  25. package/dist/extensions/pi.cjs.map +1 -1
  26. package/dist/extensions/pi.mjs +24 -19
  27. package/dist/extensions/pi.mjs.map +1 -1
  28. package/dist/extensions/server.cjs +24 -14
  29. package/dist/extensions/server.cjs.map +1 -1
  30. package/dist/extensions/server.mjs +24 -14
  31. package/dist/extensions/server.mjs.map +1 -1
  32. package/dist/{headless-VCrJwDA4.mjs → headless-BDvWJKzm.mjs} +2 -2
  33. package/dist/{headless-VCrJwDA4.mjs.map → headless-BDvWJKzm.mjs.map} +1 -1
  34. package/dist/{headless-GskOzCa9.cjs → headless-Ch9darGJ.cjs} +2 -2
  35. package/dist/{headless-GskOzCa9.cjs.map → headless-Ch9darGJ.cjs.map} +1 -1
  36. package/dist/{index-BQGW9tYs.d.mts → index-BNJZCSzx.d.mts} +3 -2
  37. package/dist/index-BNJZCSzx.d.mts.map +1 -0
  38. package/dist/{index-DXoHpKQr.d.mts → index-BWxvlYO8.d.mts} +3 -2
  39. package/dist/index-BWxvlYO8.d.mts.map +1 -0
  40. package/dist/{index-DmA_W3dl.d.mts → index-C5LvdST6.d.mts} +2 -2
  41. package/dist/index-C5LvdST6.d.mts.map +1 -0
  42. package/dist/{index-DyhRa0QZ.d.mts → index-IRH2NxgG.d.mts} +3 -2
  43. package/dist/index-IRH2NxgG.d.mts.map +1 -0
  44. package/dist/index.cjs +4 -4
  45. package/dist/index.d.mts +4 -4
  46. package/dist/index.mjs +4 -4
  47. package/dist/{launcher-CTimVrt1.mjs → launcher-DT3B7nFi.mjs} +6 -4
  48. package/dist/launcher-DT3B7nFi.mjs.map +1 -0
  49. package/dist/{launcher-BHoIIfBj.cjs → launcher-s03kVk1v.cjs} +6 -4
  50. package/dist/launcher-s03kVk1v.cjs.map +1 -0
  51. package/dist/launcher.cjs +1 -1
  52. package/dist/launcher.d.mts +2 -1
  53. package/dist/launcher.d.mts.map +1 -1
  54. package/dist/launcher.mjs +1 -1
  55. package/dist/{ptyHost-BE-c973q.mjs → ptyHost-BMrUFpMX.mjs} +6 -4
  56. package/dist/ptyHost-BMrUFpMX.mjs.map +1 -0
  57. package/dist/{ptyHost-CtAZDgqM.cjs → ptyHost-BsChzCm7.cjs} +6 -4
  58. package/dist/ptyHost-BsChzCm7.cjs.map +1 -0
  59. package/dist/ptyHost.cjs +1 -1
  60. package/dist/ptyHost.d.mts +2 -1
  61. package/dist/ptyHost.d.mts.map +1 -1
  62. package/dist/ptyHost.mjs +1 -1
  63. package/dist/{reconcile-C01MBfQo.cjs → reconcile-B109E0xL.cjs} +3 -4
  64. package/dist/{reconcile-C01MBfQo.cjs.map → reconcile-B109E0xL.cjs.map} +1 -1
  65. package/dist/{reconcile-BCCw_eKT.mjs → reconcile-Cv_M9ctf.mjs} +3 -4
  66. package/dist/{reconcile-BCCw_eKT.mjs.map → reconcile-Cv_M9ctf.mjs.map} +1 -1
  67. package/dist/reconcile.cjs +1 -1
  68. package/dist/reconcile.mjs +1 -1
  69. package/dist/{rmuxBackend-D7PCLWaO.mjs → rmuxBackend-B7w7KaF3.mjs} +12 -13
  70. package/dist/rmuxBackend-B7w7KaF3.mjs.map +1 -0
  71. package/dist/{rmuxBackend-B0pfBR08.cjs → rmuxBackend-D3ZQCkTw.cjs} +12 -13
  72. package/dist/rmuxBackend-D3ZQCkTw.cjs.map +1 -0
  73. package/dist/rmuxBackend.cjs +1 -1
  74. package/dist/rmuxBackend.d.mts +1 -1
  75. package/dist/rmuxBackend.mjs +1 -1
  76. package/dist/{rtkProcessor-DTgY2fEs.mjs → rtkProcessor-C1cmQhJO.mjs} +8 -6
  77. package/dist/rtkProcessor-C1cmQhJO.mjs.map +1 -0
  78. package/dist/{rtkProcessor-KcjJAoEk.cjs → rtkProcessor-DJedzL3y.cjs} +8 -6
  79. package/dist/rtkProcessor-DJedzL3y.cjs.map +1 -0
  80. package/dist/rtkProcessor.cjs +1 -1
  81. package/dist/rtkProcessor.d.mts +1 -1
  82. package/dist/rtkProcessor.mjs +1 -1
  83. package/dist/{runnerDependencies-DI_m0npT.mjs → runnerDependencies-CsizryxY.mjs} +25 -22
  84. package/dist/runnerDependencies-CsizryxY.mjs.map +1 -0
  85. package/dist/{runnerDependencies-Dx8M_rcu.cjs → runnerDependencies-Dq8oIvGv.cjs} +25 -22
  86. package/dist/runnerDependencies-Dq8oIvGv.cjs.map +1 -0
  87. package/dist/runnerDependencies.cjs +1 -1
  88. package/dist/runnerDependencies.d.mts +1 -1
  89. package/dist/runnerDependencies.mjs +1 -1
  90. package/dist/{runnerPaths-spSxzwRP.cjs → runnerPaths-BSkjI5EO.cjs} +27 -20
  91. package/dist/runnerPaths-BSkjI5EO.cjs.map +1 -0
  92. package/dist/{runnerPaths-CfwlQ53O.mjs → runnerPaths-xeHQjhYj.mjs} +27 -20
  93. package/dist/runnerPaths-xeHQjhYj.mjs.map +1 -0
  94. package/dist/runnerPaths.cjs +1 -1
  95. package/dist/runnerPaths.d.mts +3 -0
  96. package/dist/runnerPaths.d.mts.map +1 -1
  97. package/dist/runnerPaths.mjs +1 -1
  98. package/dist/{runnerRegistry-DNfUnaES.mjs → runnerRegistry-CpIH63zQ.mjs} +3 -3
  99. package/dist/runnerRegistry-CpIH63zQ.mjs.map +1 -0
  100. package/dist/{runnerRegistry-C2r2fUY8.cjs → runnerRegistry-DcN71T16.cjs} +3 -3
  101. package/dist/runnerRegistry-DcN71T16.cjs.map +1 -0
  102. package/dist/runnerRegistry.cjs +1 -1
  103. package/dist/runnerRegistry.d.mts +1 -1
  104. package/dist/runnerRegistry.mjs +1 -1
  105. package/dist/{type-Dv5Zq3up.d.mts → type-U7Y6NN2B.d.mts} +2 -2
  106. package/dist/{type-Dv5Zq3up.d.mts.map → type-U7Y6NN2B.d.mts.map} +1 -1
  107. package/dist/types.d.mts +1 -1
  108. package/package.json +24 -17
  109. package/dist/bashRunService-B6PmOl6j.cjs.map +0 -1
  110. package/dist/bashRunService-_5_HI6BZ.mjs.map +0 -1
  111. package/dist/index-BQGW9tYs.d.mts.map +0 -1
  112. package/dist/index-DXoHpKQr.d.mts.map +0 -1
  113. package/dist/index-DmA_W3dl.d.mts.map +0 -1
  114. package/dist/index-DyhRa0QZ.d.mts.map +0 -1
  115. package/dist/launcher-BHoIIfBj.cjs.map +0 -1
  116. package/dist/launcher-CTimVrt1.mjs.map +0 -1
  117. package/dist/ptyHost-BE-c973q.mjs.map +0 -1
  118. package/dist/ptyHost-CtAZDgqM.cjs.map +0 -1
  119. package/dist/rmuxBackend-B0pfBR08.cjs.map +0 -1
  120. package/dist/rmuxBackend-D7PCLWaO.mjs.map +0 -1
  121. package/dist/rtkProcessor-DTgY2fEs.mjs.map +0 -1
  122. package/dist/rtkProcessor-KcjJAoEk.cjs.map +0 -1
  123. package/dist/runnerDependencies-DI_m0npT.mjs.map +0 -1
  124. package/dist/runnerDependencies-Dx8M_rcu.cjs.map +0 -1
  125. package/dist/runnerPaths-CfwlQ53O.mjs.map +0 -1
  126. package/dist/runnerPaths-spSxzwRP.cjs.map +0 -1
  127. package/dist/runnerRegistry-C2r2fUY8.cjs.map +0 -1
  128. package/dist/runnerRegistry-DNfUnaES.mjs.map +0 -1
@@ -24,18 +24,22 @@ var BashRunService = class {
24
24
  registry;
25
25
  clock;
26
26
  rtkProcessor;
27
- constructor(launcher, rmuxBackend, namer, registry, clock, rtkProcessor) {
27
+ cwd;
28
+ env;
29
+ constructor(launcher, rmuxBackend, namer, registry, clock, rtkProcessor, cwd = process.cwd(), env = process.env) {
28
30
  this.launcher = launcher;
29
31
  this.rmuxBackend = rmuxBackend;
30
32
  this.namer = namer;
31
33
  this.registry = registry;
32
34
  this.clock = clock;
33
35
  this.rtkProcessor = rtkProcessor;
36
+ this.cwd = cwd;
37
+ this.env = env;
34
38
  }
35
39
  async run(request) {
36
40
  const id = runnerId();
37
41
  const name = await this.namer.allocate(request.command, request.sessionId, request.name);
38
- const cwd = request.cwd ?? process.cwd();
42
+ const cwd = request.cwd ?? this.cwd;
39
43
  const interactive = request.interactive === true;
40
44
  if (request.signal?.aborted) return {
41
45
  kind: FAILED,
@@ -240,7 +244,7 @@ var BashRunService = class {
240
244
  const cancels = [];
241
245
  const racers = [handle.completion().catch((error) => error)];
242
246
  racers.push(new Promise((resolve) => {
243
- cancels.push(this.clock.after(getBackgroundThresholdMs(), () => resolve(TIMED_OUT)));
247
+ cancels.push(this.clock.after(getBackgroundThresholdMs(this.env), () => resolve(TIMED_OUT)));
244
248
  }));
245
249
  if (timeoutMs !== void 0 && timeoutMs > 0) racers.push(new Promise((resolve) => {
246
250
  cancels.push(this.clock.after(timeoutMs, () => resolve(DEADLINE)));
@@ -320,4 +324,4 @@ var BashRunService = class {
320
324
  //#endregion
321
325
  export { BashRunService as t };
322
326
 
323
- //# sourceMappingURL=bashRunService-_5_HI6BZ.mjs.map
327
+ //# sourceMappingURL=bashRunService-DVCoTO2o.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bashRunService-DVCoTO2o.mjs","names":[],"sources":["../src/constants/bashRunService.ts","../src/services/bashRunService/index.ts"],"sourcesContent":["export const RUNNER_ID_RANDOM_BYTES = 3;\nexport const OUTPUT_UPDATE_POLL_MS = 100;\nexport const COMPLETED = 'completed';\nexport const FAILED = 'failed';\nexport const LAUNCHER_ERROR = 'launcher_error';\nexport const SIGNALED = 'signaled';\n","import { randomBytes } from 'node:crypto';\n\nimport {\n COMPLETED,\n FAILED,\n LAUNCHER_ERROR,\n OUTPUT_UPDATE_POLL_MS,\n RUNNER_ID_RANDOM_BYTES,\n SIGNALED,\n} from '../../constants/bashRunService';\nimport type {\n BashRunRequest,\n BashRunResult,\n CompletedRun,\n IBashRunService,\n PromotedRun,\n} from '../../types/bashRunService';\nimport type { IClock } from '../../types/clock';\nimport type { ILauncher, RunHandle } from '../../types/launcher';\nimport type { IRmuxBackend } from '../../types/rmuxBackend';\nimport { type IRtkProcessor, RTK_FAILED_WARNING } from '../../types/rtkProcessor';\nimport type { IRunnerRegistry } from '../../types/runnerRegistry';\nimport { getBackgroundThresholdMs } from '../runnerConfig';\nimport type { IRunnerNamer } from '../runnerNamer/type';\n\n/** The command outlived the background threshold and should be promoted. */\nconst TIMED_OUT = Symbol('threshold-reached');\n/** The command outlived the caller's explicit timeout and should be stopped. */\nconst DEADLINE = Symbol('deadline-reached');\n/** The caller cancelled the command and its owned runner should be stopped. */\nconst ABORTED = Symbol('aborted');\n\nfunction runnerId(): string {\n const timestamp = Date.now().toString(36);\n const random = randomBytes(RUNNER_ID_RANDOM_BYTES).toString('base64url');\n return `${timestamp}-${random}`;\n}\n\nexport class BashRunService implements IBashRunService {\n constructor(\n private readonly launcher: ILauncher,\n private readonly rmuxBackend: IRmuxBackend,\n private readonly namer: IRunnerNamer,\n private readonly registry: IRunnerRegistry,\n private readonly clock: IClock,\n private readonly rtkProcessor: IRtkProcessor,\n private readonly cwd: string = process.cwd(),\n private readonly env: Readonly<Record<string, string | undefined>> = process.env,\n ) {}\n\n async run(request: BashRunRequest): Promise<BashRunResult> {\n const id = runnerId();\n const name = await this.namer.allocate(request.command, request.sessionId, request.name);\n const cwd = request.cwd ?? this.cwd;\n const interactive = request.interactive === true;\n if (request.signal?.aborted) return { kind: FAILED, id, name, error: 'Operation aborted' };\n\n let handle: RunHandle;\n try {\n const rmuxHandle = await this.rmuxBackend.launch({\n id,\n name,\n command: request.command,\n cwd,\n sessionId: request.sessionId,\n interactive,\n });\n if (interactive && !rmuxHandle) {\n return { kind: FAILED, id, name, error: 'RMUX is required for interactive commands but is unavailable' };\n }\n handle =\n rmuxHandle ?? this.launcher.launch({ id, name, command: request.command, cwd, sessionId: request.sessionId });\n } catch (error) {\n return { kind: FAILED, id, name, error: error instanceof Error ? error.message : String(error) };\n }\n\n if (handle.pid === undefined) {\n return { kind: FAILED, id, name, error: 'The command did not start, so it cannot be supervised' };\n }\n if (request.signal?.aborted) {\n await handle.stop();\n return { kind: FAILED, id, name, error: 'Operation aborted' };\n }\n\n try {\n await this.registry.register({\n id,\n name,\n pid: handle.pid,\n command: request.command,\n cwd,\n logPath: handle.logPath,\n interactive,\n sessionId: request.sessionId,\n backend: handle.backend,\n ...(handle.backendTarget ? { backendTarget: handle.backendTarget } : {}),\n });\n } catch (error) {\n await handle.stop();\n return { kind: FAILED, id, name, error: error instanceof Error ? error.message : String(error) };\n }\n\n // An interactive run has nothing to wait for: it is going to prompt, so\n // the model needs the runner name before it can answer.\n if (request.background === true || interactive) {\n return this.promote(handle, request.background === true ? 'requested' : 'interactive', request.signal);\n }\n\n const outputUpdates = request.onOutput ? this.followOutput(handle, request.onOutput) : undefined;\n try {\n const outcome = await this.race(handle, request.timeoutMs, request.signal);\n outputUpdates?.flush();\n if (outcome === TIMED_OUT) {\n return this.promote(handle, 'threshold', request.signal);\n }\n if (outcome === ABORTED) {\n await handle.stop();\n outputUpdates?.flush();\n await this.registry.complete(id, { reason: 'stopped', code: null, signal: 'SIGTERM' });\n return {\n kind: COMPLETED,\n id,\n name,\n output: handle.output(),\n exitCode: null,\n signal: 'SIGTERM',\n logPath: handle.logPath,\n backend: handle.backend,\n aborted: true,\n };\n }\n if (outcome === DEADLINE) {\n // An explicit timeout means the caller wants the command dead, not\n // supervised, so it is stopped rather than promoted.\n await handle.stop();\n outputUpdates?.flush();\n await this.registry.complete(id, { reason: 'timed_out', code: null, signal: 'SIGTERM' });\n return {\n kind: COMPLETED,\n id,\n name,\n output: handle.output(),\n exitCode: null,\n signal: 'SIGTERM',\n logPath: handle.logPath,\n backend: handle.backend,\n timedOut: true,\n };\n }\n if (outcome instanceof Error) {\n await this.registry.complete(id, { reason: LAUNCHER_ERROR, code: null, signal: null });\n return { kind: FAILED, id, name, error: outcome.message };\n }\n\n await this.registry.complete(id, {\n reason: outcome.signal ? SIGNALED : outcome.code === 0 ? COMPLETED : FAILED,\n code: outcome.code,\n signal: outcome.signal,\n });\n\n return {\n kind: COMPLETED,\n id,\n name,\n output: handle.output(),\n exitCode: outcome.code,\n signal: outcome.signal,\n logPath: handle.logPath,\n backend: handle.backend,\n ...(await this.processLog(request.command, handle.logPath)),\n };\n } finally {\n outputUpdates?.dispose();\n }\n }\n\n private async processLog(command: string, logPath: string): Promise<Pick<CompletedRun, 'rtkOutput' | 'rtkWarning'>> {\n try {\n const result = await this.rtkProcessor.process({ command, logPath });\n if (result.kind === 'processed') return { rtkOutput: result.result };\n if (result.kind === 'fallback') return { rtkWarning: result.warning };\n return {};\n } catch {\n return { rtkWarning: RTK_FAILED_WARNING };\n }\n }\n\n private followOutput(\n handle: RunHandle,\n onOutput: NonNullable<BashRunRequest['onOutput']>,\n ): { flush(): void; dispose(): void } {\n let active = true;\n let previous = '';\n let cancelPoll: (() => void) | undefined;\n const flush = (): void => {\n if (!active) return;\n const output = handle.output();\n if (output === previous) return;\n previous = output;\n onOutput(output);\n };\n const poll = (): void => {\n if (!active) return;\n flush();\n if (active) cancelPoll = this.clock.after(OUTPUT_UPDATE_POLL_MS, poll);\n };\n cancelPoll = this.clock.after(OUTPUT_UPDATE_POLL_MS, poll);\n return {\n flush,\n dispose: () => {\n active = false;\n cancelPoll?.();\n cancelPoll = undefined;\n },\n };\n }\n\n /**\n * Waits for the command, giving up at the background threshold or at the\n * caller's timeout, whichever comes first.\n *\n * A rejected completion is returned rather than thrown so the caller can\n * report a start failure as a tool result instead of an exception.\n */\n private async race(\n handle: RunHandle,\n timeoutMs: number | undefined,\n signal: AbortSignal | undefined,\n ): Promise<\n Awaited<ReturnType<RunHandle['completion']>> | Error | typeof TIMED_OUT | typeof DEADLINE | typeof ABORTED\n > {\n const cancels: Array<() => void> = [];\n const racers: Array<Promise<unknown>> = [handle.completion().catch((error: Error) => error)];\n\n racers.push(\n new Promise((resolve) => {\n cancels.push(this.clock.after(getBackgroundThresholdMs(this.env), () => resolve(TIMED_OUT)));\n }),\n );\n if (timeoutMs !== undefined && timeoutMs > 0) {\n racers.push(\n new Promise((resolve) => {\n cancels.push(this.clock.after(timeoutMs, () => resolve(DEADLINE)));\n }),\n );\n }\n if (signal) {\n racers.push(\n new Promise((resolve) => {\n if (signal.aborted) {\n resolve(ABORTED);\n return;\n }\n const abort = (): void => resolve(ABORTED);\n signal.addEventListener('abort', abort, { once: true });\n cancels.push(() => signal.removeEventListener('abort', abort));\n }),\n );\n }\n\n try {\n return (await Promise.race(racers)) as\n | Awaited<ReturnType<RunHandle['completion']>>\n | Error\n | typeof TIMED_OUT\n | typeof DEADLINE\n | typeof ABORTED;\n } finally {\n for (const cancel of cancels) cancel();\n }\n }\n\n private async promote(\n handle: RunHandle,\n reason: PromotedRun['reason'],\n signal: AbortSignal | undefined,\n ): Promise<BashRunResult> {\n if (handle.pid === undefined)\n return { kind: FAILED, id: handle.id, name: handle.name, error: 'The command did not start' };\n await this.registry.markPromoted(handle.id);\n // Registration first: an unregistered runner is invisible, and the handle\n // stops buffering the moment it is detached.\n handle.detach();\n let abortStop: Promise<boolean> | undefined;\n const abort = (): void => {\n abortStop = handle\n .stop()\n .then(async () => {\n await this.registry.complete(handle.id, { reason: 'stopped', code: null, signal: 'SIGTERM' });\n return true;\n })\n .catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.emitWarning(`Failed to stop aborted runner ${handle.id}: ${message}`);\n return false;\n });\n };\n if (signal) {\n if (signal.aborted) abort();\n else signal.addEventListener('abort', abort, { once: true });\n }\n void handle\n .completion()\n .then(\n async (outcome) => {\n if (await abortStop) return;\n await this.registry.complete(handle.id, {\n reason: outcome.signal ? SIGNALED : outcome.code === 0 ? COMPLETED : FAILED,\n code: outcome.code,\n signal: outcome.signal,\n });\n },\n async () => {\n if (await abortStop) return;\n await this.registry.complete(handle.id, { reason: LAUNCHER_ERROR, code: null, signal: null });\n },\n )\n .catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.emitWarning(`Failed to complete runner ${handle.id}: ${message}`);\n })\n .finally(() => signal?.removeEventListener('abort', abort));\n\n return {\n kind: 'promoted',\n id: handle.id,\n name: handle.name,\n pid: handle.pid,\n logPath: handle.logPath,\n backend: handle.backend,\n reason,\n };\n }\n}\n"],"mappings":"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"}
@@ -24,18 +24,22 @@ var BashRunService = class {
24
24
  registry;
25
25
  clock;
26
26
  rtkProcessor;
27
- constructor(launcher, rmuxBackend, namer, registry, clock, rtkProcessor) {
27
+ cwd;
28
+ env;
29
+ constructor(launcher, rmuxBackend, namer, registry, clock, rtkProcessor, cwd = process.cwd(), env = process.env) {
28
30
  this.launcher = launcher;
29
31
  this.rmuxBackend = rmuxBackend;
30
32
  this.namer = namer;
31
33
  this.registry = registry;
32
34
  this.clock = clock;
33
35
  this.rtkProcessor = rtkProcessor;
36
+ this.cwd = cwd;
37
+ this.env = env;
34
38
  }
35
39
  async run(request) {
36
40
  const id = runnerId();
37
41
  const name = await this.namer.allocate(request.command, request.sessionId, request.name);
38
- const cwd = request.cwd ?? process.cwd();
42
+ const cwd = request.cwd ?? this.cwd;
39
43
  const interactive = request.interactive === true;
40
44
  if (request.signal?.aborted) return {
41
45
  kind: FAILED,
@@ -240,7 +244,7 @@ var BashRunService = class {
240
244
  const cancels = [];
241
245
  const racers = [handle.completion().catch((error) => error)];
242
246
  racers.push(new Promise((resolve) => {
243
- cancels.push(this.clock.after(require_runnerConfig.getBackgroundThresholdMs(), () => resolve(TIMED_OUT)));
247
+ cancels.push(this.clock.after(require_runnerConfig.getBackgroundThresholdMs(this.env), () => resolve(TIMED_OUT)));
244
248
  }));
245
249
  if (timeoutMs !== void 0 && timeoutMs > 0) racers.push(new Promise((resolve) => {
246
250
  cancels.push(this.clock.after(timeoutMs, () => resolve(DEADLINE)));
@@ -325,4 +329,4 @@ Object.defineProperty(exports, "BashRunService", {
325
329
  }
326
330
  });
327
331
 
328
- //# sourceMappingURL=bashRunService-B6PmOl6j.cjs.map
332
+ //# sourceMappingURL=bashRunService-DbUkhN8y.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bashRunService-DbUkhN8y.cjs","names":["randomBytes","RTK_FAILED_WARNING","getBackgroundThresholdMs"],"sources":["../src/constants/bashRunService.ts","../src/services/bashRunService/index.ts"],"sourcesContent":["export const RUNNER_ID_RANDOM_BYTES = 3;\nexport const OUTPUT_UPDATE_POLL_MS = 100;\nexport const COMPLETED = 'completed';\nexport const FAILED = 'failed';\nexport const LAUNCHER_ERROR = 'launcher_error';\nexport const SIGNALED = 'signaled';\n","import { randomBytes } from 'node:crypto';\n\nimport {\n COMPLETED,\n FAILED,\n LAUNCHER_ERROR,\n OUTPUT_UPDATE_POLL_MS,\n RUNNER_ID_RANDOM_BYTES,\n SIGNALED,\n} from '../../constants/bashRunService';\nimport type {\n BashRunRequest,\n BashRunResult,\n CompletedRun,\n IBashRunService,\n PromotedRun,\n} from '../../types/bashRunService';\nimport type { IClock } from '../../types/clock';\nimport type { ILauncher, RunHandle } from '../../types/launcher';\nimport type { IRmuxBackend } from '../../types/rmuxBackend';\nimport { type IRtkProcessor, RTK_FAILED_WARNING } from '../../types/rtkProcessor';\nimport type { IRunnerRegistry } from '../../types/runnerRegistry';\nimport { getBackgroundThresholdMs } from '../runnerConfig';\nimport type { IRunnerNamer } from '../runnerNamer/type';\n\n/** The command outlived the background threshold and should be promoted. */\nconst TIMED_OUT = Symbol('threshold-reached');\n/** The command outlived the caller's explicit timeout and should be stopped. */\nconst DEADLINE = Symbol('deadline-reached');\n/** The caller cancelled the command and its owned runner should be stopped. */\nconst ABORTED = Symbol('aborted');\n\nfunction runnerId(): string {\n const timestamp = Date.now().toString(36);\n const random = randomBytes(RUNNER_ID_RANDOM_BYTES).toString('base64url');\n return `${timestamp}-${random}`;\n}\n\nexport class BashRunService implements IBashRunService {\n constructor(\n private readonly launcher: ILauncher,\n private readonly rmuxBackend: IRmuxBackend,\n private readonly namer: IRunnerNamer,\n private readonly registry: IRunnerRegistry,\n private readonly clock: IClock,\n private readonly rtkProcessor: IRtkProcessor,\n private readonly cwd: string = process.cwd(),\n private readonly env: Readonly<Record<string, string | undefined>> = process.env,\n ) {}\n\n async run(request: BashRunRequest): Promise<BashRunResult> {\n const id = runnerId();\n const name = await this.namer.allocate(request.command, request.sessionId, request.name);\n const cwd = request.cwd ?? this.cwd;\n const interactive = request.interactive === true;\n if (request.signal?.aborted) return { kind: FAILED, id, name, error: 'Operation aborted' };\n\n let handle: RunHandle;\n try {\n const rmuxHandle = await this.rmuxBackend.launch({\n id,\n name,\n command: request.command,\n cwd,\n sessionId: request.sessionId,\n interactive,\n });\n if (interactive && !rmuxHandle) {\n return { kind: FAILED, id, name, error: 'RMUX is required for interactive commands but is unavailable' };\n }\n handle =\n rmuxHandle ?? this.launcher.launch({ id, name, command: request.command, cwd, sessionId: request.sessionId });\n } catch (error) {\n return { kind: FAILED, id, name, error: error instanceof Error ? error.message : String(error) };\n }\n\n if (handle.pid === undefined) {\n return { kind: FAILED, id, name, error: 'The command did not start, so it cannot be supervised' };\n }\n if (request.signal?.aborted) {\n await handle.stop();\n return { kind: FAILED, id, name, error: 'Operation aborted' };\n }\n\n try {\n await this.registry.register({\n id,\n name,\n pid: handle.pid,\n command: request.command,\n cwd,\n logPath: handle.logPath,\n interactive,\n sessionId: request.sessionId,\n backend: handle.backend,\n ...(handle.backendTarget ? { backendTarget: handle.backendTarget } : {}),\n });\n } catch (error) {\n await handle.stop();\n return { kind: FAILED, id, name, error: error instanceof Error ? error.message : String(error) };\n }\n\n // An interactive run has nothing to wait for: it is going to prompt, so\n // the model needs the runner name before it can answer.\n if (request.background === true || interactive) {\n return this.promote(handle, request.background === true ? 'requested' : 'interactive', request.signal);\n }\n\n const outputUpdates = request.onOutput ? this.followOutput(handle, request.onOutput) : undefined;\n try {\n const outcome = await this.race(handle, request.timeoutMs, request.signal);\n outputUpdates?.flush();\n if (outcome === TIMED_OUT) {\n return this.promote(handle, 'threshold', request.signal);\n }\n if (outcome === ABORTED) {\n await handle.stop();\n outputUpdates?.flush();\n await this.registry.complete(id, { reason: 'stopped', code: null, signal: 'SIGTERM' });\n return {\n kind: COMPLETED,\n id,\n name,\n output: handle.output(),\n exitCode: null,\n signal: 'SIGTERM',\n logPath: handle.logPath,\n backend: handle.backend,\n aborted: true,\n };\n }\n if (outcome === DEADLINE) {\n // An explicit timeout means the caller wants the command dead, not\n // supervised, so it is stopped rather than promoted.\n await handle.stop();\n outputUpdates?.flush();\n await this.registry.complete(id, { reason: 'timed_out', code: null, signal: 'SIGTERM' });\n return {\n kind: COMPLETED,\n id,\n name,\n output: handle.output(),\n exitCode: null,\n signal: 'SIGTERM',\n logPath: handle.logPath,\n backend: handle.backend,\n timedOut: true,\n };\n }\n if (outcome instanceof Error) {\n await this.registry.complete(id, { reason: LAUNCHER_ERROR, code: null, signal: null });\n return { kind: FAILED, id, name, error: outcome.message };\n }\n\n await this.registry.complete(id, {\n reason: outcome.signal ? SIGNALED : outcome.code === 0 ? COMPLETED : FAILED,\n code: outcome.code,\n signal: outcome.signal,\n });\n\n return {\n kind: COMPLETED,\n id,\n name,\n output: handle.output(),\n exitCode: outcome.code,\n signal: outcome.signal,\n logPath: handle.logPath,\n backend: handle.backend,\n ...(await this.processLog(request.command, handle.logPath)),\n };\n } finally {\n outputUpdates?.dispose();\n }\n }\n\n private async processLog(command: string, logPath: string): Promise<Pick<CompletedRun, 'rtkOutput' | 'rtkWarning'>> {\n try {\n const result = await this.rtkProcessor.process({ command, logPath });\n if (result.kind === 'processed') return { rtkOutput: result.result };\n if (result.kind === 'fallback') return { rtkWarning: result.warning };\n return {};\n } catch {\n return { rtkWarning: RTK_FAILED_WARNING };\n }\n }\n\n private followOutput(\n handle: RunHandle,\n onOutput: NonNullable<BashRunRequest['onOutput']>,\n ): { flush(): void; dispose(): void } {\n let active = true;\n let previous = '';\n let cancelPoll: (() => void) | undefined;\n const flush = (): void => {\n if (!active) return;\n const output = handle.output();\n if (output === previous) return;\n previous = output;\n onOutput(output);\n };\n const poll = (): void => {\n if (!active) return;\n flush();\n if (active) cancelPoll = this.clock.after(OUTPUT_UPDATE_POLL_MS, poll);\n };\n cancelPoll = this.clock.after(OUTPUT_UPDATE_POLL_MS, poll);\n return {\n flush,\n dispose: () => {\n active = false;\n cancelPoll?.();\n cancelPoll = undefined;\n },\n };\n }\n\n /**\n * Waits for the command, giving up at the background threshold or at the\n * caller's timeout, whichever comes first.\n *\n * A rejected completion is returned rather than thrown so the caller can\n * report a start failure as a tool result instead of an exception.\n */\n private async race(\n handle: RunHandle,\n timeoutMs: number | undefined,\n signal: AbortSignal | undefined,\n ): Promise<\n Awaited<ReturnType<RunHandle['completion']>> | Error | typeof TIMED_OUT | typeof DEADLINE | typeof ABORTED\n > {\n const cancels: Array<() => void> = [];\n const racers: Array<Promise<unknown>> = [handle.completion().catch((error: Error) => error)];\n\n racers.push(\n new Promise((resolve) => {\n cancels.push(this.clock.after(getBackgroundThresholdMs(this.env), () => resolve(TIMED_OUT)));\n }),\n );\n if (timeoutMs !== undefined && timeoutMs > 0) {\n racers.push(\n new Promise((resolve) => {\n cancels.push(this.clock.after(timeoutMs, () => resolve(DEADLINE)));\n }),\n );\n }\n if (signal) {\n racers.push(\n new Promise((resolve) => {\n if (signal.aborted) {\n resolve(ABORTED);\n return;\n }\n const abort = (): void => resolve(ABORTED);\n signal.addEventListener('abort', abort, { once: true });\n cancels.push(() => signal.removeEventListener('abort', abort));\n }),\n );\n }\n\n try {\n return (await Promise.race(racers)) as\n | Awaited<ReturnType<RunHandle['completion']>>\n | Error\n | typeof TIMED_OUT\n | typeof DEADLINE\n | typeof ABORTED;\n } finally {\n for (const cancel of cancels) cancel();\n }\n }\n\n private async promote(\n handle: RunHandle,\n reason: PromotedRun['reason'],\n signal: AbortSignal | undefined,\n ): Promise<BashRunResult> {\n if (handle.pid === undefined)\n return { kind: FAILED, id: handle.id, name: handle.name, error: 'The command did not start' };\n await this.registry.markPromoted(handle.id);\n // Registration first: an unregistered runner is invisible, and the handle\n // stops buffering the moment it is detached.\n handle.detach();\n let abortStop: Promise<boolean> | undefined;\n const abort = (): void => {\n abortStop = handle\n .stop()\n .then(async () => {\n await this.registry.complete(handle.id, { reason: 'stopped', code: null, signal: 'SIGTERM' });\n return true;\n })\n .catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.emitWarning(`Failed to stop aborted runner ${handle.id}: ${message}`);\n return false;\n });\n };\n if (signal) {\n if (signal.aborted) abort();\n else signal.addEventListener('abort', abort, { once: true });\n }\n void handle\n .completion()\n .then(\n async (outcome) => {\n if (await abortStop) return;\n await this.registry.complete(handle.id, {\n reason: outcome.signal ? SIGNALED : outcome.code === 0 ? COMPLETED : FAILED,\n code: outcome.code,\n signal: outcome.signal,\n });\n },\n async () => {\n if (await abortStop) return;\n await this.registry.complete(handle.id, { reason: LAUNCHER_ERROR, code: null, signal: null });\n },\n )\n .catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.emitWarning(`Failed to complete runner ${handle.id}: ${message}`);\n })\n .finally(() => signal?.removeEventListener('abort', abort));\n\n return {\n kind: 'promoted',\n id: handle.id,\n name: handle.name,\n pid: handle.pid,\n logPath: handle.logPath,\n backend: handle.backend,\n reason,\n };\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_bashRunService = require("./bashRunService-B6PmOl6j.cjs");
2
+ const require_bashRunService = require("./bashRunService-DbUkhN8y.cjs");
3
3
  exports.BashRunService = require_bashRunService.BashRunService;
@@ -13,7 +13,9 @@ export declare class BashRunService implements IBashRunService {
13
13
  private readonly registry;
14
14
  private readonly clock;
15
15
  private readonly rtkProcessor;
16
- constructor(launcher: ILauncher, rmuxBackend: IRmuxBackend, namer: IRunnerNamer, registry: IRunnerRegistry, clock: IClock, rtkProcessor: IRtkProcessor);
16
+ private readonly cwd;
17
+ private readonly env;
18
+ constructor(launcher: ILauncher, rmuxBackend: IRmuxBackend, namer: IRunnerNamer, registry: IRunnerRegistry, clock: IClock, rtkProcessor: IRtkProcessor, cwd?: string, env?: Readonly<Record<string, string | undefined>>);
17
19
  run(request: BashRunRequest): Promise<BashRunResult>;
18
20
  private processLog;
19
21
  private followOutput;
@@ -1 +1 @@
1
- {"version":3,"file":"bashRunService.d.mts","names":[],"sources":["../src/services/bashRunService/index.ts"],"mappings":";;;;;;;;qBAsCa,0BAA0B;mBAElB;mBACA;mBACA;mBACA;mBACA;mBACA;EANnB,YACmB,UAAU,WACV,aAAa,cACb,OAAO,cACP,UAAU,iBACV,OAAO,QACP,cAAc;EAG3B,IAAI,SAAS,iBAAiB,QAAQ;UA8H9B;UAWN;;;;;;;;UAqCM;UAgDA"}
1
+ {"version":3,"file":"bashRunService.d.mts","names":[],"sources":["../src/services/bashRunService/index.ts"],"mappings":";;;;;;;;qBAsCa,0BAA0B;mBAElB;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;EARnB,YACmB,UAAU,WACV,aAAa,cACb,OAAO,cACP,UAAU,iBACV,OAAO,QACP,cAAc,eACd,cACA,MAAK,SAAS;EAG3B,IAAI,SAAS,iBAAiB,QAAQ;UA8H9B;UAWN;;;;;;;;UAqCM;UAgDA"}
@@ -1,2 +1,2 @@
1
- import { t as BashRunService } from "./bashRunService-_5_HI6BZ.mjs";
1
+ import { t as BashRunService } from "./bashRunService-DVCoTO2o.mjs";
2
2
  export { BashRunService };
@@ -30,7 +30,7 @@ function createBashTool(dependencies, renderers = {}) {
30
30
  promptGuidelines: bashPromptGuidelines(),
31
31
  parameters: BashParamsSchema,
32
32
  renderShell: "self",
33
- async execute(_toolCallId, params, signal, onUpdate, _ctx) {
33
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
34
34
  const { command, timeout, background, interactive, name } = params;
35
35
  let onOutput;
36
36
  if (onUpdate) onUpdate(textResult(`Starting ${interactive === true ? "interactive runner" : background === true ? "background runner" : "command"}...`));
@@ -45,7 +45,8 @@ function createBashTool(dependencies, renderers = {}) {
45
45
  name,
46
46
  ...onOutput ? { onOutput } : {},
47
47
  ...signal ? { signal } : {},
48
- sessionId: await dependencies.getSessionId()
48
+ sessionId: await dependencies.getSessionId(),
49
+ cwd: ctx.cwd
49
50
  });
50
51
  if (signal?.aborted) throw new Error("Operation aborted");
51
52
  } catch (error) {
@@ -61,4 +62,4 @@ function createBashTool(dependencies, renderers = {}) {
61
62
  //#endregion
62
63
  export { bashPromptGuidelines as n, createBashTool as r, BASH_PROMPT_SNIPPET as t };
63
64
 
64
- //# sourceMappingURL=bashTool-DSLtCIZs.mjs.map
65
+ //# sourceMappingURL=bashTool-BX4UF_cF.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool-DSLtCIZs.mjs","names":[],"sources":["../src/services/bashTool/index.ts"],"sourcesContent":["import { definePiTool, type PiToolDeclaration } from '@agimon-ai/doompi-core/piExtension';\n\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, MS_PER_SECOND } from '../../constants/bashTool';\nimport { type BashParams, BashParamsSchema } from '../../schemas/bashTool';\nimport { formatRunResult, parseResultPragma, textResult } from '../../services/bashResult';\nimport { getBackgroundThresholdMs } from '../../services/runnerConfig';\nimport { type ToolResult } from '../../types/bashResult';\nimport type { BashRunResult } from '../../types/bashRunService';\nimport type { BashToolDependencies, BashToolRenderers } from '../../types/bashTool';\n\nexport const BASH_PROMPT_SNIPPET =\n 'Execute shell commands with bounded foreground output and supervised background runners';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner with an id and streaming log path. You are messaged automatically when it exits, so never sleep, poll, or pgrep to wait for one.`,\n 'Pass background: true only for commands you know will remain active, such as dev servers, watchers, and tails.',\n 'Pass interactive: true only when the command will prompt for input. Use Runner Space for terminal input; avoid interactive mode otherwise because its logs are noisier.',\n 'Results are already bounded and the full log is saved to disk. Do not pipe to head or tail: it adds nothing and leaves the live log empty while the command runs.',\n 'On failure, use the returned output first. Inspect the saved log only when the result says output was truncated or no useful output was returned. Never retry an unchanged command merely to recover output.',\n 'Stop background runners when they are no longer needed. Every runner is stopped automatically when the session ends.',\n ];\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function createBashTool(\n dependencies: BashToolDependencies,\n renderers: BashToolRenderers = {},\n): PiToolDeclaration {\n return definePiTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute one Bash command in the current working directory. Foreground commands return bounded output; commands that outlive the threshold return a supervised runner id and streaming log path. Do not rerun a command merely to recover output; inspect the saved log only when the result identifies missing context.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, signal, onUpdate, _ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n ...(signal ? { signal } : {}),\n sessionId: await dependencies.getSessionId(),\n });\n if (signal?.aborted) throw new Error('Operation aborted');\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute command: ${message}`,\n 'Next: verify the command, runtime, and working directory. Retry only after correcting the cause.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result, parseResultPragma(params.command), dependencies.summarizeLog);\n },\n\n ...renderers,\n });\n}\n"],"mappings":";;;;;AAUA,MAAa,sBACX;;AAGF,SAAgB,qBAAqB,cAAc,yBAAyB,GAAa;CACvF,OAAO;EACL,iCAAiC,KAAK,MAAM,cAAc,aAAa,EAAE;EACzE;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;AAQA,SAAgB,eACd,cACA,YAA+B,CAAC,GACb;CACnB,OAAO,aAAa;EAClB,MAAM;EACN,OAAO;EACP,aACE;EACF,eAAe;EACf,kBAAkB,qBAAqB;EACvC,YAAY;EAIZ,aAAa;EAEb,MAAM,QAAQ,aAAa,QAAQ,QAAQ,UAAU,MAA2B;GAC9E,MAAM,EAAE,SAAS,SAAS,YAAY,aAAa,SAAS;GAC5D,IAAI;GACJ,IAAI,UAGF,SAAS,WAAW,YADlB,gBAAgB,OAAO,uBAAuB,eAAe,OAAO,sBAAsB,UACvD,IAAI,CAAC;GAE5C,IAAI,eAAe,QAAQ,gBAAgB,QAAQ,UACjD,YAAY,WAAW,SAAS,WAAW,MAAM,CAAC;GAEpD,IAAI;GACJ,IAAI;IACF,SAAS,MAAM,aAAa,eAAe,IAAI;KAC7C;KACA,WAAW,YAAY,KAAA,IAAY,KAAA,IAAY,UAAU;KACzD;KACA;KACA;KACA,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;KAC/B,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;KAC3B,WAAW,MAAM,aAAa,aAAa;IAC7C,CAAC;IACD,IAAI,QAAQ,SAAS,MAAM,IAAI,MAAM,mBAAmB;GAC1D,SAAS,OAAO;IACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IACrE,MAAM,IAAI,MACR,CACE,8BAA8B,WAC9B,kGACF,CAAC,CAAC,KAAK,IAAI,GACX,EAAE,OAAO,MAAM,CACjB;GACF;GAEA,IAAI,OAAO,SAAS,YAAY,aAAa,gBAAgB,OAAO,EAAE;GACtE,OAAO,gBAAgB,QAAQ,kBAAkB,OAAO,OAAO,GAAG,aAAa,YAAY;EAC7F;EAEA,GAAG;CACL,CAAC;AACH"}
1
+ {"version":3,"file":"bashTool-BX4UF_cF.mjs","names":[],"sources":["../src/services/bashTool/index.ts"],"sourcesContent":["import { definePiTool, type PiToolDeclaration } from '@agimon-ai/doompi-core/piExtension';\n\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, MS_PER_SECOND } from '../../constants/bashTool';\nimport { type BashParams, BashParamsSchema } from '../../schemas/bashTool';\nimport { formatRunResult, parseResultPragma, textResult } from '../../services/bashResult';\nimport { getBackgroundThresholdMs } from '../../services/runnerConfig';\nimport { type ToolResult } from '../../types/bashResult';\nimport type { BashRunResult } from '../../types/bashRunService';\nimport type { BashToolDependencies, BashToolRenderers } from '../../types/bashTool';\n\nexport const BASH_PROMPT_SNIPPET =\n 'Execute shell commands with bounded foreground output and supervised background runners';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner with an id and streaming log path. You are messaged automatically when it exits, so never sleep, poll, or pgrep to wait for one.`,\n 'Pass background: true only for commands you know will remain active, such as dev servers, watchers, and tails.',\n 'Pass interactive: true only when the command will prompt for input. Use Runner Space for terminal input; avoid interactive mode otherwise because its logs are noisier.',\n 'Results are already bounded and the full log is saved to disk. Do not pipe to head or tail: it adds nothing and leaves the live log empty while the command runs.',\n 'On failure, use the returned output first. Inspect the saved log only when the result says output was truncated or no useful output was returned. Never retry an unchanged command merely to recover output.',\n 'Stop background runners when they are no longer needed. Every runner is stopped automatically when the session ends.',\n ];\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function createBashTool(\n dependencies: BashToolDependencies,\n renderers: BashToolRenderers = {},\n): PiToolDeclaration {\n return definePiTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute one Bash command in the current working directory. Foreground commands return bounded output; commands that outlive the threshold return a supervised runner id and streaming log path. Do not rerun a command merely to recover output; inspect the saved log only when the result identifies missing context.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, signal, onUpdate, ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n ...(signal ? { signal } : {}),\n sessionId: await dependencies.getSessionId(),\n cwd: ctx.cwd,\n });\n if (signal?.aborted) throw new Error('Operation aborted');\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute command: ${message}`,\n 'Next: verify the command, runtime, and working directory. Retry only after correcting the cause.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result, parseResultPragma(params.command), dependencies.summarizeLog);\n },\n\n ...renderers,\n });\n}\n"],"mappings":";;;;;AAUA,MAAa,sBACX;;AAGF,SAAgB,qBAAqB,cAAc,yBAAyB,GAAa;CACvF,OAAO;EACL,iCAAiC,KAAK,MAAM,cAAc,aAAa,EAAE;EACzE;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;AAQA,SAAgB,eACd,cACA,YAA+B,CAAC,GACb;CACnB,OAAO,aAAa;EAClB,MAAM;EACN,OAAO;EACP,aACE;EACF,eAAe;EACf,kBAAkB,qBAAqB;EACvC,YAAY;EAIZ,aAAa;EAEb,MAAM,QAAQ,aAAa,QAAQ,QAAQ,UAAU,KAA0B;GAC7E,MAAM,EAAE,SAAS,SAAS,YAAY,aAAa,SAAS;GAC5D,IAAI;GACJ,IAAI,UAGF,SAAS,WAAW,YADlB,gBAAgB,OAAO,uBAAuB,eAAe,OAAO,sBAAsB,UACvD,IAAI,CAAC;GAE5C,IAAI,eAAe,QAAQ,gBAAgB,QAAQ,UACjD,YAAY,WAAW,SAAS,WAAW,MAAM,CAAC;GAEpD,IAAI;GACJ,IAAI;IACF,SAAS,MAAM,aAAa,eAAe,IAAI;KAC7C;KACA,WAAW,YAAY,KAAA,IAAY,KAAA,IAAY,UAAU;KACzD;KACA;KACA;KACA,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;KAC/B,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;KAC3B,WAAW,MAAM,aAAa,aAAa;KAC3C,KAAK,IAAI;IACX,CAAC;IACD,IAAI,QAAQ,SAAS,MAAM,IAAI,MAAM,mBAAmB;GAC1D,SAAS,OAAO;IACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IACrE,MAAM,IAAI,MACR,CACE,8BAA8B,WAC9B,kGACF,CAAC,CAAC,KAAK,IAAI,GACX,EAAE,OAAO,MAAM,CACjB;GACF;GAEA,IAAI,OAAO,SAAS,YAAY,aAAa,gBAAgB,OAAO,EAAE;GACtE,OAAO,gBAAgB,QAAQ,kBAAkB,OAAO,OAAO,GAAG,aAAa,YAAY;EAC7F;EAEA,GAAG;CACL,CAAC;AACH"}
@@ -30,7 +30,7 @@ function createBashTool(dependencies, renderers = {}) {
30
30
  promptGuidelines: bashPromptGuidelines(),
31
31
  parameters: require_bashTool.BashParamsSchema,
32
32
  renderShell: "self",
33
- async execute(_toolCallId, params, signal, onUpdate, _ctx) {
33
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
34
34
  const { command, timeout, background, interactive, name } = params;
35
35
  let onOutput;
36
36
  if (onUpdate) onUpdate(require_bashResult.textResult(`Starting ${interactive === true ? "interactive runner" : background === true ? "background runner" : "command"}...`));
@@ -45,7 +45,8 @@ function createBashTool(dependencies, renderers = {}) {
45
45
  name,
46
46
  ...onOutput ? { onOutput } : {},
47
47
  ...signal ? { signal } : {},
48
- sessionId: await dependencies.getSessionId()
48
+ sessionId: await dependencies.getSessionId(),
49
+ cwd: ctx.cwd
49
50
  });
50
51
  if (signal?.aborted) throw new Error("Operation aborted");
51
52
  } catch (error) {
@@ -78,4 +79,4 @@ Object.defineProperty(exports, "createBashTool", {
78
79
  }
79
80
  });
80
81
 
81
- //# sourceMappingURL=bashTool-C2jX5J1U.cjs.map
82
+ //# sourceMappingURL=bashTool-DzucJQKE.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool-C2jX5J1U.cjs","names":["getBackgroundThresholdMs","MS_PER_SECOND","definePiTool","BASH_TOOL_NAME","BASH_TOOL_LABEL","BashParamsSchema","textResult","formatRunResult","parseResultPragma"],"sources":["../src/services/bashTool/index.ts"],"sourcesContent":["import { definePiTool, type PiToolDeclaration } from '@agimon-ai/doompi-core/piExtension';\n\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, MS_PER_SECOND } from '../../constants/bashTool';\nimport { type BashParams, BashParamsSchema } from '../../schemas/bashTool';\nimport { formatRunResult, parseResultPragma, textResult } from '../../services/bashResult';\nimport { getBackgroundThresholdMs } from '../../services/runnerConfig';\nimport { type ToolResult } from '../../types/bashResult';\nimport type { BashRunResult } from '../../types/bashRunService';\nimport type { BashToolDependencies, BashToolRenderers } from '../../types/bashTool';\n\nexport const BASH_PROMPT_SNIPPET =\n 'Execute shell commands with bounded foreground output and supervised background runners';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner with an id and streaming log path. You are messaged automatically when it exits, so never sleep, poll, or pgrep to wait for one.`,\n 'Pass background: true only for commands you know will remain active, such as dev servers, watchers, and tails.',\n 'Pass interactive: true only when the command will prompt for input. Use Runner Space for terminal input; avoid interactive mode otherwise because its logs are noisier.',\n 'Results are already bounded and the full log is saved to disk. Do not pipe to head or tail: it adds nothing and leaves the live log empty while the command runs.',\n 'On failure, use the returned output first. Inspect the saved log only when the result says output was truncated or no useful output was returned. Never retry an unchanged command merely to recover output.',\n 'Stop background runners when they are no longer needed. Every runner is stopped automatically when the session ends.',\n ];\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function createBashTool(\n dependencies: BashToolDependencies,\n renderers: BashToolRenderers = {},\n): PiToolDeclaration {\n return definePiTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute one Bash command in the current working directory. Foreground commands return bounded output; commands that outlive the threshold return a supervised runner id and streaming log path. Do not rerun a command merely to recover output; inspect the saved log only when the result identifies missing context.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, signal, onUpdate, _ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n ...(signal ? { signal } : {}),\n sessionId: await dependencies.getSessionId(),\n });\n if (signal?.aborted) throw new Error('Operation aborted');\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute command: ${message}`,\n 'Next: verify the command, runtime, and working directory. Retry only after correcting the cause.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result, parseResultPragma(params.command), dependencies.summarizeLog);\n },\n\n ...renderers,\n });\n}\n"],"mappings":";;;;;AAUA,MAAa,sBACX;;AAGF,SAAgB,qBAAqB,cAAcA,qBAAAA,yBAAyB,GAAa;CACvF,OAAO;EACL,iCAAiC,KAAK,MAAM,cAAcC,iBAAAA,aAAa,EAAE;EACzE;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;AAQA,SAAgB,eACd,cACA,YAA+B,CAAC,GACb;CACnB,QAAA,GAAOC,mCAAAA,aAAAA,CAAa;EAClB,MAAMC,iBAAAA;EACN,OAAOC,iBAAAA;EACP,aACE;EACF,eAAe;EACf,kBAAkB,qBAAqB;EACvC,YAAYC,iBAAAA;EAIZ,aAAa;EAEb,MAAM,QAAQ,aAAa,QAAQ,QAAQ,UAAU,MAA2B;GAC9E,MAAM,EAAE,SAAS,SAAS,YAAY,aAAa,SAAS;GAC5D,IAAI;GACJ,IAAI,UAGF,SAASC,mBAAAA,WAAW,YADlB,gBAAgB,OAAO,uBAAuB,eAAe,OAAO,sBAAsB,UACvD,IAAI,CAAC;GAE5C,IAAI,eAAe,QAAQ,gBAAgB,QAAQ,UACjD,YAAY,WAAW,SAASA,mBAAAA,WAAW,MAAM,CAAC;GAEpD,IAAI;GACJ,IAAI;IACF,SAAS,MAAM,aAAa,eAAe,IAAI;KAC7C;KACA,WAAW,YAAY,KAAA,IAAY,KAAA,IAAY,UAAUL,iBAAAA;KACzD;KACA;KACA;KACA,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;KAC/B,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;KAC3B,WAAW,MAAM,aAAa,aAAa;IAC7C,CAAC;IACD,IAAI,QAAQ,SAAS,MAAM,IAAI,MAAM,mBAAmB;GAC1D,SAAS,OAAO;IACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IACrE,MAAM,IAAI,MACR,CACE,8BAA8B,WAC9B,kGACF,CAAC,CAAC,KAAK,IAAI,GACX,EAAE,OAAO,MAAM,CACjB;GACF;GAEA,IAAI,OAAO,SAAS,YAAY,aAAa,gBAAgB,OAAO,EAAE;GACtE,OAAOM,mBAAAA,gBAAgB,QAAQC,mBAAAA,kBAAkB,OAAO,OAAO,GAAG,aAAa,YAAY;EAC7F;EAEA,GAAG;CACL,CAAC;AACH"}
1
+ {"version":3,"file":"bashTool-DzucJQKE.cjs","names":["getBackgroundThresholdMs","MS_PER_SECOND","definePiTool","BASH_TOOL_NAME","BASH_TOOL_LABEL","BashParamsSchema","textResult","formatRunResult","parseResultPragma"],"sources":["../src/services/bashTool/index.ts"],"sourcesContent":["import { definePiTool, type PiToolDeclaration } from '@agimon-ai/doompi-core/piExtension';\n\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, MS_PER_SECOND } from '../../constants/bashTool';\nimport { type BashParams, BashParamsSchema } from '../../schemas/bashTool';\nimport { formatRunResult, parseResultPragma, textResult } from '../../services/bashResult';\nimport { getBackgroundThresholdMs } from '../../services/runnerConfig';\nimport { type ToolResult } from '../../types/bashResult';\nimport type { BashRunResult } from '../../types/bashRunService';\nimport type { BashToolDependencies, BashToolRenderers } from '../../types/bashTool';\n\nexport const BASH_PROMPT_SNIPPET =\n 'Execute shell commands with bounded foreground output and supervised background runners';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner with an id and streaming log path. You are messaged automatically when it exits, so never sleep, poll, or pgrep to wait for one.`,\n 'Pass background: true only for commands you know will remain active, such as dev servers, watchers, and tails.',\n 'Pass interactive: true only when the command will prompt for input. Use Runner Space for terminal input; avoid interactive mode otherwise because its logs are noisier.',\n 'Results are already bounded and the full log is saved to disk. Do not pipe to head or tail: it adds nothing and leaves the live log empty while the command runs.',\n 'On failure, use the returned output first. Inspect the saved log only when the result says output was truncated or no useful output was returned. Never retry an unchanged command merely to recover output.',\n 'Stop background runners when they are no longer needed. Every runner is stopped automatically when the session ends.',\n ];\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function createBashTool(\n dependencies: BashToolDependencies,\n renderers: BashToolRenderers = {},\n): PiToolDeclaration {\n return definePiTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute one Bash command in the current working directory. Foreground commands return bounded output; commands that outlive the threshold return a supervised runner id and streaming log path. Do not rerun a command merely to recover output; inspect the saved log only when the result identifies missing context.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, signal, onUpdate, ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n ...(signal ? { signal } : {}),\n sessionId: await dependencies.getSessionId(),\n cwd: ctx.cwd,\n });\n if (signal?.aborted) throw new Error('Operation aborted');\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute command: ${message}`,\n 'Next: verify the command, runtime, and working directory. Retry only after correcting the cause.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result, parseResultPragma(params.command), dependencies.summarizeLog);\n },\n\n ...renderers,\n });\n}\n"],"mappings":";;;;;AAUA,MAAa,sBACX;;AAGF,SAAgB,qBAAqB,cAAcA,qBAAAA,yBAAyB,GAAa;CACvF,OAAO;EACL,iCAAiC,KAAK,MAAM,cAAcC,iBAAAA,aAAa,EAAE;EACzE;EACA;EACA;EACA;EACA;CACF;AACF;;;;;;;AAQA,SAAgB,eACd,cACA,YAA+B,CAAC,GACb;CACnB,QAAA,GAAOC,mCAAAA,aAAAA,CAAa;EAClB,MAAMC,iBAAAA;EACN,OAAOC,iBAAAA;EACP,aACE;EACF,eAAe;EACf,kBAAkB,qBAAqB;EACvC,YAAYC,iBAAAA;EAIZ,aAAa;EAEb,MAAM,QAAQ,aAAa,QAAQ,QAAQ,UAAU,KAA0B;GAC7E,MAAM,EAAE,SAAS,SAAS,YAAY,aAAa,SAAS;GAC5D,IAAI;GACJ,IAAI,UAGF,SAASC,mBAAAA,WAAW,YADlB,gBAAgB,OAAO,uBAAuB,eAAe,OAAO,sBAAsB,UACvD,IAAI,CAAC;GAE5C,IAAI,eAAe,QAAQ,gBAAgB,QAAQ,UACjD,YAAY,WAAW,SAASA,mBAAAA,WAAW,MAAM,CAAC;GAEpD,IAAI;GACJ,IAAI;IACF,SAAS,MAAM,aAAa,eAAe,IAAI;KAC7C;KACA,WAAW,YAAY,KAAA,IAAY,KAAA,IAAY,UAAUL,iBAAAA;KACzD;KACA;KACA;KACA,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;KAC/B,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;KAC3B,WAAW,MAAM,aAAa,aAAa;KAC3C,KAAK,IAAI;IACX,CAAC;IACD,IAAI,QAAQ,SAAS,MAAM,IAAI,MAAM,mBAAmB;GAC1D,SAAS,OAAO;IACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;IACrE,MAAM,IAAI,MACR,CACE,8BAA8B,WAC9B,kGACF,CAAC,CAAC,KAAK,IAAI,GACX,EAAE,OAAO,MAAM,CACjB;GACF;GAEA,IAAI,OAAO,SAAS,YAAY,aAAa,gBAAgB,OAAO,EAAE;GACtE,OAAOM,mBAAAA,gBAAgB,QAAQC,mBAAAA,kBAAkB,OAAO,OAAO,GAAG,aAAa,YAAY;EAC7F;EAEA,GAAG;CACL,CAAC;AACH"}
package/dist/bashTool.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_bashResult = require("./bashResult-SkF8WzLS.cjs");
3
3
  const require_logReader = require("./logReader-7R1V06IS.cjs");
4
- const require_bashTool = require("./bashTool-C2jX5J1U.cjs");
4
+ const require_bashTool = require("./bashTool-DzucJQKE.cjs");
5
5
  //#region src/services/runnerResult/index.ts
6
6
  function formatRunnerResult(result, budget = {}) {
7
7
  return require_bashResult.formatRunResult(result, budget, require_logReader.summarizeLog);
package/dist/bashTool.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { c as formatRunResult } from "./bashResult-BKLi1Zpx.mjs";
2
2
  import { r as summarizeLog } from "./logReader-DfUVwLLJ.mjs";
3
- import { n as bashPromptGuidelines, r as createBashTool, t as BASH_PROMPT_SNIPPET } from "./bashTool-DSLtCIZs.mjs";
3
+ import { n as bashPromptGuidelines, r as createBashTool, t as BASH_PROMPT_SNIPPET } from "./bashTool-BX4UF_cF.mjs";
4
4
  //#region src/services/runnerResult/index.ts
5
5
  function formatRunnerResult(result, budget = {}) {
6
6
  return formatRunResult(result, budget, summarizeLog);
package/dist/bin/cli.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_cliApp = require("../cliApp-bWkTg1Q-.cjs");
4
- const require_runnerDependencies = require("../runnerDependencies-Dx8M_rcu.cjs");
4
+ const require_runnerDependencies = require("../runnerDependencies-Dq8oIvGv.cjs");
5
5
  let node_fs = require("node:fs");
6
6
  let node_url = require("node:url");
7
7
  let _agimon_ai_doompi_telemetry = require("@agimon-ai/doompi-telemetry");
package/dist/bin/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as runCli } from "../cliApp-4T2TCpwp.mjs";
3
- import { t as createRunnerDependencies } from "../runnerDependencies-DI_m0npT.mjs";
3
+ import { t as createRunnerDependencies } from "../runnerDependencies-CsizryxY.mjs";
4
4
  import { existsSync, realpathSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createDoomTelemetry } from "@agimon-ai/doompi-telemetry";
@@ -0,0 +1,23 @@
1
+ import { defineMcpWidget } from "@agimon-ai/doompi-core/web";
2
+ import { McpToolFields, McpToolFrame, McpToolOutput } from "@agimon-ai/doompi-web-components";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/extensions/workspaces/sessions/(frontend)/tool/bash.mcp.tsx
5
+ function BashMcpWidget(props) {
6
+ return /* @__PURE__ */ jsxs(McpToolFrame, {
7
+ ...props,
8
+ title: "Run command",
9
+ children: [/* @__PURE__ */ jsx(McpToolFields, { fields: [
10
+ ["Name", props.args.name],
11
+ ["Command", props.args.command],
12
+ ["Background", props.args.background],
13
+ ["Timeout", props.args.timeout]
14
+ ] }), /* @__PURE__ */ jsx(McpToolOutput, { result: props.result })]
15
+ });
16
+ }
17
+ //#endregion
18
+ //#region generated/mcp-ui.ts
19
+ const widgets = { "@agimon-ai/doompi-runner/bash": defineMcpWidget(BashMcpWidget) };
20
+ //#endregion
21
+ export { widgets };
22
+
23
+ //# sourceMappingURL=mcp-ui.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-ui.mjs","names":["Widget0"],"sources":["../../src/extensions/workspaces/sessions/(frontend)/tool/bash.mcp.tsx","../../generated/mcp-ui.ts"],"sourcesContent":["import { defineMcpWidget, type DoomMcpWidgetProps } from '@agimon-ai/doompi-core/web';\nimport { McpToolFrame, McpToolFields, McpToolOutput } from '@agimon-ai/doompi-web-components';\n\nfunction BashMcpWidget(props: DoomMcpWidgetProps) {\n return (\n <McpToolFrame {...props} title=\"Run command\">\n <McpToolFields\n fields={[\n ['Name', props.args.name],\n ['Command', props.args.command],\n ['Background', props.args.background],\n ['Timeout', props.args.timeout],\n ]}\n />\n <McpToolOutput result={props.result} />\n </McpToolFrame>\n );\n}\n\nexport default defineMcpWidget(BashMcpWidget);\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport type { ComponentType } from 'react';\nimport type { DoomMcpWidgetProps } from '@agimon-ai/doompi-core/mcpWidget';\nimport Widget0 from \"../src/extensions/workspaces/sessions/(frontend)/tool/bash.mcp\";\n\nexport const widgets: Readonly<Record<string, ComponentType<DoomMcpWidgetProps>>> = {\n \"@agimon-ai/doompi-runner/bash\": Widget0,\n};\n"],"mappings":";;;;AAGA,SAAS,cAAc,OAA2B;CAChD,OACE,qBAAC,cAAD;EAAc,GAAI;EAAO,OAAM;EAA/B,UAAA,CACE,oBAAC,eAAD,EACE,QAAQ;GACN,CAAC,QAAQ,MAAM,KAAK,IAAI;GACxB,CAAC,WAAW,MAAM,KAAK,OAAO;GAC9B,CAAC,cAAc,MAAM,KAAK,UAAU;GACpC,CAAC,WAAW,MAAM,KAAK,OAAO;EAChC,EACD,CAAA,GACD,oBAAC,eAAD,EAAe,QAAQ,MAAM,OAAS,CAAA,CAC1B;;AAElB;;;ACZA,MAAa,UAAuE,EAClF,iCDaa,gBAAgB,aCbIA,EACnC"}
@@ -87,6 +87,13 @@ const via = (identity, value) => value === void 0 ? void 0 : {
87
87
  ...identity,
88
88
  ...value
89
89
  };
90
+ const withWidget = (key, value) => value === void 0 ? value : {
91
+ ...value,
92
+ _meta: {
93
+ ...value._meta,
94
+ ["doompi/widget"]: key
95
+ }
96
+ };
90
97
  const mcp = (0, _agimon_ai_doompi_core_mcpFacet.defineMcpPlugin)({
91
98
  name: "@agimon-ai/doompi-runner",
92
99
  session: (context) => ({
@@ -94,7 +101,7 @@ const mcp = (0, _agimon_ai_doompi_core_mcpFacet.defineMcpPlugin)({
94
101
  return [at(doompi_use_runner_mcp_default, context)];
95
102
  },
96
103
  get tools() {
97
- return [via({ name: "bash" }, at(bash_mcp_default, context))];
104
+ return [withWidget("@agimon-ai/doompi-runner/bash", via({ name: "bash" }, at(bash_mcp_default, context)))];
98
105
  }
99
106
  })
100
107
  });
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.cjs","names":["defineMcpSkill","readPackageResource","Type","headless","promisify","execFile","defineMcpTool","defineMcpPlugin","skillDoompiUseRunner","toolBash"],"sources":["../../src/extensions/workspaces/sessions/(backend)/skill/doompi-use-runner.mcp.ts","../../src/schemas/bashTool.ts","../../src/services/ansiScrub/index.ts","../../src/services/ptyHost/index.ts","../../src/services/runnerRegistry/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp.ts","../../generated/mcp.ts"],"sourcesContent":["import { defineMcpSkill } from '@agimon-ai/doompi-core/mcpFacet';\nimport { readPackageResource } from '@agimon-ai/doompi-core/serverFacet';\n\nexport default defineMcpSkill({\n name: 'doompi-use-runner',\n description:\n 'Use Doom Pi Runner to supervise shell commands, inspect durable logs, provide interactive input, and stop background runs.',\n read: () => readPackageResource(import.meta.url, 'src/prompts/doompi-use-runner/SKILL.md'),\n});\n","import { type Static, Type } from 'typebox';\n\n/** Replaces pi's built-in tool of the same name, so hooks keyed on `bash` keep working. */\n\n/**\n * Built-in bash parameters plus the backgrounding controls.\n *\n * `command` and `timeout` keep their original names and wording so the tool is\n * a drop-in replacement. Every `description` is prompt copy the model reads.\n */\nexport const BashParamsSchema = Type.Object({\n command: Type.String({ description: 'Bash command to execute' }),\n timeout: Type.Optional(Type.Number({ description: 'Timeout in seconds (optional, no default timeout)' })),\n background: Type.Optional(\n Type.Boolean({\n description:\n 'Start the command in the background immediately instead of waiting. Use for dev servers, watchers and other commands you already know will not finish.',\n }),\n ),\n interactive: Type.Optional(\n Type.Boolean({\n description:\n 'Run under a pseudo terminal so the command can prompt. Use only when the command asks for confirmation or input; answer it with doom-runner input. Logs from interactive runs are noisier.',\n }),\n ),\n name: Type.Optional(\n Type.String({\n description: 'Preferred runner name if the command ends up in the background. A name in use gets a suffix.',\n }),\n ),\n});\n\nexport type BashParams = Static<typeof BashParamsSchema>;\n","import { CONTROL_PATTERN } from '../../constants/ansiScrub';\n/**\n * Turns terminal output into text worth grepping.\n *\n * PTY output is written for a screen, not a file: colours, cursor moves and\n * carriage-return redraws all end up in the byte stream. Without this a single\n * progress bar becomes thousands of near-identical log lines.\n */\n\n/**\n * CSI sequences (colours, cursor moves), OSC strings, the two-byte Fe escapes,\n * and the keypad-mode escapes shells emit around every prompt.\n */\nconst ANSI_PATTERN = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Bell and backspace, which a terminal consumes rather than displays. */\n// oxlint-disable-next-line no-control-regex -- matching terminal control bytes is the whole job\n\n/**\n * The same set minus SGR, the sequence that only sets colour and weight.\n *\n * The final byte class omits `m`, so `ESC[32m` survives while cursor moves,\n * OSC strings and keypad escapes are still removed.\n */\nconst ANSI_PATTERN_KEEPING_COLOUR = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-ln-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Removes escape sequences, leaving the characters a user would have seen. */\nexport function stripAnsi(text: string): string {\n return text.replace(ANSI_PATTERN, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Removes everything `stripAnsi` does except colour.\n *\n * Logs are read back by a terminal renderer as well as by grep, and a command\n * that colours its own output has already said how it should look.\n */\nexport function stripAnsiKeepingColour(text: string): string {\n return text.replace(ANSI_PATTERN_KEEPING_COLOUR, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Collapses carriage-return redraws.\n *\n * A bare `\\r` returns the cursor to the start of the current line, so whatever\n * follows overwrites it. Only the final state of each line is kept. The `\\r` of\n * a CRLF pair is a line terminator rather than a redraw, so it is normalised\n * away first: a pseudo terminal ends every line that way, and treating it as a\n * redraw would blank the whole log.\n */\nexport function collapseCarriageReturns(text: string): string {\n return text\n .replace(/\\r\\n/g, '\\n')\n .split('\\n')\n .map((line) => (line.includes('\\r') ? (line.split('\\r').at(-1) ?? '') : line))\n .join('\\n');\n}\n\n/** Full scrub applied to PTY output before it reaches the log file. */\nexport function scrubTerminalOutput(text: string): string {\n return collapseCarriageReturns(stripAnsiKeepingColour(text));\n}\n\n/** Plain-text scrub, for the copy the model reads. */\nexport function scrubTerminalOutputToPlainText(text: string): string {\n return collapseCarriageReturns(stripAnsi(text));\n}\n","import headless, { type Terminal } from '@xterm/headless';\n\nimport {\n DEFAULT_COLS,\n DEFAULT_ROWS,\n LIVENESS_POLL_MS,\n MEMORY_BUFFER_FACTOR,\n NEWLINE,\n SCROLLBACK,\n TERM_GRACE_MS,\n} from '../../constants/ptyHost';\nimport { PI_SESSION_ID_ENV } from '../../constants/session';\nimport type { IClock } from '../../types/clock';\nimport type { ILogFile } from '../../types/logFile';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IPtyHost, PtyLaunchRequest, PtyRun } from '../../types/ptyHost';\nimport type { IPtySpawner, PtyProcess } from '../../types/ptySpawner';\nimport type { ExitResult } from '../../types/spawner';\nimport { scrubTerminalOutput } from '../ansiScrub';\nimport { getResultMaxBytes } from '../runnerConfig';\nimport { NO_TERMINAL_INPUT_ENV } from '../runnerSupervisor';\n\n/** Enough history for the overlay to scroll without holding a whole build. */\n\n// Use the CommonJS default export: Node cannot detect this package's named exports.\n// A static import also lets server bundles include it without a live node_modules lookup.\nconst { Terminal: HeadlessTerminal } = headless;\n\nexport class PtyHost implements IPtyHost {\n private readonly runs = new Map<string, PtyRun>();\n\n constructor(\n private readonly spawner: IPtySpawner,\n private readonly logFile: ILogFile,\n private readonly processControl: IProcessControl,\n private readonly clock: IClock,\n ) {}\n\n async launch(request: PtyLaunchRequest): Promise<PtyRun> {\n const cols = request.cols ?? DEFAULT_COLS;\n const rows = request.rows ?? DEFAULT_ROWS;\n const writer = this.logFile.open(request.id);\n const terminal = new HeadlessTerminal({ cols, rows, scrollback: SCROLLBACK, allowProposedApi: true });\n const memoryLimit = getResultMaxBytes() * MEMORY_BUFFER_FACTOR;\n\n const child = await this.spawner.spawn({\n command: request.command,\n cwd: request.cwd,\n env: { ...process.env, ...NO_TERMINAL_INPUT_ENV, [PI_SESSION_ID_ENV]: request.sessionId },\n cols,\n rows,\n });\n\n let buffer = '';\n let buffering = true;\n let settled = false;\n let resolveCompletion: (result: ExitResult) => void = () => undefined;\n const completion = new Promise<ExitResult>((resolve) => {\n resolveCompletion = resolve;\n });\n const dataHandlers = new Set<(data: string) => void>();\n\n child.onData((data) => {\n // The terminal keeps the screen; the log keeps text worth grepping.\n terminal.write(data);\n const scrubbed = scrubTerminalOutput(data);\n writer.append(scrubbed);\n if (buffering) {\n buffer += scrubbed;\n if (buffer.length > memoryLimit) buffer = buffer.slice(-memoryLimit);\n }\n for (const handler of dataHandlers) handler(data);\n });\n\n child.onExit((result) => {\n if (settled) return;\n settled = true;\n this.runs.delete(request.name);\n writer.close();\n resolveCompletion({ code: result.exitCode, signal: null });\n });\n\n const run: PtyRun = {\n id: request.id,\n name: request.name,\n pid: child.pid,\n logPath: writer.path,\n backend: 'native',\n output: () => buffer,\n completion: () => completion,\n detach: () => {\n buffering = false;\n buffer = '';\n },\n stop: () => this.stopChild(child, request.name),\n write: (text) => child.write(text),\n screen: () => renderScreen(terminal),\n onData: (handler) => {\n dataHandlers.add(handler);\n return () => dataHandlers.delete(handler);\n },\n resize: (nextCols, nextRows) => {\n child.resize(nextCols, nextRows);\n terminal.resize(nextCols, nextRows);\n },\n };\n\n this.runs.set(request.name, run);\n return run;\n }\n\n get(name: string): PtyRun | undefined {\n return this.runs.get(name);\n }\n\n write(name: string, text: string): boolean {\n const run = this.runs.get(name);\n if (!run) return false;\n run.write(text.endsWith(NEWLINE) ? text : `${text}${NEWLINE}`);\n return true;\n }\n\n list(): PtyRun[] {\n return [...this.runs.values()];\n }\n\n async disposeAll(): Promise<void> {\n const runs = [...this.runs.values()];\n this.runs.clear();\n for (const run of runs) await run.stop();\n }\n\n private async stopChild(child: PtyProcess, name: string): Promise<boolean> {\n this.runs.delete(name);\n if (!this.processControl.isAlive(child.pid)) return false;\n\n child.kill('SIGTERM');\n const deadline = this.clock.now() + TERM_GRACE_MS;\n while (this.clock.now() < deadline) {\n if (!this.processControl.isAlive(child.pid)) return true;\n await new Promise<void>((resolve) => {\n this.clock.after(LIVENESS_POLL_MS, resolve);\n });\n }\n child.kill('SIGKILL');\n return true;\n }\n}\n\n/** The visible rows, trimmed of trailing blanks, as plain text. */\nfunction renderScreen(terminal: Terminal): string {\n const buffer = terminal.buffer.active;\n const lines: string[] = [];\n for (let row = buffer.baseY; row < buffer.baseY + terminal.rows; row += 1) {\n lines.push(buffer.getLine(row)?.translateToString(true) ?? '');\n }\n while (lines.length > 0 && lines.at(-1) === '') lines.pop();\n return lines.join(NEWLINE);\n}\n","import { execFile } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport { resolveRootSessionId } from '@agimon-ai/doompi-core/childProcess';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\n\nimport {\n COMMAND_SIDECAR_SUFFIX,\n EXIT_SIDECAR_SUFFIX,\n JSON_EXTENSION,\n NOT_FOUND_ERROR_CODE,\n REGISTRY_PATH_ENV,\n RUNNER_TAG,\n SERVICE_PREFIX,\n SERVICE_TYPE,\n} from '../../constants/runnerRegistry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** The slice of the shared registry service this wrapper depends on. */\nexport type ProcessRegistryPort = Pick<\n ProcessRegistryService,\n 'registerProcess' | 'releaseProcess' | 'listProcesses' | 'close'\n>;\n\n/** Opens the SQLite-backed registry the rest of the monorepo already shares. */\nexport function createDefaultProcessRegistry(): ProcessRegistryPort {\n return createProcessRegistryService(process.env[REGISTRY_PATH_ENV]);\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction readString(record: ProcessRegistryRecord, key: string): string {\n const value = record.metadata?.[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction readNumber(record: ProcessRegistryRecord, key: string): number {\n const value = record.metadata?.[key];\n return typeof value === 'number' ? value : 0;\n}\n\nconst runCommand = promisify(execFile);\n\n/**\n * The process start time the registry stores to prove a pid is still the\n * process it registered.\n *\n * The registry resolves it itself when the caller omits it, and it does so with\n * `execFileSync`, which stops this process for the length of a `ps` on every\n * runner that starts. Reading it here costs the same subprocess and blocks\n * nothing: the command is already running by the time a runner is registered.\n *\n * The value has to match what the registry would have read, so the command and\n * the trimming are deliberately identical to its own.\n */\nasync function processStartedAt(pid: number): Promise<string | undefined> {\n if (process.platform === 'win32') return undefined;\n try {\n const { stdout } = await runCommand('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8' });\n return stdout.trim() || undefined;\n } catch {\n return undefined;\n }\n}\n\ntype ListenerRegistration = Readonly<{ key: string; listener: () => void }>;\n\n// Facet replacement can leave an in-flight tool and its publisher on different registry instances.\nconst listenersBySession = new Map<string, Set<() => void>>();\n\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly subscriptions = new Set<ListenerRegistration>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n private readonly environment: Readonly<Record<string, string | undefined>>,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId, this.environment);\n const identity = await processStartedAt(input.pid);\n const response = await this.registry.registerProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${input.id}`,\n serviceType: SERVICE_TYPE,\n pid: input.pid,\n command: input.command,\n ...(identity === undefined ? {} : { processStartedAt: identity }),\n tags: resolveProcessTags([RUNNER_TAG, `runner:${input.id}`, sessionTag(input.sessionId)]),\n metadata: {\n id: input.id,\n name: input.name,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n startedAt,\n hostPid: process.pid,\n },\n // The caller has already resolved name collisions against live records,\n // so anything still holding this name is stale.\n force: true,\n });\n if (!response.success) throw new Error(response.error ?? `Failed to register runner ${input.name}`);\n const record: RunnerRecord = {\n id: input.id,\n name: input.name,\n pid: input.pid,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n startedAt,\n state: 'running',\n promoted: false,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n hostPid: process.pid,\n };\n this.writeRecord(record);\n this.notify(record.sessionId);\n return record;\n }\n\n async list(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n repositoryPath: this.repositoryPath(),\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listAcrossRepositories(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listBySession(sessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => record.sessionId === sessionId);\n }\n\n async listByRootSession(rootSessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId);\n }\n\n async listAll(sessionId?: string): Promise<RunnerRecord[]> {\n this.paths.ensureDirectories(sessionId);\n const records: RunnerRecord[] = [];\n const directory = this.paths.stateDirectory(sessionId);\n for (const entry of fs.readdirSync(directory)) {\n if (!isPrimaryMetadataEntry(entry)) continue;\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(`${directory}/${entry}`, 'utf8'));\n if (isRunnerRecord(parsed)) records.push(parsed);\n } catch (error) {\n process.emitWarning(`Could not read runner metadata ${entry}: ${String(error)}`);\n }\n }\n return records.sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n }\n\n async get(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const target = this.paths.statePathFor(id, sessionId);\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (!isRunnerRecord(parsed) || parsed.id !== id) {\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: invalid runner record`);\n return undefined;\n }\n return parsed;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return undefined;\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: ${String(error)}`);\n return undefined;\n }\n }\n\n async markPromoted(id: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record) return undefined;\n const promoted = { ...record, promoted: true };\n this.writeRecord(promoted);\n this.notify(promoted.sessionId);\n return promoted;\n }\n\n async complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined> {\n const persisted = await this.get(id, sessionId);\n if (persisted?.state === 'completed') {\n await this.release(id, persisted.sessionId);\n return persisted;\n }\n const active = await this.list();\n const record =\n persisted ??\n active.find((candidate) => candidate.id === id && (sessionId === undefined || candidate.sessionId === sessionId));\n if (!record) {\n const belongsToAnotherSession =\n sessionId !== undefined && active.some((candidate) => candidate.id === id && candidate.sessionId !== sessionId);\n if (!belongsToAnotherSession) await this.release(id, sessionId);\n return undefined;\n }\n const completed: RunnerRecord = {\n ...record,\n state: 'completed',\n exit: { ...outcome, finishedAt: new Date().toISOString() },\n };\n this.writeRecord(completed);\n await this.release(id, completed.sessionId);\n return completed;\n }\n\n async release(id: string, sessionId?: string): Promise<void> {\n const activeSessionId = sessionId ?? (await this.list()).find((record) => record.id === id)?.sessionId;\n const ownedSessionId = activeSessionId ?? (await this.get(id))?.sessionId;\n const response = await this.registry.releaseProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${id}`,\n serviceType: SERVICE_TYPE,\n kill: false,\n releasePort: false,\n force: true,\n });\n if (!response.success && !response.error?.includes('No matching process entry')) {\n throw new Error(response.error ?? `Failed to release runner ${id}`);\n }\n if (ownedSessionId !== undefined) this.notify(ownedSessionId);\n }\n\n async pruneDead(): Promise<string[]> {\n const dead = (await this.list()).filter((record) => !this.processControl.isAlive(record.pid));\n for (const record of dead) {\n await this.complete(record.id, { reason: 'backend_lost', code: null, signal: null }, record.sessionId);\n }\n return dead.map((record) => record.id);\n }\n\n subscribe(listener: () => void, sessionId: string): () => void {\n const subscribed = (): void => listener();\n const registration = { key: this.subscriptionKey(sessionId), listener: subscribed };\n const listeners = listenersBySession.get(registration.key) ?? new Set<() => void>();\n listeners.add(subscribed);\n listenersBySession.set(registration.key, listeners);\n this.subscriptions.add(registration);\n return () => this.removeSubscription(registration);\n }\n\n close(): void {\n for (const registration of this.subscriptions) this.removeSubscription(registration);\n this.registry.close();\n }\n\n private notify(sessionId: string): void {\n for (const listener of listenersBySession.get(this.subscriptionKey(sessionId)) ?? []) {\n try {\n listener();\n } catch (error) {\n process.emitWarning(`Runner registry listener failed: ${String(error)}`);\n }\n }\n }\n\n private removeSubscription(registration: ListenerRegistration): void {\n const listeners = listenersBySession.get(registration.key);\n listeners?.delete(registration.listener);\n if (listeners?.size === 0) listenersBySession.delete(registration.key);\n this.subscriptions.delete(registration);\n }\n\n private subscriptionKey(sessionId: string): string {\n return JSON.stringify([path.resolve(this.paths.stateDirectory(sessionId)), sessionId]);\n }\n\n private repositoryPath(): string {\n return normalizeRepositoryPath(this.paths.repositoryPath());\n }\n\n private writeRecord(record: RunnerRecord): void {\n this.paths.ensureDirectories(record.sessionId);\n const target = this.paths.statePathFor(record.id, record.sessionId);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\\n`, 'utf8');\n fs.renameSync(temporary, target);\n }\n}\n\n/** Every field is read back from metadata this class wrote on registration. */\nfunction toRunnerRecord(record: ProcessRegistryRecord): RunnerRecord {\n return {\n id: readString(record, 'id'),\n name: readString(record, 'name'),\n pid: record.pid,\n command: readString(record, 'command'),\n cwd: readString(record, 'cwd'),\n logPath: readString(record, 'logPath'),\n interactive: record.metadata?.interactive === true,\n sessionId: readString(record, 'sessionId'),\n ...(readString(record, 'rootSessionId') ? { rootSessionId: readString(record, 'rootSessionId') } : {}),\n startedAt: readString(record, 'startedAt'),\n state: 'running',\n promoted: false,\n backend: readBackend(record.metadata?.backend),\n ...(readString(record, 'backendTarget') ? { backendTarget: readString(record, 'backendTarget') } : {}),\n hostPid: readNumber(record, 'hostPid'),\n };\n}\n\nfunction readBackend(value: unknown): RunnerRecord['backend'] {\n return value === 'rmux' || value === 'tmux' ? value : 'native';\n}\n\nfunction isPrimaryMetadataEntry(entry: string): boolean {\n return (\n entry.endsWith(JSON_EXTENSION) && !entry.endsWith(COMMAND_SIDECAR_SUFFIX) && !entry.endsWith(EXIT_SIDECAR_SUFFIX)\n );\n}\n","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { createHeadlessBashTool, createHeadlessRunnersCommand } from '../../../../../services/headless';\nimport { summarizeLog } from '../../../../../services/logReader';\nimport { createRunnerDependencies } from '../../../../../services/runnerDependencies';\nimport { createRunnerServerRuntime } from '../../../../../services/runnerServerRuntime';\n\nexport const RUNNER_SERVER_SCOPE_SERVICE = 'doom/runner-server-scope';\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n 'doom/runner-server-scope': RunnerServerScope;\n }\n}\n\nexport const createRunnerServerRoot = ({ host }: DoomServerPluginContext) => {\n if (!host.context.directEvents) throw new Error('Runner headless facet requires the session direct event bus.');\n const dependencies = createRunnerDependencies({ environment: Object.freeze({ ...process.env }) });\n const runtime = createRunnerServerRuntime(dependencies, host.context.directEvents);\n const value = {\n tool: createHeadlessBashTool(\n {\n run: async (request) => {\n await runtime.ensureSession(request.sessionId);\n return dependencies.bashRunService.run(request);\n },\n },\n summarizeLog,\n ),\n command: createHeadlessRunnersCommand(dependencies),\n };\n return {\n value,\n services: [\n runtime.backgroundWorkPlugin,\n (context: Context) => {\n context.plugin((providerContext) => providerContext.provide(RUNNER_SERVER_SCOPE_SERVICE, value));\n },\n ],\n activities: [runtime.activity],\n onStop: runtime.dispose,\n };\n};\n\nexport type RunnerServerScope = Awaited<ReturnType<typeof createRunnerServerRoot>>['value'];\n","import { defineMcpTool, type DoomMcpPluginContext } from '@agimon-ai/doompi-core/mcpFacet';\n\nimport { RUNNER_SERVER_SCOPE_SERVICE, type RunnerServerScope } from '../_lib/serverRoot';\n\nexport default defineMcpTool((context: DoomMcpPluginContext) => {\n const scope = context.services.get<RunnerServerScope>(RUNNER_SERVER_SCOPE_SERVICE);\n if (!scope) throw new Error('Runner session service is unavailable.');\n return {\n ...scope.tool,\n execute(toolCallId, parameters, signal, onUpdate, execution) {\n return scope.tool.execute(\n toolCallId,\n parameters,\n AbortSignal.any([context.signal, signal ?? context.signal]),\n onUpdate,\n execution,\n );\n },\n };\n});\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineMcpPlugin, type DoomMcpSessionPlugin } from '@agimon-ai/doompi-core/mcpFacet';\n\nimport skillDoompiUseRunner from '../src/extensions/workspaces/sessions/(backend)/skill/doompi-use-runner.mcp';\nimport toolBash from '../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nexport const mcp = defineMcpPlugin({\n name: '@agimon-ai/doompi-runner',\n session: (context) => ({\n get skills(): DoomMcpSessionPlugin['skills'] { return [at(skillDoompiUseRunner, context)]; },\n get tools(): DoomMcpSessionPlugin['tools'] { return [via({ name: 'bash' }, at(toolBash, context))]; },\n }) satisfies DoomMcpSessionPlugin,\n});\n\nexport default mcp;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,IAAA,iCAAA,GAAeA,gCAAAA,eAAAA,CAAe;CAC5B,MAAM;CACN,aACE;CACF,aAAA,GAAYC,mCAAAA,oBAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,MAAqC,wCAAwC;AAC3F,CAAC;ACE+BC,QAAAA,KAAK,OAAO;CAC1C,SAASA,QAAAA,KAAK,OAAO,EAAE,aAAa,0BAA0B,CAAC;CAC/D,SAASA,QAAAA,KAAK,SAASA,QAAAA,KAAK,OAAO,EAAE,aAAa,oDAAoD,CAAC,CAAC;CACxG,YAAYA,QAAAA,KAAK,SACfA,QAAAA,KAAK,QAAQ,EACX,aACE,yJACJ,CAAC,CACH;CACA,aAAaA,QAAAA,KAAK,SAChBA,QAAAA,KAAK,QAAQ,EACX,aACE,6LACJ,CAAC,CACH;CACA,MAAMA,QAAAA,KAAK,SACTA,QAAAA,KAAK,OAAO,EACV,aAAa,+FACf,CAAC,CACH;AACF,CAAC;ACjBoB,IAAI,OACvB;CACE;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG,GACV,GACF;AAWoC,IAAI,OACtC;CACE;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG,GACV,GACF;;;;ACdA,MAAM,EAAE,UAAU,qBAAqBC,gBAAAA;CCiCjC,GAAaC,UAAAA,UAAAA,CAAUC,mBAAAA,QAAQ;;;ACnDrC,MAAa,8BAA8B;;;ACJ3C,IAAA,oBAAA,GAAeC,gCAAAA,cAAAA,EAAe,YAAkC;CAC9D,MAAM,QAAQ,QAAQ,SAAS,IAAuB,2BAA2B;CACjF,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,wCAAwC;CACpE,OAAO;EACL,GAAG,MAAM;EACT,QAAQ,YAAY,YAAY,QAAQ,UAAU,WAAW;GAC3D,OAAO,MAAM,KAAK,QAChB,YACA,YACA,YAAY,IAAI,CAAC,QAAQ,QAAQ,UAAU,QAAQ,MAAM,CAAC,GAC1D,UACA,SACF;EACF;CACF;AACF,CAAC;;;ACZD,MAAM,MAAY,OAA0B,YAC1C,OAAO,UAAU,aAAc,MAAwB,OAAO,IAAI;AACpE,MAAM,OAAU,UAAmC,UAChD,UAAU,KAAA,IAAY,KAAA,IAAY;CAAE,GAAG;CAAU,GAAI;AAAiB;AAEzE,MAAa,OAAA,GAAMC,gCAAAA,gBAAAA,CAAgB;CACjC,MAAM;CACN,UAAU,aAAa;EACrB,IAAI,SAAyC;GAAE,OAAO,CAAC,GAAGC,+BAAsB,OAAO,CAAC;EAAG;EAC3F,IAAI,QAAuC;GAAE,OAAO,CAAC,IAAI,EAAE,MAAM,OAAO,GAAG,GAAGC,kBAAU,OAAO,CAAC,CAAC;EAAG;CACtG;AACF,CAAC"}
1
+ {"version":3,"file":"mcp.cjs","names":["defineMcpSkill","readPackageResource","Type","headless","promisify","execFile","defineMcpTool","defineMcpPlugin","skillDoompiUseRunner","toolBash"],"sources":["../../src/extensions/workspaces/sessions/(backend)/skill/doompi-use-runner.mcp.ts","../../src/schemas/bashTool.ts","../../src/services/ansiScrub/index.ts","../../src/services/ptyHost/index.ts","../../src/services/runnerRegistry/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp.ts","../../generated/mcp.ts"],"sourcesContent":["import { defineMcpSkill } from '@agimon-ai/doompi-core/mcpFacet';\nimport { readPackageResource } from '@agimon-ai/doompi-core/serverFacet';\n\nexport default defineMcpSkill({\n name: 'doompi-use-runner',\n description:\n 'Use Doom Pi Runner to supervise shell commands, inspect durable logs, provide interactive input, and stop background runs.',\n read: () => readPackageResource(import.meta.url, 'src/prompts/doompi-use-runner/SKILL.md'),\n});\n","import { type Static, Type } from 'typebox';\n\n/** Replaces pi's built-in tool of the same name, so hooks keyed on `bash` keep working. */\n\n/**\n * Built-in bash parameters plus the backgrounding controls.\n *\n * `command` and `timeout` keep their original names and wording so the tool is\n * a drop-in replacement. Every `description` is prompt copy the model reads.\n */\nexport const BashParamsSchema = Type.Object({\n command: Type.String({ description: 'Bash command to execute' }),\n timeout: Type.Optional(Type.Number({ description: 'Timeout in seconds (optional, no default timeout)' })),\n background: Type.Optional(\n Type.Boolean({\n description:\n 'Start the command in the background immediately instead of waiting. Use for dev servers, watchers and other commands you already know will not finish.',\n }),\n ),\n interactive: Type.Optional(\n Type.Boolean({\n description:\n 'Run under a pseudo terminal so the command can prompt. Use only when the command asks for confirmation or input; answer it with doom-runner input. Logs from interactive runs are noisier.',\n }),\n ),\n name: Type.Optional(\n Type.String({\n description: 'Preferred runner name if the command ends up in the background. A name in use gets a suffix.',\n }),\n ),\n});\n\nexport type BashParams = Static<typeof BashParamsSchema>;\n","import { CONTROL_PATTERN } from '../../constants/ansiScrub';\n/**\n * Turns terminal output into text worth grepping.\n *\n * PTY output is written for a screen, not a file: colours, cursor moves and\n * carriage-return redraws all end up in the byte stream. Without this a single\n * progress bar becomes thousands of near-identical log lines.\n */\n\n/**\n * CSI sequences (colours, cursor moves), OSC strings, the two-byte Fe escapes,\n * and the keypad-mode escapes shells emit around every prompt.\n */\nconst ANSI_PATTERN = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Bell and backspace, which a terminal consumes rather than displays. */\n// oxlint-disable-next-line no-control-regex -- matching terminal control bytes is the whole job\n\n/**\n * The same set minus SGR, the sequence that only sets colour and weight.\n *\n * The final byte class omits `m`, so `ESC[32m` survives while cursor moves,\n * OSC strings and keypad escapes are still removed.\n */\nconst ANSI_PATTERN_KEEPING_COLOUR = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-ln-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Removes escape sequences, leaving the characters a user would have seen. */\nexport function stripAnsi(text: string): string {\n return text.replace(ANSI_PATTERN, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Removes everything `stripAnsi` does except colour.\n *\n * Logs are read back by a terminal renderer as well as by grep, and a command\n * that colours its own output has already said how it should look.\n */\nexport function stripAnsiKeepingColour(text: string): string {\n return text.replace(ANSI_PATTERN_KEEPING_COLOUR, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Collapses carriage-return redraws.\n *\n * A bare `\\r` returns the cursor to the start of the current line, so whatever\n * follows overwrites it. Only the final state of each line is kept. The `\\r` of\n * a CRLF pair is a line terminator rather than a redraw, so it is normalised\n * away first: a pseudo terminal ends every line that way, and treating it as a\n * redraw would blank the whole log.\n */\nexport function collapseCarriageReturns(text: string): string {\n return text\n .replace(/\\r\\n/g, '\\n')\n .split('\\n')\n .map((line) => (line.includes('\\r') ? (line.split('\\r').at(-1) ?? '') : line))\n .join('\\n');\n}\n\n/** Full scrub applied to PTY output before it reaches the log file. */\nexport function scrubTerminalOutput(text: string): string {\n return collapseCarriageReturns(stripAnsiKeepingColour(text));\n}\n\n/** Plain-text scrub, for the copy the model reads. */\nexport function scrubTerminalOutputToPlainText(text: string): string {\n return collapseCarriageReturns(stripAnsi(text));\n}\n","import headless, { type Terminal } from '@xterm/headless';\n\nimport {\n DEFAULT_COLS,\n DEFAULT_ROWS,\n LIVENESS_POLL_MS,\n MEMORY_BUFFER_FACTOR,\n NEWLINE,\n SCROLLBACK,\n TERM_GRACE_MS,\n} from '../../constants/ptyHost';\nimport { PI_SESSION_ID_ENV } from '../../constants/session';\nimport type { IClock } from '../../types/clock';\nimport type { ILogFile } from '../../types/logFile';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IPtyHost, PtyLaunchRequest, PtyRun } from '../../types/ptyHost';\nimport type { IPtySpawner, PtyProcess } from '../../types/ptySpawner';\nimport type { ExitResult } from '../../types/spawner';\nimport { scrubTerminalOutput } from '../ansiScrub';\nimport { getResultMaxBytes } from '../runnerConfig';\nimport { NO_TERMINAL_INPUT_ENV } from '../runnerSupervisor';\n\n/** Enough history for the overlay to scroll without holding a whole build. */\n\n// Use the CommonJS default export: Node cannot detect this package's named exports.\n// A static import also lets server bundles include it without a live node_modules lookup.\nconst { Terminal: HeadlessTerminal } = headless;\n\nexport class PtyHost implements IPtyHost {\n private readonly runs = new Map<string, PtyRun>();\n\n constructor(\n private readonly spawner: IPtySpawner,\n private readonly logFile: ILogFile,\n private readonly processControl: IProcessControl,\n private readonly clock: IClock,\n private readonly environment: Readonly<Record<string, string | undefined>> = process.env,\n ) {}\n\n async launch(request: PtyLaunchRequest): Promise<PtyRun> {\n const cols = request.cols ?? DEFAULT_COLS;\n const rows = request.rows ?? DEFAULT_ROWS;\n const writer = this.logFile.open(request.id);\n const terminal = new HeadlessTerminal({ cols, rows, scrollback: SCROLLBACK, allowProposedApi: true });\n const memoryLimit = getResultMaxBytes(this.environment) * MEMORY_BUFFER_FACTOR;\n\n const child = await this.spawner.spawn({\n command: request.command,\n cwd: request.cwd,\n env: { ...this.environment, ...NO_TERMINAL_INPUT_ENV, [PI_SESSION_ID_ENV]: request.sessionId },\n cols,\n rows,\n });\n\n let buffer = '';\n let buffering = true;\n let settled = false;\n let resolveCompletion: (result: ExitResult) => void = () => undefined;\n const completion = new Promise<ExitResult>((resolve) => {\n resolveCompletion = resolve;\n });\n const dataHandlers = new Set<(data: string) => void>();\n\n child.onData((data) => {\n // The terminal keeps the screen; the log keeps text worth grepping.\n terminal.write(data);\n const scrubbed = scrubTerminalOutput(data);\n writer.append(scrubbed);\n if (buffering) {\n buffer += scrubbed;\n if (buffer.length > memoryLimit) buffer = buffer.slice(-memoryLimit);\n }\n for (const handler of dataHandlers) handler(data);\n });\n\n child.onExit((result) => {\n if (settled) return;\n settled = true;\n this.runs.delete(request.name);\n writer.close();\n resolveCompletion({ code: result.exitCode, signal: null });\n });\n\n const run: PtyRun = {\n id: request.id,\n name: request.name,\n pid: child.pid,\n logPath: writer.path,\n backend: 'native',\n output: () => buffer,\n completion: () => completion,\n detach: () => {\n buffering = false;\n buffer = '';\n },\n stop: () => this.stopChild(child, request.name),\n write: (text) => child.write(text),\n screen: () => renderScreen(terminal),\n onData: (handler) => {\n dataHandlers.add(handler);\n return () => dataHandlers.delete(handler);\n },\n resize: (nextCols, nextRows) => {\n child.resize(nextCols, nextRows);\n terminal.resize(nextCols, nextRows);\n },\n };\n\n this.runs.set(request.name, run);\n return run;\n }\n\n get(name: string): PtyRun | undefined {\n return this.runs.get(name);\n }\n\n write(name: string, text: string): boolean {\n const run = this.runs.get(name);\n if (!run) return false;\n run.write(text.endsWith(NEWLINE) ? text : `${text}${NEWLINE}`);\n return true;\n }\n\n list(): PtyRun[] {\n return [...this.runs.values()];\n }\n\n async disposeAll(): Promise<void> {\n const runs = [...this.runs.values()];\n this.runs.clear();\n for (const run of runs) await run.stop();\n }\n\n private async stopChild(child: PtyProcess, name: string): Promise<boolean> {\n this.runs.delete(name);\n if (!this.processControl.isAlive(child.pid)) return false;\n\n child.kill('SIGTERM');\n const deadline = this.clock.now() + TERM_GRACE_MS;\n while (this.clock.now() < deadline) {\n if (!this.processControl.isAlive(child.pid)) return true;\n await new Promise<void>((resolve) => {\n this.clock.after(LIVENESS_POLL_MS, resolve);\n });\n }\n child.kill('SIGKILL');\n return true;\n }\n}\n\n/** The visible rows, trimmed of trailing blanks, as plain text. */\nfunction renderScreen(terminal: Terminal): string {\n const buffer = terminal.buffer.active;\n const lines: string[] = [];\n for (let row = buffer.baseY; row < buffer.baseY + terminal.rows; row += 1) {\n lines.push(buffer.getLine(row)?.translateToString(true) ?? '');\n }\n while (lines.length > 0 && lines.at(-1) === '') lines.pop();\n return lines.join(NEWLINE);\n}\n","import { execFile } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport { resolveRootSessionId } from '@agimon-ai/doompi-core/childProcess';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\n\nimport {\n COMMAND_SIDECAR_SUFFIX,\n EXIT_SIDECAR_SUFFIX,\n JSON_EXTENSION,\n NOT_FOUND_ERROR_CODE,\n REGISTRY_PATH_ENV,\n RUNNER_TAG,\n SERVICE_PREFIX,\n SERVICE_TYPE,\n} from '../../constants/runnerRegistry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** The slice of the shared registry service this wrapper depends on. */\nexport type ProcessRegistryPort = Pick<\n ProcessRegistryService,\n 'registerProcess' | 'releaseProcess' | 'listProcesses' | 'close'\n>;\n\n/** Opens the SQLite-backed registry the rest of the monorepo already shares. */\nexport function createDefaultProcessRegistry(\n env: Readonly<Record<string, string | undefined>> = process.env,\n): ProcessRegistryPort {\n return createProcessRegistryService(env[REGISTRY_PATH_ENV]);\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction readString(record: ProcessRegistryRecord, key: string): string {\n const value = record.metadata?.[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction readNumber(record: ProcessRegistryRecord, key: string): number {\n const value = record.metadata?.[key];\n return typeof value === 'number' ? value : 0;\n}\n\nconst runCommand = promisify(execFile);\n\n/**\n * The process start time the registry stores to prove a pid is still the\n * process it registered.\n *\n * The registry resolves it itself when the caller omits it, and it does so with\n * `execFileSync`, which stops this process for the length of a `ps` on every\n * runner that starts. Reading it here costs the same subprocess and blocks\n * nothing: the command is already running by the time a runner is registered.\n *\n * The value has to match what the registry would have read, so the command and\n * the trimming are deliberately identical to its own.\n */\nasync function processStartedAt(pid: number): Promise<string | undefined> {\n if (process.platform === 'win32') return undefined;\n try {\n const { stdout } = await runCommand('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8' });\n return stdout.trim() || undefined;\n } catch {\n return undefined;\n }\n}\n\ntype ListenerRegistration = Readonly<{ key: string; listener: () => void }>;\n\n// Facet replacement can leave an in-flight tool and its publisher on different registry instances.\nconst listenersBySession = new Map<string, Set<() => void>>();\n\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly subscriptions = new Set<ListenerRegistration>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n private readonly environment: Readonly<Record<string, string | undefined>>,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId, this.environment);\n const identity = await processStartedAt(input.pid);\n const response = await this.registry.registerProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${input.id}`,\n serviceType: SERVICE_TYPE,\n pid: input.pid,\n command: input.command,\n ...(identity === undefined ? {} : { processStartedAt: identity }),\n tags: resolveProcessTags([RUNNER_TAG, `runner:${input.id}`, sessionTag(input.sessionId)]),\n metadata: {\n id: input.id,\n name: input.name,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n startedAt,\n hostPid: process.pid,\n },\n // The caller has already resolved name collisions against live records,\n // so anything still holding this name is stale.\n force: true,\n });\n if (!response.success) throw new Error(response.error ?? `Failed to register runner ${input.name}`);\n const record: RunnerRecord = {\n id: input.id,\n name: input.name,\n pid: input.pid,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n startedAt,\n state: 'running',\n promoted: false,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n hostPid: process.pid,\n };\n this.writeRecord(record);\n this.notify(record.sessionId);\n return record;\n }\n\n async list(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n repositoryPath: this.repositoryPath(),\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listAcrossRepositories(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listBySession(sessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => record.sessionId === sessionId);\n }\n\n async listByRootSession(rootSessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId);\n }\n\n async listAll(sessionId?: string): Promise<RunnerRecord[]> {\n this.paths.ensureDirectories(sessionId);\n const records: RunnerRecord[] = [];\n const directory = this.paths.stateDirectory(sessionId);\n for (const entry of fs.readdirSync(directory)) {\n if (!isPrimaryMetadataEntry(entry)) continue;\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(`${directory}/${entry}`, 'utf8'));\n if (isRunnerRecord(parsed)) records.push(parsed);\n } catch (error) {\n process.emitWarning(`Could not read runner metadata ${entry}: ${String(error)}`);\n }\n }\n return records.sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n }\n\n async get(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const target = this.paths.statePathFor(id, sessionId);\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (!isRunnerRecord(parsed) || parsed.id !== id) {\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: invalid runner record`);\n return undefined;\n }\n return parsed;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return undefined;\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: ${String(error)}`);\n return undefined;\n }\n }\n\n async markPromoted(id: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record) return undefined;\n const promoted = { ...record, promoted: true };\n this.writeRecord(promoted);\n this.notify(promoted.sessionId);\n return promoted;\n }\n\n async complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined> {\n const persisted = await this.get(id, sessionId);\n if (persisted?.state === 'completed') {\n await this.release(id, persisted.sessionId);\n return persisted;\n }\n const active = await this.list();\n const record =\n persisted ??\n active.find((candidate) => candidate.id === id && (sessionId === undefined || candidate.sessionId === sessionId));\n if (!record) {\n const belongsToAnotherSession =\n sessionId !== undefined && active.some((candidate) => candidate.id === id && candidate.sessionId !== sessionId);\n if (!belongsToAnotherSession) await this.release(id, sessionId);\n return undefined;\n }\n const completed: RunnerRecord = {\n ...record,\n state: 'completed',\n exit: { ...outcome, finishedAt: new Date().toISOString() },\n };\n this.writeRecord(completed);\n await this.release(id, completed.sessionId);\n return completed;\n }\n\n async release(id: string, sessionId?: string): Promise<void> {\n const activeSessionId = sessionId ?? (await this.list()).find((record) => record.id === id)?.sessionId;\n const ownedSessionId = activeSessionId ?? (await this.get(id))?.sessionId;\n const response = await this.registry.releaseProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${id}`,\n serviceType: SERVICE_TYPE,\n kill: false,\n releasePort: false,\n force: true,\n });\n if (!response.success && !response.error?.includes('No matching process entry')) {\n throw new Error(response.error ?? `Failed to release runner ${id}`);\n }\n if (ownedSessionId !== undefined) this.notify(ownedSessionId);\n }\n\n async pruneDead(): Promise<string[]> {\n const dead = (await this.list()).filter((record) => !this.processControl.isAlive(record.pid));\n for (const record of dead) {\n await this.complete(record.id, { reason: 'backend_lost', code: null, signal: null }, record.sessionId);\n }\n return dead.map((record) => record.id);\n }\n\n subscribe(listener: () => void, sessionId: string): () => void {\n const subscribed = (): void => listener();\n const registration = { key: this.subscriptionKey(sessionId), listener: subscribed };\n const listeners = listenersBySession.get(registration.key) ?? new Set<() => void>();\n listeners.add(subscribed);\n listenersBySession.set(registration.key, listeners);\n this.subscriptions.add(registration);\n return () => this.removeSubscription(registration);\n }\n\n close(): void {\n for (const registration of this.subscriptions) this.removeSubscription(registration);\n this.registry.close();\n }\n\n private notify(sessionId: string): void {\n for (const listener of listenersBySession.get(this.subscriptionKey(sessionId)) ?? []) {\n try {\n listener();\n } catch (error) {\n process.emitWarning(`Runner registry listener failed: ${String(error)}`);\n }\n }\n }\n\n private removeSubscription(registration: ListenerRegistration): void {\n const listeners = listenersBySession.get(registration.key);\n listeners?.delete(registration.listener);\n if (listeners?.size === 0) listenersBySession.delete(registration.key);\n this.subscriptions.delete(registration);\n }\n\n private subscriptionKey(sessionId: string): string {\n return JSON.stringify([path.resolve(this.paths.stateDirectory(sessionId)), sessionId]);\n }\n\n private repositoryPath(): string {\n return normalizeRepositoryPath(this.paths.repositoryPath());\n }\n\n private writeRecord(record: RunnerRecord): void {\n this.paths.ensureDirectories(record.sessionId);\n const target = this.paths.statePathFor(record.id, record.sessionId);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\\n`, 'utf8');\n fs.renameSync(temporary, target);\n }\n}\n\n/** Every field is read back from metadata this class wrote on registration. */\nfunction toRunnerRecord(record: ProcessRegistryRecord): RunnerRecord {\n return {\n id: readString(record, 'id'),\n name: readString(record, 'name'),\n pid: record.pid,\n command: readString(record, 'command'),\n cwd: readString(record, 'cwd'),\n logPath: readString(record, 'logPath'),\n interactive: record.metadata?.interactive === true,\n sessionId: readString(record, 'sessionId'),\n ...(readString(record, 'rootSessionId') ? { rootSessionId: readString(record, 'rootSessionId') } : {}),\n startedAt: readString(record, 'startedAt'),\n state: 'running',\n promoted: false,\n backend: readBackend(record.metadata?.backend),\n ...(readString(record, 'backendTarget') ? { backendTarget: readString(record, 'backendTarget') } : {}),\n hostPid: readNumber(record, 'hostPid'),\n };\n}\n\nfunction readBackend(value: unknown): RunnerRecord['backend'] {\n return value === 'rmux' || value === 'tmux' ? value : 'native';\n}\n\nfunction isPrimaryMetadataEntry(entry: string): boolean {\n return (\n entry.endsWith(JSON_EXTENSION) && !entry.endsWith(COMMAND_SIDECAR_SUFFIX) && !entry.endsWith(EXIT_SIDECAR_SUFFIX)\n );\n}\n","import type { DoomServerPluginContext } from '@agimon-ai/doompi-core/serverFacet';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { createHeadlessBashTool, createHeadlessRunnersCommand } from '../../../../../services/headless';\nimport { summarizeLog } from '../../../../../services/logReader';\nimport { createRunnerDependencies } from '../../../../../services/runnerDependencies';\nimport { createRunnerServerRuntime } from '../../../../../services/runnerServerRuntime';\n\nexport const RUNNER_SERVER_SCOPE_SERVICE = 'doom/runner-server-scope';\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n 'doom/runner-server-scope': RunnerServerScope;\n }\n}\n\nexport const createRunnerServerRoot = ({ host }: DoomServerPluginContext) => {\n if (!host.context.directEvents) throw new Error('Runner headless facet requires the session direct event bus.');\n if (!host.context.cwd || !host.context.environment) {\n throw new Error('Runner headless facet requires session cwd and environment.');\n }\n const dependencies = createRunnerDependencies({ cwd: host.context.cwd, environment: host.context.environment });\n const runtime = createRunnerServerRuntime(dependencies, host.context.directEvents);\n const value = {\n tool: createHeadlessBashTool(\n {\n run: async (request) => {\n await runtime.ensureSession(request.sessionId);\n return dependencies.bashRunService.run(request);\n },\n },\n summarizeLog,\n ),\n command: createHeadlessRunnersCommand(dependencies),\n };\n return {\n value,\n services: [\n runtime.backgroundWorkPlugin,\n (context: Context) => {\n context.plugin((providerContext) => providerContext.provide(RUNNER_SERVER_SCOPE_SERVICE, value));\n },\n ],\n activities: [runtime.activity],\n onStop: runtime.dispose,\n };\n};\n\nexport type RunnerServerScope = Awaited<ReturnType<typeof createRunnerServerRoot>>['value'];\n","import { defineMcpTool, type DoomMcpPluginContext } from '@agimon-ai/doompi-core/mcpFacet';\n\nimport { RUNNER_SERVER_SCOPE_SERVICE, type RunnerServerScope } from '../_lib/serverRoot';\n\nexport default defineMcpTool((context: DoomMcpPluginContext) => {\n const scope = context.services.get<RunnerServerScope>(RUNNER_SERVER_SCOPE_SERVICE);\n if (!scope) throw new Error('Runner session service is unavailable.');\n return {\n ...scope.tool,\n execute(toolCallId, parameters, signal, onUpdate, execution) {\n return scope.tool.execute(\n toolCallId,\n parameters,\n AbortSignal.any([context.signal, signal ?? context.signal]),\n onUpdate,\n execution,\n );\n },\n };\n});\n","// Generated by @agimon-ai/doompi-build. Do not edit by hand.\nimport { defineMcpPlugin, type DoomMcpSessionPlugin } from '@agimon-ai/doompi-core/mcpFacet';\n\nimport skillDoompiUseRunner from '../src/extensions/workspaces/sessions/(backend)/skill/doompi-use-runner.mcp';\nimport toolBash from '../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nconst withWidget = <T>(key: string, value: T): T => value === undefined ? value : ({\n ...value, _meta: { ...(value as { _meta?: object })._meta, [\"doompi/widget\"]: key },\n});\n\nexport const mcp = defineMcpPlugin({\n name: '@agimon-ai/doompi-runner',\n session: (context) => ({\n get skills(): DoomMcpSessionPlugin['skills'] { return [at(skillDoompiUseRunner, context)]; },\n get tools(): DoomMcpSessionPlugin['tools'] { return [withWidget(\"@agimon-ai/doompi-runner/bash\", via({ name: 'bash' }, at(toolBash, context)))]; },\n }) satisfies DoomMcpSessionPlugin,\n});\n\nexport default mcp;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,IAAA,iCAAA,GAAeA,gCAAAA,eAAAA,CAAe;CAC5B,MAAM;CACN,aACE;CACF,aAAA,GAAYC,mCAAAA,oBAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,MAAqC,wCAAwC;AAC3F,CAAC;ACE+BC,QAAAA,KAAK,OAAO;CAC1C,SAASA,QAAAA,KAAK,OAAO,EAAE,aAAa,0BAA0B,CAAC;CAC/D,SAASA,QAAAA,KAAK,SAASA,QAAAA,KAAK,OAAO,EAAE,aAAa,oDAAoD,CAAC,CAAC;CACxG,YAAYA,QAAAA,KAAK,SACfA,QAAAA,KAAK,QAAQ,EACX,aACE,yJACJ,CAAC,CACH;CACA,aAAaA,QAAAA,KAAK,SAChBA,QAAAA,KAAK,QAAQ,EACX,aACE,6LACJ,CAAC,CACH;CACA,MAAMA,QAAAA,KAAK,SACTA,QAAAA,KAAK,OAAO,EACV,aAAa,+FACf,CAAC,CACH;AACF,CAAC;ACjBoB,IAAI,OACvB;CACE;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG,GACV,GACF;AAWoC,IAAI,OACtC;CACE;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG,GACV,GACF;;;;ACdA,MAAM,EAAE,UAAU,qBAAqBC,gBAAAA;CCmCjC,GAAaC,UAAAA,UAAAA,CAAUC,mBAAAA,QAAQ;;;ACrDrC,MAAa,8BAA8B;;;ACJ3C,IAAA,oBAAA,GAAeC,gCAAAA,cAAAA,EAAe,YAAkC;CAC9D,MAAM,QAAQ,QAAQ,SAAS,IAAuB,2BAA2B;CACjF,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,wCAAwC;CACpE,OAAO;EACL,GAAG,MAAM;EACT,QAAQ,YAAY,YAAY,QAAQ,UAAU,WAAW;GAC3D,OAAO,MAAM,KAAK,QAChB,YACA,YACA,YAAY,IAAI,CAAC,QAAQ,QAAQ,UAAU,QAAQ,MAAM,CAAC,GAC1D,UACA,SACF;EACF;CACF;AACF,CAAC;;;ACZD,MAAM,MAAY,OAA0B,YAC1C,OAAO,UAAU,aAAc,MAAwB,OAAO,IAAI;AACpE,MAAM,OAAU,UAAmC,UAChD,UAAU,KAAA,IAAY,KAAA,IAAY;CAAE,GAAG;CAAU,GAAI;AAAiB;AAEzE,MAAM,cAAiB,KAAa,UAAgB,UAAU,KAAA,IAAY,QAAS;CACjF,GAAG;CAAO,OAAO;EAAE,GAAI,MAA6B;GAAQ,kBAAkB;CAAI;AACpF;AAEA,MAAa,OAAA,GAAMC,gCAAAA,gBAAAA,CAAgB;CACjC,MAAM;CACN,UAAU,aAAa;EACrB,IAAI,SAAyC;GAAE,OAAO,CAAC,GAAGC,+BAAsB,OAAO,CAAC;EAAG;EAC3F,IAAI,QAAuC;GAAE,OAAO,CAAC,WAAW,iCAAiC,IAAI,EAAE,MAAM,OAAO,GAAG,GAAGC,kBAAU,OAAO,CAAC,CAAC,CAAC;EAAG;CACnJ;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.mts","names":[],"sources":["../../generated/mcp.ts"],"mappings":";qBAYa,+CAAG"}
1
+ {"version":3,"file":"mcp.d.mts","names":[],"sources":["../../generated/mcp.ts"],"mappings":";qBAgBa,+CAAG"}
@@ -60,6 +60,13 @@ const via = (identity, value) => value === void 0 ? void 0 : {
60
60
  ...identity,
61
61
  ...value
62
62
  };
63
+ const withWidget = (key, value) => value === void 0 ? value : {
64
+ ...value,
65
+ _meta: {
66
+ ...value._meta,
67
+ ["doompi/widget"]: key
68
+ }
69
+ };
63
70
  const mcp = defineMcpPlugin({
64
71
  name: "@agimon-ai/doompi-runner",
65
72
  session: (context) => ({
@@ -67,7 +74,7 @@ const mcp = defineMcpPlugin({
67
74
  return [at(doompi_use_runner_mcp_default, context)];
68
75
  },
69
76
  get tools() {
70
- return [via({ name: "bash" }, at(bash_mcp_default, context))];
77
+ return [withWidget("@agimon-ai/doompi-runner/bash", via({ name: "bash" }, at(bash_mcp_default, context)))];
71
78
  }
72
79
  })
73
80
  });