@agimon-ai/doompi-runner 0.0.1-alpha.65 → 0.0.1-alpha.68

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 (45) hide show
  1. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.cjs +1 -1
  2. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.cjs.map +1 -1
  3. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.d.cts +0 -2
  4. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.d.cts.map +1 -1
  5. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.d.mts +0 -2
  6. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.d.mts.map +1 -1
  7. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.mjs +1 -1
  8. package/dist/src/adapters/RunnerRegistry/RunnerRegistry.mjs.map +1 -1
  9. package/dist/src/commands/bash/bashTool.cjs +1 -1
  10. package/dist/src/commands/bash/bashTool.cjs.map +1 -1
  11. package/dist/src/commands/bash/bashTool.d.cts.map +1 -1
  12. package/dist/src/commands/bash/bashTool.d.mts.map +1 -1
  13. package/dist/src/commands/bash/bashTool.mjs +1 -1
  14. package/dist/src/commands/bash/bashTool.mjs.map +1 -1
  15. package/dist/src/schemas/bashTool.cjs +1 -1
  16. package/dist/src/schemas/bashTool.cjs.map +1 -1
  17. package/dist/src/schemas/bashTool.d.cts +0 -3
  18. package/dist/src/schemas/bashTool.d.cts.map +1 -1
  19. package/dist/src/schemas/bashTool.d.mts +0 -3
  20. package/dist/src/schemas/bashTool.d.mts.map +1 -1
  21. package/dist/src/schemas/bashTool.mjs +1 -1
  22. package/dist/src/schemas/bashTool.mjs.map +1 -1
  23. package/dist/src/tui/bashRender.cjs +1 -1
  24. package/dist/src/tui/bashRender.cjs.map +1 -1
  25. package/dist/src/tui/bashRender.d.cts.map +1 -1
  26. package/dist/src/tui/bashRender.d.mts.map +1 -1
  27. package/dist/src/tui/bashRender.mjs +1 -1
  28. package/dist/src/tui/bashRender.mjs.map +1 -1
  29. package/dist/src/types/runnerRegistry.d.cts +0 -21
  30. package/dist/src/types/runnerRegistry.d.cts.map +1 -1
  31. package/dist/src/types/runnerRegistry.d.mts +0 -21
  32. package/dist/src/types/runnerRegistry.d.mts.map +1 -1
  33. package/dist/tool/schema.cjs +1 -1
  34. package/dist/tool/schema.d.cts +2 -2
  35. package/dist/tool/schema.d.mts +2 -2
  36. package/dist/tool/schema.mjs +1 -1
  37. package/package.json +18 -16
  38. package/skills/doom-runner/SKILL.md +6 -1
  39. package/src/web/components/BashToolMessage.tsx +2 -2
  40. package/src/web/components/LaunchRunnerDialog.tsx +8 -11
  41. package/src/web/components/RunnerLogPanel.tsx +23 -23
  42. package/src/web/components/RunnerShellPanel.tsx +5 -5
  43. package/src/web/components/RunnersActivitySection.tsx +6 -6
  44. package/src/web/components/RunnersPanel.tsx +12 -12
  45. package/src/web/lib/bashToolFormat.ts +0 -2
@@ -1,2 +1,2 @@
1
- const e=require("../../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/runs/runnerRecord.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("@agimon-ai/doompi-extension-contracts/child-process"),a=require("@agimon-ai/foundation-process-registry");const o=`tool`,s=`doom-runner:`,c=`doom-runner`;function l(){return(0,a.createProcessRegistryService)(process.env.PROCESS_REGISTRY_PATH)}function u(e){return`session:${e}`}function d(e,t){let n=e.metadata?.[t];return typeof n==`string`?n:``}function f(e,t){let n=e.metadata?.[t];return typeof n==`number`?n:0}var p=class{paths;processControl;registry;listeners=new Set;constructor(e,t,n){this.paths=e,this.processControl=t,this.registry=n}async register(e){let t=new Date().toISOString(),n=(0,i.resolveRootSessionId)(e.sessionId),r=await this.registry.registerProcess({repositoryPath:this.repositoryPath(),serviceName:`${s}${e.id}`,serviceType:o,pid:e.pid,command:e.command,tags:(0,a.resolveProcessTags)([c,`runner:${e.id}`,u(e.sessionId)]),metadata:{id:e.id,name:e.name,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:n,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},startedAt:t,hostPid:process.pid},force:!0});if(!r.success)throw Error(r.error??`Failed to register runner ${e.name}`);let l={id:e.id,name:e.name,pid:e.pid,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:n,startedAt:t,state:`running`,promoted:!1,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},...e.alarmMs?{alarm:{intervalMs:e.alarmMs,lastFiredAt:t}}:{},hostPid:process.pid};return this.writeRecord(l),this.notify(),l}async list(){return(await this.registry.listProcesses({repositoryPath:this.repositoryPath(),serviceType:o,tags:(0,a.resolveProcessTags)([c])})).map(m)}async listAcrossRepositories(){return(await this.registry.listProcesses({serviceType:o,tags:(0,a.resolveProcessTags)([c])})).map(m)}async listBySession(e){return(await this.list()).filter(t=>t.sessionId===e)}async listByRootSession(e){return(await this.list()).filter(t=>(t.rootSessionId??t.sessionId)===e)}async listAll(e){this.paths.ensureDirectories(e);let r=[],i=this.paths.stateDirectory(e);for(let e of n.default.readdirSync(i))if(g(e))try{let a=JSON.parse(n.default.readFileSync(`${i}/${e}`,`utf8`));t.isRunnerRecord(a)&&r.push(a)}catch(t){process.emitWarning(`Could not read runner metadata ${e}: ${String(t)}`)}return r.sort((e,t)=>t.startedAt.localeCompare(e.startedAt))}async get(e,i){let a=this.paths.statePathFor(e,i);try{let i=JSON.parse(n.default.readFileSync(a,`utf8`));if(!t.isRunnerRecord(i)||i.id!==e){process.emitWarning(`Could not read runner metadata ${r.default.basename(a)}: invalid runner record`);return}return i}catch(e){if(e.code===`ENOENT`)return;process.emitWarning(`Could not read runner metadata ${r.default.basename(a)}: ${String(e)}`);return}}async markPromoted(e){let t=await this.get(e);if(!t)return;let n={...t,promoted:!0};return this.writeRecord(n),this.notify(),n}async clearAlarm(e,t){let n=await this.get(e,t);if(!n)return;if(!n.alarm)return n;let r={...n};return delete r.alarm,this.writeRecord(r),this.notify(),r}async markAlarmFired(e,t){let n=await this.get(e);if(!n?.alarm||n.state!==`running`)return;let r={...n,alarm:{...n.alarm,lastFiredAt:t}};return this.writeRecord(r),this.notify(),r}async complete(e,t,n){let r=await this.get(e,n);if(r?.state===`completed`)return await this.release(e),r;let i=await this.list(),a=r??i.find(t=>t.id===e&&(n===void 0||t.sessionId===n));if(!a){n!==void 0&&i.some(t=>t.id===e&&t.sessionId!==n)||await this.release(e);return}let o={...a,state:`completed`,exit:{...t,finishedAt:new Date().toISOString()}};return this.writeRecord(o),await this.release(e),o}async release(e){let t=await this.registry.releaseProcess({repositoryPath:this.repositoryPath(),serviceName:`${s}${e}`,serviceType:o,kill:!1,releasePort:!1,force:!0});if(!t.success&&!t.error?.includes(`No matching process entry`))throw Error(t.error??`Failed to release runner ${e}`);this.notify()}async pruneDead(){let e=(await this.list()).filter(e=>!this.processControl.isAlive(e.pid));for(let t of e)await this.complete(t.id,{reason:`backend_lost`,code:null,signal:null},t.sessionId);return e.map(e=>e.id)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}close(){this.listeners.clear(),this.registry.close()}notify(){for(let e of this.listeners)e()}repositoryPath(){return(0,a.normalizeRepositoryPath)(this.paths.repositoryPath())}writeRecord(e){this.paths.ensureDirectories(e.sessionId);let t=this.paths.statePathFor(e.id,e.sessionId),r=`${t}.${process.pid}.tmp`;n.default.writeFileSync(r,`${JSON.stringify(e,null,2)}\n`,`utf8`),n.default.renameSync(r,t)}};function m(e){return{id:d(e,`id`),name:d(e,`name`),pid:e.pid,command:d(e,`command`),cwd:d(e,`cwd`),logPath:d(e,`logPath`),interactive:e.metadata?.interactive===!0,sessionId:d(e,`sessionId`),...d(e,`rootSessionId`)?{rootSessionId:d(e,`rootSessionId`)}:{},startedAt:d(e,`startedAt`),state:`running`,promoted:!1,backend:h(e.metadata?.backend),...d(e,`backendTarget`)?{backendTarget:d(e,`backendTarget`)}:{},hostPid:f(e,`hostPid`)}}function h(e){return e===`rmux`||e===`tmux`?e:`native`}function g(e){return e.endsWith(`.json`)&&!e.endsWith(`.command.json`)&&!e.endsWith(`.exit.json`)}exports.RunnerRegistry=p,exports.createDefaultProcessRegistry=l;
1
+ const e=require("../../../_virtual/_rolldown/runtime.cjs"),t=require("../../services/runs/runnerRecord.cjs");let n=require("node:fs");n=e.__toESM(n,1);let r=require("node:path");r=e.__toESM(r,1);let i=require("@agimon-ai/doompi-extension-contracts/child-process"),a=require("@agimon-ai/foundation-process-registry");const o=`tool`,s=`doom-runner:`,c=`doom-runner`;function l(){return(0,a.createProcessRegistryService)(process.env.PROCESS_REGISTRY_PATH)}function u(e){return`session:${e}`}function d(e,t){let n=e.metadata?.[t];return typeof n==`string`?n:``}function f(e,t){let n=e.metadata?.[t];return typeof n==`number`?n:0}var p=class{paths;processControl;registry;listeners=new Set;constructor(e,t,n){this.paths=e,this.processControl=t,this.registry=n}async register(e){let t=new Date().toISOString(),n=(0,i.resolveRootSessionId)(e.sessionId),r=await this.registry.registerProcess({repositoryPath:this.repositoryPath(),serviceName:`${s}${e.id}`,serviceType:o,pid:e.pid,command:e.command,tags:(0,a.resolveProcessTags)([c,`runner:${e.id}`,u(e.sessionId)]),metadata:{id:e.id,name:e.name,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:n,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},startedAt:t,hostPid:process.pid},force:!0});if(!r.success)throw Error(r.error??`Failed to register runner ${e.name}`);let l={id:e.id,name:e.name,pid:e.pid,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:n,startedAt:t,state:`running`,promoted:!1,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},hostPid:process.pid};return this.writeRecord(l),this.notify(),l}async list(){return(await this.registry.listProcesses({repositoryPath:this.repositoryPath(),serviceType:o,tags:(0,a.resolveProcessTags)([c])})).map(m)}async listAcrossRepositories(){return(await this.registry.listProcesses({serviceType:o,tags:(0,a.resolveProcessTags)([c])})).map(m)}async listBySession(e){return(await this.list()).filter(t=>t.sessionId===e)}async listByRootSession(e){return(await this.list()).filter(t=>(t.rootSessionId??t.sessionId)===e)}async listAll(e){this.paths.ensureDirectories(e);let r=[],i=this.paths.stateDirectory(e);for(let e of n.default.readdirSync(i))if(g(e))try{let a=JSON.parse(n.default.readFileSync(`${i}/${e}`,`utf8`));t.isRunnerRecord(a)&&r.push(a)}catch(t){process.emitWarning(`Could not read runner metadata ${e}: ${String(t)}`)}return r.sort((e,t)=>t.startedAt.localeCompare(e.startedAt))}async get(e,i){let a=this.paths.statePathFor(e,i);try{let i=JSON.parse(n.default.readFileSync(a,`utf8`));if(!t.isRunnerRecord(i)||i.id!==e){process.emitWarning(`Could not read runner metadata ${r.default.basename(a)}: invalid runner record`);return}return i}catch(e){if(e.code===`ENOENT`)return;process.emitWarning(`Could not read runner metadata ${r.default.basename(a)}: ${String(e)}`);return}}async markPromoted(e){let t=await this.get(e);if(!t)return;let n={...t,promoted:!0};return this.writeRecord(n),this.notify(),n}async complete(e,t,n){let r=await this.get(e,n);if(r?.state===`completed`)return await this.release(e),r;let i=await this.list(),a=r??i.find(t=>t.id===e&&(n===void 0||t.sessionId===n));if(!a){n!==void 0&&i.some(t=>t.id===e&&t.sessionId!==n)||await this.release(e);return}let o={...a,state:`completed`,exit:{...t,finishedAt:new Date().toISOString()}};return this.writeRecord(o),await this.release(e),o}async release(e){let t=await this.registry.releaseProcess({repositoryPath:this.repositoryPath(),serviceName:`${s}${e}`,serviceType:o,kill:!1,releasePort:!1,force:!0});if(!t.success&&!t.error?.includes(`No matching process entry`))throw Error(t.error??`Failed to release runner ${e}`);this.notify()}async pruneDead(){let e=(await this.list()).filter(e=>!this.processControl.isAlive(e.pid));for(let t of e)await this.complete(t.id,{reason:`backend_lost`,code:null,signal:null},t.sessionId);return e.map(e=>e.id)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}close(){this.listeners.clear(),this.registry.close()}notify(){for(let e of this.listeners)e()}repositoryPath(){return(0,a.normalizeRepositoryPath)(this.paths.repositoryPath())}writeRecord(e){this.paths.ensureDirectories(e.sessionId);let t=this.paths.statePathFor(e.id,e.sessionId),r=`${t}.${process.pid}.tmp`;n.default.writeFileSync(r,`${JSON.stringify(e,null,2)}\n`,`utf8`),n.default.renameSync(r,t)}};function m(e){return{id:d(e,`id`),name:d(e,`name`),pid:e.pid,command:d(e,`command`),cwd:d(e,`cwd`),logPath:d(e,`logPath`),interactive:e.metadata?.interactive===!0,sessionId:d(e,`sessionId`),...d(e,`rootSessionId`)?{rootSessionId:d(e,`rootSessionId`)}:{},startedAt:d(e,`startedAt`),state:`running`,promoted:!1,backend:h(e.metadata?.backend),...d(e,`backendTarget`)?{backendTarget:d(e,`backendTarget`)}:{},hostPid:f(e,`hostPid`)}}function h(e){return e===`rmux`||e===`tmux`?e:`native`}function g(e){return e.endsWith(`.json`)&&!e.endsWith(`.command.json`)&&!e.endsWith(`.exit.json`)}exports.RunnerRegistry=p,exports.createDefaultProcessRegistry=l;
2
2
  //# sourceMappingURL=RunnerRegistry.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"RunnerRegistry.cjs","names":["createProcessRegistryService","resolveRootSessionId","resolveProcessTags","fs","isRunnerRecord","path","normalizeRepositoryPath"],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IRunnerPaths } from '../../services/RunnerPaths/types';\nimport { isRunnerRecord } from '../../services/runs/runnerRecord.ts';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\n\nconst SERVICE_TYPE = 'tool';\nconst SERVICE_PREFIX = 'doom-runner:';\nconst RUNNER_TAG = 'doom-runner';\nconst REGISTRY_PATH_ENV = 'PROCESS_REGISTRY_PATH';\nconst JSON_EXTENSION = '.json';\nconst COMMAND_SIDECAR_SUFFIX = '.command.json';\nconst EXIT_SIDECAR_SUFFIX = '.exit.json';\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\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\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly listeners = new Set<() => void>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId);\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 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 ...(input.alarmMs ? { alarm: { intervalMs: input.alarmMs, lastFiredAt: startedAt } } : {}),\n hostPid: process.pid,\n };\n this.writeRecord(record);\n this.notify();\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();\n return promoted;\n }\n\n async clearAlarm(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id, sessionId);\n if (!record) return undefined;\n if (!record.alarm) return record;\n const cleared: RunnerRecord = { ...record };\n delete cleared.alarm;\n this.writeRecord(cleared);\n this.notify();\n return cleared;\n }\n\n async markAlarmFired(id: string, firedAt: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record?.alarm || record.state !== 'running') return undefined;\n const fired: RunnerRecord = { ...record, alarm: { ...record.alarm, lastFiredAt: firedAt } };\n this.writeRecord(fired);\n this.notify();\n return fired;\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);\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);\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);\n return completed;\n }\n\n async release(id: string): Promise<void> {\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 this.notify();\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): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n close(): void {\n this.listeners.clear();\n this.registry.close();\n }\n\n private notify(): void {\n for (const listener of this.listeners) listener();\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"],"mappings":"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"}
