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

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 (39) 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 +15 -15
  38. package/skills/doom-runner/SKILL.md +6 -1
  39. 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":"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"}
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":"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"}
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
@@ -1 +1 @@
1
- {"version":3,"file":"bashRender.cjs","names":["wrapTextWithAnsi","truncateToWidth","os","highlightCode","renderToolBadge","DoomToolCall","formatSize"],"sources":["../../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { DoomToolCall, renderToolBadge } from '@agimon-ai/doompi-ui/toolChrome';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\n/* oxlint-disable no-control-regex -- matching SGR requires the ESC control character */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: Matching SGR requires the ESC control character.\nconst SGR_PATTERN = /\\u001b\\[[0-9;]*m/;\n/* oxlint-enable no-control-regex */\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.alarm !== undefined) flags.push(`alarm ${args.alarm}s`);\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = `${renderToolBadge('bash', theme)} ${command}`;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new DoomToolCall(text);\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown },\n options: { expanded: boolean; isPartial: boolean; isError?: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (options.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : ' failed';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n const status = meta.length > 0 ? ` ${meta.join(' · ')}` : failed ? ' failed' : body.length === 0 ? ' done' : '';\n if (status.length > 0) lines.push(glyph + theme.fg('dim', status));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bashRender.cjs","names":["wrapTextWithAnsi","truncateToWidth","os","highlightCode","renderToolBadge","DoomToolCall","formatSize"],"sources":["../../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { DoomToolCall, renderToolBadge } from '@agimon-ai/doompi-ui/toolChrome';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\n/* oxlint-disable no-control-regex -- matching SGR requires the ESC control character */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: Matching SGR requires the ESC control character.\nconst SGR_PATTERN = /\\u001b\\[[0-9;]*m/;\n/* oxlint-enable no-control-regex */\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = `${renderToolBadge('bash', theme)} ${command}`;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new DoomToolCall(text);\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown },\n options: { expanded: boolean; isPartial: boolean; isError?: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (options.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : ' failed';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n const status = meta.length > 0 ? ` ${meta.join(' · ')}` : failed ? ' failed' : body.length === 0 ? ' done' : '';\n if (status.length > 0) lines.push(glyph + theme.fg('dim', status));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"bashRender.d.cts","names":[],"sources":["../../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;YAmBY,eAAe,cAAc;;iBAqExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;wBAIc,eAAe,cAAc;;wBAK7B,gBAAgB;;wBAUhB,eAAe,cAAc;wBAkB7B,kBAAkB;;wBAKlB,gBAAgB,MAAM;wBAUtB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;wBAYxF,oBAAoB,SAAS;wBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;GAC5D;EAAW;EAAmB;EAAoB;GAClD,OAAO,QACN"}
1
+ {"version":3,"file":"bashRender.d.cts","names":[],"sources":["../../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;YAmBY,eAAe,cAAc;;iBAqExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;wBAIc,eAAe,cAAc;;wBAK7B,gBAAgB;;wBAUhB,eAAe,cAAc;wBAkB7B,kBAAkB;;wBAKlB,gBAAgB,MAAM;wBAStB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;wBAYxF,oBAAoB,SAAS;wBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;GAC5D;EAAW;EAAmB;EAAoB;GAClD,OAAO,QACN"}
@@ -1 +1 @@
1
- {"version":3,"file":"bashRender.d.mts","names":[],"sources":["../../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;YAmBY,eAAe,cAAc;;iBAqExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;wBAIc,eAAe,cAAc;;wBAK7B,gBAAgB;;wBAUhB,eAAe,cAAc;wBAkB7B,kBAAkB;;wBAKlB,gBAAgB,MAAM;wBAUtB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;wBAYxF,oBAAoB,SAAS;wBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;GAC5D;EAAW;EAAmB;EAAoB;GAClD,OAAO,QACN"}
1
+ {"version":3,"file":"bashRender.d.mts","names":[],"sources":["../../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;YAmBY,eAAe,cAAc;;iBAqExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;wBAIc,eAAe,cAAc;;wBAK7B,gBAAgB;;wBAUhB,eAAe,cAAc;wBAkB7B,kBAAkB;;wBAKlB,gBAAgB,MAAM;wBAStB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;wBAYxF,oBAAoB,SAAS;wBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;GAC5D;EAAW;EAAmB;EAAoB;GAClD,OAAO,QACN"}
@@ -1,5 +1,5 @@
1
1
  import{formatSize as e}from"../commands/bash/responseEnvelope.mjs";import t from"node:os";import{DoomToolCall as n,renderToolBadge as r}from"@agimon-ai/doompi-ui/toolChrome";import{highlightCode as i}from"@earendil-works/pi-coding-agent";import{truncateToWidth as a,wrapTextWithAnsi as o}from"@earendil-works/pi-tui";var s=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=>o(e,t)):this.lines.map(e=>a(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 c(e,t,n){return new s(e,{wrap:t,separator:e=>n.fg(`borderMuted`,e),trailingBlank:!0})}const l=/\u001b\[[0-9;]*m/;function u(e,t,n){return l.test(e)?e:n.fg(t,e)}function d(e,n=t.homedir()){return n.length>0?e.split(n).join(`~`):e}function f(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 p(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 m(e){return d(f(e))}function h(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 g(e,t,a=i){let o=a(m(e.command),`bash`).join(` `),s=`${r(`bash`,t)} ${o}`,c=h(e);return c.length>0&&(s+=` ${t.fg(`muted`,`· ${c.join(` · `)}`)}`),new n(s)}function _(t){let n=[];return t.lines!==void 0&&n.push(`${t.lines.toLocaleString(`en-US`)} lines`),t.fileSize!==void 0&&n.push(e(t.fileSize)),t.runner!==void 0&&n.push(t.runner),n.join(` · `)}function v(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 p(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 m(e){return d(f(e))}function h(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 g(e,t,a=i){let o=a(m(e.command),`bash`).join(` `),s=`${r(`bash`,t)} ${o}`,c=h(e);return c.length>0&&(s+=` ${t.fg(`muted`,`· ${c.join(` · `)}`)}`),new n(s)}function _(t){let n=[];return t.lines!==void 0&&n.push(`${t.lines.toLocaleString(`en-US`)} lines`),t.fileSize!==void 0&&n.push(e(t.fileSize)),t.runner!==void 0&&n.push(t.runner),n.join(` · `)}function v(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 y(e){return(e.content??[]).map(e=>e.text??``).join(``)}function b(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 x(e,t,n){let r=e.details??{};if(t.isPartial){let t=b(y(e),12),r=n.fg(`warning`,`◐`)+n.fg(`dim`,` running`);return c([...t.map(e=>u(e,`toolOutput`,n)),r],!1,n)}if(t.isError===!0){let i=b(y(e),t.expanded?2**53-1:12),a=_(r),o=a.length>0?` ${a}`:` failed`;return c([...i.map(e=>u(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 c([n.fg(`accent`,`● ${e}`)+n.fg(`muted`,`${t} · background`)],!1,n)}let i=v(r,t.expanded),a=r.exitCode!==void 0&&r.exitCode!==null,o=r.timedOut===!0||a&&r.exitCode!==0,s=o?n.fg(`error`,`✗`):n.fg(`success`,`✓`),l=[];r.timedOut===!0?l.push(`timed out`):o&&l.push(`exit ${r.exitCode}`);let d=_(r);d.length>0&&l.push(d),!t.expanded&&(r.tailLines??0)>i.length&&l.push(`ctrl+o`);let f=i.map(e=>u(e,`toolOutput`,n)),p=l.length>0?` ${l.join(` · `)}`:o?` failed`:i.length===0?` done`:``;return p.length>0&&f.push(s+n.fg(`dim`,p)),c(f,t.expanded,n)}export{d as abbreviateHome,f as collapseCommand,m as formatBashCommand,h as formatBashFlags,_ as formatResultSummary,g as renderBashCall,x as renderBashResult,p as truncateMiddle};
5
5
  //# sourceMappingURL=bashRender.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bashRender.mjs","names":[],"sources":["../../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { DoomToolCall, renderToolBadge } from '@agimon-ai/doompi-ui/toolChrome';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\n/* oxlint-disable no-control-regex -- matching SGR requires the ESC control character */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: Matching SGR requires the ESC control character.\nconst SGR_PATTERN = /\\u001b\\[[0-9;]*m/;\n/* oxlint-enable no-control-regex */\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.alarm !== undefined) flags.push(`alarm ${args.alarm}s`);\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = `${renderToolBadge('bash', theme)} ${command}`;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new DoomToolCall(text);\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown },\n options: { expanded: boolean; isPartial: boolean; isError?: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (options.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : ' failed';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n const status = meta.length > 0 ? ` ${meta.join(' · ')}` : failed ? ' failed' : body.length === 0 ? ' done' : '';\n if (status.length > 0) lines.push(glyph + theme.fg('dim', status));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"bashRender.mjs","names":[],"sources":["../../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { DoomToolCall, renderToolBadge } from '@agimon-ai/doompi-ui/toolChrome';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\n/* oxlint-disable no-control-regex -- matching SGR requires the ESC control character */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: Matching SGR requires the ESC control character.\nconst SGR_PATTERN = /\\u001b\\[[0-9;]*m/;\n/* oxlint-enable no-control-regex */\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = `${renderToolBadge('bash', theme)} ${command}`;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new DoomToolCall(text);\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown },\n options: { expanded: boolean; isPartial: boolean; isError?: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (options.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : ' failed';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n const status = meta.length > 0 ? ` ${meta.join(' · ')}` : failed ? ' failed' : body.length === 0 ? ' done' : '';\n if (status.length > 0) lines.push(glyph + theme.fg('dim', status));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
@@ -9,12 +9,6 @@ export interface RunnerExit {
9
9
  stopReason?: string;
10
10
  finishedAt: string;
11
11
  }
12
- /** A recurring snapshot of a running runner, pushed to the model until it exits. */
13
- export interface RunnerAlarm {
14
- intervalMs: number;
15
- /** The last fire, or the runner's start time before the first one. */
16
- lastFiredAt: string;
17
- }
18
12
  /** A command this extension supervises, retained after completion for CLI access. */
19
13
  export interface RunnerRecord {
20
14
  id: string;
@@ -34,8 +28,6 @@ export interface RunnerRecord {
34
28
  backend: RunnerBackend;
35
29
  backendTarget?: string;
36
30
  exit?: RunnerExit;
37
- /** Present only while a runner is running with an alarm attached. */
38
- alarm?: RunnerAlarm;
39
31
  /** pid of the pi process that launched it, for orphan detection. */
40
32
  hostPid: number;
41
33
  }
@@ -50,8 +42,6 @@ export interface RegisterRunnerInput {
50
42
  sessionId: string;
51
43
  backend: RunnerBackend;
52
44
  backendTarget?: string;
53
- /** Arms a recurring snapshot at this interval. */
54
- alarmMs?: number;
55
45
  }
56
46
  export interface CompleteRunnerInput {
57
47
  reason: RunnerExitReason;
@@ -74,17 +64,6 @@ export interface IRunnerRegistry {
74
64
  get(id: string, sessionId?: string): Promise<RunnerRecord | undefined>;
75
65
  markPromoted(id: string): Promise<RunnerRecord | undefined>;
76
66
  complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined>;
77
- /** Disarms an alarm. Safe to call on a runner that has none. */
78
- clearAlarm(id: string, sessionId?: string): Promise<RunnerRecord | undefined>;
79
- /**
80
- * Reschedules an alarm from `firedAt`, returning the record the caller may
81
- * report on.
82
- *
83
- * Returns undefined when the alarm was disarmed or the runner finished since
84
- * the caller read it, which is what stops a concurrent `alarm stop` from
85
- * being overwritten by an in-flight fire.
86
- */
87
- markAlarmFired(id: string, firedAt: string): Promise<RunnerRecord | undefined>;
88
67
  /** Removes the active process entry while retaining run metadata. */
89
68
  release(id: string): Promise<void>;
90
69
  /** Marks entries whose process is gone as lost. Returns their ids. */
@@ -1 +1 @@
1
- {"version":3,"file":"runnerRegistry.d.cts","names":[],"sources":["../../../src/types/runnerRegistry.ts"],"mappings":";YAAY;YACA;YACA;iBASK;EACf,QAAQ;EACR;EACA,QAAQ,OAAO;EACf;EACA;;;iBAIe;EACf;;EAEA;;;iBAIe;EACf;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA,OAAO;EACP;EACA,SAAS;EACT;EACA,OAAO;;EAEP,QAAQ;;EAER;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,SAAS;EACT;;EAEA;;iBAGe;EACf,QAAQ;EACR;EACA,QAAQ,OAAO;EACf;;;iBAIe;EACf,SAAS,OAAO,sBAAsB,QAAQ;EAC9C,QAAQ,QAAQ;;EAEhB,0BAA0B,QAAQ;;EAElC,cAAc,oBAAoB,QAAQ;;EAE1C,kBAAkB,wBAAwB,QAAQ;;EAElD,QAAQ,qBAAqB,QAAQ;EACrC,IAAI,YAAY,qBAAqB,QAAQ;EAC7C,aAAa,aAAa,QAAQ;EAClC,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;;EAEhF,WAAW,YAAY,qBAAqB,QAAQ;;;;;;;;;EASpD,eAAe,YAAY,kBAAkB,QAAQ;;EAErD,QAAQ,aAAa;;EAErB,aAAa;;EAEb,UAAU;EACV"}
1
+ {"version":3,"file":"runnerRegistry.d.cts","names":[],"sources":["../../../src/types/runnerRegistry.ts"],"mappings":";YAAY;YACA;YACA;iBASK;EACf,QAAQ;EACR;EACA,QAAQ,OAAO;EACf;EACA;;;iBAIe;EACf;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA,OAAO;EACP;EACA,SAAS;EACT;EACA,OAAO;;EAEP;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,SAAS;EACT;;iBAGe;EACf,QAAQ;EACR;EACA,QAAQ,OAAO;EACf;;;iBAIe;EACf,SAAS,OAAO,sBAAsB,QAAQ;EAC9C,QAAQ,QAAQ;;EAEhB,0BAA0B,QAAQ;;EAElC,cAAc,oBAAoB,QAAQ;;EAE1C,kBAAkB,wBAAwB,QAAQ;;EAElD,QAAQ,qBAAqB,QAAQ;EACrC,IAAI,YAAY,qBAAqB,QAAQ;EAC7C,aAAa,aAAa,QAAQ;EAClC,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;;EAEhF,QAAQ,aAAa;;EAErB,aAAa;;EAEb,UAAU;EACV"}
@@ -9,12 +9,6 @@ export interface RunnerExit {
9
9
  stopReason?: string;
10
10
  finishedAt: string;
11
11
  }
12
- /** A recurring snapshot of a running runner, pushed to the model until it exits. */
13
- export interface RunnerAlarm {
14
- intervalMs: number;
15
- /** The last fire, or the runner's start time before the first one. */
16
- lastFiredAt: string;
17
- }
18
12
  /** A command this extension supervises, retained after completion for CLI access. */
19
13
  export interface RunnerRecord {
20
14
  id: string;
@@ -34,8 +28,6 @@ export interface RunnerRecord {
34
28
  backend: RunnerBackend;
35
29
  backendTarget?: string;
36
30
  exit?: RunnerExit;
37
- /** Present only while a runner is running with an alarm attached. */
38
- alarm?: RunnerAlarm;
39
31
  /** pid of the pi process that launched it, for orphan detection. */
40
32
  hostPid: number;
41
33
  }
@@ -50,8 +42,6 @@ export interface RegisterRunnerInput {
50
42
  sessionId: string;
51
43
  backend: RunnerBackend;
52
44
  backendTarget?: string;
53
- /** Arms a recurring snapshot at this interval. */
54
- alarmMs?: number;
55
45
  }
56
46
  export interface CompleteRunnerInput {
57
47
  reason: RunnerExitReason;
@@ -74,17 +64,6 @@ export interface IRunnerRegistry {
74
64
  get(id: string, sessionId?: string): Promise<RunnerRecord | undefined>;
75
65
  markPromoted(id: string): Promise<RunnerRecord | undefined>;
76
66
  complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined>;
77
- /** Disarms an alarm. Safe to call on a runner that has none. */
78
- clearAlarm(id: string, sessionId?: string): Promise<RunnerRecord | undefined>;
79
- /**
80
- * Reschedules an alarm from `firedAt`, returning the record the caller may
81
- * report on.
82
- *
83
- * Returns undefined when the alarm was disarmed or the runner finished since
84
- * the caller read it, which is what stops a concurrent `alarm stop` from
85
- * being overwritten by an in-flight fire.
86
- */
87
- markAlarmFired(id: string, firedAt: string): Promise<RunnerRecord | undefined>;
88
67
  /** Removes the active process entry while retaining run metadata. */
89
68
  release(id: string): Promise<void>;
90
69
  /** Marks entries whose process is gone as lost. Returns their ids. */
@@ -1 +1 @@
1
- {"version":3,"file":"runnerRegistry.d.mts","names":[],"sources":["../../../src/types/runnerRegistry.ts"],"mappings":";YAAY;YACA;YACA;iBASK;EACf,QAAQ;EACR;EACA,QAAQ,OAAO;EACf;EACA;;;iBAIe;EACf;;EAEA;;;iBAIe;EACf;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA,OAAO;EACP;EACA,SAAS;EACT;EACA,OAAO;;EAEP,QAAQ;;EAER;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,SAAS;EACT;;EAEA;;iBAGe;EACf,QAAQ;EACR;EACA,QAAQ,OAAO;EACf;;;iBAIe;EACf,SAAS,OAAO,sBAAsB,QAAQ;EAC9C,QAAQ,QAAQ;;EAEhB,0BAA0B,QAAQ;;EAElC,cAAc,oBAAoB,QAAQ;;EAE1C,kBAAkB,wBAAwB,QAAQ;;EAElD,QAAQ,qBAAqB,QAAQ;EACrC,IAAI,YAAY,qBAAqB,QAAQ;EAC7C,aAAa,aAAa,QAAQ;EAClC,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;;EAEhF,WAAW,YAAY,qBAAqB,QAAQ;;;;;;;;;EASpD,eAAe,YAAY,kBAAkB,QAAQ;;EAErD,QAAQ,aAAa;;EAErB,aAAa;;EAEb,UAAU;EACV"}
1
+ {"version":3,"file":"runnerRegistry.d.mts","names":[],"sources":["../../../src/types/runnerRegistry.ts"],"mappings":";YAAY;YACA;YACA;iBASK;EACf,QAAQ;EACR;EACA,QAAQ,OAAO;EACf;EACA;;;iBAIe;EACf;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA,OAAO;EACP;EACA,SAAS;EACT;EACA,OAAO;;EAEP;;iBAGe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,SAAS;EACT;;iBAGe;EACf,QAAQ;EACR;EACA,QAAQ,OAAO;EACf;;;iBAIe;EACf,SAAS,OAAO,sBAAsB,QAAQ;EAC9C,QAAQ,QAAQ;;EAEhB,0BAA0B,QAAQ;;EAElC,cAAc,oBAAoB,QAAQ;;EAE1C,kBAAkB,wBAAwB,QAAQ;;EAElD,QAAQ,qBAAqB,QAAQ;EACrC,IAAI,YAAY,qBAAqB,QAAQ;EAC7C,aAAa,aAAa,QAAQ;EAClC,SAAS,YAAY,SAAS,qBAAqB,qBAAqB,QAAQ;;EAEhF,QAAQ,aAAa;;EAErB,aAAa;;EAEb,UAAU;EACV"}
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../src/schemas/bashTool.cjs");exports.BASH_TOOL_LABEL=e.BASH_TOOL_LABEL,exports.BASH_TOOL_NAME=e.BASH_TOOL_NAME,exports.BashParamsSchema=e.BashParamsSchema,exports.MIN_ALARM_SECONDS=e.MIN_ALARM_SECONDS;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../src/schemas/bashTool.cjs");exports.BASH_TOOL_LABEL=e.BASH_TOOL_LABEL,exports.BASH_TOOL_NAME=e.BASH_TOOL_NAME,exports.BashParamsSchema=e.BashParamsSchema;
@@ -1,2 +1,2 @@
1
- import { BASH_TOOL_LABEL, BASH_TOOL_NAME, BashParams, BashParamsSchema, MIN_ALARM_SECONDS } from "../src/schemas/bashTool.cjs";
2
- export { BASH_TOOL_LABEL, BASH_TOOL_NAME, BashParams, BashParamsSchema, MIN_ALARM_SECONDS };
1
+ import { BASH_TOOL_LABEL, BASH_TOOL_NAME, BashParams, BashParamsSchema } from "../src/schemas/bashTool.cjs";
2
+ export { BASH_TOOL_LABEL, BASH_TOOL_NAME, BashParams, BashParamsSchema };
@@ -1,2 +1,2 @@
1
- import { BASH_TOOL_LABEL, BASH_TOOL_NAME, BashParams, BashParamsSchema, MIN_ALARM_SECONDS } from "../src/schemas/bashTool.mjs";
2
- export { BASH_TOOL_LABEL, BASH_TOOL_NAME, BashParams, BashParamsSchema, MIN_ALARM_SECONDS };
1
+ import { BASH_TOOL_LABEL, BASH_TOOL_NAME, BashParams, BashParamsSchema } from "../src/schemas/bashTool.mjs";
2
+ export { BASH_TOOL_LABEL, BASH_TOOL_NAME, BashParams, BashParamsSchema };
@@ -1 +1 @@
1
- import{BASH_TOOL_LABEL as e,BASH_TOOL_NAME as t,BashParamsSchema as n,MIN_ALARM_SECONDS as r}from"../src/schemas/bashTool.mjs";export{e as BASH_TOOL_LABEL,t as BASH_TOOL_NAME,n as BashParamsSchema,r as MIN_ALARM_SECONDS};
1
+ import{BASH_TOOL_LABEL as e,BASH_TOOL_NAME as t,BashParamsSchema as n}from"../src/schemas/bashTool.mjs";export{e as BASH_TOOL_LABEL,t as BASH_TOOL_NAME,n as BashParamsSchema};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-runner",
3
- "version": "0.0.1-alpha.65",
3
+ "version": "0.0.1-alpha.67",
4
4
  "description": "Supervised shell execution, background process control, and run logs for Pi coding agents.",
5
5
  "keywords": [
6
6
  "ai",
@@ -324,12 +324,12 @@
324
324
  "@xterm/headless": "6.0.0",
325
325
  "hono": "4.13.5",
326
326
  "typebox": "1.3.25",
327
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.64",
328
- "@agimon-ai/doompi-telemetry": "0.0.1-alpha.63",
329
- "@agimon-ai/doompi-ui": "0.0.1-alpha.65",
330
- "@agimon-ai/doompi-web-components": "0.0.1-alpha.24",
331
- "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.27",
332
- "@agimon-ai/doompi-web-security": "0.0.1-alpha.26"
327
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.66",
328
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.65",
329
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.67",
330
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.26",
331
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.29",
332
+ "@agimon-ai/doompi-web-security": "0.0.1-alpha.28"
333
333
  },
334
334
  "devDependencies": {
335
335
  "@earendil-works/pi-coding-agent": "0.85.1",
@@ -357,14 +357,14 @@
357
357
  }
358
358
  },
359
359
  "optionalDependencies": {
360
- "@agimon-ai/doompi-runner-rmux-darwin-arm64": "0.0.1-alpha.61",
361
- "@agimon-ai/doompi-runner-rmux-darwin-x64": "0.0.1-alpha.61",
362
- "@agimon-ai/doompi-runner-rmux-linux-x64": "0.0.1-alpha.61",
363
- "@agimon-ai/doompi-runner-rmux-linux-arm64": "0.0.1-alpha.61",
364
- "@agimon-ai/doompi-runner-rtk-darwin-arm64": "0.0.1-alpha.61",
365
- "@agimon-ai/doompi-runner-rtk-linux-arm64": "0.0.1-alpha.61",
366
- "@agimon-ai/doompi-runner-rtk-darwin-x64": "0.0.1-alpha.61",
367
- "@agimon-ai/doompi-runner-rtk-linux-x64": "0.0.1-alpha.61"
360
+ "@agimon-ai/doompi-runner-rmux-darwin-arm64": "0.0.1-alpha.63",
361
+ "@agimon-ai/doompi-runner-rmux-linux-arm64": "0.0.1-alpha.63",
362
+ "@agimon-ai/doompi-runner-rmux-linux-x64": "0.0.1-alpha.63",
363
+ "@agimon-ai/doompi-runner-rtk-darwin-arm64": "0.0.1-alpha.63",
364
+ "@agimon-ai/doompi-runner-rtk-darwin-x64": "0.0.1-alpha.63",
365
+ "@agimon-ai/doompi-runner-rtk-linux-arm64": "0.0.1-alpha.63",
366
+ "@agimon-ai/doompi-runner-rtk-linux-x64": "0.0.1-alpha.63",
367
+ "@agimon-ai/doompi-runner-rmux-darwin-x64": "0.0.1-alpha.63"
368
368
  },
369
369
  "engines": {
370
370
  "node": ">=22.19.0"
@@ -54,7 +54,12 @@ Use the unique runner ID returned by `bash`:
54
54
  - **`input` needs `interactive: true`.** A normal runner does not expose stdin.
55
55
  - **Stop what you no longer need.** Everything still running is stopped when
56
56
  the session ends, but a forgotten dev server holds a port until then.
57
- - **Do not poll.** Check a runner when you have a reason to, not on a loop.
57
+ - **You are told when a runner exits.** A completion message with the exit
58
+ status and log path arrives on its own. Never sleep, poll, or pgrep to wait
59
+ for one. Check a runner when you have a reason to, not on a loop.
60
+ - **Do not pipe to head or tail.** Results are already bounded and the full log
61
+ is on disk. A tail pipe also leaves the live log empty until the command
62
+ exits, which blanks the log pane in Runner Space.
58
63
 
59
64
  ## Where things live
60
65
 
@@ -26,7 +26,6 @@ export interface BashCallArgs {
26
26
  background?: unknown;
27
27
  interactive?: unknown;
28
28
  name?: unknown;
29
- alarm?: unknown;
30
29
  }
31
30
 
32
31
  /** What formatRunResult reports, read only by the renderers. */
@@ -109,7 +108,6 @@ export function formatBashFlags(args: BashCallArgs): string[] {
109
108
  const flags: string[] = [];
110
109
  if (args.background === true) flags.push('bg');
111
110
  if (args.interactive === true) flags.push('tty');
112
- if (typeof args.alarm === 'number') flags.push(`alarm ${args.alarm}s`);
113
111
  if (typeof args.timeout === 'number') flags.push(`${args.timeout}s`);
114
112
  if (typeof args.name === 'string' && args.name.length > 0) flags.push(args.name);
115
113
  return flags;