1
+ {"version":3,"file":"RunnerRegistry.cjs","names":["createProcessRegistryService","resolveRootSessionId","resolveProcessTags","fs","isRunnerRecord","path","normalizeRepositoryPath"],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IRunnerPaths } from '../../services/RunnerPaths/types';\nimport { isRunnerRecord } from '../../services/runs/runnerRecord.ts';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\n\nconst SERVICE_TYPE = 'tool';\nconst SERVICE_PREFIX = 'doom-runner:';\nconst RUNNER_TAG = 'doom-runner';\nconst REGISTRY_PATH_ENV = 'PROCESS_REGISTRY_PATH';\nconst JSON_EXTENSION = '.json';\nconst COMMAND_SIDECAR_SUFFIX = '.command.json';\nconst EXIT_SIDECAR_SUFFIX = '.exit.json';\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\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\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly listeners = new Set<() => void>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId);\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 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();\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();\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);\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);\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);\n return completed;\n }\n\n async release(id: string): Promise<void> {\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 this.notify();\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): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n close(): void {\n this.listeners.clear();\n this.registry.close();\n }\n\n private notify(): void {\n for (const listener of this.listeners) listener();\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"],"mappings":"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"}
@@ -21,8 +21,6 @@ export declare class RunnerRegistry implements IRunnerRegistry {
21
21
  listAll(sessionId?: string): Promise<RunnerRecord[]>;
22
22
  get(id: string, sessionId?: string): Promise<RunnerRecord | undefined>;
23
23
  markPromoted(id: string): Promise<RunnerRecord | undefined>;
24
- clearAlarm(id: string, sessionId?: string): Promise<RunnerRecord | undefined>;
25
- markAlarmFired(id: string, firedAt: string): Promise<RunnerRecord | undefined>;
26
24
  complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined>;
27
25
  release(id: string): Promise<void>;
28
26
  pruneDead(): Promise<string[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"RunnerRegistry.d.cts","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"mappings":";;;;;;YA8BY,sBAAsB,KAChC;;wBAKc,gCAAgC;qBAkBnC,0BAA0B;mBAIlB;mBACA;mBACA;mBALF;EAEjB,YACmB,OAAO,cACP,gBAAgB,iBAChB,UAAU;EAGvB,SAAS,OAAO,sBAAsB,QAAQ;EAoD9C,QAAQ,QAAQ;EAShB,0BAA0B,QAAQ;EAQlC,cAAc,oBAAoB,QAAQ;EAK1C,kBAAkB,wBAAwB,QAAQ;EAKlD,QAAQ,qBAAqB,QAAQ;EAgBrC,IAAI,YAAY,qBAAqB,QAAQ;EAgB7C,aAAa,aAAa,QAAQ;EASlC,WAAW,YAAY,qBAAqB,QAAQ;EAWpD,eAAe,YAAY,kBAAkB,QAAQ;EASrD,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;EA0BhF,QAAQ,aAAa;EAerB,aAAa;EAQnB,UAAU;EAKV;UAKQ;UAIA;UAIA"}
1
+ {"version":3,"file":"RunnerRegistry.d.cts","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"mappings":";;;;;;YA8BY,sBAAsB,KAChC;;wBAKc,gCAAgC;qBAkBnC,0BAA0B;mBAIlB;mBACA;mBACA;mBALF;EAEjB,YACmB,OAAO,cACP,gBAAgB,iBAChB,UAAU;EAGvB,SAAS,OAAO,sBAAsB,QAAQ;EAmD9C,QAAQ,QAAQ;EAShB,0BAA0B,QAAQ;EAQlC,cAAc,oBAAoB,QAAQ;EAK1C,kBAAkB,wBAAwB,QAAQ;EAKlD,QAAQ,qBAAqB,QAAQ;EAgBrC,IAAI,YAAY,qBAAqB,QAAQ;EAgB7C,aAAa,aAAa,QAAQ;EASlC,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;EA0BhF,QAAQ,aAAa;EAerB,aAAa;EAQnB,UAAU;EAKV;UAKQ;UAIA;UAIA"}
@@ -21,8 +21,6 @@ export declare class RunnerRegistry implements IRunnerRegistry {
21
21
  listAll(sessionId?: string): Promise<RunnerRecord[]>;
22
22
  get(id: string, sessionId?: string): Promise<RunnerRecord | undefined>;
23
23
  markPromoted(id: string): Promise<RunnerRecord | undefined>;
24
- clearAlarm(id: string, sessionId?: string): Promise<RunnerRecord | undefined>;
25
- markAlarmFired(id: string, firedAt: string): Promise<RunnerRecord | undefined>;
26
24
  complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined>;
27
25
  release(id: string): Promise<void>;
28
26
  pruneDead(): Promise<string[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"RunnerRegistry.d.mts","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"mappings":";;;;;;YA8BY,sBAAsB,KAChC;;wBAKc,gCAAgC;qBAkBnC,0BAA0B;mBAIlB;mBACA;mBACA;mBALF;EAEjB,YACmB,OAAO,cACP,gBAAgB,iBAChB,UAAU;EAGvB,SAAS,OAAO,sBAAsB,QAAQ;EAoD9C,QAAQ,QAAQ;EAShB,0BAA0B,QAAQ;EAQlC,cAAc,oBAAoB,QAAQ;EAK1C,kBAAkB,wBAAwB,QAAQ;EAKlD,QAAQ,qBAAqB,QAAQ;EAgBrC,IAAI,YAAY,qBAAqB,QAAQ;EAgB7C,aAAa,aAAa,QAAQ;EASlC,WAAW,YAAY,qBAAqB,QAAQ;EAWpD,eAAe,YAAY,kBAAkB,QAAQ;EASrD,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;EA0BhF,QAAQ,aAAa;EAerB,aAAa;EAQnB,UAAU;EAKV;UAKQ;UAIA;UAIA"}
1
+ {"version":3,"file":"RunnerRegistry.d.mts","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"mappings":";;;;;;YA8BY,sBAAsB,KAChC;;wBAKc,gCAAgC;qBAkBnC,0BAA0B;mBAIlB;mBACA;mBACA;mBALF;EAEjB,YACmB,OAAO,cACP,gBAAgB,iBAChB,UAAU;EAGvB,SAAS,OAAO,sBAAsB,QAAQ;EAmD9C,QAAQ,QAAQ;EAShB,0BAA0B,QAAQ;EAQlC,cAAc,oBAAoB,QAAQ;EAK1C,kBAAkB,wBAAwB,QAAQ;EAKlD,QAAQ,qBAAqB,QAAQ;EAgBrC,IAAI,YAAY,qBAAqB,QAAQ;EAgB7C,aAAa,aAAa,QAAQ;EASlC,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;EA0BhF,QAAQ,aAAa;EAerB,aAAa;EAQnB,UAAU;EAKV;UAKQ;UAIA;UAIA"}
@@ -1,2 +1,2 @@
1
- import{isRunnerRecord as e}from"../../services/runs/runnerRecord.mjs";import t from"node:fs";import n from"node:path";import{resolveRootSessionId as r}from"@agimon-ai/doompi-extension-contracts/child-process";import{createProcessRegistryService as i,normalizeRepositoryPath as a,resolveProcessTags as o}from"@agimon-ai/foundation-process-registry";const s=`tool`,c=`doom-runner:`,l=`doom-runner`;function u(){return i(process.env.PROCESS_REGISTRY_PATH)}function d(e){return`session:${e}`}function f(e,t){let n=e.metadata?.[t];return typeof n==`string`?n:``}function p(e,t){let n=e.metadata?.[t];return typeof n==`number`?n:0}var m=class{paths;processControl;registry;listeners=new Set;constructor(e,t,n){this.paths=e,this.processControl=t,this.registry=n}async register(e){let t=new Date().toISOString(),n=r(e.sessionId),i=await this.registry.registerProcess({repositoryPath:this.repositoryPath(),serviceName:`${c}${e.id}`,serviceType:s,pid:e.pid,command:e.command,tags:o([l,`runner:${e.id}`,d(e.sessionId)]),metadata:{id:e.id,name:e.name,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:n,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},startedAt:t,hostPid:process.pid},force:!0});if(!i.success)throw Error(i.error??`Failed to register runner ${e.name}`);let a={id:e.id,name:e.name,pid:e.pid,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:n,startedAt:t,state:`running`,promoted:!1,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},...e.alarmMs?{alarm:{intervalMs:e.alarmMs,lastFiredAt:t}}:{},hostPid:process.pid};return this.writeRecord(a),this.notify(),a}async list(){return(await this.registry.listProcesses({repositoryPath:this.repositoryPath(),serviceType:s,tags:o([l])})).map(h)}async listAcrossRepositories(){return(await this.registry.listProcesses({serviceType:s,tags:o([l])})).map(h)}async listBySession(e){return(await this.list()).filter(t=>t.sessionId===e)}async listByRootSession(e){return(await this.list()).filter(t=>(t.rootSessionId??t.sessionId)===e)}async listAll(n){this.paths.ensureDirectories(n);let r=[],i=this.paths.stateDirectory(n);for(let n of t.readdirSync(i))if(_(n))try{let a=JSON.parse(t.readFileSync(`${i}/${n}`,`utf8`));e(a)&&r.push(a)}catch(e){process.emitWarning(`Could not read runner metadata ${n}: ${String(e)}`)}return r.sort((e,t)=>t.startedAt.localeCompare(e.startedAt))}async get(r,i){let a=this.paths.statePathFor(r,i);try{let i=JSON.parse(t.readFileSync(a,`utf8`));if(!e(i)||i.id!==r){process.emitWarning(`Could not read runner metadata ${n.basename(a)}: invalid runner record`);return}return i}catch(e){if(e.code===`ENOENT`)return;process.emitWarning(`Could not read runner metadata ${n.basename(a)}: ${String(e)}`);return}}async markPromoted(e){let t=await this.get(e);if(!t)return;let n={...t,promoted:!0};return this.writeRecord(n),this.notify(),n}async clearAlarm(e,t){let n=await this.get(e,t);if(!n)return;if(!n.alarm)return n;let r={...n};return delete r.alarm,this.writeRecord(r),this.notify(),r}async markAlarmFired(e,t){let n=await this.get(e);if(!n?.alarm||n.state!==`running`)return;let r={...n,alarm:{...n.alarm,lastFiredAt:t}};return this.writeRecord(r),this.notify(),r}async complete(e,t,n){let r=await this.get(e,n);if(r?.state===`completed`)return await this.release(e),r;let i=await this.list(),a=r??i.find(t=>t.id===e&&(n===void 0||t.sessionId===n));if(!a){n!==void 0&&i.some(t=>t.id===e&&t.sessionId!==n)||await this.release(e);return}let o={...a,state:`completed`,exit:{...t,finishedAt:new Date().toISOString()}};return this.writeRecord(o),await this.release(e),o}async release(e){let t=await this.registry.releaseProcess({repositoryPath:this.repositoryPath(),serviceName:`${c}${e}`,serviceType:s,kill:!1,releasePort:!1,force:!0});if(!t.success&&!t.error?.includes(`No matching process entry`))throw Error(t.error??`Failed to release runner ${e}`);this.notify()}async pruneDead(){let e=(await this.list()).filter(e=>!this.processControl.isAlive(e.pid));for(let t of e)await this.complete(t.id,{reason:`backend_lost`,code:null,signal:null},t.sessionId);return e.map(e=>e.id)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}close(){this.listeners.clear(),this.registry.close()}notify(){for(let e of this.listeners)e()}repositoryPath(){return a(this.paths.repositoryPath())}writeRecord(e){this.paths.ensureDirectories(e.sessionId);let n=this.paths.statePathFor(e.id,e.sessionId),r=`${n}.${process.pid}.tmp`;t.writeFileSync(r,`${JSON.stringify(e,null,2)}\n`,`utf8`),t.renameSync(r,n)}};function h(e){return{id:f(e,`id`),name:f(e,`name`),pid:e.pid,command:f(e,`command`),cwd:f(e,`cwd`),logPath:f(e,`logPath`),interactive:e.metadata?.interactive===!0,sessionId:f(e,`sessionId`),...f(e,`rootSessionId`)?{rootSessionId:f(e,`rootSessionId`)}:{},startedAt:f(e,`startedAt`),state:`running`,promoted:!1,backend:g(e.metadata?.backend),...f(e,`backendTarget`)?{backendTarget:f(e,`backendTarget`)}:{},hostPid:p(e,`hostPid`)}}function g(e){return e===`rmux`||e===`tmux`?e:`native`}function _(e){return e.endsWith(`.json`)&&!e.endsWith(`.command.json`)&&!e.endsWith(`.exit.json`)}export{m as RunnerRegistry,u as createDefaultProcessRegistry};
1
+ import{isRunnerRecord as e}from"../../services/runs/runnerRecord.mjs";import t from"node:fs";import n from"node:path";import{resolveRootSessionId as r}from"@agimon-ai/doompi-extension-contracts/child-process";import{createProcessRegistryService as i,normalizeRepositoryPath as a,resolveProcessTags as o}from"@agimon-ai/foundation-process-registry";const s=`tool`,c=`doom-runner:`,l=`doom-runner`;function u(){return i(process.env.PROCESS_REGISTRY_PATH)}function d(e){return`session:${e}`}function f(e,t){let n=e.metadata?.[t];return typeof n==`string`?n:``}function p(e,t){let n=e.metadata?.[t];return typeof n==`number`?n:0}var m=class{paths;processControl;registry;listeners=new Set;constructor(e,t,n){this.paths=e,this.processControl=t,this.registry=n}async register(e){let t=new Date().toISOString(),n=r(e.sessionId),i=await this.registry.registerProcess({repositoryPath:this.repositoryPath(),serviceName:`${c}${e.id}`,serviceType:s,pid:e.pid,command:e.command,tags:o([l,`runner:${e.id}`,d(e.sessionId)]),metadata:{id:e.id,name:e.name,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:n,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},startedAt:t,hostPid:process.pid},force:!0});if(!i.success)throw Error(i.error??`Failed to register runner ${e.name}`);let a={id:e.id,name:e.name,pid:e.pid,command:e.command,cwd:e.cwd,logPath:e.logPath,interactive:e.interactive,sessionId:e.sessionId,rootSessionId:n,startedAt:t,state:`running`,promoted:!1,backend:e.backend,...e.backendTarget?{backendTarget:e.backendTarget}:{},hostPid:process.pid};return this.writeRecord(a),this.notify(),a}async list(){return(await this.registry.listProcesses({repositoryPath:this.repositoryPath(),serviceType:s,tags:o([l])})).map(h)}async listAcrossRepositories(){return(await this.registry.listProcesses({serviceType:s,tags:o([l])})).map(h)}async listBySession(e){return(await this.list()).filter(t=>t.sessionId===e)}async listByRootSession(e){return(await this.list()).filter(t=>(t.rootSessionId??t.sessionId)===e)}async listAll(n){this.paths.ensureDirectories(n);let r=[],i=this.paths.stateDirectory(n);for(let n of t.readdirSync(i))if(_(n))try{let a=JSON.parse(t.readFileSync(`${i}/${n}`,`utf8`));e(a)&&r.push(a)}catch(e){process.emitWarning(`Could not read runner metadata ${n}: ${String(e)}`)}return r.sort((e,t)=>t.startedAt.localeCompare(e.startedAt))}async get(r,i){let a=this.paths.statePathFor(r,i);try{let i=JSON.parse(t.readFileSync(a,`utf8`));if(!e(i)||i.id!==r){process.emitWarning(`Could not read runner metadata ${n.basename(a)}: invalid runner record`);return}return i}catch(e){if(e.code===`ENOENT`)return;process.emitWarning(`Could not read runner metadata ${n.basename(a)}: ${String(e)}`);return}}async markPromoted(e){let t=await this.get(e);if(!t)return;let n={...t,promoted:!0};return this.writeRecord(n),this.notify(),n}async complete(e,t,n){let r=await this.get(e,n);if(r?.state===`completed`)return await this.release(e),r;let i=await this.list(),a=r??i.find(t=>t.id===e&&(n===void 0||t.sessionId===n));if(!a){n!==void 0&&i.some(t=>t.id===e&&t.sessionId!==n)||await this.release(e);return}let o={...a,state:`completed`,exit:{...t,finishedAt:new Date().toISOString()}};return this.writeRecord(o),await this.release(e),o}async release(e){let t=await this.registry.releaseProcess({repositoryPath:this.repositoryPath(),serviceName:`${c}${e}`,serviceType:s,kill:!1,releasePort:!1,force:!0});if(!t.success&&!t.error?.includes(`No matching process entry`))throw Error(t.error??`Failed to release runner ${e}`);this.notify()}async pruneDead(){let e=(await this.list()).filter(e=>!this.processControl.isAlive(e.pid));for(let t of e)await this.complete(t.id,{reason:`backend_lost`,code:null,signal:null},t.sessionId);return e.map(e=>e.id)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}close(){this.listeners.clear(),this.registry.close()}notify(){for(let e of this.listeners)e()}repositoryPath(){return a(this.paths.repositoryPath())}writeRecord(e){this.paths.ensureDirectories(e.sessionId);let n=this.paths.statePathFor(e.id,e.sessionId),r=`${n}.${process.pid}.tmp`;t.writeFileSync(r,`${JSON.stringify(e,null,2)}\n`,`utf8`),t.renameSync(r,n)}};function h(e){return{id:f(e,`id`),name:f(e,`name`),pid:e.pid,command:f(e,`command`),cwd:f(e,`cwd`),logPath:f(e,`logPath`),interactive:e.metadata?.interactive===!0,sessionId:f(e,`sessionId`),...f(e,`rootSessionId`)?{rootSessionId:f(e,`rootSessionId`)}:{},startedAt:f(e,`startedAt`),state:`running`,promoted:!1,backend:g(e.metadata?.backend),...f(e,`backendTarget`)?{backendTarget:f(e,`backendTarget`)}:{},hostPid:p(e,`hostPid`)}}function g(e){return e===`rmux`||e===`tmux`?e:`native`}function _(e){return e.endsWith(`.json`)&&!e.endsWith(`.command.json`)&&!e.endsWith(`.exit.json`)}export{m as RunnerRegistry,u as createDefaultProcessRegistry};
2
2
  //# sourceMappingURL=RunnerRegistry.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"RunnerRegistry.mjs","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IRunnerPaths } from '../../services/RunnerPaths/types';\nimport { isRunnerRecord } from '../../services/runs/runnerRecord.ts';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\n\nconst SERVICE_TYPE = 'tool';\nconst SERVICE_PREFIX = 'doom-runner:';\nconst RUNNER_TAG = 'doom-runner';\nconst REGISTRY_PATH_ENV = 'PROCESS_REGISTRY_PATH';\nconst JSON_EXTENSION = '.json';\nconst COMMAND_SIDECAR_SUFFIX = '.command.json';\nconst EXIT_SIDECAR_SUFFIX = '.exit.json';\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\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\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly listeners = new Set<() => void>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId);\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 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 ...(input.alarmMs ? { alarm: { intervalMs: input.alarmMs, lastFiredAt: startedAt } } : {}),\n hostPid: process.pid,\n };\n this.writeRecord(record);\n this.notify();\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();\n return promoted;\n }\n\n async clearAlarm(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id, sessionId);\n if (!record) return undefined;\n if (!record.alarm) return record;\n const cleared: RunnerRecord = { ...record };\n delete cleared.alarm;\n this.writeRecord(cleared);\n this.notify();\n return cleared;\n }\n\n async markAlarmFired(id: string, firedAt: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record?.alarm || record.state !== 'running') return undefined;\n const fired: RunnerRecord = { ...record, alarm: { ...record.alarm, lastFiredAt: firedAt } };\n this.writeRecord(fired);\n this.notify();\n return fired;\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);\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);\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);\n return completed;\n }\n\n async release(id: string): Promise<void> {\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 this.notify();\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): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n close(): void {\n this.listeners.clear();\n this.registry.close();\n }\n\n private notify(): void {\n for (const listener of this.listeners) listener();\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"],"mappings":"4VAoBA,MAAM,EAAe,OACf,EAAiB,eACjB,EAAa,cAcnB,SAAgB,GAAoD,CAClE,OAAO,EAA6B,QAAQ,IAAI,qBAAkB,CACpE,CAEA,SAAS,EAAW,EAA2B,CAC7C,MAAO,WAAW,GACpB,CAEA,SAAS,EAAW,EAA+B,EAAqB,CACtE,IAAM,EAAQ,EAAO,WAAW,GAChC,OAAO,OAAO,GAAU,SAAW,EAAQ,EAC7C,CAEA,SAAS,EAAW,EAA+B,EAAqB,CACtE,IAAM,EAAQ,EAAO,WAAW,GAChC,OAAO,OAAO,GAAU,SAAW,EAAQ,CAC7C,CAEA,IAAa,EAAb,KAAuD,CAIlC,MACA,eACA,SALnB,UAA6B,IAAI,IAEjC,YACE,EACA,EACA,EACA,CAHiB,KAAA,MAAA,EACA,KAAA,eAAA,EACA,KAAA,SAAA,CAChB,CAEH,MAAM,SAAS,EAAmD,CAChE,IAAM,EAAY,IAAI,KAAK,CAAA,CAAE,YAAY,EACnC,EAAgB,EAAqB,EAAM,SAAS,EACpD,EAAW,MAAM,KAAK,SAAS,gBAAgB,CACnD,eAAgB,KAAK,eAAe,EACpC,YAAa,GAAG,IAAiB,EAAM,KACvC,YAAa,EACb,IAAK,EAAM,IACX,QAAS,EAAM,QACf,KAAM,EAAmB,CAAC,EAAY,UAAU,EAAM,KAAM,EAAW,EAAM,SAAS,CAAC,CAAC,EACxF,SAAU,CACR,GAAI,EAAM,GACV,KAAM,EAAM,KACZ,QAAS,EAAM,QACf,IAAK,EAAM,IACX,QAAS,EAAM,QACf,YAAa,EAAM,YACnB,UAAW,EAAM,UACjB,gBACA,QAAS,EAAM,QACf,GAAI,EAAM,cAAgB,CAAE,cAAe,EAAM,aAAc,EAAI,CAAC,EACpE,YACA,QAAS,QAAQ,GACnB,EAGA,MAAO,EACT,CAAC,EACD,GAAI,CAAC,EAAS,QAAS,MAAU,MAAM,EAAS,OAAS,6BAA6B,EAAM,MAAM,EAClG,IAAM,EAAuB,CAC3B,GAAI,EAAM,GACV,KAAM,EAAM,KACZ,IAAK,EAAM,IACX,QAAS,EAAM,QACf,IAAK,EAAM,IACX,QAAS,EAAM,QACf,YAAa,EAAM,YACnB,UAAW,EAAM,UACjB,gBACA,YACA,MAAO,UACP,SAAU,GACV,QAAS,EAAM,QACf,GAAI,EAAM,cAAgB,CAAE,cAAe,EAAM,aAAc,EAAI,CAAC,EACpE,GAAI,EAAM,QAAU,CAAE,MAAO,CAAE,WAAY,EAAM,QAAS,YAAa,CAAU,CAAE,EAAI,CAAC,EACxF,QAAS,QAAQ,GACnB,EAGA,OAFA,KAAK,YAAY,CAAM,EACvB,KAAK,OAAO,EACL,CACT,CAEA,MAAM,MAAgC,CAMpC,OAAO,MALe,KAAK,SAAS,cAAc,CAChD,eAAgB,KAAK,eAAe,EACpC,YAAa,EACb,KAAM,EAAmB,CAAC,CAAU,CAAC,CACvC,CAAC,EAAA,CACc,IAAI,CAAc,CACnC,CAEA,MAAM,wBAAkD,CAKtD,OAAO,MAJe,KAAK,SAAS,cAAc,CAChD,YAAa,EACb,KAAM,EAAmB,CAAC,CAAU,CAAC,CACvC,CAAC,EAAA,CACc,IAAI,CAAc,CACnC,CAEA,MAAM,cAAc,EAA4C,CAE9D,OAAO,MADe,KAAK,KAAK,EAAA,CACjB,OAAQ,GAAW,EAAO,YAAc,CAAS,CAClE,CAEA,MAAM,kBAAkB,EAAgD,CAEtE,OAAO,MADe,KAAK,KAAK,EAAA,CACjB,OAAQ,IAAY,EAAO,eAAiB,EAAO,aAAe,CAAa,CAChG,CAEA,MAAM,QAAQ,EAA6C,CACzD,KAAK,MAAM,kBAAkB,CAAS,EACtC,IAAM,EAA0B,CAAC,EAC3B,EAAY,KAAK,MAAM,eAAe,CAAS,EACrD,IAAK,IAAM,KAAS,EAAG,YAAY,CAAS,EACrC,KAAuB,CAAK,EACjC,GAAI,CACF,IAAM,EAAkB,KAAK,MAAM,EAAG,aAAa,GAAG,EAAU,GAAG,IAAS,MAAM,CAAC,EAC/E,EAAe,CAAM,GAAG,EAAQ,KAAK,CAAM,CACjD,OAAS,EAAO,CACd,QAAQ,YAAY,kCAAkC,EAAM,IAAI,OAAO,CAAK,GAAG,CACjF,CAEF,OAAO,EAAQ,MAAM,EAAM,IAAU,EAAM,UAAU,cAAc,EAAK,SAAS,CAAC,CACpF,CAEA,MAAM,IAAI,EAAY,EAAuD,CAC3E,IAAM,EAAS,KAAK,MAAM,aAAa,EAAI,CAAS,EACpD,GAAI,CACF,IAAM,EAAkB,KAAK,MAAM,EAAG,aAAa,EAAQ,MAAM,CAAC,EAClE,GAAI,CAAC,EAAe,CAAM,GAAK,EAAO,KAAO,EAAI,CAC/C,QAAQ,YAAY,kCAAkC,EAAK,SAAS,CAAM,EAAE,wBAAwB,EACpG,MACF,CACA,OAAO,CACT,OAAS,EAAO,CACd,GAAK,EAAgC,OAAS,SAAsB,OACpE,QAAQ,YAAY,kCAAkC,EAAK,SAAS,CAAM,EAAE,IAAI,OAAO,CAAK,GAAG,EAC/F,MACF,CACF,CAEA,MAAM,aAAa,EAA+C,CAChE,IAAM,EAAS,MAAM,KAAK,IAAI,CAAE,EAChC,GAAI,CAAC,EAAQ,OACb,IAAM,EAAW,CAAE,GAAG,EAAQ,SAAU,EAAK,EAG7C,OAFA,KAAK,YAAY,CAAQ,EACzB,KAAK,OAAO,EACL,CACT,CAEA,MAAM,WAAW,EAAY,EAAuD,CAClF,IAAM,EAAS,MAAM,KAAK,IAAI,EAAI,CAAS,EAC3C,GAAI,CAAC,EAAQ,OACb,GAAI,CAAC,EAAO,MAAO,OAAO,EAC1B,IAAM,EAAwB,CAAE,GAAG,CAAO,EAI1C,OAHA,OAAO,EAAQ,MACf,KAAK,YAAY,CAAO,EACxB,KAAK,OAAO,EACL,CACT,CAEA,MAAM,eAAe,EAAY,EAAoD,CACnF,IAAM,EAAS,MAAM,KAAK,IAAI,CAAE,EAChC,GAAI,CAAC,GAAQ,OAAS,EAAO,QAAU,UAAW,OAClD,IAAM,EAAsB,CAAE,GAAG,EAAQ,MAAO,CAAE,GAAG,EAAO,MAAO,YAAa,CAAQ,CAAE,EAG1F,OAFA,KAAK,YAAY,CAAK,EACtB,KAAK,OAAO,EACL,CACT,CAEA,MAAM,SAAS,EAAY,EAA8B,EAAuD,CAC9G,IAAM,EAAY,MAAM,KAAK,IAAI,EAAI,CAAS,EAC9C,GAAI,GAAW,QAAU,YAEvB,OADA,MAAM,KAAK,QAAQ,CAAE,EACd,EAET,IAAM,EAAS,MAAM,KAAK,KAAK,EACzB,EACJ,GACA,EAAO,KAAM,GAAc,EAAU,KAAO,IAAO,IAAc,IAAA,IAAa,EAAU,YAAc,EAAU,EAClH,GAAI,CAAC,EAAQ,CAET,IAAc,IAAA,IAAa,EAAO,KAAM,GAAc,EAAU,KAAO,GAAM,EAAU,YAAc,CAAS,GAClF,MAAM,KAAK,QAAQ,CAAE,EACnD,MACF,CACA,IAAM,EAA0B,CAC9B,GAAG,EACH,MAAO,YACP,KAAM,CAAE,GAAG,EAAS,WAAY,IAAI,KAAK,CAAA,CAAE,YAAY,CAAE,CAC3D,EAGA,OAFA,KAAK,YAAY,CAAS,EAC1B,MAAM,KAAK,QAAQ,CAAE,EACd,CACT,CAEA,MAAM,QAAQ,EAA2B,CACvC,IAAM,EAAW,MAAM,KAAK,SAAS,eAAe,CAClD,eAAgB,KAAK,eAAe,EACpC,YAAa,GAAG,IAAiB,IACjC,YAAa,EACb,KAAM,GACN,YAAa,GACb,MAAO,EACT,CAAC,EACD,GAAI,CAAC,EAAS,SAAW,CAAC,EAAS,OAAO,SAAS,2BAA2B,EAC5E,MAAU,MAAM,EAAS,OAAS,4BAA4B,GAAI,EAEpE,KAAK,OAAO,CACd,CAEA,MAAM,WAA+B,CACnC,IAAM,GAAQ,MAAM,KAAK,KAAK,EAAA,CAAG,OAAQ,GAAW,CAAC,KAAK,eAAe,QAAQ,EAAO,GAAG,CAAC,EAC5F,IAAK,IAAM,KAAU,EACnB,MAAM,KAAK,SAAS,EAAO,GAAI,CAAE,OAAQ,eAAgB,KAAM,KAAM,OAAQ,IAAK,EAAG,EAAO,SAAS,EAEvG,OAAO,EAAK,IAAK,GAAW,EAAO,EAAE,CACvC,CAEA,UAAU,EAAkC,CAE1C,OADA,KAAK,UAAU,IAAI,CAAQ,MACd,KAAK,UAAU,OAAO,CAAQ,CAC7C,CAEA,OAAc,CACZ,KAAK,UAAU,MAAM,EACrB,KAAK,SAAS,MAAM,CACtB,CAEA,QAAuB,CACrB,IAAK,IAAM,KAAY,KAAK,UAAW,EAAS,CAClD,CAEA,gBAAiC,CAC/B,OAAO,EAAwB,KAAK,MAAM,eAAe,CAAC,CAC5D,CAEA,YAAoB,EAA4B,CAC9C,KAAK,MAAM,kBAAkB,EAAO,SAAS,EAC7C,IAAM,EAAS,KAAK,MAAM,aAAa,EAAO,GAAI,EAAO,SAAS,EAC5D,EAAY,GAAG,EAAO,GAAG,QAAQ,IAAI,MAC3C,EAAG,cAAc,EAAW,GAAG,KAAK,UAAU,EAAQ,KAAM,CAAC,EAAE,IAAK,MAAM,EAC1E,EAAG,WAAW,EAAW,CAAM,CACjC,CACF,EAGA,SAAS,EAAe,EAA6C,CACnE,MAAO,CACL,GAAI,EAAW,EAAQ,IAAI,EAC3B,KAAM,EAAW,EAAQ,MAAM,EAC/B,IAAK,EAAO,IACZ,QAAS,EAAW,EAAQ,SAAS,EACrC,IAAK,EAAW,EAAQ,KAAK,EAC7B,QAAS,EAAW,EAAQ,SAAS,EACrC,YAAa,EAAO,UAAU,cAAgB,GAC9C,UAAW,EAAW,EAAQ,WAAW,EACzC,GAAI,EAAW,EAAQ,eAAe,EAAI,CAAE,cAAe,EAAW,EAAQ,eAAe,CAAE,EAAI,CAAC,EACpG,UAAW,EAAW,EAAQ,WAAW,EACzC,MAAO,UACP,SAAU,GACV,QAAS,EAAY,EAAO,UAAU,OAAO,EAC7C,GAAI,EAAW,EAAQ,eAAe,EAAI,CAAE,cAAe,EAAW,EAAQ,eAAe,CAAE,EAAI,CAAC,EACpG,QAAS,EAAW,EAAQ,SAAS,CACvC,CACF,CAEA,SAAS,EAAY,EAAyC,CAC5D,OAAO,IAAU,QAAU,IAAU,OAAS,EAAQ,QACxD,CAEA,SAAS,EAAuB,EAAwB,CACtD,OACE,EAAM,SAAS,OAAc,GAAK,CAAC,EAAM,SAAS,eAAsB,GAAK,CAAC,EAAM,SAAS,YAAmB,CAEpH"}
1
+ {"version":3,"file":"RunnerRegistry.mjs","names":[],"sources":["../../../../src/adapters/RunnerRegistry/RunnerRegistry.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { resolveRootSessionId } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IRunnerPaths } from '../../services/RunnerPaths/types';\nimport { isRunnerRecord } from '../../services/runs/runnerRecord.ts';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n} from '../../types/runnerRegistry';\n\nconst SERVICE_TYPE = 'tool';\nconst SERVICE_PREFIX = 'doom-runner:';\nconst RUNNER_TAG = 'doom-runner';\nconst REGISTRY_PATH_ENV = 'PROCESS_REGISTRY_PATH';\nconst JSON_EXTENSION = '.json';\nconst COMMAND_SIDECAR_SUFFIX = '.command.json';\nconst EXIT_SIDECAR_SUFFIX = '.exit.json';\nconst NOT_FOUND_ERROR_CODE = 'ENOENT';\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\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly listeners = new Set<() => void>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId);\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 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();\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();\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);\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);\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);\n return completed;\n }\n\n async release(id: string): Promise<void> {\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 this.notify();\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): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n close(): void {\n this.listeners.clear();\n this.registry.close();\n }\n\n private notify(): void {\n for (const listener of this.listeners) listener();\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"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- const e=require("../../types/config.cjs"),t=require("../../services/AnsiScrub/ansiScrub.cjs"),n=require("./responseEnvelope.cjs"),r=require("../../schemas/bashTool.cjs"),i=require("../../tui/bashRender.cjs"),a=1e3,o={requested:`Started in the background`,threshold:`Still running after the background threshold`,interactive:`Started interactively`},s=`Execute shell commands with bounded foreground output and supervised background runners`;function c(t=e.getBackgroundThresholdMs()){return[`A command still running after ${Math.round(t/a)} seconds remains active as a background runner with an id and streaming log path.`,`Pass background: true only for commands you know will remain active, such as dev servers, watchers, and tails.`,`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.`,`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.`,`Stop background runners when they are no longer needed. Every runner is stopped automatically when the session ends.`]}function l(e,t){e.registerTool({name:r.BASH_TOOL_NAME,label:r.BASH_TOOL_LABEL,description:`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.`,promptSnippet:s,promptGuidelines:c(),parameters:r.BashParamsSchema,renderShell:`self`,async execute(e,r,i,o,s){let{command:c,timeout:l,background:u,interactive:d,name:p}=r,m;o&&o(n.textResult(`Starting ${d===!0?`interactive runner`:u===!0?`background runner`:`command`}...`)),u!==!0&&d!==!0&&o&&(m=e=>o(n.textResult(e)));let h;try{h=await t.bashRunService.run({command:c,timeoutMs:l===void 0?void 0:l*a,background:u,interactive:d,name:p,...m?{onOutput:m}:{},sessionId:await t.getSessionId()})}catch(e){let t=e instanceof Error?e.message:String(e);throw Error([`Could not execute command: ${t}`,`Next: verify the command, runtime, and working directory. Retry only after correcting the cause.`].join(`
1
+ const e=require("../../types/config.cjs"),t=require("../../services/AnsiScrub/ansiScrub.cjs"),n=require("./responseEnvelope.cjs"),r=require("../../schemas/bashTool.cjs"),i=require("../../tui/bashRender.cjs"),a=1e3,o={requested:`Started in the background`,threshold:`Still running after the background threshold`,interactive:`Started interactively`},s=`Execute shell commands with bounded foreground output and supervised background runners`;function c(t=e.getBackgroundThresholdMs()){return[`A command still running after ${Math.round(t/a)} 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.`,`Pass background: true only for commands you know will remain active, such as dev servers, watchers, and tails.`,`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.`,`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.`,`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.`,`Stop background runners when they are no longer needed. Every runner is stopped automatically when the session ends.`]}function l(e,t){e.registerTool({name:r.BASH_TOOL_NAME,label:r.BASH_TOOL_LABEL,description:`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.`,promptSnippet:s,promptGuidelines:c(),parameters:r.BashParamsSchema,renderShell:`self`,async execute(e,r,i,o,s){let{command:c,timeout:l,background:u,interactive:d,name:p}=r,m;o&&o(n.textResult(`Starting ${d===!0?`interactive runner`:u===!0?`background runner`:`command`}...`)),u!==!0&&d!==!0&&o&&(m=e=>o(n.textResult(e)));let h;try{h=await t.bashRunService.run({command:c,timeoutMs:l===void 0?void 0:l*a,background:u,interactive:d,name:p,...m?{onOutput:m}:{},sessionId:await t.getSessionId()})}catch(e){let t=e instanceof Error?e.message:String(e);throw Error([`Could not execute command: ${t}`,`Next: verify the command, runtime, and working directory. Retry only after correcting the cause.`].join(`
2
2
  `),{cause:e})}return h.kind===`promoted`&&t.onRunnerStarted(h.id),f(h,n.parseResultPragma(r.command))},renderCall(e,t,n){return i.renderBashCall(e,t)},renderResult(e,t,n,r){return i.renderBashResult(e,{...t,isError:r.isError},n)}})}function u(e){return e.timedOut===!0||e.signal!==null||e.exitCode!==null&&e.exitCode!==0}function d(e){if(e.timedOut===!0)return`Timed out: exceeded the requested timeout.`;if(e.signal!==null)return`Signal: ${e.signal}`;if(e.exitCode===null)return`Exit status unavailable.`;if(e.exitCode!==0)return`Exit: ${e.exitCode}`}function f(r,i={}){if(r.kind===`failed`)throw Error([`Could not start runner "${r.name}": ${r.error}`,`Next: correct the reported launch or supervision problem. Retry only after changing the command or environment.`].join(`
3
3
  `));if(r.kind===`promoted`){let e=[`${o[r.reason]}: runner "${r.name}" (${r.id}).`,`Streaming log: ${r.logPath}`,`Inspect: doom-runner logs ${r.id}`].join(`
4
4
  `);return n.textResult(e,{id:r.id,runner:r.name,pid:r.pid,logPath:r.logPath,promoted:!0,reason:r.reason})}let a=!u(r),s=i.maxLines??e.getResultMaxLines(),c=i.maxBytes??(a?e.getSuccessResultMaxBytes():e.getResultMaxBytes()),l=i.maxTokens??(a?e.getSuccessResultMaxTokens():e.getResultMaxTokens()),f=n.summarizeLog(r.logPath,s,c,l),p=!r.rtkOutput&&f.tail.length===0&&r.output.length>0,m,h,g,_,v;if(r.rtkOutput){let e=Buffer.byteLength(r.rtkOutput.output,`utf8`)<r.rtkOutput.bytes?n.composeExcerpt(r.rtkOutput.head,r.rtkOutput.output,r.rtkOutput.lines,s,c,[],l):n.boundExcerpt(r.rtkOutput.output,s,c,l);m=e.text,h=e.lines,g=r.rtkOutput.lines,_=r.rtkOutput.bytes,v=g>h||_>Buffer.byteLength(m,`utf8`)}else m=p?r.output:f.tail,h=p?n.countLines(m):f.tailLines,g=Math.max(f.lines,h),_=p?Buffer.byteLength(m,`utf8`):f.bytes,v=!p&&(g>h||_>Buffer.byteLength(m,`utf8`));let y=t.stripAnsi(m).replace(/\r?\n$/,``),b=u(r),x=d(r),S=[];if(y.length===0)S.push(x===void 0?`Completed with no output.`:`No output.`);else if(v){let e=r.rtkOutput?`RTK ${r.rtkOutput.filter} excerpt`:`Log excerpt`;S.push(`${e} (${h.toLocaleString(`en-US`)} of ${g.toLocaleString(`en-US`)} lines):\n${y}`)}else S.push(y);if(x!==void 0&&S.push(x),r.rtkWarning&&S.push(r.rtkWarning),v){let e=r.rtkOutput?`Complete raw log`:`Full log`;S.push(`${e}: ${r.logPath} (${n.formatSize(f.bytes)}, ${f.lines.toLocaleString(`en-US`)} lines); inspect with doom-runner logs ${r.id}`)}else b&&y.length===0&&S.push(`Log: ${r.logPath}`,`Next: run one read-only diagnostic; retry only after correcting the cause.`);let C=n.boundResultText(S.join(`
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool.cjs","names":["getBackgroundThresholdMs","BASH_TOOL_NAME","BASH_TOOL_LABEL","BashParamsSchema","textResult","parseResultPragma","renderBashCall","renderBashResult","getResultMaxLines","getSuccessResultMaxBytes","getResultMaxBytes","getSuccessResultMaxTokens","getResultMaxTokens","summarizeLog","composeExcerpt","boundExcerpt","countLines","stripAnsi","formatSize","boundResultText"],"sources":["../../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/ansiScrub';\nimport type { BashRunResult, CompletedRun, IBashRunService } from '../../types/bashRunService';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport {\n getBackgroundThresholdMs,\n getResultMaxBytes,\n getResultMaxLines,\n getResultMaxTokens,\n getSuccessResultMaxBytes,\n getSuccessResultMaxTokens,\n} from '../../types/config.ts';\nimport {\n boundExcerpt,\n boundResultText,\n composeExcerpt,\n parseResultPragma,\n type ResultBudget,\n countLines,\n formatSize,\n summarizeLog,\n type ToolResult,\n textResult,\n} from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold',\n interactive: 'Started interactively',\n};\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.`,\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 '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\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string | Promise<string>;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\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 registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\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 sessionId: await dependencies.getSessionId(),\n });\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));\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, context) {\n return renderBashResult(result, { ...options, isError: context.isError }, theme);\n },\n });\n}\n\nfunction completionFailed(result: CompletedRun): boolean {\n return result.timedOut === true || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0);\n}\n\nfunction completionStatus(result: CompletedRun): string | undefined {\n if (result.timedOut === true) return 'Timed out: exceeded the requested timeout.';\n if (result.signal !== null) return `Signal: ${result.signal}`;\n if (result.exitCode === null) return 'Exit status unavailable.';\n if (result.exitCode !== 0) return `Exit: ${result.exitCode}`;\n return undefined;\n}\n\nexport function formatRunResult(result: BashRunResult, budget: ResultBudget = {}): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n 'Next: correct the reported launch or supervision problem. Retry only after changing the command or environment.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason}: runner \"${result.name}\" (${result.id}).`,\n `Streaming log: ${result.logPath}`,\n `Inspect: doom-runner logs ${result.id}`,\n ].join('\\n');\n\n return textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n });\n }\n\n // Same shape either way; a success just buys less of it. Exiting 0 has already\n // reported the outcome, so its output is worth a fraction of a failure's.\n const succeeded = !completionFailed(result);\n const maxLines = budget.maxLines ?? getResultMaxLines();\n // An explicit pragma still wins: asking for a wider result is the point of it.\n const maxBytes = budget.maxBytes ?? (succeeded ? getSuccessResultMaxBytes() : getResultMaxBytes());\n const maxTokens = budget.maxTokens ?? (succeeded ? getSuccessResultMaxTokens() : getResultMaxTokens());\n const log = summarizeLog(result.logPath, maxLines, maxBytes, maxTokens);\n const useCapturedOutput = !result.rtkOutput && log.tail.length === 0 && result.output.length > 0;\n let tail: string;\n let tailLines: number;\n let outputLines: number;\n let outputBytes: number;\n let truncated: boolean;\n if (result.rtkOutput) {\n const clipped = Buffer.byteLength(result.rtkOutput.output, 'utf8') < result.rtkOutput.bytes;\n const bounded = clipped\n ? composeExcerpt(\n result.rtkOutput.head,\n result.rtkOutput.output,\n result.rtkOutput.lines,\n maxLines,\n maxBytes,\n [],\n maxTokens,\n )\n : boundExcerpt(result.rtkOutput.output, maxLines, maxBytes, maxTokens);\n tail = bounded.text;\n tailLines = bounded.lines;\n outputLines = result.rtkOutput.lines;\n outputBytes = result.rtkOutput.bytes;\n truncated = outputLines > tailLines || outputBytes > Buffer.byteLength(tail, 'utf8');\n } else {\n tail = useCapturedOutput ? result.output : log.tail;\n tailLines = useCapturedOutput ? countLines(tail) : log.tailLines;\n outputLines = Math.max(log.lines, tailLines);\n outputBytes = useCapturedOutput ? Buffer.byteLength(tail, 'utf8') : log.bytes;\n truncated = !useCapturedOutput && (outputLines > tailLines || outputBytes > Buffer.byteLength(tail, 'utf8'));\n }\n const plainTail = stripAnsi(tail).replace(/\\r?\\n$/, '');\n const failed = completionFailed(result);\n const status = completionStatus(result);\n const textLines: string[] = [];\n\n if (plainTail.length === 0) {\n textLines.push(status === undefined ? 'Completed with no output.' : 'No output.');\n } else if (truncated) {\n const label = result.rtkOutput ? `RTK ${result.rtkOutput.filter} excerpt` : 'Log excerpt';\n textLines.push(\n `${label} (${tailLines.toLocaleString('en-US')} of ${outputLines.toLocaleString('en-US')} lines):\\n${plainTail}`,\n );\n } else {\n textLines.push(plainTail);\n }\n if (status !== undefined) textLines.push(status);\n if (result.rtkWarning) textLines.push(result.rtkWarning);\n\n if (truncated) {\n const label = result.rtkOutput ? 'Complete raw log' : 'Full log';\n textLines.push(\n `${label}: ${result.logPath} (${formatSize(log.bytes)}, ${log.lines.toLocaleString('en-US')} lines); inspect with doom-runner logs ${result.id}`,\n );\n } else if (failed && plainTail.length === 0) {\n textLines.push(\n `Log: ${result.logPath}`,\n 'Next: run one read-only diagnostic; retry only after correcting the cause.',\n );\n }\n\n const text = boundResultText(textLines.join('\\n'), maxBytes);\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: useCapturedOutput ? tailLines : log.lines,\n tail,\n tailLines,\n ...(result.rtkOutput\n ? {\n rtkFilter: result.rtkOutput.filter,\n rtkOutputBytes: result.rtkOutput.bytes,\n rtkOutputLines: result.rtkOutput.lines,\n }\n : {}),\n ...(result.rtkWarning ? { rtkWarning: result.rtkWarning } : {}),\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n\n if (failed) throw new Error(text);\n return textResult(text, details);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bashTool.cjs","names":["getBackgroundThresholdMs","BASH_TOOL_NAME","BASH_TOOL_LABEL","BashParamsSchema","textResult","parseResultPragma","renderBashCall","renderBashResult","getResultMaxLines","getSuccessResultMaxBytes","getResultMaxBytes","getSuccessResultMaxTokens","getResultMaxTokens","summarizeLog","composeExcerpt","boundExcerpt","countLines","stripAnsi","formatSize","boundResultText"],"sources":["../../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/ansiScrub';\nimport type { BashRunResult, CompletedRun, IBashRunService } from '../../types/bashRunService';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport {\n getBackgroundThresholdMs,\n getResultMaxBytes,\n getResultMaxLines,\n getResultMaxTokens,\n getSuccessResultMaxBytes,\n getSuccessResultMaxTokens,\n} from '../../types/config.ts';\nimport {\n boundExcerpt,\n boundResultText,\n composeExcerpt,\n parseResultPragma,\n type ResultBudget,\n countLines,\n formatSize,\n summarizeLog,\n type ToolResult,\n textResult,\n} from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold',\n interactive: 'Started interactively',\n};\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\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string | Promise<string>;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\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 registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\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 sessionId: await dependencies.getSessionId(),\n });\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));\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, context) {\n return renderBashResult(result, { ...options, isError: context.isError }, theme);\n },\n });\n}\n\nfunction completionFailed(result: CompletedRun): boolean {\n return result.timedOut === true || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0);\n}\n\nfunction completionStatus(result: CompletedRun): string | undefined {\n if (result.timedOut === true) return 'Timed out: exceeded the requested timeout.';\n if (result.signal !== null) return `Signal: ${result.signal}`;\n if (result.exitCode === null) return 'Exit status unavailable.';\n if (result.exitCode !== 0) return `Exit: ${result.exitCode}`;\n return undefined;\n}\n\nexport function formatRunResult(result: BashRunResult, budget: ResultBudget = {}): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n 'Next: correct the reported launch or supervision problem. Retry only after changing the command or environment.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason}: runner \"${result.name}\" (${result.id}).`,\n `Streaming log: ${result.logPath}`,\n `Inspect: doom-runner logs ${result.id}`,\n ].join('\\n');\n\n return textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n });\n }\n\n // Same shape either way; a success just buys less of it. Exiting 0 has already\n // reported the outcome, so its output is worth a fraction of a failure's.\n const succeeded = !completionFailed(result);\n const maxLines = budget.maxLines ?? getResultMaxLines();\n // An explicit pragma still wins: asking for a wider result is the point of it.\n const maxBytes = budget.maxBytes ?? (succeeded ? getSuccessResultMaxBytes() : getResultMaxBytes());\n const maxTokens = budget.maxTokens ?? (succeeded ? getSuccessResultMaxTokens() : getResultMaxTokens());\n const log = summarizeLog(result.logPath, maxLines, maxBytes, maxTokens);\n const useCapturedOutput = !result.rtkOutput && log.tail.length === 0 && result.output.length > 0;\n let tail: string;\n let tailLines: number;\n let outputLines: number;\n let outputBytes: number;\n let truncated: boolean;\n if (result.rtkOutput) {\n const clipped = Buffer.byteLength(result.rtkOutput.output, 'utf8') < result.rtkOutput.bytes;\n const bounded = clipped\n ? composeExcerpt(\n result.rtkOutput.head,\n result.rtkOutput.output,\n result.rtkOutput.lines,\n maxLines,\n maxBytes,\n [],\n maxTokens,\n )\n : boundExcerpt(result.rtkOutput.output, maxLines, maxBytes, maxTokens);\n tail = bounded.text;\n tailLines = bounded.lines;\n outputLines = result.rtkOutput.lines;\n outputBytes = result.rtkOutput.bytes;\n truncated = outputLines > tailLines || outputBytes > Buffer.byteLength(tail, 'utf8');\n } else {\n tail = useCapturedOutput ? result.output : log.tail;\n tailLines = useCapturedOutput ? countLines(tail) : log.tailLines;\n outputLines = Math.max(log.lines, tailLines);\n outputBytes = useCapturedOutput ? Buffer.byteLength(tail, 'utf8') : log.bytes;\n truncated = !useCapturedOutput && (outputLines > tailLines || outputBytes > Buffer.byteLength(tail, 'utf8'));\n }\n const plainTail = stripAnsi(tail).replace(/\\r?\\n$/, '');\n const failed = completionFailed(result);\n const status = completionStatus(result);\n const textLines: string[] = [];\n\n if (plainTail.length === 0) {\n textLines.push(status === undefined ? 'Completed with no output.' : 'No output.');\n } else if (truncated) {\n const label = result.rtkOutput ? `RTK ${result.rtkOutput.filter} excerpt` : 'Log excerpt';\n textLines.push(\n `${label} (${tailLines.toLocaleString('en-US')} of ${outputLines.toLocaleString('en-US')} lines):\\n${plainTail}`,\n );\n } else {\n textLines.push(plainTail);\n }\n if (status !== undefined) textLines.push(status);\n if (result.rtkWarning) textLines.push(result.rtkWarning);\n\n if (truncated) {\n const label = result.rtkOutput ? 'Complete raw log' : 'Full log';\n textLines.push(\n `${label}: ${result.logPath} (${formatSize(log.bytes)}, ${log.lines.toLocaleString('en-US')} lines); inspect with doom-runner logs ${result.id}`,\n );\n } else if (failed && plainTail.length === 0) {\n textLines.push(\n `Log: ${result.logPath}`,\n 'Next: run one read-only diagnostic; retry only after correcting the cause.',\n );\n }\n\n const text = boundResultText(textLines.join('\\n'), maxBytes);\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: useCapturedOutput ? tailLines : log.lines,\n tail,\n tailLines,\n ...(result.rtkOutput\n ? {\n rtkFilter: result.rtkOutput.filter,\n rtkOutputBytes: result.rtkOutput.bytes,\n rtkOutputLines: result.rtkOutput.lines,\n }\n : {}),\n ...(result.rtkWarning ? { rtkWarning: result.rtkWarning } : {}),\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n\n if (failed) throw new Error(text);\n return textResult(text, details);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool.d.cts","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"mappings":";;;;qBAkCa;;wBAIG,qBAAqB;iBAUpB;EACf,gBAAgB;EAChB,yBAAyB;;EAEzB,gBAAgB;;;;;;;;wBASF,iBAAiB,IAAI,cAAc,cAAc;wBA2EjD,gBAAgB,QAAQ,eAAe,SAAQ,eAAoB"}
1
+ {"version":3,"file":"bashTool.d.cts","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"mappings":";;;;qBAkCa;;wBAIG,qBAAqB;iBAWpB;EACf,gBAAgB;EAChB,yBAAyB;;EAEzB,gBAAgB;;;;;;;;wBASF,iBAAiB,IAAI,cAAc,cAAc;wBA2EjD,gBAAgB,QAAQ,eAAe,SAAQ,eAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool.d.mts","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"mappings":";;;;qBAkCa;;wBAIG,qBAAqB;iBAUpB;EACf,gBAAgB;EAChB,yBAAyB;;EAEzB,gBAAgB;;;;;;;;wBASF,iBAAiB,IAAI,cAAc,cAAc;wBA2EjD,gBAAgB,QAAQ,eAAe,SAAQ,eAAoB"}
1
+ {"version":3,"file":"bashTool.d.mts","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"mappings":";;;;qBAkCa;;wBAIG,qBAAqB;iBAWpB;EACf,gBAAgB;EAChB,yBAAyB;;EAEzB,gBAAgB;;;;;;;;wBASF,iBAAiB,IAAI,cAAc,cAAc;wBA2EjD,gBAAgB,QAAQ,eAAe,SAAQ,eAAoB"}
@@ -1,4 +1,4 @@
1
- import{getBackgroundThresholdMs as e,getResultMaxBytes as t,getResultMaxLines as n,getResultMaxTokens as r,getSuccessResultMaxBytes as i,getSuccessResultMaxTokens as a}from"../../types/config.mjs";import{stripAnsi as o}from"../../services/AnsiScrub/ansiScrub.mjs";import{boundExcerpt as s,boundResultText as c,composeExcerpt as l,countLines as u,formatSize as d,parseResultPragma as f,summarizeLog as p,textResult as m}from"./responseEnvelope.mjs";import{BASH_TOOL_LABEL as h,BASH_TOOL_NAME as g,BashParamsSchema as _}from"../../schemas/bashTool.mjs";import{renderBashCall as v,renderBashResult as y}from"../../tui/bashRender.mjs";const b=1e3,x={requested:`Started in the background`,threshold:`Still running after the background threshold`,interactive:`Started interactively`},S=`Execute shell commands with bounded foreground output and supervised background runners`;function C(t=e()){return[`A command still running after ${Math.round(t/b)} seconds remains active as a background runner with an id and streaming log path.`,`Pass background: true only for commands you know will remain active, such as dev servers, watchers, and tails.`,`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.`,`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.`,`Stop background runners when they are no longer needed. Every runner is stopped automatically when the session ends.`]}function w(e,t){e.registerTool({name:g,label:h,description:`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.`,promptSnippet:S,promptGuidelines:C(),parameters:_,renderShell:`self`,async execute(e,n,r,i,a){let{command:o,timeout:s,background:c,interactive:l,name:u}=n,d;i&&i(m(`Starting ${l===!0?`interactive runner`:c===!0?`background runner`:`command`}...`)),c!==!0&&l!==!0&&i&&(d=e=>i(m(e)));let p;try{p=await t.bashRunService.run({command:o,timeoutMs:s===void 0?void 0:s*b,background:c,interactive:l,name:u,...d?{onOutput:d}:{},sessionId:await t.getSessionId()})}catch(e){let t=e instanceof Error?e.message:String(e);throw Error([`Could not execute command: ${t}`,`Next: verify the command, runtime, and working directory. Retry only after correcting the cause.`].join(`
1
+ import{getBackgroundThresholdMs as e,getResultMaxBytes as t,getResultMaxLines as n,getResultMaxTokens as r,getSuccessResultMaxBytes as i,getSuccessResultMaxTokens as a}from"../../types/config.mjs";import{stripAnsi as o}from"../../services/AnsiScrub/ansiScrub.mjs";import{boundExcerpt as s,boundResultText as c,composeExcerpt as l,countLines as u,formatSize as d,parseResultPragma as f,summarizeLog as p,textResult as m}from"./responseEnvelope.mjs";import{BASH_TOOL_LABEL as h,BASH_TOOL_NAME as g,BashParamsSchema as _}from"../../schemas/bashTool.mjs";import{renderBashCall as v,renderBashResult as y}from"../../tui/bashRender.mjs";const b=1e3,x={requested:`Started in the background`,threshold:`Still running after the background threshold`,interactive:`Started interactively`},S=`Execute shell commands with bounded foreground output and supervised background runners`;function C(t=e()){return[`A command still running after ${Math.round(t/b)} 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.`,`Pass background: true only for commands you know will remain active, such as dev servers, watchers, and tails.`,`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.`,`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.`,`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.`,`Stop background runners when they are no longer needed. Every runner is stopped automatically when the session ends.`]}function w(e,t){e.registerTool({name:g,label:h,description:`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.`,promptSnippet:S,promptGuidelines:C(),parameters:_,renderShell:`self`,async execute(e,n,r,i,a){let{command:o,timeout:s,background:c,interactive:l,name:u}=n,d;i&&i(m(`Starting ${l===!0?`interactive runner`:c===!0?`background runner`:`command`}...`)),c!==!0&&l!==!0&&i&&(d=e=>i(m(e)));let p;try{p=await t.bashRunService.run({command:o,timeoutMs:s===void 0?void 0:s*b,background:c,interactive:l,name:u,...d?{onOutput:d}:{},sessionId:await t.getSessionId()})}catch(e){let t=e instanceof Error?e.message:String(e);throw Error([`Could not execute command: ${t}`,`Next: verify the command, runtime, and working directory. Retry only after correcting the cause.`].join(`
2
2
  `),{cause:e})}return p.kind===`promoted`&&t.onRunnerStarted(p.id),D(p,f(n.command))},renderCall(e,t,n){return v(e,t)},renderResult(e,t,n,r){return y(e,{...t,isError:r.isError},n)}})}function T(e){return e.timedOut===!0||e.signal!==null||e.exitCode!==null&&e.exitCode!==0}function E(e){if(e.timedOut===!0)return`Timed out: exceeded the requested timeout.`;if(e.signal!==null)return`Signal: ${e.signal}`;if(e.exitCode===null)return`Exit status unavailable.`;if(e.exitCode!==0)return`Exit: ${e.exitCode}`}function D(e,f={}){if(e.kind===`failed`)throw Error([`Could not start runner "${e.name}": ${e.error}`,`Next: correct the reported launch or supervision problem. Retry only after changing the command or environment.`].join(`
3
3
  `));if(e.kind===`promoted`){let t=[`${x[e.reason]}: runner "${e.name}" (${e.id}).`,`Streaming log: ${e.logPath}`,`Inspect: doom-runner logs ${e.id}`].join(`
4
4
  `);return m(t,{id:e.id,runner:e.name,pid:e.pid,logPath:e.logPath,promoted:!0,reason:e.reason})}let h=!T(e),g=f.maxLines??n(),_=f.maxBytes??(h?i():t()),v=f.maxTokens??(h?a():r()),y=p(e.logPath,g,_,v),b=!e.rtkOutput&&y.tail.length===0&&e.output.length>0,S,C,w,D,O;if(e.rtkOutput){let t=Buffer.byteLength(e.rtkOutput.output,`utf8`)<e.rtkOutput.bytes?l(e.rtkOutput.head,e.rtkOutput.output,e.rtkOutput.lines,g,_,[],v):s(e.rtkOutput.output,g,_,v);S=t.text,C=t.lines,w=e.rtkOutput.lines,D=e.rtkOutput.bytes,O=w>C||D>Buffer.byteLength(S,`utf8`)}else S=b?e.output:y.tail,C=b?u(S):y.tailLines,w=Math.max(y.lines,C),D=b?Buffer.byteLength(S,`utf8`):y.bytes,O=!b&&(w>C||D>Buffer.byteLength(S,`utf8`));let k=o(S).replace(/\r?\n$/,``),A=T(e),j=E(e),M=[];if(k.length===0)M.push(j===void 0?`Completed with no output.`:`No output.`);else if(O){let t=e.rtkOutput?`RTK ${e.rtkOutput.filter} excerpt`:`Log excerpt`;M.push(`${t} (${C.toLocaleString(`en-US`)} of ${w.toLocaleString(`en-US`)} lines):\n${k}`)}else M.push(k);if(j!==void 0&&M.push(j),e.rtkWarning&&M.push(e.rtkWarning),O){let t=e.rtkOutput?`Complete raw log`:`Full log`;M.push(`${t}: ${e.logPath} (${d(y.bytes)}, ${y.lines.toLocaleString(`en-US`)} lines); inspect with doom-runner logs ${e.id}`)}else A&&k.length===0&&M.push(`Log: ${e.logPath}`,`Next: run one read-only diagnostic; retry only after correcting the cause.`);let N=c(M.join(`
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool.mjs","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/ansiScrub';\nimport type { BashRunResult, CompletedRun, IBashRunService } from '../../types/bashRunService';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport {\n getBackgroundThresholdMs,\n getResultMaxBytes,\n getResultMaxLines,\n getResultMaxTokens,\n getSuccessResultMaxBytes,\n getSuccessResultMaxTokens,\n} from '../../types/config.ts';\nimport {\n boundExcerpt,\n boundResultText,\n composeExcerpt,\n parseResultPragma,\n type ResultBudget,\n countLines,\n formatSize,\n summarizeLog,\n type ToolResult,\n textResult,\n} from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold',\n interactive: 'Started interactively',\n};\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.`,\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 '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\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string | Promise<string>;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\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 registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\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 sessionId: await dependencies.getSessionId(),\n });\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));\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, context) {\n return renderBashResult(result, { ...options, isError: context.isError }, theme);\n },\n });\n}\n\nfunction completionFailed(result: CompletedRun): boolean {\n return result.timedOut === true || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0);\n}\n\nfunction completionStatus(result: CompletedRun): string | undefined {\n if (result.timedOut === true) return 'Timed out: exceeded the requested timeout.';\n if (result.signal !== null) return `Signal: ${result.signal}`;\n if (result.exitCode === null) return 'Exit status unavailable.';\n if (result.exitCode !== 0) return `Exit: ${result.exitCode}`;\n return undefined;\n}\n\nexport function formatRunResult(result: BashRunResult, budget: ResultBudget = {}): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n 'Next: correct the reported launch or supervision problem. Retry only after changing the command or environment.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason}: runner \"${result.name}\" (${result.id}).`,\n `Streaming log: ${result.logPath}`,\n `Inspect: doom-runner logs ${result.id}`,\n ].join('\\n');\n\n return textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n });\n }\n\n // Same shape either way; a success just buys less of it. Exiting 0 has already\n // reported the outcome, so its output is worth a fraction of a failure's.\n const succeeded = !completionFailed(result);\n const maxLines = budget.maxLines ?? getResultMaxLines();\n // An explicit pragma still wins: asking for a wider result is the point of it.\n const maxBytes = budget.maxBytes ?? (succeeded ? getSuccessResultMaxBytes() : getResultMaxBytes());\n const maxTokens = budget.maxTokens ?? (succeeded ? getSuccessResultMaxTokens() : getResultMaxTokens());\n const log = summarizeLog(result.logPath, maxLines, maxBytes, maxTokens);\n const useCapturedOutput = !result.rtkOutput && log.tail.length === 0 && result.output.length > 0;\n let tail: string;\n let tailLines: number;\n let outputLines: number;\n let outputBytes: number;\n let truncated: boolean;\n if (result.rtkOutput) {\n const clipped = Buffer.byteLength(result.rtkOutput.output, 'utf8') < result.rtkOutput.bytes;\n const bounded = clipped\n ? composeExcerpt(\n result.rtkOutput.head,\n result.rtkOutput.output,\n result.rtkOutput.lines,\n maxLines,\n maxBytes,\n [],\n maxTokens,\n )\n : boundExcerpt(result.rtkOutput.output, maxLines, maxBytes, maxTokens);\n tail = bounded.text;\n tailLines = bounded.lines;\n outputLines = result.rtkOutput.lines;\n outputBytes = result.rtkOutput.bytes;\n truncated = outputLines > tailLines || outputBytes > Buffer.byteLength(tail, 'utf8');\n } else {\n tail = useCapturedOutput ? result.output : log.tail;\n tailLines = useCapturedOutput ? countLines(tail) : log.tailLines;\n outputLines = Math.max(log.lines, tailLines);\n outputBytes = useCapturedOutput ? Buffer.byteLength(tail, 'utf8') : log.bytes;\n truncated = !useCapturedOutput && (outputLines > tailLines || outputBytes > Buffer.byteLength(tail, 'utf8'));\n }\n const plainTail = stripAnsi(tail).replace(/\\r?\\n$/, '');\n const failed = completionFailed(result);\n const status = completionStatus(result);\n const textLines: string[] = [];\n\n if (plainTail.length === 0) {\n textLines.push(status === undefined ? 'Completed with no output.' : 'No output.');\n } else if (truncated) {\n const label = result.rtkOutput ? `RTK ${result.rtkOutput.filter} excerpt` : 'Log excerpt';\n textLines.push(\n `${label} (${tailLines.toLocaleString('en-US')} of ${outputLines.toLocaleString('en-US')} lines):\\n${plainTail}`,\n );\n } else {\n textLines.push(plainTail);\n }\n if (status !== undefined) textLines.push(status);\n if (result.rtkWarning) textLines.push(result.rtkWarning);\n\n if (truncated) {\n const label = result.rtkOutput ? 'Complete raw log' : 'Full log';\n textLines.push(\n `${label}: ${result.logPath} (${formatSize(log.bytes)}, ${log.lines.toLocaleString('en-US')} lines); inspect with doom-runner logs ${result.id}`,\n );\n } else if (failed && plainTail.length === 0) {\n textLines.push(\n `Log: ${result.logPath}`,\n 'Next: run one read-only diagnostic; retry only after correcting the cause.',\n );\n }\n\n const text = boundResultText(textLines.join('\\n'), maxBytes);\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: useCapturedOutput ? tailLines : log.lines,\n tail,\n tailLines,\n ...(result.rtkOutput\n ? {\n rtkFilter: result.rtkOutput.filter,\n rtkOutputBytes: result.rtkOutput.bytes,\n rtkOutputLines: result.rtkOutput.lines,\n }\n : {}),\n ...(result.rtkWarning ? { rtkWarning: result.rtkWarning } : {}),\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n\n if (failed) throw new Error(text);\n return textResult(text, details);\n}\n"],"mappings":"unBA0BA,MAAM,EAAgB,IAEhB,EAA+E,CACnF,UAAW,4BACX,UAAW,+CACX,YAAa,uBACf,EAEa,EACX,0FAGF,SAAgB,EAAqB,EAAc,EAAyB,EAAa,CACvF,MAAO,CACL,iCAAiC,KAAK,MAAM,EAAc,CAAa,EAAE,mFACzE,iHACA,0KACA,+MACA,sHACF,CACF,CAeA,SAAgB,EAAiB,EAAkB,EAA0C,CAC3F,EAAG,aAAa,CACd,KAAM,EACN,MAAO,EACP,YACE,0TACF,cAAe,EACf,iBAAkB,EAAqB,EACvC,WAAY,EAIZ,YAAa,OAEb,MAAM,QAAQ,EAAa,EAAQ,EAAS,EAAU,EAA2B,CAC/E,GAAM,CAAE,UAAS,UAAS,aAAY,cAAa,QAAS,EACxD,EACA,GAGF,EAAS,EAAW,YADlB,IAAgB,GAAO,qBAAuB,IAAe,GAAO,oBAAsB,UACvD,IAAI,CAAC,EAExC,IAAe,IAAQ,IAAgB,IAAQ,IACjD,EAAY,GAAW,EAAS,EAAW,CAAM,CAAC,GAEpD,IAAI,EACJ,GAAI,CACF,EAAS,MAAM,EAAa,eAAe,IAAI,CAC7C,UACA,UAAW,IAAY,IAAA,GAAY,IAAA,GAAY,EAAU,EACzD,aACA,cACA,OACA,GAAI,EAAW,CAAE,UAAS,EAAI,CAAC,EAC/B,UAAW,MAAM,EAAa,aAAa,CAC7C,CAAC,CACH,OAAS,EAAO,CACd,IAAM,EAAU,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EACrE,MAAU,MACR,CACE,8BAA8B,IAC9B,kGACF,CAAC,CAAC,KAAK;CAAI,EACX,CAAE,MAAO,CAAM,CACjB,CACF,CAGA,OADI,EAAO,OAAS,YAAY,EAAa,gBAAgB,EAAO,EAAE,EAC/D,EAAgB,EAAQ,EAAkB,EAAO,OAAO,CAAC,CAClE,EAIA,WAAW,EAAM,EAAO,EAAU,CAChC,OAAO,EAAe,EAAoB,CAAK,CACjD,EAEA,aAAa,EAAQ,EAAS,EAAO,EAAS,CAC5C,OAAO,EAAiB,EAAQ,CAAE,GAAG,EAAS,QAAS,EAAQ,OAAQ,EAAG,CAAK,CACjF,CACF,CAAC,CACH,CAEA,SAAS,EAAiB,EAA+B,CACvD,OAAO,EAAO,WAAa,IAAQ,EAAO,SAAW,MAAS,EAAO,WAAa,MAAQ,EAAO,WAAa,CAChH,CAEA,SAAS,EAAiB,EAA0C,CAClE,GAAI,EAAO,WAAa,GAAM,MAAO,6CACrC,GAAI,EAAO,SAAW,KAAM,MAAO,WAAW,EAAO,SACrD,GAAI,EAAO,WAAa,KAAM,MAAO,2BACrC,GAAI,EAAO,WAAa,EAAG,MAAO,SAAS,EAAO,UAEpD,CAEA,SAAgB,EAAgB,EAAuB,EAAuB,CAAC,EAAe,CAC5F,GAAI,EAAO,OAAS,SAClB,MAAU,MACR,CACE,2BAA2B,EAAO,KAAK,KAAK,EAAO,QACnD,iHACF,CAAC,CAAC,KAAK;CAAI,CACb,EAGF,GAAI,EAAO,OAAS,WAAY,CAE9B,IAAM,EAAO,CACX,GAFa,EAAkB,EAAO,QAE5B,YAAY,EAAO,KAAK,KAAK,EAAO,GAAG,IACjD,kBAAkB,EAAO,UACzB,6BAA6B,EAAO,IACtC,CAAC,CAAC,KAAK;CAAI,EAEX,OAAO,EAAW,EAAM,CACtB,GAAI,EAAO,GACX,OAAQ,EAAO,KACf,IAAK,EAAO,IACZ,QAAS,EAAO,QAChB,SAAU,GACV,OAAQ,EAAO,MACjB,CAAC,CACH,CAIA,IAAM,EAAY,CAAC,EAAiB,CAAM,EACpC,EAAW,EAAO,UAAY,EAAkB,EAEhD,EAAW,EAAO,WAAa,EAAY,EAAyB,EAAI,EAAkB,GAC1F,EAAY,EAAO,YAAc,EAAY,EAA0B,EAAI,EAAmB,GAC9F,EAAM,EAAa,EAAO,QAAS,EAAU,EAAU,CAAS,EAChE,EAAoB,CAAC,EAAO,WAAa,EAAI,KAAK,SAAW,GAAK,EAAO,OAAO,OAAS,EAC3F,EACA,EACA,EACA,EACA,EACJ,GAAI,EAAO,UAAW,CAEpB,IAAM,EADU,OAAO,WAAW,EAAO,UAAU,OAAQ,MAAM,EAAI,EAAO,UAAU,MAElF,EACE,EAAO,UAAU,KACjB,EAAO,UAAU,OACjB,EAAO,UAAU,MACjB,EACA,EACA,CAAC,EACD,CACF,EACA,EAAa,EAAO,UAAU,OAAQ,EAAU,EAAU,CAAS,EACvE,EAAO,EAAQ,KACf,EAAY,EAAQ,MACpB,EAAc,EAAO,UAAU,MAC/B,EAAc,EAAO,UAAU,MAC/B,EAAY,EAAc,GAAa,EAAc,OAAO,WAAW,EAAM,MAAM,CACrF,KACE,GAAO,EAAoB,EAAO,OAAS,EAAI,KAC/C,EAAY,EAAoB,EAAW,CAAI,EAAI,EAAI,UACvD,EAAc,KAAK,IAAI,EAAI,MAAO,CAAS,EAC3C,EAAc,EAAoB,OAAO,WAAW,EAAM,MAAM,EAAI,EAAI,MACxE,EAAY,CAAC,IAAsB,EAAc,GAAa,EAAc,OAAO,WAAW,EAAM,MAAM,GAE5G,IAAM,EAAY,EAAU,CAAI,CAAC,CAAC,QAAQ,SAAU,EAAE,EAChD,EAAS,EAAiB,CAAM,EAChC,EAAS,EAAiB,CAAM,EAChC,EAAsB,CAAC,EAE7B,GAAI,EAAU,SAAW,EACvB,EAAU,KAAK,IAAW,IAAA,GAAY,4BAA8B,YAAY,OAC3E,GAAI,EAAW,CACpB,IAAM,EAAQ,EAAO,UAAY,OAAO,EAAO,UAAU,OAAO,UAAY,cAC5E,EAAU,KACR,GAAG,EAAM,IAAI,EAAU,eAAe,OAAO,EAAE,MAAM,EAAY,eAAe,OAAO,EAAE,YAAY,GACvG,CACF,MACE,EAAU,KAAK,CAAS,EAK1B,GAHI,IAAW,IAAA,IAAW,EAAU,KAAK,CAAM,EAC3C,EAAO,YAAY,EAAU,KAAK,EAAO,UAAU,EAEnD,EAAW,CACb,IAAM,EAAQ,EAAO,UAAY,mBAAqB,WACtD,EAAU,KACR,GAAG,EAAM,IAAI,EAAO,QAAQ,IAAI,EAAW,EAAI,KAAK,EAAE,IAAI,EAAI,MAAM,eAAe,OAAO,EAAE,yCAAyC,EAAO,IAC9I,CACF,MAAW,GAAU,EAAU,SAAW,GACxC,EAAU,KACR,QAAQ,EAAO,UACf,4EACF,EAGF,IAAM,EAAO,EAAgB,EAAU,KAAK;CAAI,EAAG,CAAQ,EACrD,EAAU,CACd,GAAI,EAAO,GACX,OAAQ,EAAO,KACf,SAAU,EAAO,SACjB,QAAS,EAAO,QAChB,QAAS,EAAO,QAChB,SAAU,EAAI,MACd,MAAO,EAAoB,EAAY,EAAI,MAC3C,OACA,YACA,GAAI,EAAO,UACP,CACE,UAAW,EAAO,UAAU,OAC5B,eAAgB,EAAO,UAAU,MACjC,eAAgB,EAAO,UAAU,KACnC,EACA,CAAC,EACL,GAAI,EAAO,WAAa,CAAE,WAAY,EAAO,UAAW,EAAI,CAAC,EAC7D,GAAI,EAAO,SAAW,CAAE,SAAU,EAAK,EAAI,CAAC,CAC9C,EAEA,GAAI,EAAQ,MAAU,MAAM,CAAI,EAChC,OAAO,EAAW,EAAM,CAAO,CACjC"}
1
+ {"version":3,"file":"bashTool.mjs","names":[],"sources":["../../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/ansiScrub';\nimport type { BashRunResult, CompletedRun, IBashRunService } from '../../types/bashRunService';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport {\n getBackgroundThresholdMs,\n getResultMaxBytes,\n getResultMaxLines,\n getResultMaxTokens,\n getSuccessResultMaxBytes,\n getSuccessResultMaxTokens,\n} from '../../types/config.ts';\nimport {\n boundExcerpt,\n boundResultText,\n composeExcerpt,\n parseResultPragma,\n type ResultBudget,\n countLines,\n formatSize,\n summarizeLog,\n type ToolResult,\n textResult,\n} from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold',\n interactive: 'Started interactively',\n};\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\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string | Promise<string>;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\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 registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\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 sessionId: await dependencies.getSessionId(),\n });\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));\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, context) {\n return renderBashResult(result, { ...options, isError: context.isError }, theme);\n },\n });\n}\n\nfunction completionFailed(result: CompletedRun): boolean {\n return result.timedOut === true || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0);\n}\n\nfunction completionStatus(result: CompletedRun): string | undefined {\n if (result.timedOut === true) return 'Timed out: exceeded the requested timeout.';\n if (result.signal !== null) return `Signal: ${result.signal}`;\n if (result.exitCode === null) return 'Exit status unavailable.';\n if (result.exitCode !== 0) return `Exit: ${result.exitCode}`;\n return undefined;\n}\n\nexport function formatRunResult(result: BashRunResult, budget: ResultBudget = {}): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n 'Next: correct the reported launch or supervision problem. Retry only after changing the command or environment.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason}: runner \"${result.name}\" (${result.id}).`,\n `Streaming log: ${result.logPath}`,\n `Inspect: doom-runner logs ${result.id}`,\n ].join('\\n');\n\n return textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n });\n }\n\n // Same shape either way; a success just buys less of it. Exiting 0 has already\n // reported the outcome, so its output is worth a fraction of a failure's.\n const succeeded = !completionFailed(result);\n const maxLines = budget.maxLines ?? getResultMaxLines();\n // An explicit pragma still wins: asking for a wider result is the point of it.\n const maxBytes = budget.maxBytes ?? (succeeded ? getSuccessResultMaxBytes() : getResultMaxBytes());\n const maxTokens = budget.maxTokens ?? (succeeded ? getSuccessResultMaxTokens() : getResultMaxTokens());\n const log = summarizeLog(result.logPath, maxLines, maxBytes, maxTokens);\n const useCapturedOutput = !result.rtkOutput && log.tail.length === 0 && result.output.length > 0;\n let tail: string;\n let tailLines: number;\n let outputLines: number;\n let outputBytes: number;\n let truncated: boolean;\n if (result.rtkOutput) {\n const clipped = Buffer.byteLength(result.rtkOutput.output, 'utf8') < result.rtkOutput.bytes;\n const bounded = clipped\n ? composeExcerpt(\n result.rtkOutput.head,\n result.rtkOutput.output,\n result.rtkOutput.lines,\n maxLines,\n maxBytes,\n [],\n maxTokens,\n )\n : boundExcerpt(result.rtkOutput.output, maxLines, maxBytes, maxTokens);\n tail = bounded.text;\n tailLines = bounded.lines;\n outputLines = result.rtkOutput.lines;\n outputBytes = result.rtkOutput.bytes;\n truncated = outputLines > tailLines || outputBytes > Buffer.byteLength(tail, 'utf8');\n } else {\n tail = useCapturedOutput ? result.output : log.tail;\n tailLines = useCapturedOutput ? countLines(tail) : log.tailLines;\n outputLines = Math.max(log.lines, tailLines);\n outputBytes = useCapturedOutput ? Buffer.byteLength(tail, 'utf8') : log.bytes;\n truncated = !useCapturedOutput && (outputLines > tailLines || outputBytes > Buffer.byteLength(tail, 'utf8'));\n }\n const plainTail = stripAnsi(tail).replace(/\\r?\\n$/, '');\n const failed = completionFailed(result);\n const status = completionStatus(result);\n const textLines: string[] = [];\n\n if (plainTail.length === 0) {\n textLines.push(status === undefined ? 'Completed with no output.' : 'No output.');\n } else if (truncated) {\n const label = result.rtkOutput ? `RTK ${result.rtkOutput.filter} excerpt` : 'Log excerpt';\n textLines.push(\n `${label} (${tailLines.toLocaleString('en-US')} of ${outputLines.toLocaleString('en-US')} lines):\\n${plainTail}`,\n );\n } else {\n textLines.push(plainTail);\n }\n if (status !== undefined) textLines.push(status);\n if (result.rtkWarning) textLines.push(result.rtkWarning);\n\n if (truncated) {\n const label = result.rtkOutput ? 'Complete raw log' : 'Full log';\n textLines.push(\n `${label}: ${result.logPath} (${formatSize(log.bytes)}, ${log.lines.toLocaleString('en-US')} lines); inspect with doom-runner logs ${result.id}`,\n );\n } else if (failed && plainTail.length === 0) {\n textLines.push(\n `Log: ${result.logPath}`,\n 'Next: run one read-only diagnostic; retry only after correcting the cause.',\n );\n }\n\n const text = boundResultText(textLines.join('\\n'), maxBytes);\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: useCapturedOutput ? tailLines : log.lines,\n tail,\n tailLines,\n ...(result.rtkOutput\n ? {\n rtkFilter: result.rtkOutput.filter,\n rtkOutputBytes: result.rtkOutput.bytes,\n rtkOutputLines: result.rtkOutput.lines,\n }\n : {}),\n ...(result.rtkWarning ? { rtkWarning: result.rtkWarning } : {}),\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n\n if (failed) throw new Error(text);\n return textResult(text, details);\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- let e=require("typebox");const t=e.Type.Object({command:e.Type.String({description:`Bash command to execute`}),timeout:e.Type.Optional(e.Type.Number({description:`Timeout in seconds (optional, no default timeout)`})),background:e.Type.Optional(e.Type.Boolean({description:`Start the command in the background immediately instead of waiting. Use for dev servers, watchers and other commands you already know will not finish.`})),interactive:e.Type.Optional(e.Type.Boolean({description:`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.`})),name:e.Type.Optional(e.Type.String({description:`Preferred runner name if the command ends up in the background. A name in use gets a suffix.`})),alarm:e.Type.Optional(e.Type.Number({description:`Send you a snapshot of this runner every N seconds, so you never have to poll it. Implies background. Use it to watch a dev server or a long build, or to wake yourself on a schedule by backgrounding a sleep loop. Minimum 10 seconds. It stops on its own when the process exits, or early with doom-runner alarm stop <id>.`}))});exports.BASH_TOOL_LABEL=`bash`,exports.BASH_TOOL_NAME=`bash`,exports.BashParamsSchema=t,exports.MIN_ALARM_SECONDS=10;
1
+ let e=require("typebox");const t=e.Type.Object({command:e.Type.String({description:`Bash command to execute`}),timeout:e.Type.Optional(e.Type.Number({description:`Timeout in seconds (optional, no default timeout)`})),background:e.Type.Optional(e.Type.Boolean({description:`Start the command in the background immediately instead of waiting. Use for dev servers, watchers and other commands you already know will not finish.`})),interactive:e.Type.Optional(e.Type.Boolean({description:`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.`})),name:e.Type.Optional(e.Type.String({description:`Preferred runner name if the command ends up in the background. A name in use gets a suffix.`}))});exports.BASH_TOOL_LABEL=`bash`,exports.BASH_TOOL_NAME=`bash`,exports.BashParamsSchema=t;
2
2
  //# sourceMappingURL=bashTool.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool.cjs","names":["Type"],"sources":["../../../src/schemas/bashTool.ts"],"sourcesContent":["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. */\nexport const BASH_TOOL_NAME = 'bash';\nexport const BASH_TOOL_LABEL = 'bash';\n\n/** Floor for `alarm`, so a short interval cannot flood the conversation. */\nexport const MIN_ALARM_SECONDS = 10;\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 alarm: Type.Optional(\n Type.Number({\n description: `Send you a snapshot of this runner every N seconds, so you never have to poll it. Implies background. Use it to watch a dev server or a long build, or to wake yourself on a schedule by backgrounding a sleep loop. Minimum ${MIN_ALARM_SECONDS} seconds. It stops on its own when the process exits, or early with doom-runner alarm stop <id>.`,\n }),\n ),\n});\n\nexport type BashParams = Static<typeof BashParamsSchema>;\n"],"mappings":"yBAGA,MAYa,EAAmBA,EAAAA,KAAK,OAAO,CAC1C,QAASA,EAAAA,KAAK,OAAO,CAAE,YAAa,yBAA0B,CAAC,EAC/D,QAASA,EAAAA,KAAK,SAASA,EAAAA,KAAK,OAAO,CAAE,YAAa,mDAAoD,CAAC,CAAC,EACxG,WAAYA,EAAAA,KAAK,SACfA,EAAAA,KAAK,QAAQ,CACX,YACE,wJACJ,CAAC,CACH,EACA,YAAaA,EAAAA,KAAK,SAChBA,EAAAA,KAAK,QAAQ,CACX,YACE,4LACJ,CAAC,CACH,EACA,KAAMA,EAAAA,KAAK,SACTA,EAAAA,KAAK,OAAO,CACV,YAAa,8FACf,CAAC,CACH,EACA,MAAOA,EAAAA,KAAK,SACVA,EAAAA,KAAK,OAAO,CACV,YAAa,iUACf,CAAC,CACH,CACF,CAAC"}
1
+ {"version":3,"file":"bashTool.cjs","names":["Type"],"sources":["../../../src/schemas/bashTool.ts"],"sourcesContent":["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. */\nexport const BASH_TOOL_NAME = 'bash';\nexport const BASH_TOOL_LABEL = 'bash';\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"],"mappings":"yBAGA,MASa,EAAmBA,EAAAA,KAAK,OAAO,CAC1C,QAASA,EAAAA,KAAK,OAAO,CAAE,YAAa,yBAA0B,CAAC,EAC/D,QAASA,EAAAA,KAAK,SAASA,EAAAA,KAAK,OAAO,CAAE,YAAa,mDAAoD,CAAC,CAAC,EACxG,WAAYA,EAAAA,KAAK,SACfA,EAAAA,KAAK,QAAQ,CACX,YACE,wJACJ,CAAC,CACH,EACA,YAAaA,EAAAA,KAAK,SAChBA,EAAAA,KAAK,QAAQ,CACX,YACE,4LACJ,CAAC,CACH,EACA,KAAMA,EAAAA,KAAK,SACTA,EAAAA,KAAK,OAAO,CACV,YAAa,8FACf,CAAC,CACH,CACF,CAAC"}
@@ -3,8 +3,6 @@ import { Static, Type } from "typebox";
3
3
  /** Replaces pi's built-in tool of the same name, so hooks keyed on `bash` keep working. */
4
4
  export declare const BASH_TOOL_NAME = "bash";
5
5
  export declare const BASH_TOOL_LABEL = "bash";
6
- /** Floor for `alarm`, so a short interval cannot flood the conversation. */
7
- export declare const MIN_ALARM_SECONDS = 10;
8
6
  /**
9
7
  * Built-in bash parameters plus the backgrounding controls.
10
8
  *
@@ -17,7 +15,6 @@ export declare const BashParamsSchema: Type.TObject<{
17
15
  background: Type.TOptional<Type.TBoolean>;
18
16
  interactive: Type.TOptional<Type.TBoolean>;
19
17
  name: Type.TOptional<Type.TString>;
20
- alarm: Type.TOptional<Type.TNumber>;
21
18
  }>;
22
19
  export type BashParams = Static<typeof BashParamsSchema>;
23
20
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool.d.cts","names":[],"sources":["../../../src/schemas/bashTool.ts"],"mappings":";;;qBAGa;qBACA;;qBAGA;;;;;;;qBAQA,kBAAgB,KAAA;;;;;;;;YA2BjB,aAAa,cAAc"}
1
+ {"version":3,"file":"bashTool.d.cts","names":[],"sources":["../../../src/schemas/bashTool.ts"],"mappings":";;;qBAGa;qBACA;;;;;;;qBAQA,kBAAgB,KAAA;;;;;;;YAsBjB,aAAa,cAAc"}
@@ -3,8 +3,6 @@ import { Static, Type } from "typebox";
3
3
  /** Replaces pi's built-in tool of the same name, so hooks keyed on `bash` keep working. */
4
4
  export declare const BASH_TOOL_NAME = "bash";
5
5
  export declare const BASH_TOOL_LABEL = "bash";
6
- /** Floor for `alarm`, so a short interval cannot flood the conversation. */
7
- export declare const MIN_ALARM_SECONDS = 10;
8
6
  /**
9
7
  * Built-in bash parameters plus the backgrounding controls.
10
8
  *
@@ -17,7 +15,6 @@ export declare const BashParamsSchema: Type.TObject<{
17
15
  background: Type.TOptional<Type.TBoolean>;
18
16
  interactive: Type.TOptional<Type.TBoolean>;
19
17
  name: Type.TOptional<Type.TString>;
20
- alarm: Type.TOptional<Type.TNumber>;
21
18
  }>;
22
19
  export type BashParams = Static<typeof BashParamsSchema>;
23
20
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool.d.mts","names":[],"sources":["../../../src/schemas/bashTool.ts"],"mappings":";;;qBAGa;qBACA;;qBAGA;;;;;;;qBAQA,kBAAgB,KAAA;;;;;;;;YA2BjB,aAAa,cAAc"}
1
+ {"version":3,"file":"bashTool.d.mts","names":[],"sources":["../../../src/schemas/bashTool.ts"],"mappings":";;;qBAGa;qBACA;;;;;;;qBAQA,kBAAgB,KAAA;;;;;;;YAsBjB,aAAa,cAAc"}
@@ -1,2 +1,2 @@
1
- import{Type as e}from"typebox";const t=`bash`,n=`bash`,r=10,i=e.Object({command:e.String({description:`Bash command to execute`}),timeout:e.Optional(e.Number({description:`Timeout in seconds (optional, no default timeout)`})),background:e.Optional(e.Boolean({description:`Start the command in the background immediately instead of waiting. Use for dev servers, watchers and other commands you already know will not finish.`})),interactive:e.Optional(e.Boolean({description:`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.`})),name:e.Optional(e.String({description:`Preferred runner name if the command ends up in the background. A name in use gets a suffix.`})),alarm:e.Optional(e.Number({description:`Send you a snapshot of this runner every N seconds, so you never have to poll it. Implies background. Use it to watch a dev server or a long build, or to wake yourself on a schedule by backgrounding a sleep loop. Minimum 10 seconds. It stops on its own when the process exits, or early with doom-runner alarm stop <id>.`}))});export{n as BASH_TOOL_LABEL,t as BASH_TOOL_NAME,i as BashParamsSchema,r as MIN_ALARM_SECONDS};
1
+ import{Type as e}from"typebox";const t=`bash`,n=`bash`,r=e.Object({command:e.String({description:`Bash command to execute`}),timeout:e.Optional(e.Number({description:`Timeout in seconds (optional, no default timeout)`})),background:e.Optional(e.Boolean({description:`Start the command in the background immediately instead of waiting. Use for dev servers, watchers and other commands you already know will not finish.`})),interactive:e.Optional(e.Boolean({description:`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.`})),name:e.Optional(e.String({description:`Preferred runner name if the command ends up in the background. A name in use gets a suffix.`}))});export{n as BASH_TOOL_LABEL,t as BASH_TOOL_NAME,r as BashParamsSchema};
2
2
  //# sourceMappingURL=bashTool.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bashTool.mjs","names":[],"sources":["../../../src/schemas/bashTool.ts"],"sourcesContent":["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. */\nexport const BASH_TOOL_NAME = 'bash';\nexport const BASH_TOOL_LABEL = 'bash';\n\n/** Floor for `alarm`, so a short interval cannot flood the conversation. */\nexport const MIN_ALARM_SECONDS = 10;\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 alarm: Type.Optional(\n Type.Number({\n description: `Send you a snapshot of this runner every N seconds, so you never have to poll it. Implies background. Use it to watch a dev server or a long build, or to wake yourself on a schedule by backgrounding a sleep loop. Minimum ${MIN_ALARM_SECONDS} seconds. It stops on its own when the process exits, or early with doom-runner alarm stop <id>.`,\n }),\n ),\n});\n\nexport type BashParams = Static<typeof BashParamsSchema>;\n"],"mappings":"+BAGA,MAAa,EAAiB,OACjB,EAAkB,OAGlB,EAAoB,GAQpB,EAAmB,EAAK,OAAO,CAC1C,QAAS,EAAK,OAAO,CAAE,YAAa,yBAA0B,CAAC,EAC/D,QAAS,EAAK,SAAS,EAAK,OAAO,CAAE,YAAa,mDAAoD,CAAC,CAAC,EACxG,WAAY,EAAK,SACf,EAAK,QAAQ,CACX,YACE,wJACJ,CAAC,CACH,EACA,YAAa,EAAK,SAChB,EAAK,QAAQ,CACX,YACE,4LACJ,CAAC,CACH,EACA,KAAM,EAAK,SACT,EAAK,OAAO,CACV,YAAa,8FACf,CAAC,CACH,EACA,MAAO,EAAK,SACV,EAAK,OAAO,CACV,YAAa,iUACf,CAAC,CACH,CACF,CAAC"}
1
+ {"version":3,"file":"bashTool.mjs","names":[],"sources":["../../../src/schemas/bashTool.ts"],"sourcesContent":["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. */\nexport const BASH_TOOL_NAME = 'bash';\nexport const BASH_TOOL_LABEL = 'bash';\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"],"mappings":"+BAGA,MAAa,EAAiB,OACjB,EAAkB,OAQlB,EAAmB,EAAK,OAAO,CAC1C,QAAS,EAAK,OAAO,CAAE,YAAa,yBAA0B,CAAC,EAC/D,QAAS,EAAK,SAAS,EAAK,OAAO,CAAE,YAAa,mDAAoD,CAAC,CAAC,EACxG,WAAY,EAAK,SACf,EAAK,QAAQ,CACX,YACE,wJACJ,CAAC,CACH,EACA,YAAa,EAAK,SAChB,EAAK,QAAQ,CACX,YACE,4LACJ,CAAC,CACH,EACA,KAAM,EAAK,SACT,EAAK,OAAO,CACV,YAAa,8FACf,CAAC,CACH,CACF,CAAC"}
@@ -1,5 +1,5 @@
1
1
  const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../commands/bash/responseEnvelope.cjs");let n=require("node:os");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi-ui/toolChrome"),i=require("@earendil-works/pi-coding-agent"),a=require("@earendil-works/pi-tui");var o=class{lines;layout;constructor(e,t){this.lines=e,this.layout=t}render(e){if(e<=0)return[];let t=Math.max(1,e-2),n=this.layout.wrap?this.lines.flatMap(e=>(0,a.wrapTextWithAnsi)(e,t)):this.lines.map(e=>(0,a.truncateToWidth)(e,t,`…`));this.layout.separator&&n.unshift(this.layout.separator(`─`.repeat(t)));let r=n.map(e=>` ${e}`);return this.layout.trailingBlank&&r.push(``),r}invalidate(){}};function s(e,t,n){return new o(e,{wrap:t,separator:e=>n.fg(`borderMuted`,e),trailingBlank:!0})}const c=/\u001b\[[0-9;]*m/;function l(e,t,n){return c.test(e)?e:n.fg(t,e)}function u(e,t=n.default.homedir()){return t.length>0?e.split(t).join(`~`):e}function d(e){let t=e.split(`
2
- `).map(e=>e.trim()).filter(e=>e.length>0),n=t[0]??``;return t.length>1?`${n} … +${t.length-1} lines`:n}function f(e,t=160){if(e.length<=t)return e;let n=t-3,r=Math.ceil(n/2),i=e.slice(0,r),a=e.slice(e.length-(n-r)),o=i.lastIndexOf(` `);o>i.length/2&&(i=i.slice(0,o));let s=a.indexOf(` `);return s>=0&&s<a.length/2&&(a=a.slice(s+1)),`${i} … ${a}`}function p(e){return u(d(e))}function m(e){let t=[];return e.background===!0&&t.push(`bg`),e.interactive===!0&&t.push(`tty`),e.alarm!==void 0&&t.push(`alarm ${e.alarm}s`),e.timeout!==void 0&&t.push(`${e.timeout}s`),e.name!==void 0&&e.name.length>0&&t.push(e.name),t}function h(e,t,n=i.highlightCode){let a=n(p(e.command),`bash`).join(` `),o=`${(0,r.renderToolBadge)(`bash`,t)} ${a}`,s=m(e);return s.length>0&&(o+=` ${t.fg(`muted`,`· ${s.join(` · `)}`)}`),new r.DoomToolCall(o)}function g(e){let n=[];return e.lines!==void 0&&n.push(`${e.lines.toLocaleString(`en-US`)} lines`),e.fileSize!==void 0&&n.push(t.formatSize(e.fileSize)),e.runner!==void 0&&n.push(e.runner),n.join(` · `)}function _(e,t){let n=(e.tail??``).split(`
2
+ `).map(e=>e.trim()).filter(e=>e.length>0),n=t[0]??``;return t.length>1?`${n} … +${t.length-1} lines`:n}function f(e,t=160){if(e.length<=t)return e;let n=t-3,r=Math.ceil(n/2),i=e.slice(0,r),a=e.slice(e.length-(n-r)),o=i.lastIndexOf(` `);o>i.length/2&&(i=i.slice(0,o));let s=a.indexOf(` `);return s>=0&&s<a.length/2&&(a=a.slice(s+1)),`${i} … ${a}`}function p(e){return u(d(e))}function m(e){let t=[];return e.background===!0&&t.push(`bg`),e.interactive===!0&&t.push(`tty`),e.timeout!==void 0&&t.push(`${e.timeout}s`),e.name!==void 0&&e.name.length>0&&t.push(e.name),t}function h(e,t,n=i.highlightCode){let a=n(p(e.command),`bash`).join(` `),o=`${(0,r.renderToolBadge)(`bash`,t)} ${a}`,s=m(e);return s.length>0&&(o+=` ${t.fg(`muted`,`· ${s.join(` · `)}`)}`),new r.DoomToolCall(o)}function g(e){let n=[];return e.lines!==void 0&&n.push(`${e.lines.toLocaleString(`en-US`)} lines`),e.fileSize!==void 0&&n.push(t.formatSize(e.fileSize)),e.runner!==void 0&&n.push(e.runner),n.join(` · `)}function _(e,t){let n=(e.tail??``).split(`
3
3
  `);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return t||n.length<=12?n:n.slice(-12)}function v(e){return(e.content??[]).map(e=>e.text??``).join(``)}function y(e,t){let n=e.split(`
4
4
  `);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return n.length<=t?n:n.slice(-t)}function b(e,t,n){let r=e.details??{};if(t.isPartial){let t=y(v(e),12),r=n.fg(`warning`,`◐`)+n.fg(`dim`,` running`);return s([...t.map(e=>l(e,`toolOutput`,n)),r],!1,n)}if(t.isError===!0){let i=y(v(e),t.expanded?2**53-1:12),a=g(r),o=a.length>0?` ${a}`:` failed`;return s([...i.map(e=>l(e,`toolOutput`,n)),n.fg(`error`,`✗`)+n.fg(`dim`,o)],t.expanded,n)}if(r.promoted===!0){let e=r.runner??r.id??`runner`,t=r.id===void 0?``:` · ${r.id}`;return s([n.fg(`accent`,`● ${e}`)+n.fg(`muted`,`${t} · background`)],!1,n)}let i=_(r,t.expanded),a=r.exitCode!==void 0&&r.exitCode!==null,o=r.timedOut===!0||a&&r.exitCode!==0,c=o?n.fg(`error`,`✗`):n.fg(`success`,`✓`),u=[];r.timedOut===!0?u.push(`timed out`):o&&u.push(`exit ${r.exitCode}`);let d=g(r);d.length>0&&u.push(d),!t.expanded&&(r.tailLines??0)>i.length&&u.push(`ctrl+o`);let f=i.map(e=>l(e,`toolOutput`,n)),p=u.length>0?` ${u.join(` · `)}`:o?` failed`:i.length===0?` done`:``;return p.length>0&&f.push(c+n.fg(`dim`,p)),s(f,t.expanded,n)}exports.abbreviateHome=u,exports.collapseCommand=d,exports.formatBashCommand=p,exports.formatBashFlags=m,exports.formatResultSummary=g,exports.renderBashCall=h,exports.renderBashResult=b,exports.truncateMiddle=f;
5
5
  //# sourceMappingURL=bashRender.cjs.map