@agimon-ai/po-runner 0.0.0-alpha.46 → 0.0.0-alpha.47
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.
- package/dist/bin/cli.cjs +3 -3
- package/dist/bin/cli.mjs +3 -3
- package/dist/bin/runnerHost.cjs +2 -2
- package/dist/bin/runnerHost.mjs +2 -2
- package/dist/{cliApp-DkhnAFR1.cjs → cliApp-GKcqD6CT.cjs} +2 -2
- package/dist/{cliApp-DkhnAFR1.cjs.map → cliApp-GKcqD6CT.cjs.map} +1 -1
- package/dist/{cliApp-DjU_M0ho.mjs → cliApp-g7APOwcU.mjs} +2 -2
- package/dist/{cliApp-DjU_M0ho.mjs.map → cliApp-g7APOwcU.mjs.map} +1 -1
- package/dist/cliApp.cjs +1 -1
- package/dist/cliApp.mjs +1 -1
- package/dist/extensions/mcp.cjs +1 -1
- package/dist/extensions/mcp.cjs.map +1 -1
- package/dist/extensions/mcp.mjs +1 -1
- package/dist/extensions/mcp.mjs.map +1 -1
- package/dist/extensions/server.cjs +5 -5
- package/dist/extensions/server.mjs +5 -5
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{launcher-rqDsTfQZ.cjs → launcher-7Ho1Rp7S.cjs} +2 -2
- package/dist/{launcher-rqDsTfQZ.cjs.map → launcher-7Ho1Rp7S.cjs.map} +1 -1
- package/dist/{launcher-Cl-4uv52.mjs → launcher-Dw4pXVq9.mjs} +2 -2
- package/dist/{launcher-Cl-4uv52.mjs.map → launcher-Dw4pXVq9.mjs.map} +1 -1
- package/dist/launcher.cjs +1 -1
- package/dist/launcher.mjs +1 -1
- package/dist/{ptyHost-BJp7tYhN.mjs → ptyHost-D8zJaYg3.mjs} +2 -2
- package/dist/{ptyHost-BJp7tYhN.mjs.map → ptyHost-D8zJaYg3.mjs.map} +1 -1
- package/dist/{ptyHost-DJTgxbjn.cjs → ptyHost-DfXM9RCJ.cjs} +2 -2
- package/dist/{ptyHost-DJTgxbjn.cjs.map → ptyHost-DfXM9RCJ.cjs.map} +1 -1
- package/dist/ptyHost.cjs +1 -1
- package/dist/ptyHost.mjs +1 -1
- package/dist/{runnerDependencies-Ca9jPi1A.mjs → runnerDependencies-CxcowAwe.mjs} +5 -5
- package/dist/{runnerDependencies-Ca9jPi1A.mjs.map → runnerDependencies-CxcowAwe.mjs.map} +1 -1
- package/dist/{runnerDependencies-s6vttB3n.cjs → runnerDependencies-DihSv3kW.cjs} +5 -5
- package/dist/{runnerDependencies-s6vttB3n.cjs.map → runnerDependencies-DihSv3kW.cjs.map} +1 -1
- package/dist/runnerDependencies.cjs +1 -1
- package/dist/runnerDependencies.mjs +1 -1
- package/dist/{runnerPaths-DMYG6DZ3.mjs → runnerPaths-BiO-WuKm.mjs} +4 -4
- package/dist/runnerPaths-BiO-WuKm.mjs.map +1 -0
- package/dist/{runnerPaths-BW-ebQfP.cjs → runnerPaths-Dkl43cmS.cjs} +5 -5
- package/dist/runnerPaths-Dkl43cmS.cjs.map +1 -0
- package/dist/runnerPaths.cjs +1 -1
- package/dist/runnerPaths.d.mts.map +1 -1
- package/dist/runnerPaths.mjs +1 -1
- package/dist/{runnerRegistry-B434F5lR.mjs → runnerRegistry-CUKzavaV.mjs} +2 -2
- package/dist/runnerRegistry-CUKzavaV.mjs.map +1 -0
- package/dist/{runnerRegistry-sjOISoLL.cjs → runnerRegistry-qIYq2pZW.cjs} +3 -3
- package/dist/runnerRegistry-qIYq2pZW.cjs.map +1 -0
- package/dist/runnerRegistry.cjs +1 -1
- package/dist/runnerRegistry.mjs +1 -1
- package/dist/session-3oSU3QjI.mjs +26 -0
- package/dist/session-3oSU3QjI.mjs.map +1 -0
- package/dist/session-CRql5E2T.cjs +25 -0
- package/dist/session-CRql5E2T.cjs.map +1 -0
- package/dist/{session-BvqzhXZY.cjs → session-D_nH4XhL.cjs} +16 -2
- package/dist/session-D_nH4XhL.cjs.map +1 -0
- package/dist/session-IgtrocZP.mjs +8 -0
- package/dist/session-IgtrocZP.mjs.map +1 -0
- package/dist/session.cjs +3 -2
- package/dist/session.d.mts +2 -0
- package/dist/session.d.mts.map +1 -1
- package/dist/session.mjs +3 -3
- package/package.json +16 -16
- package/dist/runnerPaths-BW-ebQfP.cjs.map +0 -1
- package/dist/runnerPaths-DMYG6DZ3.mjs.map +0 -1
- package/dist/runnerRegistry-B434F5lR.mjs.map +0 -1
- package/dist/runnerRegistry-sjOISoLL.cjs.map +0 -1
- package/dist/session-BKYnH4R6.mjs +0 -18
- package/dist/session-BKYnH4R6.mjs.map +0 -1
- package/dist/session-BvqzhXZY.cjs.map +0 -1
- package/dist/session-DE2Acesp.mjs +0 -6
- package/dist/session-DE2Acesp.mjs.map +0 -1
- package/dist/session-SfIPtdZO.cjs +0 -11
- package/dist/session-SfIPtdZO.cjs.map +0 -1
package/dist/bin/cli.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const require_runnerSequenceStore = require("../runnerSequenceStore-Ck6M-8r_.cjs");
|
|
4
|
-
const require_cliApp = require("../cliApp-
|
|
5
|
-
const require_session = require("../session-
|
|
6
|
-
const require_runnerDependencies = require("../runnerDependencies-
|
|
4
|
+
const require_cliApp = require("../cliApp-GKcqD6CT.cjs");
|
|
5
|
+
const require_session = require("../session-D_nH4XhL.cjs");
|
|
6
|
+
const require_runnerDependencies = require("../runnerDependencies-DihSv3kW.cjs");
|
|
7
7
|
let node_fs_promises = require("node:fs/promises");
|
|
8
8
|
let node_url = require("node:url");
|
|
9
9
|
let _agimon_ai_po_core_telemetry = require("@agimon-ai/po-core/telemetry");
|
package/dist/bin/cli.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as createRunnerSequenceStore } from "../runnerSequenceStore-CaY7S_Sd.mjs";
|
|
3
|
-
import { n as runnerCliHelp, t as runCli } from "../cliApp-
|
|
4
|
-
import { t as inheritedSessionId } from "../session-
|
|
5
|
-
import { t as createRunnerDependencies } from "../runnerDependencies-
|
|
3
|
+
import { n as runnerCliHelp, t as runCli } from "../cliApp-g7APOwcU.mjs";
|
|
4
|
+
import { t as inheritedSessionId } from "../session-3oSU3QjI.mjs";
|
|
5
|
+
import { t as createRunnerDependencies } from "../runnerDependencies-CxcowAwe.mjs";
|
|
6
6
|
import { realpath } from "node:fs/promises";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { createPoTelemetry } from "@agimon-ai/po-core/telemetry";
|
package/dist/bin/runnerHost.cjs
CHANGED
|
@@ -177,8 +177,8 @@ async function readMarker(marker) {
|
|
|
177
177
|
async function loadCleaner() {
|
|
178
178
|
const [{ cleanupRunnerSession }, { RunnerPaths }, { createDefaultProcessRegistry }] = await Promise.all([
|
|
179
179
|
Promise.resolve().then(() => require("../runnerHost-BXk4sPBP.cjs")).then((n) => n.runnerHost_exports),
|
|
180
|
-
Promise.resolve().then(() => require("../runnerPaths-
|
|
181
|
-
Promise.resolve().then(() => require("../runnerRegistry-
|
|
180
|
+
Promise.resolve().then(() => require("../runnerPaths-Dkl43cmS.cjs")).then((n) => n.runnerPaths_exports),
|
|
181
|
+
Promise.resolve().then(() => require("../runnerRegistry-qIYq2pZW.cjs")).then((n) => n.runnerRegistry_exports)
|
|
182
182
|
]);
|
|
183
183
|
const registries = /* @__PURE__ */ new Map();
|
|
184
184
|
process.once("beforeExit", () => {
|
package/dist/bin/runnerHost.mjs
CHANGED
|
@@ -173,8 +173,8 @@ async function readMarker(marker) {
|
|
|
173
173
|
async function loadCleaner() {
|
|
174
174
|
const [{ cleanupRunnerSession }, { RunnerPaths }, { createDefaultProcessRegistry }] = await Promise.all([
|
|
175
175
|
import("../runnerHost-B2Jiba1U.mjs").then((n) => n.r),
|
|
176
|
-
import("../runnerPaths-
|
|
177
|
-
import("../runnerRegistry-
|
|
176
|
+
import("../runnerPaths-BiO-WuKm.mjs").then((n) => n.r),
|
|
177
|
+
import("../runnerRegistry-CUKzavaV.mjs").then((n) => n.r)
|
|
178
178
|
]);
|
|
179
179
|
const registries = /* @__PURE__ */ new Map();
|
|
180
180
|
process.once("beforeExit", () => {
|
|
@@ -2,7 +2,7 @@ const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
|
2
2
|
const require_bashResult = require("./bashResult-B1w7qkQW.cjs");
|
|
3
3
|
const require_reconcile = require("./reconcile-DjF9T1hN.cjs");
|
|
4
4
|
const require_runnerSequenceStore = require("./runnerSequenceStore-Ck6M-8r_.cjs");
|
|
5
|
-
const require_session = require("./session-
|
|
5
|
+
const require_session = require("./session-D_nH4XhL.cjs");
|
|
6
6
|
let node_fs_promises = require("node:fs/promises");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
8
|
node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
|
|
@@ -304,4 +304,4 @@ Object.defineProperty(exports, "runnerCliHelp", {
|
|
|
304
304
|
}
|
|
305
305
|
});
|
|
306
306
|
|
|
307
|
-
//# sourceMappingURL=cliApp-
|
|
307
|
+
//# sourceMappingURL=cliApp-GKcqD6CT.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cliApp-DkhnAFR1.cjs","names":["inheritedSessionId","isSequenceId","requiredSessionId","formatRunnerLine","open","stat","stopRunnerProcess"],"sources":["../src/constants/cliApp.ts","../src/services/cliApp/index.ts"],"sourcesContent":["export const DEFAULT_LOG_LINES = 200;\nexport const FOLLOW_INTERVAL_MS = 250;\nexport const COMPLETED_STATE = 'completed';\nexport const STOPPED_REASON = 'stopped';\nexport const TERMINATION_SIGNAL = 'SIGTERM';\n","import fs from 'node:fs';\nimport { open, stat } from 'node:fs/promises';\n\nimport {\n COMPLETED_STATE,\n DEFAULT_LOG_LINES,\n FOLLOW_INTERVAL_MS,\n STOPPED_REASON,\n TERMINATION_SIGNAL,\n} from '../../constants/cliApp';\nimport { formatRunnerLine } from '../../services/bashResult';\nimport { inheritedSessionId, requiredSessionId } from '../../services/session';\nimport type { ILauncher } from '../../types/launcher';\nimport type { ILogReader } from '../../types/logReader';\nimport type { IRmuxBackend } from '../../types/rmuxBackend';\nimport type { IRunnerRegistry, RunnerRecord } from '../../types/runnerRegistry';\nimport { stopRunnerProcess } from '../reconcile';\nimport { isSequenceId, type RunnerSequenceStore } from '../runnerSequenceStore';\n\nconst MULTIPLEXER_BACKENDS = new Set(['rmux', 'tmux']);\n\nexport interface CliDependencies {\n registry: IRunnerRegistry;\n launcher: ILauncher;\n rmuxBackend: IRmuxBackend;\n logReader: ILogReader;\n env?: NodeJS.ProcessEnv;\n sequences?: RunnerSequenceStore;\n stdout(text: string): void;\n stderr(text: string): void;\n readStdin(): Promise<string>;\n}\n\nexport async function runCli(argv: readonly string[], dependencies: CliDependencies): Promise<number> {\n const [command, ...args] = argv;\n if (command !== undefined && command !== '--help' && command !== '-h' && !inheritedSessionId(dependencies.env)) {\n dependencies.stderr('PI_SESSION_ID is required for po-runner commands');\n return 1;\n }\n if (command && args[0] && isSequenceId(args[0])) return sequenceCommand(command, args, dependencies);\n switch (command) {\n case 'list':\n return list(args, dependencies);\n case 'status':\n return status(args, dependencies);\n case 'logs':\n return logs(args, dependencies);\n case 'stop':\n return stop(args, dependencies);\n case 'stop-all':\n return stopAll(args, dependencies);\n case 'input':\n return input(args, dependencies);\n case '--help':\n case '-h':\n case undefined:\n dependencies.stdout(runnerCliHelp());\n return 0;\n default:\n dependencies.stderr(`Unknown command: ${command}\\n\\n${runnerCliHelp()}`);\n return 1;\n }\n}\n\nasync function sequenceCommand(\n command: string,\n args: readonly string[],\n dependencies: CliDependencies,\n): Promise<number> {\n const store = dependencies.sequences;\n if (!store) {\n dependencies.stderr('Sequence controls are unavailable.');\n return 1;\n }\n const record = await store.get(args[0]!);\n if (!record) {\n dependencies.stderr(`No sequence with id ${args[0]}`);\n return 1;\n }\n if (command === 'stop') {\n if (record.state !== 'running') {\n dependencies.stderr(`Sequence ${record.id} is already completed`);\n return 1;\n }\n await store.requestStop(record.id, option(args, '--reason'));\n dependencies.stdout(`Stop requested for sequence ${record.id}`);\n return 0;\n }\n if (command === 'status') {\n dependencies.stdout(\n [\n `ID: ${record.id}`,\n `Name: ${record.name}`,\n `State: ${record.state}`,\n `Session: ${record.sessionId}`,\n ...(record.outcome ? [`Outcome: ${record.outcome}`] : []),\n ...record.stages.map((stage) => `Stage ${stage.index + 1}: ${stage.state}${stage.id ? ` (${stage.id})` : ''}`),\n ].join('\\n'),\n );\n return 0;\n }\n if (command === 'logs') {\n if (args.includes('--follow')) {\n dependencies.stderr('Sequence logs are finite snapshots; inspect status before reading again.');\n return 1;\n }\n if (record.result)\n dependencies.stdout(\n record.result.content.flatMap((part) => (part.type === 'text' ? [part.text] : [])).join('\\n'),\n );\n else\n dependencies.stdout(\n [\n `Sequence ${record.id} is running.`,\n ...record.stages.filter((stage) => stage.logPath).map((stage) => `${stage.index + 1}. ${stage.logPath}`),\n ].join('\\n'),\n );\n return 0;\n }\n dependencies.stderr(\n command === 'input' ? 'Command sequences are noninteractive.' : `Unknown sequence command: ${command}`,\n );\n return 1;\n}\n\nasync function list(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const sessionId = requiredSessionId(dependencies.env);\n const records = args.includes('--all')\n ? await dependencies.registry.listAll(sessionId)\n : await dependencies.registry.listBySession(sessionId);\n const sequences = (await dependencies.sequences?.list(args.includes('--all'))) ?? [];\n const rows = [\n ...records.map(formatRecord),\n ...sequences.map((record) => `${record.id} ${record.state} sequence ${record.name}`),\n ];\n dependencies.stdout(rows.length > 0 ? rows.join('\\n') : 'No runners');\n return 0;\n}\n\nasync function status(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n dependencies.stdout(\n [\n `ID: ${record.id}`,\n `Name: ${record.name}`,\n `State: ${record.state}`,\n `Backend: ${record.backend}`,\n ...(record.backendTarget ? [`Backend target: ${record.backendTarget}`] : []),\n `PID: ${record.pid}`,\n `Command: ${record.command}`,\n `CWD: ${record.cwd}`,\n `Session: ${record.sessionId}`,\n `Log: ${record.logPath}`,\n ...(record.exit ? [`Exit: ${formatExit(record)}`] : []),\n ].join('\\n'),\n );\n return 0;\n}\n\nasync function logs(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n const lines = positiveInteger(option(args, '--lines')) ?? DEFAULT_LOG_LINES;\n const result = await dependencies.logReader.read(record.logPath, { lines });\n if (!result.exists) {\n dependencies.stderr(`Runner ${record.id} has not written a log yet`);\n return 1;\n }\n if (result.text) dependencies.stdout(result.text);\n if (!args.includes('--follow')) return 0;\n return follow(record, result.fileSize, dependencies);\n}\n\nasync function stop(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n if (record.state !== 'running') {\n dependencies.stderr(`Runner ${record.id} is already ${record.state}`);\n return 1;\n }\n const reason = option(args, '--reason');\n if (!(await stopRecord(record, dependencies, reason))) {\n dependencies.stderr(`Could not stop runner ${record.id}`);\n return 1;\n }\n await dependencies.registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: TERMINATION_SIGNAL,\n ...(reason ? { stopReason: reason } : {}),\n },\n record.sessionId,\n );\n dependencies.stdout(`Stopped ${record.id}\\nLog: ${record.logPath}`);\n return 0;\n}\n\nasync function stopAll(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const sessionId = requiredSessionId(dependencies.env);\n const records = await dependencies.registry.listBySession(sessionId);\n const reason = option(args, '--reason');\n for (const sequence of (await dependencies.sequences?.list()) ?? []) {\n await dependencies.sequences!.requestStop(sequence.id, reason);\n dependencies.stdout(`Stop requested for sequence ${sequence.id}`);\n }\n const stopped: RunnerRecord[] = [];\n for (const record of records) {\n if (!(await stopRecord(record, dependencies, reason))) continue;\n await dependencies.registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: TERMINATION_SIGNAL,\n ...(reason ? { stopReason: reason } : {}),\n },\n record.sessionId,\n );\n stopped.push(record);\n }\n dependencies.stdout(\n records.length === 0\n ? `No active runners for session ${sessionId}`\n : `Stopped ${stopped.length} runner(s) for session ${sessionId}: ${stopped.map((record) => record.id).join(', ')}`,\n );\n if (stopped.length !== records.length) {\n dependencies.stderr(`Could not stop ${records.length - stopped.length} runner(s) for session ${sessionId}`);\n return 1;\n }\n return 0;\n}\n\nasync function input(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n if (record.state === COMPLETED_STATE) {\n dependencies.stderr(`Runner ${record.id} is already completed; use po-runner logs ${record.id}`);\n return 1;\n }\n if (!record.interactive) {\n dependencies.stderr(`Runner ${record.id} was not started with interactive: true`);\n return 1;\n }\n const supplied = option(args, '--text');\n const text = supplied ?? (await dependencies.readStdin());\n const payload = args.includes('--enter') && !text.endsWith('\\n') ? `${text}\\n` : text;\n if (!MULTIPLEXER_BACKENDS.has(record.backend) || !record.backendTarget) {\n dependencies.stderr(`Runner ${record.id} uses the ${record.backend} backend, which does not expose CLI input`);\n return 1;\n }\n if (!(await dependencies.rmuxBackend.input(record.backendTarget, payload))) {\n dependencies.stderr(`Runner ${record.id} is no longer accepting RMUX input`);\n return 1;\n }\n dependencies.stdout(`Sent input to ${record.id}`);\n return 0;\n}\n\nasync function requiredRecord(\n id: string | undefined,\n dependencies: CliDependencies,\n): Promise<RunnerRecord | undefined> {\n if (!id) {\n dependencies.stderr('A runner ID is required');\n return undefined;\n }\n const sessionId = requiredSessionId(dependencies.env);\n const scopedRecord = await dependencies.registry.get(id, sessionId);\n if (!scopedRecord) {\n dependencies.stderr(`No runner with id ${id}`);\n return undefined;\n }\n return scopedRecord;\n}\n\nfunction formatRecord(record: RunnerRecord): string {\n const active = formatRunnerLine(record, Date.now());\n return `${record.id} ${record.state} ${record.backend} ${active}`;\n}\n\nfunction formatExit(record: RunnerRecord): string {\n const exit = record.exit;\n if (!exit) return 'unknown';\n const detail = exit.code !== null ? ` code ${exit.code}` : exit.signal ? ` ${exit.signal}` : '';\n const attribution = exit.terminationReason ? ` [${exit.terminationReason}]` : '';\n const note = exit.stopReason ? ` (${exit.stopReason})` : '';\n return `${exit.reason}${detail}${attribution}${note}`;\n}\n\nfunction option(args: readonly string[], name: string): string | undefined {\n const index = args.indexOf(name);\n return index >= 0 ? args[index + 1] : undefined;\n}\n\nfunction positiveInteger(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const parsed = Number(value);\n return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\nasync function follow(record: RunnerRecord, initialSize: number, dependencies: CliDependencies): Promise<number> {\n let offset = initialSize;\n return new Promise<number>((resolve) => {\n let stopped = false;\n let stateTimer: NodeJS.Timeout;\n let reads = Promise.resolve();\n const drain = async (current: fs.Stats): Promise<void> => {\n if (current.size < offset) offset = 0;\n if (current.size === offset) return;\n const length = current.size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = await open(record.logPath, 'r');\n try {\n const { bytesRead } = await descriptor.read(buffer, 0, length, offset);\n if (bytesRead > 0) dependencies.stdout(buffer.subarray(0, bytesRead).toString('utf8'));\n offset += bytesRead;\n } finally {\n await descriptor.close();\n }\n };\n const report = (error: unknown): void => {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n dependencies.stderr(`Runner ${record.id} failed to flush its final log output: ${String(error)}`);\n }\n };\n const listener = (current: fs.Stats): void => {\n if (stopped) return;\n reads = reads.then(() => drain(current)).catch(report);\n };\n const stopFollowing = (): void => {\n if (stopped) return;\n stopped = true;\n fs.unwatchFile(record.logPath, listener);\n clearInterval(stateTimer);\n process.off('SIGINT', stopFollowing);\n reads = reads.then(async () => drain(await stat(record.logPath))).catch(report);\n void reads.then(() => resolve(0));\n };\n process.once('SIGINT', stopFollowing);\n fs.watchFile(record.logPath, { interval: FOLLOW_INTERVAL_MS }, listener);\n stateTimer = setInterval(() => {\n void dependencies.registry.get(record.id).then((current) => {\n if (current?.state === COMPLETED_STATE) stopFollowing();\n });\n }, FOLLOW_INTERVAL_MS);\n });\n}\n\nasync function stopRecord(record: RunnerRecord, dependencies: CliDependencies, reason?: string): Promise<boolean> {\n return stopRunnerProcess(record, dependencies.launcher, dependencies.rmuxBackend, {\n registry: dependencies.registry,\n intent: { reason: STOPPED_REASON, terminationReason: 'user_stop', ...(reason ? { stopReason: reason } : {}) },\n });\n}\n\nexport function runnerCliHelp(): string {\n return [\n 'Usage: po-runner <command>',\n '',\n 'Commands:',\n ' list [--all]',\n ' status <id>',\n ' logs <id> [--lines N] [--follow]',\n ' stop <id> [--reason <text>]',\n ' stop-all [--reason <text>]',\n ' input <id> [--text <text>] [--enter]',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cliApp-GKcqD6CT.cjs","names":["inheritedSessionId","isSequenceId","requiredSessionId","formatRunnerLine","open","stat","stopRunnerProcess"],"sources":["../src/constants/cliApp.ts","../src/services/cliApp/index.ts"],"sourcesContent":["export const DEFAULT_LOG_LINES = 200;\nexport const FOLLOW_INTERVAL_MS = 250;\nexport const COMPLETED_STATE = 'completed';\nexport const STOPPED_REASON = 'stopped';\nexport const TERMINATION_SIGNAL = 'SIGTERM';\n","import fs from 'node:fs';\nimport { open, stat } from 'node:fs/promises';\n\nimport {\n COMPLETED_STATE,\n DEFAULT_LOG_LINES,\n FOLLOW_INTERVAL_MS,\n STOPPED_REASON,\n TERMINATION_SIGNAL,\n} from '../../constants/cliApp';\nimport { formatRunnerLine } from '../../services/bashResult';\nimport { inheritedSessionId, requiredSessionId } from '../../services/session';\nimport type { ILauncher } from '../../types/launcher';\nimport type { ILogReader } from '../../types/logReader';\nimport type { IRmuxBackend } from '../../types/rmuxBackend';\nimport type { IRunnerRegistry, RunnerRecord } from '../../types/runnerRegistry';\nimport { stopRunnerProcess } from '../reconcile';\nimport { isSequenceId, type RunnerSequenceStore } from '../runnerSequenceStore';\n\nconst MULTIPLEXER_BACKENDS = new Set(['rmux', 'tmux']);\n\nexport interface CliDependencies {\n registry: IRunnerRegistry;\n launcher: ILauncher;\n rmuxBackend: IRmuxBackend;\n logReader: ILogReader;\n env?: NodeJS.ProcessEnv;\n sequences?: RunnerSequenceStore;\n stdout(text: string): void;\n stderr(text: string): void;\n readStdin(): Promise<string>;\n}\n\nexport async function runCli(argv: readonly string[], dependencies: CliDependencies): Promise<number> {\n const [command, ...args] = argv;\n if (command !== undefined && command !== '--help' && command !== '-h' && !inheritedSessionId(dependencies.env)) {\n dependencies.stderr('PI_SESSION_ID is required for po-runner commands');\n return 1;\n }\n if (command && args[0] && isSequenceId(args[0])) return sequenceCommand(command, args, dependencies);\n switch (command) {\n case 'list':\n return list(args, dependencies);\n case 'status':\n return status(args, dependencies);\n case 'logs':\n return logs(args, dependencies);\n case 'stop':\n return stop(args, dependencies);\n case 'stop-all':\n return stopAll(args, dependencies);\n case 'input':\n return input(args, dependencies);\n case '--help':\n case '-h':\n case undefined:\n dependencies.stdout(runnerCliHelp());\n return 0;\n default:\n dependencies.stderr(`Unknown command: ${command}\\n\\n${runnerCliHelp()}`);\n return 1;\n }\n}\n\nasync function sequenceCommand(\n command: string,\n args: readonly string[],\n dependencies: CliDependencies,\n): Promise<number> {\n const store = dependencies.sequences;\n if (!store) {\n dependencies.stderr('Sequence controls are unavailable.');\n return 1;\n }\n const record = await store.get(args[0]!);\n if (!record) {\n dependencies.stderr(`No sequence with id ${args[0]}`);\n return 1;\n }\n if (command === 'stop') {\n if (record.state !== 'running') {\n dependencies.stderr(`Sequence ${record.id} is already completed`);\n return 1;\n }\n await store.requestStop(record.id, option(args, '--reason'));\n dependencies.stdout(`Stop requested for sequence ${record.id}`);\n return 0;\n }\n if (command === 'status') {\n dependencies.stdout(\n [\n `ID: ${record.id}`,\n `Name: ${record.name}`,\n `State: ${record.state}`,\n `Session: ${record.sessionId}`,\n ...(record.outcome ? [`Outcome: ${record.outcome}`] : []),\n ...record.stages.map((stage) => `Stage ${stage.index + 1}: ${stage.state}${stage.id ? ` (${stage.id})` : ''}`),\n ].join('\\n'),\n );\n return 0;\n }\n if (command === 'logs') {\n if (args.includes('--follow')) {\n dependencies.stderr('Sequence logs are finite snapshots; inspect status before reading again.');\n return 1;\n }\n if (record.result)\n dependencies.stdout(\n record.result.content.flatMap((part) => (part.type === 'text' ? [part.text] : [])).join('\\n'),\n );\n else\n dependencies.stdout(\n [\n `Sequence ${record.id} is running.`,\n ...record.stages.filter((stage) => stage.logPath).map((stage) => `${stage.index + 1}. ${stage.logPath}`),\n ].join('\\n'),\n );\n return 0;\n }\n dependencies.stderr(\n command === 'input' ? 'Command sequences are noninteractive.' : `Unknown sequence command: ${command}`,\n );\n return 1;\n}\n\nasync function list(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const sessionId = requiredSessionId(dependencies.env);\n const records = args.includes('--all')\n ? await dependencies.registry.listAll(sessionId)\n : await dependencies.registry.listBySession(sessionId);\n const sequences = (await dependencies.sequences?.list(args.includes('--all'))) ?? [];\n const rows = [\n ...records.map(formatRecord),\n ...sequences.map((record) => `${record.id} ${record.state} sequence ${record.name}`),\n ];\n dependencies.stdout(rows.length > 0 ? rows.join('\\n') : 'No runners');\n return 0;\n}\n\nasync function status(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n dependencies.stdout(\n [\n `ID: ${record.id}`,\n `Name: ${record.name}`,\n `State: ${record.state}`,\n `Backend: ${record.backend}`,\n ...(record.backendTarget ? [`Backend target: ${record.backendTarget}`] : []),\n `PID: ${record.pid}`,\n `Command: ${record.command}`,\n `CWD: ${record.cwd}`,\n `Session: ${record.sessionId}`,\n `Log: ${record.logPath}`,\n ...(record.exit ? [`Exit: ${formatExit(record)}`] : []),\n ].join('\\n'),\n );\n return 0;\n}\n\nasync function logs(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n const lines = positiveInteger(option(args, '--lines')) ?? DEFAULT_LOG_LINES;\n const result = await dependencies.logReader.read(record.logPath, { lines });\n if (!result.exists) {\n dependencies.stderr(`Runner ${record.id} has not written a log yet`);\n return 1;\n }\n if (result.text) dependencies.stdout(result.text);\n if (!args.includes('--follow')) return 0;\n return follow(record, result.fileSize, dependencies);\n}\n\nasync function stop(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n if (record.state !== 'running') {\n dependencies.stderr(`Runner ${record.id} is already ${record.state}`);\n return 1;\n }\n const reason = option(args, '--reason');\n if (!(await stopRecord(record, dependencies, reason))) {\n dependencies.stderr(`Could not stop runner ${record.id}`);\n return 1;\n }\n await dependencies.registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: TERMINATION_SIGNAL,\n ...(reason ? { stopReason: reason } : {}),\n },\n record.sessionId,\n );\n dependencies.stdout(`Stopped ${record.id}\\nLog: ${record.logPath}`);\n return 0;\n}\n\nasync function stopAll(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const sessionId = requiredSessionId(dependencies.env);\n const records = await dependencies.registry.listBySession(sessionId);\n const reason = option(args, '--reason');\n for (const sequence of (await dependencies.sequences?.list()) ?? []) {\n await dependencies.sequences!.requestStop(sequence.id, reason);\n dependencies.stdout(`Stop requested for sequence ${sequence.id}`);\n }\n const stopped: RunnerRecord[] = [];\n for (const record of records) {\n if (!(await stopRecord(record, dependencies, reason))) continue;\n await dependencies.registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: TERMINATION_SIGNAL,\n ...(reason ? { stopReason: reason } : {}),\n },\n record.sessionId,\n );\n stopped.push(record);\n }\n dependencies.stdout(\n records.length === 0\n ? `No active runners for session ${sessionId}`\n : `Stopped ${stopped.length} runner(s) for session ${sessionId}: ${stopped.map((record) => record.id).join(', ')}`,\n );\n if (stopped.length !== records.length) {\n dependencies.stderr(`Could not stop ${records.length - stopped.length} runner(s) for session ${sessionId}`);\n return 1;\n }\n return 0;\n}\n\nasync function input(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n if (record.state === COMPLETED_STATE) {\n dependencies.stderr(`Runner ${record.id} is already completed; use po-runner logs ${record.id}`);\n return 1;\n }\n if (!record.interactive) {\n dependencies.stderr(`Runner ${record.id} was not started with interactive: true`);\n return 1;\n }\n const supplied = option(args, '--text');\n const text = supplied ?? (await dependencies.readStdin());\n const payload = args.includes('--enter') && !text.endsWith('\\n') ? `${text}\\n` : text;\n if (!MULTIPLEXER_BACKENDS.has(record.backend) || !record.backendTarget) {\n dependencies.stderr(`Runner ${record.id} uses the ${record.backend} backend, which does not expose CLI input`);\n return 1;\n }\n if (!(await dependencies.rmuxBackend.input(record.backendTarget, payload))) {\n dependencies.stderr(`Runner ${record.id} is no longer accepting RMUX input`);\n return 1;\n }\n dependencies.stdout(`Sent input to ${record.id}`);\n return 0;\n}\n\nasync function requiredRecord(\n id: string | undefined,\n dependencies: CliDependencies,\n): Promise<RunnerRecord | undefined> {\n if (!id) {\n dependencies.stderr('A runner ID is required');\n return undefined;\n }\n const sessionId = requiredSessionId(dependencies.env);\n const scopedRecord = await dependencies.registry.get(id, sessionId);\n if (!scopedRecord) {\n dependencies.stderr(`No runner with id ${id}`);\n return undefined;\n }\n return scopedRecord;\n}\n\nfunction formatRecord(record: RunnerRecord): string {\n const active = formatRunnerLine(record, Date.now());\n return `${record.id} ${record.state} ${record.backend} ${active}`;\n}\n\nfunction formatExit(record: RunnerRecord): string {\n const exit = record.exit;\n if (!exit) return 'unknown';\n const detail = exit.code !== null ? ` code ${exit.code}` : exit.signal ? ` ${exit.signal}` : '';\n const attribution = exit.terminationReason ? ` [${exit.terminationReason}]` : '';\n const note = exit.stopReason ? ` (${exit.stopReason})` : '';\n return `${exit.reason}${detail}${attribution}${note}`;\n}\n\nfunction option(args: readonly string[], name: string): string | undefined {\n const index = args.indexOf(name);\n return index >= 0 ? args[index + 1] : undefined;\n}\n\nfunction positiveInteger(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const parsed = Number(value);\n return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\nasync function follow(record: RunnerRecord, initialSize: number, dependencies: CliDependencies): Promise<number> {\n let offset = initialSize;\n return new Promise<number>((resolve) => {\n let stopped = false;\n let stateTimer: NodeJS.Timeout;\n let reads = Promise.resolve();\n const drain = async (current: fs.Stats): Promise<void> => {\n if (current.size < offset) offset = 0;\n if (current.size === offset) return;\n const length = current.size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = await open(record.logPath, 'r');\n try {\n const { bytesRead } = await descriptor.read(buffer, 0, length, offset);\n if (bytesRead > 0) dependencies.stdout(buffer.subarray(0, bytesRead).toString('utf8'));\n offset += bytesRead;\n } finally {\n await descriptor.close();\n }\n };\n const report = (error: unknown): void => {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n dependencies.stderr(`Runner ${record.id} failed to flush its final log output: ${String(error)}`);\n }\n };\n const listener = (current: fs.Stats): void => {\n if (stopped) return;\n reads = reads.then(() => drain(current)).catch(report);\n };\n const stopFollowing = (): void => {\n if (stopped) return;\n stopped = true;\n fs.unwatchFile(record.logPath, listener);\n clearInterval(stateTimer);\n process.off('SIGINT', stopFollowing);\n reads = reads.then(async () => drain(await stat(record.logPath))).catch(report);\n void reads.then(() => resolve(0));\n };\n process.once('SIGINT', stopFollowing);\n fs.watchFile(record.logPath, { interval: FOLLOW_INTERVAL_MS }, listener);\n stateTimer = setInterval(() => {\n void dependencies.registry.get(record.id).then((current) => {\n if (current?.state === COMPLETED_STATE) stopFollowing();\n });\n }, FOLLOW_INTERVAL_MS);\n });\n}\n\nasync function stopRecord(record: RunnerRecord, dependencies: CliDependencies, reason?: string): Promise<boolean> {\n return stopRunnerProcess(record, dependencies.launcher, dependencies.rmuxBackend, {\n registry: dependencies.registry,\n intent: { reason: STOPPED_REASON, terminationReason: 'user_stop', ...(reason ? { stopReason: reason } : {}) },\n });\n}\n\nexport function runnerCliHelp(): string {\n return [\n 'Usage: po-runner <command>',\n '',\n 'Commands:',\n ' list [--all]',\n ' status <id>',\n ' logs <id> [--lines N] [--follow]',\n ' stop <id> [--reason <text>]',\n ' stop-all [--reason <text>]',\n ' input <id> [--text <text>] [--enter]',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { l as formatRunnerLine } from "./bashResult-ntL4Umz3.mjs";
|
|
2
2
|
import { n as stopRunnerProcess } from "./reconcile-DUaX_vKZ.mjs";
|
|
3
3
|
import { n as isSequenceId } from "./runnerSequenceStore-CaY7S_Sd.mjs";
|
|
4
|
-
import { n as requiredSessionId, t as inheritedSessionId } from "./session-
|
|
4
|
+
import { n as requiredSessionId, t as inheritedSessionId } from "./session-3oSU3QjI.mjs";
|
|
5
5
|
import { open, stat } from "node:fs/promises";
|
|
6
6
|
import fs$1 from "node:fs";
|
|
7
7
|
const STOPPED_REASON = "stopped";
|
|
@@ -291,4 +291,4 @@ function runnerCliHelp() {
|
|
|
291
291
|
//#endregion
|
|
292
292
|
export { runnerCliHelp as n, runCli as t };
|
|
293
293
|
|
|
294
|
-
//# sourceMappingURL=cliApp-
|
|
294
|
+
//# sourceMappingURL=cliApp-g7APOwcU.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cliApp-DjU_M0ho.mjs","names":[],"sources":["../src/constants/cliApp.ts","../src/services/cliApp/index.ts"],"sourcesContent":["export const DEFAULT_LOG_LINES = 200;\nexport const FOLLOW_INTERVAL_MS = 250;\nexport const COMPLETED_STATE = 'completed';\nexport const STOPPED_REASON = 'stopped';\nexport const TERMINATION_SIGNAL = 'SIGTERM';\n","import fs from 'node:fs';\nimport { open, stat } from 'node:fs/promises';\n\nimport {\n COMPLETED_STATE,\n DEFAULT_LOG_LINES,\n FOLLOW_INTERVAL_MS,\n STOPPED_REASON,\n TERMINATION_SIGNAL,\n} from '../../constants/cliApp';\nimport { formatRunnerLine } from '../../services/bashResult';\nimport { inheritedSessionId, requiredSessionId } from '../../services/session';\nimport type { ILauncher } from '../../types/launcher';\nimport type { ILogReader } from '../../types/logReader';\nimport type { IRmuxBackend } from '../../types/rmuxBackend';\nimport type { IRunnerRegistry, RunnerRecord } from '../../types/runnerRegistry';\nimport { stopRunnerProcess } from '../reconcile';\nimport { isSequenceId, type RunnerSequenceStore } from '../runnerSequenceStore';\n\nconst MULTIPLEXER_BACKENDS = new Set(['rmux', 'tmux']);\n\nexport interface CliDependencies {\n registry: IRunnerRegistry;\n launcher: ILauncher;\n rmuxBackend: IRmuxBackend;\n logReader: ILogReader;\n env?: NodeJS.ProcessEnv;\n sequences?: RunnerSequenceStore;\n stdout(text: string): void;\n stderr(text: string): void;\n readStdin(): Promise<string>;\n}\n\nexport async function runCli(argv: readonly string[], dependencies: CliDependencies): Promise<number> {\n const [command, ...args] = argv;\n if (command !== undefined && command !== '--help' && command !== '-h' && !inheritedSessionId(dependencies.env)) {\n dependencies.stderr('PI_SESSION_ID is required for po-runner commands');\n return 1;\n }\n if (command && args[0] && isSequenceId(args[0])) return sequenceCommand(command, args, dependencies);\n switch (command) {\n case 'list':\n return list(args, dependencies);\n case 'status':\n return status(args, dependencies);\n case 'logs':\n return logs(args, dependencies);\n case 'stop':\n return stop(args, dependencies);\n case 'stop-all':\n return stopAll(args, dependencies);\n case 'input':\n return input(args, dependencies);\n case '--help':\n case '-h':\n case undefined:\n dependencies.stdout(runnerCliHelp());\n return 0;\n default:\n dependencies.stderr(`Unknown command: ${command}\\n\\n${runnerCliHelp()}`);\n return 1;\n }\n}\n\nasync function sequenceCommand(\n command: string,\n args: readonly string[],\n dependencies: CliDependencies,\n): Promise<number> {\n const store = dependencies.sequences;\n if (!store) {\n dependencies.stderr('Sequence controls are unavailable.');\n return 1;\n }\n const record = await store.get(args[0]!);\n if (!record) {\n dependencies.stderr(`No sequence with id ${args[0]}`);\n return 1;\n }\n if (command === 'stop') {\n if (record.state !== 'running') {\n dependencies.stderr(`Sequence ${record.id} is already completed`);\n return 1;\n }\n await store.requestStop(record.id, option(args, '--reason'));\n dependencies.stdout(`Stop requested for sequence ${record.id}`);\n return 0;\n }\n if (command === 'status') {\n dependencies.stdout(\n [\n `ID: ${record.id}`,\n `Name: ${record.name}`,\n `State: ${record.state}`,\n `Session: ${record.sessionId}`,\n ...(record.outcome ? [`Outcome: ${record.outcome}`] : []),\n ...record.stages.map((stage) => `Stage ${stage.index + 1}: ${stage.state}${stage.id ? ` (${stage.id})` : ''}`),\n ].join('\\n'),\n );\n return 0;\n }\n if (command === 'logs') {\n if (args.includes('--follow')) {\n dependencies.stderr('Sequence logs are finite snapshots; inspect status before reading again.');\n return 1;\n }\n if (record.result)\n dependencies.stdout(\n record.result.content.flatMap((part) => (part.type === 'text' ? [part.text] : [])).join('\\n'),\n );\n else\n dependencies.stdout(\n [\n `Sequence ${record.id} is running.`,\n ...record.stages.filter((stage) => stage.logPath).map((stage) => `${stage.index + 1}. ${stage.logPath}`),\n ].join('\\n'),\n );\n return 0;\n }\n dependencies.stderr(\n command === 'input' ? 'Command sequences are noninteractive.' : `Unknown sequence command: ${command}`,\n );\n return 1;\n}\n\nasync function list(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const sessionId = requiredSessionId(dependencies.env);\n const records = args.includes('--all')\n ? await dependencies.registry.listAll(sessionId)\n : await dependencies.registry.listBySession(sessionId);\n const sequences = (await dependencies.sequences?.list(args.includes('--all'))) ?? [];\n const rows = [\n ...records.map(formatRecord),\n ...sequences.map((record) => `${record.id} ${record.state} sequence ${record.name}`),\n ];\n dependencies.stdout(rows.length > 0 ? rows.join('\\n') : 'No runners');\n return 0;\n}\n\nasync function status(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n dependencies.stdout(\n [\n `ID: ${record.id}`,\n `Name: ${record.name}`,\n `State: ${record.state}`,\n `Backend: ${record.backend}`,\n ...(record.backendTarget ? [`Backend target: ${record.backendTarget}`] : []),\n `PID: ${record.pid}`,\n `Command: ${record.command}`,\n `CWD: ${record.cwd}`,\n `Session: ${record.sessionId}`,\n `Log: ${record.logPath}`,\n ...(record.exit ? [`Exit: ${formatExit(record)}`] : []),\n ].join('\\n'),\n );\n return 0;\n}\n\nasync function logs(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n const lines = positiveInteger(option(args, '--lines')) ?? DEFAULT_LOG_LINES;\n const result = await dependencies.logReader.read(record.logPath, { lines });\n if (!result.exists) {\n dependencies.stderr(`Runner ${record.id} has not written a log yet`);\n return 1;\n }\n if (result.text) dependencies.stdout(result.text);\n if (!args.includes('--follow')) return 0;\n return follow(record, result.fileSize, dependencies);\n}\n\nasync function stop(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n if (record.state !== 'running') {\n dependencies.stderr(`Runner ${record.id} is already ${record.state}`);\n return 1;\n }\n const reason = option(args, '--reason');\n if (!(await stopRecord(record, dependencies, reason))) {\n dependencies.stderr(`Could not stop runner ${record.id}`);\n return 1;\n }\n await dependencies.registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: TERMINATION_SIGNAL,\n ...(reason ? { stopReason: reason } : {}),\n },\n record.sessionId,\n );\n dependencies.stdout(`Stopped ${record.id}\\nLog: ${record.logPath}`);\n return 0;\n}\n\nasync function stopAll(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const sessionId = requiredSessionId(dependencies.env);\n const records = await dependencies.registry.listBySession(sessionId);\n const reason = option(args, '--reason');\n for (const sequence of (await dependencies.sequences?.list()) ?? []) {\n await dependencies.sequences!.requestStop(sequence.id, reason);\n dependencies.stdout(`Stop requested for sequence ${sequence.id}`);\n }\n const stopped: RunnerRecord[] = [];\n for (const record of records) {\n if (!(await stopRecord(record, dependencies, reason))) continue;\n await dependencies.registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: TERMINATION_SIGNAL,\n ...(reason ? { stopReason: reason } : {}),\n },\n record.sessionId,\n );\n stopped.push(record);\n }\n dependencies.stdout(\n records.length === 0\n ? `No active runners for session ${sessionId}`\n : `Stopped ${stopped.length} runner(s) for session ${sessionId}: ${stopped.map((record) => record.id).join(', ')}`,\n );\n if (stopped.length !== records.length) {\n dependencies.stderr(`Could not stop ${records.length - stopped.length} runner(s) for session ${sessionId}`);\n return 1;\n }\n return 0;\n}\n\nasync function input(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n if (record.state === COMPLETED_STATE) {\n dependencies.stderr(`Runner ${record.id} is already completed; use po-runner logs ${record.id}`);\n return 1;\n }\n if (!record.interactive) {\n dependencies.stderr(`Runner ${record.id} was not started with interactive: true`);\n return 1;\n }\n const supplied = option(args, '--text');\n const text = supplied ?? (await dependencies.readStdin());\n const payload = args.includes('--enter') && !text.endsWith('\\n') ? `${text}\\n` : text;\n if (!MULTIPLEXER_BACKENDS.has(record.backend) || !record.backendTarget) {\n dependencies.stderr(`Runner ${record.id} uses the ${record.backend} backend, which does not expose CLI input`);\n return 1;\n }\n if (!(await dependencies.rmuxBackend.input(record.backendTarget, payload))) {\n dependencies.stderr(`Runner ${record.id} is no longer accepting RMUX input`);\n return 1;\n }\n dependencies.stdout(`Sent input to ${record.id}`);\n return 0;\n}\n\nasync function requiredRecord(\n id: string | undefined,\n dependencies: CliDependencies,\n): Promise<RunnerRecord | undefined> {\n if (!id) {\n dependencies.stderr('A runner ID is required');\n return undefined;\n }\n const sessionId = requiredSessionId(dependencies.env);\n const scopedRecord = await dependencies.registry.get(id, sessionId);\n if (!scopedRecord) {\n dependencies.stderr(`No runner with id ${id}`);\n return undefined;\n }\n return scopedRecord;\n}\n\nfunction formatRecord(record: RunnerRecord): string {\n const active = formatRunnerLine(record, Date.now());\n return `${record.id} ${record.state} ${record.backend} ${active}`;\n}\n\nfunction formatExit(record: RunnerRecord): string {\n const exit = record.exit;\n if (!exit) return 'unknown';\n const detail = exit.code !== null ? ` code ${exit.code}` : exit.signal ? ` ${exit.signal}` : '';\n const attribution = exit.terminationReason ? ` [${exit.terminationReason}]` : '';\n const note = exit.stopReason ? ` (${exit.stopReason})` : '';\n return `${exit.reason}${detail}${attribution}${note}`;\n}\n\nfunction option(args: readonly string[], name: string): string | undefined {\n const index = args.indexOf(name);\n return index >= 0 ? args[index + 1] : undefined;\n}\n\nfunction positiveInteger(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const parsed = Number(value);\n return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\nasync function follow(record: RunnerRecord, initialSize: number, dependencies: CliDependencies): Promise<number> {\n let offset = initialSize;\n return new Promise<number>((resolve) => {\n let stopped = false;\n let stateTimer: NodeJS.Timeout;\n let reads = Promise.resolve();\n const drain = async (current: fs.Stats): Promise<void> => {\n if (current.size < offset) offset = 0;\n if (current.size === offset) return;\n const length = current.size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = await open(record.logPath, 'r');\n try {\n const { bytesRead } = await descriptor.read(buffer, 0, length, offset);\n if (bytesRead > 0) dependencies.stdout(buffer.subarray(0, bytesRead).toString('utf8'));\n offset += bytesRead;\n } finally {\n await descriptor.close();\n }\n };\n const report = (error: unknown): void => {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n dependencies.stderr(`Runner ${record.id} failed to flush its final log output: ${String(error)}`);\n }\n };\n const listener = (current: fs.Stats): void => {\n if (stopped) return;\n reads = reads.then(() => drain(current)).catch(report);\n };\n const stopFollowing = (): void => {\n if (stopped) return;\n stopped = true;\n fs.unwatchFile(record.logPath, listener);\n clearInterval(stateTimer);\n process.off('SIGINT', stopFollowing);\n reads = reads.then(async () => drain(await stat(record.logPath))).catch(report);\n void reads.then(() => resolve(0));\n };\n process.once('SIGINT', stopFollowing);\n fs.watchFile(record.logPath, { interval: FOLLOW_INTERVAL_MS }, listener);\n stateTimer = setInterval(() => {\n void dependencies.registry.get(record.id).then((current) => {\n if (current?.state === COMPLETED_STATE) stopFollowing();\n });\n }, FOLLOW_INTERVAL_MS);\n });\n}\n\nasync function stopRecord(record: RunnerRecord, dependencies: CliDependencies, reason?: string): Promise<boolean> {\n return stopRunnerProcess(record, dependencies.launcher, dependencies.rmuxBackend, {\n registry: dependencies.registry,\n intent: { reason: STOPPED_REASON, terminationReason: 'user_stop', ...(reason ? { stopReason: reason } : {}) },\n });\n}\n\nexport function runnerCliHelp(): string {\n return [\n 'Usage: po-runner <command>',\n '',\n 'Commands:',\n ' list [--all]',\n ' status <id>',\n ' logs <id> [--lines N] [--follow]',\n ' stop <id> [--reason <text>]',\n ' stop-all [--reason <text>]',\n ' input <id> [--text <text>] [--enter]',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cliApp-g7APOwcU.mjs","names":[],"sources":["../src/constants/cliApp.ts","../src/services/cliApp/index.ts"],"sourcesContent":["export const DEFAULT_LOG_LINES = 200;\nexport const FOLLOW_INTERVAL_MS = 250;\nexport const COMPLETED_STATE = 'completed';\nexport const STOPPED_REASON = 'stopped';\nexport const TERMINATION_SIGNAL = 'SIGTERM';\n","import fs from 'node:fs';\nimport { open, stat } from 'node:fs/promises';\n\nimport {\n COMPLETED_STATE,\n DEFAULT_LOG_LINES,\n FOLLOW_INTERVAL_MS,\n STOPPED_REASON,\n TERMINATION_SIGNAL,\n} from '../../constants/cliApp';\nimport { formatRunnerLine } from '../../services/bashResult';\nimport { inheritedSessionId, requiredSessionId } from '../../services/session';\nimport type { ILauncher } from '../../types/launcher';\nimport type { ILogReader } from '../../types/logReader';\nimport type { IRmuxBackend } from '../../types/rmuxBackend';\nimport type { IRunnerRegistry, RunnerRecord } from '../../types/runnerRegistry';\nimport { stopRunnerProcess } from '../reconcile';\nimport { isSequenceId, type RunnerSequenceStore } from '../runnerSequenceStore';\n\nconst MULTIPLEXER_BACKENDS = new Set(['rmux', 'tmux']);\n\nexport interface CliDependencies {\n registry: IRunnerRegistry;\n launcher: ILauncher;\n rmuxBackend: IRmuxBackend;\n logReader: ILogReader;\n env?: NodeJS.ProcessEnv;\n sequences?: RunnerSequenceStore;\n stdout(text: string): void;\n stderr(text: string): void;\n readStdin(): Promise<string>;\n}\n\nexport async function runCli(argv: readonly string[], dependencies: CliDependencies): Promise<number> {\n const [command, ...args] = argv;\n if (command !== undefined && command !== '--help' && command !== '-h' && !inheritedSessionId(dependencies.env)) {\n dependencies.stderr('PI_SESSION_ID is required for po-runner commands');\n return 1;\n }\n if (command && args[0] && isSequenceId(args[0])) return sequenceCommand(command, args, dependencies);\n switch (command) {\n case 'list':\n return list(args, dependencies);\n case 'status':\n return status(args, dependencies);\n case 'logs':\n return logs(args, dependencies);\n case 'stop':\n return stop(args, dependencies);\n case 'stop-all':\n return stopAll(args, dependencies);\n case 'input':\n return input(args, dependencies);\n case '--help':\n case '-h':\n case undefined:\n dependencies.stdout(runnerCliHelp());\n return 0;\n default:\n dependencies.stderr(`Unknown command: ${command}\\n\\n${runnerCliHelp()}`);\n return 1;\n }\n}\n\nasync function sequenceCommand(\n command: string,\n args: readonly string[],\n dependencies: CliDependencies,\n): Promise<number> {\n const store = dependencies.sequences;\n if (!store) {\n dependencies.stderr('Sequence controls are unavailable.');\n return 1;\n }\n const record = await store.get(args[0]!);\n if (!record) {\n dependencies.stderr(`No sequence with id ${args[0]}`);\n return 1;\n }\n if (command === 'stop') {\n if (record.state !== 'running') {\n dependencies.stderr(`Sequence ${record.id} is already completed`);\n return 1;\n }\n await store.requestStop(record.id, option(args, '--reason'));\n dependencies.stdout(`Stop requested for sequence ${record.id}`);\n return 0;\n }\n if (command === 'status') {\n dependencies.stdout(\n [\n `ID: ${record.id}`,\n `Name: ${record.name}`,\n `State: ${record.state}`,\n `Session: ${record.sessionId}`,\n ...(record.outcome ? [`Outcome: ${record.outcome}`] : []),\n ...record.stages.map((stage) => `Stage ${stage.index + 1}: ${stage.state}${stage.id ? ` (${stage.id})` : ''}`),\n ].join('\\n'),\n );\n return 0;\n }\n if (command === 'logs') {\n if (args.includes('--follow')) {\n dependencies.stderr('Sequence logs are finite snapshots; inspect status before reading again.');\n return 1;\n }\n if (record.result)\n dependencies.stdout(\n record.result.content.flatMap((part) => (part.type === 'text' ? [part.text] : [])).join('\\n'),\n );\n else\n dependencies.stdout(\n [\n `Sequence ${record.id} is running.`,\n ...record.stages.filter((stage) => stage.logPath).map((stage) => `${stage.index + 1}. ${stage.logPath}`),\n ].join('\\n'),\n );\n return 0;\n }\n dependencies.stderr(\n command === 'input' ? 'Command sequences are noninteractive.' : `Unknown sequence command: ${command}`,\n );\n return 1;\n}\n\nasync function list(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const sessionId = requiredSessionId(dependencies.env);\n const records = args.includes('--all')\n ? await dependencies.registry.listAll(sessionId)\n : await dependencies.registry.listBySession(sessionId);\n const sequences = (await dependencies.sequences?.list(args.includes('--all'))) ?? [];\n const rows = [\n ...records.map(formatRecord),\n ...sequences.map((record) => `${record.id} ${record.state} sequence ${record.name}`),\n ];\n dependencies.stdout(rows.length > 0 ? rows.join('\\n') : 'No runners');\n return 0;\n}\n\nasync function status(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n dependencies.stdout(\n [\n `ID: ${record.id}`,\n `Name: ${record.name}`,\n `State: ${record.state}`,\n `Backend: ${record.backend}`,\n ...(record.backendTarget ? [`Backend target: ${record.backendTarget}`] : []),\n `PID: ${record.pid}`,\n `Command: ${record.command}`,\n `CWD: ${record.cwd}`,\n `Session: ${record.sessionId}`,\n `Log: ${record.logPath}`,\n ...(record.exit ? [`Exit: ${formatExit(record)}`] : []),\n ].join('\\n'),\n );\n return 0;\n}\n\nasync function logs(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n const lines = positiveInteger(option(args, '--lines')) ?? DEFAULT_LOG_LINES;\n const result = await dependencies.logReader.read(record.logPath, { lines });\n if (!result.exists) {\n dependencies.stderr(`Runner ${record.id} has not written a log yet`);\n return 1;\n }\n if (result.text) dependencies.stdout(result.text);\n if (!args.includes('--follow')) return 0;\n return follow(record, result.fileSize, dependencies);\n}\n\nasync function stop(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n if (record.state !== 'running') {\n dependencies.stderr(`Runner ${record.id} is already ${record.state}`);\n return 1;\n }\n const reason = option(args, '--reason');\n if (!(await stopRecord(record, dependencies, reason))) {\n dependencies.stderr(`Could not stop runner ${record.id}`);\n return 1;\n }\n await dependencies.registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: TERMINATION_SIGNAL,\n ...(reason ? { stopReason: reason } : {}),\n },\n record.sessionId,\n );\n dependencies.stdout(`Stopped ${record.id}\\nLog: ${record.logPath}`);\n return 0;\n}\n\nasync function stopAll(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const sessionId = requiredSessionId(dependencies.env);\n const records = await dependencies.registry.listBySession(sessionId);\n const reason = option(args, '--reason');\n for (const sequence of (await dependencies.sequences?.list()) ?? []) {\n await dependencies.sequences!.requestStop(sequence.id, reason);\n dependencies.stdout(`Stop requested for sequence ${sequence.id}`);\n }\n const stopped: RunnerRecord[] = [];\n for (const record of records) {\n if (!(await stopRecord(record, dependencies, reason))) continue;\n await dependencies.registry.complete(\n record.id,\n {\n reason: STOPPED_REASON,\n code: null,\n signal: TERMINATION_SIGNAL,\n ...(reason ? { stopReason: reason } : {}),\n },\n record.sessionId,\n );\n stopped.push(record);\n }\n dependencies.stdout(\n records.length === 0\n ? `No active runners for session ${sessionId}`\n : `Stopped ${stopped.length} runner(s) for session ${sessionId}: ${stopped.map((record) => record.id).join(', ')}`,\n );\n if (stopped.length !== records.length) {\n dependencies.stderr(`Could not stop ${records.length - stopped.length} runner(s) for session ${sessionId}`);\n return 1;\n }\n return 0;\n}\n\nasync function input(args: readonly string[], dependencies: CliDependencies): Promise<number> {\n const record = await requiredRecord(args[0], dependencies);\n if (!record) return 1;\n if (record.state === COMPLETED_STATE) {\n dependencies.stderr(`Runner ${record.id} is already completed; use po-runner logs ${record.id}`);\n return 1;\n }\n if (!record.interactive) {\n dependencies.stderr(`Runner ${record.id} was not started with interactive: true`);\n return 1;\n }\n const supplied = option(args, '--text');\n const text = supplied ?? (await dependencies.readStdin());\n const payload = args.includes('--enter') && !text.endsWith('\\n') ? `${text}\\n` : text;\n if (!MULTIPLEXER_BACKENDS.has(record.backend) || !record.backendTarget) {\n dependencies.stderr(`Runner ${record.id} uses the ${record.backend} backend, which does not expose CLI input`);\n return 1;\n }\n if (!(await dependencies.rmuxBackend.input(record.backendTarget, payload))) {\n dependencies.stderr(`Runner ${record.id} is no longer accepting RMUX input`);\n return 1;\n }\n dependencies.stdout(`Sent input to ${record.id}`);\n return 0;\n}\n\nasync function requiredRecord(\n id: string | undefined,\n dependencies: CliDependencies,\n): Promise<RunnerRecord | undefined> {\n if (!id) {\n dependencies.stderr('A runner ID is required');\n return undefined;\n }\n const sessionId = requiredSessionId(dependencies.env);\n const scopedRecord = await dependencies.registry.get(id, sessionId);\n if (!scopedRecord) {\n dependencies.stderr(`No runner with id ${id}`);\n return undefined;\n }\n return scopedRecord;\n}\n\nfunction formatRecord(record: RunnerRecord): string {\n const active = formatRunnerLine(record, Date.now());\n return `${record.id} ${record.state} ${record.backend} ${active}`;\n}\n\nfunction formatExit(record: RunnerRecord): string {\n const exit = record.exit;\n if (!exit) return 'unknown';\n const detail = exit.code !== null ? ` code ${exit.code}` : exit.signal ? ` ${exit.signal}` : '';\n const attribution = exit.terminationReason ? ` [${exit.terminationReason}]` : '';\n const note = exit.stopReason ? ` (${exit.stopReason})` : '';\n return `${exit.reason}${detail}${attribution}${note}`;\n}\n\nfunction option(args: readonly string[], name: string): string | undefined {\n const index = args.indexOf(name);\n return index >= 0 ? args[index + 1] : undefined;\n}\n\nfunction positiveInteger(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const parsed = Number(value);\n return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\nasync function follow(record: RunnerRecord, initialSize: number, dependencies: CliDependencies): Promise<number> {\n let offset = initialSize;\n return new Promise<number>((resolve) => {\n let stopped = false;\n let stateTimer: NodeJS.Timeout;\n let reads = Promise.resolve();\n const drain = async (current: fs.Stats): Promise<void> => {\n if (current.size < offset) offset = 0;\n if (current.size === offset) return;\n const length = current.size - offset;\n const buffer = Buffer.alloc(length);\n const descriptor = await open(record.logPath, 'r');\n try {\n const { bytesRead } = await descriptor.read(buffer, 0, length, offset);\n if (bytesRead > 0) dependencies.stdout(buffer.subarray(0, bytesRead).toString('utf8'));\n offset += bytesRead;\n } finally {\n await descriptor.close();\n }\n };\n const report = (error: unknown): void => {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n dependencies.stderr(`Runner ${record.id} failed to flush its final log output: ${String(error)}`);\n }\n };\n const listener = (current: fs.Stats): void => {\n if (stopped) return;\n reads = reads.then(() => drain(current)).catch(report);\n };\n const stopFollowing = (): void => {\n if (stopped) return;\n stopped = true;\n fs.unwatchFile(record.logPath, listener);\n clearInterval(stateTimer);\n process.off('SIGINT', stopFollowing);\n reads = reads.then(async () => drain(await stat(record.logPath))).catch(report);\n void reads.then(() => resolve(0));\n };\n process.once('SIGINT', stopFollowing);\n fs.watchFile(record.logPath, { interval: FOLLOW_INTERVAL_MS }, listener);\n stateTimer = setInterval(() => {\n void dependencies.registry.get(record.id).then((current) => {\n if (current?.state === COMPLETED_STATE) stopFollowing();\n });\n }, FOLLOW_INTERVAL_MS);\n });\n}\n\nasync function stopRecord(record: RunnerRecord, dependencies: CliDependencies, reason?: string): Promise<boolean> {\n return stopRunnerProcess(record, dependencies.launcher, dependencies.rmuxBackend, {\n registry: dependencies.registry,\n intent: { reason: STOPPED_REASON, terminationReason: 'user_stop', ...(reason ? { stopReason: reason } : {}) },\n });\n}\n\nexport function runnerCliHelp(): string {\n return [\n 'Usage: po-runner <command>',\n '',\n 'Commands:',\n ' list [--all]',\n ' status <id>',\n ' logs <id> [--lines N] [--follow]',\n ' stop <id> [--reason <text>]',\n ' stop-all [--reason <text>]',\n ' input <id> [--text <text>] [--enter]',\n ].join('\\n');\n}\n"],"mappings":"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"}
|
package/dist/cliApp.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_cliApp = require("./cliApp-
|
|
2
|
+
const require_cliApp = require("./cliApp-GKcqD6CT.cjs");
|
|
3
3
|
exports.runCli = require_cliApp.runCli;
|
|
4
4
|
exports.runnerCliHelp = require_cliApp.runnerCliHelp;
|
package/dist/cliApp.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as runCli, n as runnerCliHelp } from "./cliApp-
|
|
1
|
+
export { t as runCli, n as runnerCliHelp } from "./cliApp-g7APOwcU.mjs";
|
package/dist/extensions/mcp.cjs
CHANGED
|
@@ -36,9 +36,9 @@ _xterm_headless = __toESM(_xterm_headless, 1);
|
|
|
36
36
|
require("@rmux/sdk");
|
|
37
37
|
let node_util = require("node:util");
|
|
38
38
|
require("@agimon-ai/foundation-process-registry");
|
|
39
|
-
require("@agimon-ai/po-core/childProcess");
|
|
40
39
|
require("typebox/value");
|
|
41
40
|
require("@agimon-ai/po-core/backgroundWork");
|
|
41
|
+
require("@agimon-ai/po-core/childProcess");
|
|
42
42
|
require("@agimon-ai/po-core/extensionFile");
|
|
43
43
|
require("@agimon-ai/po-core/telemetry");
|
|
44
44
|
require("@earendil-works/pi-coding-agent");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.cjs","names":["defineMcpSkill","readPackageResource","Type","headless","promisify","execFile","defineMcpTool","defineMcpPlugin","skillHelpHowToRunner","toolBash"],"sources":["../../src/extensions/workspaces/sessions/(backend)/skill/help-how-to-runner.mcp.ts","../../src/constants/runnerSettings.ts","../../src/constants/runnerSequence.ts","../../src/schemas/bashTool.ts","../../src/services/ansiScrub/index.ts","../../src/services/ptyHost/index.ts","../../src/services/runnerRegistry/index.ts","../../src/services/runnerSettings/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp.ts","../../generated/mcp.ts"],"sourcesContent":["import { defineMcpSkill } from '@agimon-ai/po-core/mcpFacet';\nimport { readPackageResource } from '@agimon-ai/po-core/serverPlugin';\n\nexport default defineMcpSkill({\n name: 'help-how-to-runner',\n description:\n 'Use Po Pi Runner to supervise shell commands, inspect durable logs, provide interactive input, and stop background runs.',\n read: () => readPackageResource(import.meta.url, 'src/prompts/help-how-to-runner/SKILL.md'),\n});\n","export const RUNNER_SETTINGS_FILE = 'po-runner.json';\nexport const RATIO_KEYS = ['headRatio', 'errorBudgetRatio'] as const;\nexport const MAX_RESULT_BYTES_CEILING = 262_144;\nexport const MAX_RESULT_LINES_CEILING = 5_000;\nexport const MAX_RESULT_TOKENS_CEILING = 100_000;\nexport const MAX_ERROR_ENTRIES_CEILING = 100;\nexport const MAX_PATTERNS = 32;\nexport const MAX_PATTERN_LENGTH = 512;\n","export const MAX_SEQUENCE_COMMANDS = 32;\nexport const MAX_SEQUENCE_COMMAND_LENGTH = 65_536;\nexport const MAX_SEQUENCE_RECORD_BYTES = 4 * 1024 * 1024;\nexport const SEQUENCE_DIRECTORY = 'sequences';\nexport const SEQUENCE_PREFIX = 'sequence-';\n","import { type Static, Type } from 'typebox';\n\nimport { MAX_SEQUENCE_COMMANDS, MAX_SEQUENCE_COMMAND_LENGTH } from '../constants/runnerSequence';\n\n/** Replaces pi's built-in `bash` tool. Descriptions are agent-facing prompt copy. */\nexport const BashParamsSchema = Type.Object({\n command: Type.Array(Type.String({ minLength: 1, maxLength: MAX_SEQUENCE_COMMAND_LENGTH }), {\n minItems: 1,\n maxItems: MAX_SEQUENCE_COMMANDS,\n description:\n 'Bash commands to run in order, stopping on first failure. Use a one-item array for one command. Each command gets a full log; return all log paths and only the last executed result. Do not pipe or redirect output to a file, head, or tail. Logs are saved automatically and returned output is bounded.',\n }),\n timeout: Type.Optional(\n Type.Number({ description: 'Timeout in seconds for each command (optional, no default timeout)' }),\n ),\n background: Type.Optional(\n Type.Boolean({\n description:\n 'Start the command sequence in the background immediately. 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 only the last command under a pseudo terminal when it needs input. Earlier commands are noninteractive. Send input to the last stage runner ID with runner.input in code mode, or po-runner input without code mode.',\n }),\n ),\n name: Type.Optional(\n Type.String({\n description: 'Preferred runner name if the command ends up in the background. A name in use gets a suffix.',\n }),\n ),\n});\n\nexport type BashParams = Static<typeof BashParamsSchema>;\n","import { CONTROL_PATTERN } from '../../constants/ansiScrub';\n/**\n * Turns terminal output into text worth grepping.\n *\n * PTY output is written for a screen, not a file: colours, cursor moves and\n * carriage-return redraws all end up in the byte stream. Without this a single\n * progress bar becomes thousands of near-identical log lines.\n */\n\n/**\n * CSI sequences (colours, cursor moves), OSC strings, the two-byte Fe escapes,\n * and the keypad-mode escapes shells emit around every prompt.\n */\nconst ANSI_PATTERN = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Bell and backspace, which a terminal consumes rather than displays. */\n// oxlint-disable-next-line no-control-regex -- matching terminal control bytes is the whole job\n\n/**\n * The same set minus SGR, the sequence that only sets colour and weight.\n *\n * The final byte class omits `m`, so `ESC[32m` survives while cursor moves,\n * OSC strings and keypad escapes are still removed.\n */\nconst ANSI_PATTERN_KEEPING_COLOUR = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-ln-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Removes escape sequences, leaving the characters a user would have seen. */\nexport function stripAnsi(text: string): string {\n return text.replace(ANSI_PATTERN, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Removes everything `stripAnsi` does except colour.\n *\n * Logs are read back by a terminal renderer as well as by grep, and a command\n * that colours its own output has already said how it should look.\n */\nexport function stripAnsiKeepingColour(text: string): string {\n return text.replace(ANSI_PATTERN_KEEPING_COLOUR, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Collapses carriage-return redraws.\n *\n * A bare `\\r` returns the cursor to the start of the current line, so whatever\n * follows overwrites it. Only the final state of each line is kept. The `\\r` of\n * a CRLF pair is a line terminator rather than a redraw, so it is normalised\n * away first: a pseudo terminal ends every line that way, and treating it as a\n * redraw would blank the whole log.\n */\nexport function collapseCarriageReturns(text: string): string {\n return text\n .replace(/\\r\\n/g, '\\n')\n .split('\\n')\n .map((line) => (line.includes('\\r') ? (line.split('\\r').at(-1) ?? '') : line))\n .join('\\n');\n}\n\n/** Full scrub applied to PTY output before it reaches the log file. */\nexport function scrubTerminalOutput(text: string): string {\n return collapseCarriageReturns(stripAnsiKeepingColour(text));\n}\n\n/** Plain-text scrub, for the copy the model reads. */\nexport function scrubTerminalOutputToPlainText(text: string): string {\n return collapseCarriageReturns(stripAnsi(text));\n}\n","import headless, { type Terminal } from '@xterm/headless';\n\nimport {\n DEFAULT_COLS,\n DEFAULT_ROWS,\n LIVENESS_POLL_MS,\n MEMORY_BUFFER_FACTOR,\n NEWLINE,\n SCROLLBACK,\n TERM_GRACE_MS,\n} from '../../constants/ptyHost';\nimport { PI_SESSION_ID_ENV } from '../../constants/session';\nimport type { IClock } from '../../types/clock';\nimport type { ILogFile } from '../../types/logFile';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IPtyHost, PtyLaunchRequest, PtyRun } from '../../types/ptyHost';\nimport type { IPtySpawner, PtyProcess } from '../../types/ptySpawner';\nimport type { RunnerSettings } from '../../types/runnerSettings';\nimport type { ExitResult } from '../../types/spawner';\nimport { scrubTerminalOutput } from '../ansiScrub';\nimport { getResultMaxBytes } from '../runnerConfig';\nimport { NO_TERMINAL_INPUT_ENV } from '../runnerSupervisor';\n\n/** Enough history for the overlay to scroll without holding a whole build. */\n\n// Use the CommonJS default export: Node cannot detect this package's named exports.\n// A static import also lets server bundles include it without a live node_modules lookup.\nconst { Terminal: HeadlessTerminal } = headless;\n\nexport class PtyHost implements IPtyHost {\n private readonly runs = new Map<string, PtyRun>();\n\n constructor(\n private readonly spawner: IPtySpawner,\n private readonly logFile: ILogFile,\n private readonly processControl: IProcessControl,\n private readonly clock: IClock,\n private readonly environment: Readonly<Record<string, string | undefined>> = process.env,\n private readonly settings?: RunnerSettings,\n ) {}\n\n async launch(request: PtyLaunchRequest): Promise<PtyRun> {\n const cols = request.cols ?? DEFAULT_COLS;\n const rows = request.rows ?? DEFAULT_ROWS;\n const writer = await this.logFile.open(request.id);\n const terminal = new HeadlessTerminal({ cols, rows, scrollback: SCROLLBACK, allowProposedApi: true });\n const memoryLimit = getResultMaxBytes(this.environment, this.settings) * MEMORY_BUFFER_FACTOR;\n\n const child = await this.spawner.spawn({\n command: request.command,\n cwd: request.cwd,\n env: { ...this.environment, ...NO_TERMINAL_INPUT_ENV, [PI_SESSION_ID_ENV]: request.sessionId },\n cols,\n rows,\n });\n\n let buffer = '';\n let buffering = true;\n let settled = false;\n let resolveCompletion: (result: ExitResult) => void = () => undefined;\n let rejectCompletion: (error: unknown) => void = () => undefined;\n const completion = new Promise<ExitResult>((resolve, reject) => {\n resolveCompletion = resolve;\n rejectCompletion = reject;\n });\n const dataHandlers = new Set<(data: string) => void>();\n\n child.onData((data) => {\n // The terminal keeps the screen; the log keeps text worth grepping.\n terminal.write(data);\n const scrubbed = scrubTerminalOutput(data);\n void writer.append(scrubbed).catch((error: unknown) => process.emitWarning(String(error)));\n if (buffering) {\n buffer += scrubbed;\n if (buffer.length > memoryLimit) buffer = buffer.slice(-memoryLimit);\n }\n for (const handler of dataHandlers) handler(data);\n });\n\n child.onExit(async (result) => {\n if (settled) return;\n settled = true;\n this.runs.delete(request.name);\n try {\n await writer.close();\n resolveCompletion({ code: result.exitCode, signal: null });\n } catch (error) {\n rejectCompletion(error);\n }\n });\n\n const run: PtyRun = {\n id: request.id,\n name: request.name,\n pid: child.pid,\n logPath: writer.path,\n backend: 'native',\n output: async () => buffer,\n completion: () => completion,\n detach: () => {\n buffering = false;\n buffer = '';\n },\n stop: () => this.stopChild(child, request.name),\n write: (text) => child.write(text),\n screen: () => renderScreen(terminal),\n onData: (handler) => {\n dataHandlers.add(handler);\n return () => dataHandlers.delete(handler);\n },\n resize: (nextCols, nextRows) => {\n child.resize(nextCols, nextRows);\n terminal.resize(nextCols, nextRows);\n },\n };\n\n this.runs.set(request.name, run);\n return run;\n }\n\n get(name: string): PtyRun | undefined {\n return this.runs.get(name);\n }\n\n write(name: string, text: string): boolean {\n const run = this.runs.get(name);\n if (!run) return false;\n run.write(text.endsWith(NEWLINE) ? text : `${text}${NEWLINE}`);\n return true;\n }\n\n list(): PtyRun[] {\n return [...this.runs.values()];\n }\n\n async disposeAll(): Promise<void> {\n const runs = [...this.runs.values()];\n this.runs.clear();\n for (const run of runs) await run.stop();\n }\n\n private async stopChild(child: PtyProcess, name: string): Promise<boolean> {\n this.runs.delete(name);\n if (!this.processControl.isAlive(child.pid)) return false;\n\n child.kill('SIGTERM');\n const deadline = this.clock.now() + TERM_GRACE_MS;\n while (this.clock.now() < deadline) {\n if (!this.processControl.isAlive(child.pid)) return true;\n await new Promise<void>((resolve) => {\n this.clock.after(LIVENESS_POLL_MS, resolve);\n });\n }\n child.kill('SIGKILL');\n return true;\n }\n}\n\n/** The visible rows, trimmed of trailing blanks, as plain text. */\nfunction renderScreen(terminal: Terminal): string {\n const buffer = terminal.buffer.active;\n const lines: string[] = [];\n for (let row = buffer.baseY; row < buffer.baseY + terminal.rows; row += 1) {\n lines.push(buffer.getLine(row)?.translateToString(true) ?? '');\n }\n while (lines.length > 0 && lines.at(-1) === '') lines.pop();\n return lines.join(NEWLINE);\n}\n","import { execFile } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport { resolveRootSessionId } from '@agimon-ai/po-core/childProcess';\n\nimport {\n COMMAND_SIDECAR_SUFFIX,\n EXIT_SIDECAR_SUFFIX,\n JSON_EXTENSION,\n NOT_FOUND_ERROR_CODE,\n REGISTRY_PATH_ENV,\n RUNNER_TAG,\n SERVICE_PREFIX,\n SERVICE_TYPE,\n} from '../../constants/runnerRegistry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n RunnerTerminationIntent,\n} from '../../types/runnerRegistry';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** The slice of the shared registry service this wrapper depends on. */\nexport type ProcessRegistryPort = Pick<\n ProcessRegistryService,\n 'registerProcess' | 'releaseProcess' | 'listProcesses' | 'close'\n>;\n\n/** Opens the SQLite-backed registry the rest of the monorepo already shares. */\nexport function createDefaultProcessRegistry(\n env: Readonly<Record<string, string | undefined>> = process.env,\n): ProcessRegistryPort {\n return createProcessRegistryService(env[REGISTRY_PATH_ENV]);\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction readString(record: ProcessRegistryRecord, key: string): string {\n const value = record.metadata?.[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction readNumber(record: ProcessRegistryRecord, key: string): number {\n const value = record.metadata?.[key];\n return typeof value === 'number' ? value : 0;\n}\n\nconst runCommand = promisify(execFile);\n\n/**\n * The process start time the registry stores to prove a pid is still the\n * process it registered.\n *\n * The registry resolves it itself when the caller omits it, and it does so with\n * `execFileSync`, which stops this process for the length of a `ps` on every\n * runner that starts. Reading it here costs the same subprocess and blocks\n * nothing: the command is already running by the time a runner is registered.\n *\n * The value has to match what the registry would have read, so the command and\n * the trimming are deliberately identical to its own.\n */\nasync function processStartedAt(pid: number): Promise<string | undefined> {\n if (process.platform === 'win32') return undefined;\n try {\n const { stdout } = await runCommand('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8' });\n return stdout.trim() || undefined;\n } catch {\n return undefined;\n }\n}\n\ntype ListenerRegistration = Readonly<{ key: string; listener: () => void }>;\n\n// Server plugin replacement can leave an in-flight tool and its publisher on different registry instances.\nconst listenersBySession = new Map<string, Set<() => void>>();\n\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly subscriptions = new Set<ListenerRegistration>();\n private recordWrites: Promise<void> = Promise.resolve();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n private readonly environment: Readonly<Record<string, string | undefined>>,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId, this.environment);\n const identity = await processStartedAt(input.pid);\n const response = await this.registry.registerProcess({\n repositoryPath: await this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${input.id}`,\n serviceType: SERVICE_TYPE,\n pid: input.pid,\n command: input.command,\n ...(identity === undefined ? {} : { processStartedAt: identity }),\n tags: resolveProcessTags([RUNNER_TAG, `runner:${input.id}`, sessionTag(input.sessionId)]),\n metadata: {\n id: input.id,\n name: input.name,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n startedAt,\n hostPid: process.pid,\n },\n // The caller has already resolved name collisions against live records,\n // so anything still holding this name is stale.\n force: true,\n });\n if (!response.success) throw new Error(response.error ?? `Failed to register runner ${input.name}`);\n const record: RunnerRecord = {\n id: input.id,\n name: input.name,\n pid: input.pid,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n startedAt,\n state: 'running',\n promoted: false,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n hostPid: process.pid,\n };\n await this.writeRecord(record);\n this.notify(record.sessionId);\n return record;\n }\n\n async list(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n repositoryPath: await this.repositoryPath(),\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 await this.paths.ensureDirectories(sessionId);\n const records: RunnerRecord[] = [];\n const directory = this.paths.stateDirectory(sessionId);\n for (const entry of await fs.readdir(directory)) {\n if (!isPrimaryMetadataEntry(entry)) continue;\n try {\n const parsed: unknown = JSON.parse(await fs.readFile(`${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(await fs.readFile(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 await this.writeRecord(promoted);\n this.notify(promoted.sessionId);\n return promoted;\n }\n\n async requestTermination(\n id: string,\n intent: RunnerTerminationIntent | undefined,\n sessionId?: string,\n ): Promise<RunnerRecord | undefined> {\n const record = await this.get(id, sessionId);\n if (!record) return undefined;\n if (record.state === 'completed') return record;\n if (intent !== undefined && record.terminationIntent !== undefined) return record;\n const { terminationIntent: _previous, ...rest } = record;\n const updated: RunnerRecord = { ...rest, ...(intent ? { terminationIntent: intent } : {}) };\n await this.writeRecord(updated);\n return updated;\n }\n\n async complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined> {\n const persisted = await this.get(id, sessionId);\n if (persisted?.state === 'completed') {\n await this.release(id, persisted.sessionId);\n return persisted;\n }\n const active = await this.list();\n const record =\n persisted ??\n active.find((candidate) => candidate.id === id && (sessionId === undefined || candidate.sessionId === sessionId));\n if (!record) {\n const belongsToAnotherSession =\n sessionId !== undefined && active.some((candidate) => candidate.id === id && candidate.sessionId !== sessionId);\n if (!belongsToAnotherSession) await this.release(id, sessionId);\n return undefined;\n }\n const completed: RunnerRecord = {\n ...record,\n state: 'completed',\n exit: {\n ...outcome,\n ...(outcome.signal !== null && outcome.terminationReason === undefined && record.terminationIntent === undefined\n ? { terminationReason: 'external_signal' as const }\n : {}),\n ...record.terminationIntent,\n finishedAt: new Date().toISOString(),\n },\n };\n await this.writeRecord(completed);\n await this.release(id, completed.sessionId);\n return completed;\n }\n\n async release(id: string, sessionId?: string): Promise<void> {\n const activeSessionId = sessionId ?? (await this.list()).find((record) => record.id === id)?.sessionId;\n const ownedSessionId = activeSessionId ?? (await this.get(id))?.sessionId;\n const response = await this.registry.releaseProcess({\n repositoryPath: await this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${id}`,\n serviceType: SERVICE_TYPE,\n kill: false,\n releasePort: false,\n force: true,\n });\n if (!response.success && !response.error?.includes('No matching process entry')) {\n throw new Error(response.error ?? `Failed to release runner ${id}`);\n }\n if (ownedSessionId !== undefined) this.notify(ownedSessionId);\n }\n\n async pruneDead(): Promise<string[]> {\n const dead = (await this.list()).filter((record) => !this.processControl.isAlive(record.pid));\n for (const record of dead) {\n await this.complete(record.id, { reason: 'backend_lost', code: null, signal: null }, record.sessionId);\n }\n return dead.map((record) => record.id);\n }\n\n subscribe(listener: () => void, sessionId: string): () => void {\n const subscribed = (): void => listener();\n const registration = { key: this.subscriptionKey(sessionId), listener: subscribed };\n const listeners = listenersBySession.get(registration.key) ?? new Set<() => void>();\n listeners.add(subscribed);\n listenersBySession.set(registration.key, listeners);\n this.subscriptions.add(registration);\n return () => this.removeSubscription(registration);\n }\n\n close(): void {\n for (const registration of this.subscriptions) this.removeSubscription(registration);\n this.registry.close();\n }\n\n private notify(sessionId: string): void {\n for (const listener of listenersBySession.get(this.subscriptionKey(sessionId)) ?? []) {\n try {\n listener();\n } catch (error) {\n process.emitWarning(`Runner registry listener failed: ${String(error)}`);\n }\n }\n }\n\n private removeSubscription(registration: ListenerRegistration): void {\n const listeners = listenersBySession.get(registration.key);\n listeners?.delete(registration.listener);\n if (listeners?.size === 0) listenersBySession.delete(registration.key);\n this.subscriptions.delete(registration);\n }\n\n private subscriptionKey(sessionId: string): string {\n return JSON.stringify([path.resolve(this.paths.stateDirectory(sessionId)), sessionId]);\n }\n\n private async repositoryPath(): Promise<string> {\n return normalizeRepositoryPath(await this.paths.repositoryPath());\n }\n\n private writeRecord(record: RunnerRecord): Promise<void> {\n const write = this.recordWrites.then(async () => {\n await this.paths.ensureDirectories(record.sessionId);\n const target = this.paths.statePathFor(record.id, record.sessionId);\n const temporary = `${target}.${process.pid}.tmp`;\n await fs.writeFile(temporary, `${JSON.stringify(record, null, 2)}\\n`, 'utf8');\n await fs.rename(temporary, target);\n });\n // Keep later writes ordered even after a failed write, which its caller observes.\n this.recordWrites = write.catch(() => undefined);\n return write;\n }\n}\n\n/** Every field is read back from metadata this class wrote on registration. */\nfunction toRunnerRecord(record: ProcessRegistryRecord): RunnerRecord {\n return {\n id: readString(record, 'id'),\n name: readString(record, 'name'),\n pid: record.pid,\n command: readString(record, 'command'),\n cwd: readString(record, 'cwd'),\n logPath: readString(record, 'logPath'),\n interactive: record.metadata?.interactive === true,\n sessionId: readString(record, 'sessionId'),\n ...(readString(record, 'rootSessionId') ? { rootSessionId: readString(record, 'rootSessionId') } : {}),\n startedAt: readString(record, 'startedAt'),\n state: 'running',\n promoted: false,\n backend: readBackend(record.metadata?.backend),\n ...(readString(record, 'backendTarget') ? { backendTarget: readString(record, 'backendTarget') } : {}),\n hostPid: readNumber(record, 'hostPid'),\n };\n}\n\nfunction readBackend(value: unknown): RunnerRecord['backend'] {\n return value === 'rmux' || value === 'tmux' ? value : 'native';\n}\n\nfunction isPrimaryMetadataEntry(entry: string): boolean {\n return (\n entry.endsWith(JSON_EXTENSION) && !entry.endsWith(COMMAND_SIDECAR_SUFFIX) && !entry.endsWith(EXIT_SIDECAR_SUFFIX)\n );\n}\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { BACKGROUND_CONTEXT, readTextIfExists, type FileSystem } from '@agimon-ai/po-core/platform';\nimport { CONFIG_DIR_NAME } from '@earendil-works/pi-coding-agent';\n\nimport {\n MAX_ERROR_ENTRIES_CEILING,\n MAX_PATTERNS,\n MAX_PATTERN_LENGTH,\n MAX_RESULT_BYTES_CEILING,\n MAX_RESULT_LINES_CEILING,\n MAX_RESULT_TOKENS_CEILING,\n RATIO_KEYS,\n RUNNER_SETTINGS_FILE,\n} from '../../constants/runnerSettings';\nimport type { IRunnerSettingsLoader, RunnerSettings, RunnerSettingsLoad } from '../../types/runnerSettings';\n\n/** Pi's convention is one JSON file per extension under the config directory. */\n\nconst EMPTY: RunnerSettingsLoad = { settings: {}, issues: [] };\nconst COUNT_KEYS = [\n 'maxResultBytes',\n 'maxResultLines',\n 'successMaxResultBytes',\n 'maxResultTokens',\n 'successMaxResultTokens',\n 'errorMaxEntries',\n 'errorMaxVariantsJoined',\n] as const;\nconst KNOWN_KEYS = new Set<string>([...RATIO_KEYS, ...COUNT_KEYS, 'errorPatterns']);\n/** Ceilings so a project file cannot flood the model's context. */\nconst CEILINGS: Readonly<Record<string, number>> = {\n maxResultBytes: MAX_RESULT_BYTES_CEILING,\n successMaxResultBytes: MAX_RESULT_BYTES_CEILING,\n maxResultTokens: MAX_RESULT_TOKENS_CEILING,\n successMaxResultTokens: MAX_RESULT_TOKENS_CEILING,\n maxResultLines: MAX_RESULT_LINES_CEILING,\n errorMaxEntries: MAX_ERROR_ENTRIES_CEILING,\n errorMaxVariantsJoined: MAX_ERROR_ENTRIES_CEILING,\n};\n\nexport class RunnerSettingsLoader implements IRunnerSettingsLoader {\n constructor(private readonly files?: FileSystem) {}\n\n async load(cwd: string, trusted: boolean): Promise<RunnerSettingsLoad> {\n // Project-local configuration is untrusted input until the project is.\n if (!trusted) return EMPTY;\n\n let raw: string;\n try {\n const target = path.join(cwd, CONFIG_DIR_NAME, RUNNER_SETTINGS_FILE);\n const text = this.files\n ? await readTextIfExists(this.files, target, BACKGROUND_CONTEXT)\n : await fs.readFile(target, 'utf8');\n if (text === undefined) return EMPTY;\n raw = text;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return EMPTY;\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} could not be read`] };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} is not valid JSON`] };\n }\n return validate(parsed);\n }\n}\n\nfunction validate(parsed: unknown): RunnerSettingsLoad {\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} must contain a JSON object`] };\n }\n\n const source = parsed as Record<string, unknown>;\n const settings: Record<string, unknown> = {};\n const issues: string[] = [];\n // A rejected key is reported rather than dropped: a silent typo reads as a\n // setting that simply does not work.\n for (const key of Object.keys(source)) if (!KNOWN_KEYS.has(key)) issues.push(`unknown key ${key}`);\n\n for (const key of COUNT_KEYS) {\n const value = source[key];\n if (value === undefined) continue;\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n issues.push(`${key} must be a positive integer`);\n continue;\n }\n settings[key] = Math.min(value, CEILINGS[key] ?? value);\n }\n\n for (const key of RATIO_KEYS) {\n const value = source[key];\n if (value === undefined) continue;\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value >= 1) {\n issues.push(`${key} must be a number above 0 and below 1`);\n continue;\n }\n settings[key] = value;\n }\n\n const patterns = source.errorPatterns;\n if (patterns !== undefined) {\n if (!Array.isArray(patterns) || patterns.some((entry) => typeof entry !== 'string')) {\n issues.push('errorPatterns must be an array of strings');\n } else {\n const accepted: string[] = [];\n for (const pattern of patterns.slice(0, MAX_PATTERNS) as string[]) {\n if (pattern.length > MAX_PATTERN_LENGTH) {\n issues.push('errorPatterns entry is too long');\n continue;\n }\n try {\n new RegExp(pattern, 'iu');\n accepted.push(pattern);\n } catch {\n issues.push(`errorPatterns entry is not a valid regular expression: ${pattern}`);\n }\n }\n if (accepted.length > 0) settings.errorPatterns = accepted;\n }\n }\n\n return { settings: settings as RunnerSettings, issues };\n}\n","import { PO_CODEMODE_SERVICE, readPoCodemode } from '@agimon-ai/po-core/codemode';\nimport { readPoFs, PO_SESSION_TEMP_DIR } from '@agimon-ai/po-core/platform';\nimport type { PoServerPluginContext } from '@agimon-ai/po-core/serverPlugin';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { REGISTRY_PATH_ENV } from '../../../../../constants/runnerRegistry';\nimport {\n HISTORY_SWEEP_INTERVAL_MS,\n RUNNER_FINISHED_MESSAGE,\n RUNNER_STATUS_POLL_MS,\n} from '../../../../../constants/runnerRuntime';\nimport { RUNNER_SETTINGS_FILE } from '../../../../../constants/runnerSettings';\nimport { SystemClock } from '../../../../../services/clock';\nimport { createHeadlessBashTool, createHeadlessRunnersCommand } from '../../../../../services/headless';\nimport { LifelineObserver } from '../../../../../services/lifeline';\nimport { summarizeLog } from '../../../../../services/logReader';\nimport { createRunnerDependencies } from '../../../../../services/runnerDependencies';\nimport { createRunnerHost } from '../../../../../services/runnerHost';\nimport { createRunnerNamespace } from '../../../../../services/runnerNamespace';\nimport { createRunnerOperations } from '../../../../../services/runnerOperations';\nimport { createDefaultProcessRegistry } from '../../../../../services/runnerRegistry';\nimport { createRunnerSequenceService } from '../../../../../services/runnerSequence';\nimport { createRunnerSequenceStore } from '../../../../../services/runnerSequenceStore';\nimport { createRunnerServerRuntime } from '../../../../../services/runnerServerRuntime';\nimport { RunnerSettingsLoader } from '../../../../../services/runnerSettings';\n\nexport const RUNNER_SERVER_SCOPE_SERVICE = 'po/runner-server-scope';\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n 'po/runner-server-scope': RunnerServerScope;\n }\n}\n\nexport const createRunnerServerRoot = async ({ context, host, agent, signal, resources }: PoServerPluginContext) => {\n if (!host.context.directEvents) throw new Error('Runner server plugin requires the session direct event bus.');\n if (!host.context.cwd || !host.context.environment) {\n throw new Error('Runner server plugin requires session cwd and environment.');\n }\n // Inherit the current headless Pi bridge's isProjectTrusted: () => true.\n // This preserves runner configuration parity, not a new general trust policy.\n const fs = readPoFs(context);\n const loaded = await new RunnerSettingsLoader(fs).load(host.context.cwd, true);\n const settings = Object.freeze({\n ...loaded.settings,\n ...(loaded.settings.errorPatterns ? { errorPatterns: Object.freeze([...loaded.settings.errorPatterns]) } : {}),\n });\n for (const issue of loaded.issues) process.emitWarning(`${RUNNER_SETTINGS_FILE}: ${issue}`);\n const environment = host.context.environment;\n if (!environment[PO_SESSION_TEMP_DIR])\n throw new Error('Runner requires the host-admitted session temporary directory.');\n // One per registry for this build in the hub: every session shares the connection and the loop that\n // keeps house for it, and the last session to close disposes them.\n const runnerHost = await resources.acquire(\n ['runner-host', environment[REGISTRY_PATH_ENV] ?? null],\n () =>\n createRunnerHost({\n processRegistry: createDefaultProcessRegistry(environment),\n clock: new SystemClock(),\n statusPollMs: RUNNER_STATUS_POLL_MS,\n historySweepMs: HISTORY_SWEEP_INTERVAL_MS,\n onWarning: (message) => process.emitWarning(message),\n }),\n (shared) => shared.dispose(),\n );\n // One observer process cleans up after every session of the registry, rather than one each.\n const lifelineObserver = await resources.acquire(\n ['runner-lifeline-observer', environment[REGISTRY_PATH_ENV] ?? null],\n () => new LifelineObserver(environment),\n (shared) => shared.close(),\n );\n const dependencies = createRunnerDependencies({\n settings,\n cwd: host.context.cwd,\n environment,\n processRegistry: runnerHost.processRegistry,\n lifelineObserver,\n ...(host.context.executionBudget === undefined ? {} : { executionBudget: host.context.executionBudget }),\n });\n const execution = agent?.context;\n const wakeAgent =\n execution &&\n (async (content: string, requestId: string, notice: string): Promise<void> => {\n try {\n // 'steer' joins a running turn and starts one on an idle session.\n // Reuse completion identity across retries and acknowledge admission, not settlement.\n // A custom message, not a prompt: the model reads it as input, while the\n // transcript shows the notice as an event rather than as the person's words.\n if (!execution.session.admitMessage) throw new Error('The session cannot admit a runner notification.');\n await execution.session.admitMessage(\n { type: RUNNER_FINISHED_MESSAGE, text: content, display: true, details: { notice } },\n 'steer',\n requestId,\n );\n } catch (error) {\n // Keep completion ownership with the watcher until admission succeeds.\n await Promise.resolve()\n .then(() => execution.client.notify({ body: String(error), level: 'warning' }))\n .catch(() => undefined);\n throw error;\n }\n });\n const runtime = createRunnerServerRuntime(dependencies, runnerHost, host.context.directEvents, wakeAgent, execution);\n const sequences =\n fs && execution\n ? createRunnerSequenceService({\n dependencies,\n store: createRunnerSequenceStore(fs, dependencies.paths, execution.sessionId),\n sessionId: execution.sessionId,\n signal,\n summarizeLog: (path, lines, bytes, tokens) => summarizeLog(path, lines, bytes, tokens, settings),\n onChange: () => runtime.sequenceChanged(),\n onWarning: (message) => process.emitWarning(message),\n onComplete: async (record) => {\n if (!record.result) return;\n await wakeAgent?.(\n record.result.content.flatMap((part) => (part.type === 'text' ? [part.text] : [])).join('\\n'),\n `runner-sequence-finished:${JSON.stringify([record.sessionId, record.id])}`,\n `Command sequence ${record.name} finished: ${record.outcome ?? record.state}`,\n );\n },\n })\n : undefined;\n if (sequences) runtime.setSequences(sequences);\n const bash = (wakeOnExit: boolean) =>\n createHeadlessBashTool(\n sequences\n ? async (params, context, toolCallId, callSignal, onUpdate) => {\n await runtime.ensureSession(context.sessionId);\n return sequences.run(params, context, toolCallId, callSignal, onUpdate, wakeOnExit);\n }\n : undefined,\n dependencies.environment,\n );\n const value = {\n sequences,\n tool: bash(true),\n // Remote MCP runners must not wake the local agent; the remote caller polls instead.\n remoteTool: bash(false),\n command: createHeadlessRunnersCommand(dependencies, {\n // Without the platform fs a missing folder still fails, only at spawn and less plainly.\n fs,\n sequences,\n onStart: (startedSessionId, failure) => runtime.reportStart(startedSessionId, failure),\n }),\n recoverCompaction: runtime.recoverCompaction,\n };\n let controlsDisposed = false;\n if (fs && execution) {\n const namespace = createRunnerNamespace({\n sessionId: execution.sessionId,\n operations: createRunnerOperations({\n ...dependencies,\n registry: dependencies.runnerRegistry,\n fs,\n sessionId: execution.sessionId,\n }),\n sequences,\n assertAvailable: () => {\n signal?.throwIfAborted();\n if (controlsDisposed) throw new Error('Runner session is disposed.');\n },\n });\n context.inject([PO_CODEMODE_SERVICE], (scoped) => {\n const registration = readPoCodemode(scoped)?.register('@agimon-ai/po-runner', namespace);\n return () => registration?.dispose();\n });\n }\n return {\n value,\n services: [\n runtime.backgroundWorkPlugin,\n (context: Context) => {\n context.plugin((providerContext) => providerContext.provide(RUNNER_SERVER_SCOPE_SERVICE, value));\n },\n ],\n activities: [runtime.activity],\n // Package install is on the session-create path; bash awaits supervision itself.\n onStart: execution\n ? () => {\n void runtime\n .ensureSession(execution.sessionId)\n .catch((error: unknown) =>\n process.emitWarning(`Could not initialize headless runner supervision: ${String(error)}`),\n );\n }\n : undefined,\n onStop: async () => {\n controlsDisposed = true;\n await runtime.dispose();\n },\n };\n};\n\nexport type RunnerServerScope = Awaited<ReturnType<typeof createRunnerServerRoot>>['value'];\n","import { defineMcpTool, type PoMcpPluginContext } from '@agimon-ai/po-core/mcpFacet';\n\nimport { RUNNER_SERVER_SCOPE_SERVICE, type RunnerServerScope } from '../_lib/serverRoot';\n\nconst pollHint = (id: string): string =>\n `You will not be notified when this runner ends. Check it with \\`po-runner status ${id}\\` and read output with \\`po-runner logs ${id} --lines 100\\`, both through this tool, until State is completed.`;\n\nexport default defineMcpTool((context: PoMcpPluginContext) => {\n const scope = context.services.get<RunnerServerScope>(RUNNER_SERVER_SCOPE_SERVICE);\n if (!scope) throw new Error('Runner session service is unavailable.');\n return {\n ...scope.remoteTool,\n description:\n \"Run an array of shell commands (bash, terminal, CLI) in the Po session's repository on the host machine, not on your local machine. Use a one-item array for one command. Commands run in order and stop on first failure. Only the last command may be interactive. Do not pipe or redirect output to a file, head, or tail: full logs are saved automatically and returned output is bounded. Long-running commands return a sequence id. No completion notice reaches you, so check with `po-runner status <id>` and `po-runner logs <id> --lines 100` instead of relaunching.\",\n async execute(toolCallId, parameters, signal, onUpdate, execution) {\n const result = await scope.remoteTool.execute(\n toolCallId,\n parameters,\n AbortSignal.any([context.signal, signal ?? context.signal]),\n onUpdate,\n execution,\n );\n const details = result.details as { promoted?: unknown; id?: unknown } | undefined;\n if (details?.promoted !== true || typeof details.id !== 'string') return result;\n return { ...result, content: [...result.content, { type: 'text', text: pollHint(details.id) }] };\n },\n };\n});\n","// Generated by @agimon-ai/po-build. Do not edit by hand.\nimport { defineMcpPlugin, type PoMcpSessionPlugin } from '@agimon-ai/po-core/mcpFacet';\n\nimport skillHelpHowToRunner from '../src/extensions/workspaces/sessions/(backend)/skill/help-how-to-runner.mcp';\nimport toolBash from '../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nconst withWidget = <T>(key: string, value: T): T => value === undefined ? value : ({\n ...value, _meta: { ...(value as { _meta?: object })._meta, [\"po/widget\"]: key },\n});\n\nexport const mcp = defineMcpPlugin({\n name: '@agimon-ai/po-runner',\n session: (context) => ({\n get skills(): PoMcpSessionPlugin['skills'] { return [at(skillHelpHowToRunner, context)]; },\n get tools(): PoMcpSessionPlugin['tools'] { return [withWidget(\"@agimon-ai/po-runner/bash\", via({ name: 'bash' }, at(toolBash, context)))]; },\n }) satisfies PoMcpSessionPlugin,\n});\n\nexport default mcp;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp.cjs","names":["defineMcpSkill","readPackageResource","Type","headless","promisify","execFile","defineMcpTool","defineMcpPlugin","skillHelpHowToRunner","toolBash"],"sources":["../../src/extensions/workspaces/sessions/(backend)/skill/help-how-to-runner.mcp.ts","../../src/constants/runnerSettings.ts","../../src/constants/runnerSequence.ts","../../src/schemas/bashTool.ts","../../src/services/ansiScrub/index.ts","../../src/services/ptyHost/index.ts","../../src/services/runnerRegistry/index.ts","../../src/services/runnerSettings/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp.ts","../../generated/mcp.ts"],"sourcesContent":["import { defineMcpSkill } from '@agimon-ai/po-core/mcpFacet';\nimport { readPackageResource } from '@agimon-ai/po-core/serverPlugin';\n\nexport default defineMcpSkill({\n name: 'help-how-to-runner',\n description:\n 'Use Po Pi Runner to supervise shell commands, inspect durable logs, provide interactive input, and stop background runs.',\n read: () => readPackageResource(import.meta.url, 'src/prompts/help-how-to-runner/SKILL.md'),\n});\n","export const RUNNER_SETTINGS_FILE = 'po-runner.json';\nexport const RATIO_KEYS = ['headRatio', 'errorBudgetRatio'] as const;\nexport const MAX_RESULT_BYTES_CEILING = 262_144;\nexport const MAX_RESULT_LINES_CEILING = 5_000;\nexport const MAX_RESULT_TOKENS_CEILING = 100_000;\nexport const MAX_ERROR_ENTRIES_CEILING = 100;\nexport const MAX_PATTERNS = 32;\nexport const MAX_PATTERN_LENGTH = 512;\n","export const MAX_SEQUENCE_COMMANDS = 32;\nexport const MAX_SEQUENCE_COMMAND_LENGTH = 65_536;\nexport const MAX_SEQUENCE_RECORD_BYTES = 4 * 1024 * 1024;\nexport const SEQUENCE_DIRECTORY = 'sequences';\nexport const SEQUENCE_PREFIX = 'sequence-';\n","import { type Static, Type } from 'typebox';\n\nimport { MAX_SEQUENCE_COMMANDS, MAX_SEQUENCE_COMMAND_LENGTH } from '../constants/runnerSequence';\n\n/** Replaces pi's built-in `bash` tool. Descriptions are agent-facing prompt copy. */\nexport const BashParamsSchema = Type.Object({\n command: Type.Array(Type.String({ minLength: 1, maxLength: MAX_SEQUENCE_COMMAND_LENGTH }), {\n minItems: 1,\n maxItems: MAX_SEQUENCE_COMMANDS,\n description:\n 'Bash commands to run in order, stopping on first failure. Use a one-item array for one command. Each command gets a full log; return all log paths and only the last executed result. Do not pipe or redirect output to a file, head, or tail. Logs are saved automatically and returned output is bounded.',\n }),\n timeout: Type.Optional(\n Type.Number({ description: 'Timeout in seconds for each command (optional, no default timeout)' }),\n ),\n background: Type.Optional(\n Type.Boolean({\n description:\n 'Start the command sequence in the background immediately. 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 only the last command under a pseudo terminal when it needs input. Earlier commands are noninteractive. Send input to the last stage runner ID with runner.input in code mode, or po-runner input without code mode.',\n }),\n ),\n name: Type.Optional(\n Type.String({\n description: 'Preferred runner name if the command ends up in the background. A name in use gets a suffix.',\n }),\n ),\n});\n\nexport type BashParams = Static<typeof BashParamsSchema>;\n","import { CONTROL_PATTERN } from '../../constants/ansiScrub';\n/**\n * Turns terminal output into text worth grepping.\n *\n * PTY output is written for a screen, not a file: colours, cursor moves and\n * carriage-return redraws all end up in the byte stream. Without this a single\n * progress bar becomes thousands of near-identical log lines.\n */\n\n/**\n * CSI sequences (colours, cursor moves), OSC strings, the two-byte Fe escapes,\n * and the keypad-mode escapes shells emit around every prompt.\n */\nconst ANSI_PATTERN = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Bell and backspace, which a terminal consumes rather than displays. */\n// oxlint-disable-next-line no-control-regex -- matching terminal control bytes is the whole job\n\n/**\n * The same set minus SGR, the sequence that only sets colour and weight.\n *\n * The final byte class omits `m`, so `ESC[32m` survives while cursor moves,\n * OSC strings and keypad escapes are still removed.\n */\nconst ANSI_PATTERN_KEEPING_COLOUR = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-ln-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Removes escape sequences, leaving the characters a user would have seen. */\nexport function stripAnsi(text: string): string {\n return text.replace(ANSI_PATTERN, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Removes everything `stripAnsi` does except colour.\n *\n * Logs are read back by a terminal renderer as well as by grep, and a command\n * that colours its own output has already said how it should look.\n */\nexport function stripAnsiKeepingColour(text: string): string {\n return text.replace(ANSI_PATTERN_KEEPING_COLOUR, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Collapses carriage-return redraws.\n *\n * A bare `\\r` returns the cursor to the start of the current line, so whatever\n * follows overwrites it. Only the final state of each line is kept. The `\\r` of\n * a CRLF pair is a line terminator rather than a redraw, so it is normalised\n * away first: a pseudo terminal ends every line that way, and treating it as a\n * redraw would blank the whole log.\n */\nexport function collapseCarriageReturns(text: string): string {\n return text\n .replace(/\\r\\n/g, '\\n')\n .split('\\n')\n .map((line) => (line.includes('\\r') ? (line.split('\\r').at(-1) ?? '') : line))\n .join('\\n');\n}\n\n/** Full scrub applied to PTY output before it reaches the log file. */\nexport function scrubTerminalOutput(text: string): string {\n return collapseCarriageReturns(stripAnsiKeepingColour(text));\n}\n\n/** Plain-text scrub, for the copy the model reads. */\nexport function scrubTerminalOutputToPlainText(text: string): string {\n return collapseCarriageReturns(stripAnsi(text));\n}\n","import headless, { type Terminal } from '@xterm/headless';\n\nimport {\n DEFAULT_COLS,\n DEFAULT_ROWS,\n LIVENESS_POLL_MS,\n MEMORY_BUFFER_FACTOR,\n NEWLINE,\n SCROLLBACK,\n TERM_GRACE_MS,\n} from '../../constants/ptyHost';\nimport { PI_SESSION_ID_ENV } from '../../constants/session';\nimport type { IClock } from '../../types/clock';\nimport type { ILogFile } from '../../types/logFile';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IPtyHost, PtyLaunchRequest, PtyRun } from '../../types/ptyHost';\nimport type { IPtySpawner, PtyProcess } from '../../types/ptySpawner';\nimport type { RunnerSettings } from '../../types/runnerSettings';\nimport type { ExitResult } from '../../types/spawner';\nimport { scrubTerminalOutput } from '../ansiScrub';\nimport { getResultMaxBytes } from '../runnerConfig';\nimport { NO_TERMINAL_INPUT_ENV } from '../runnerSupervisor';\n\n/** Enough history for the overlay to scroll without holding a whole build. */\n\n// Use the CommonJS default export: Node cannot detect this package's named exports.\n// A static import also lets server bundles include it without a live node_modules lookup.\nconst { Terminal: HeadlessTerminal } = headless;\n\nexport class PtyHost implements IPtyHost {\n private readonly runs = new Map<string, PtyRun>();\n\n constructor(\n private readonly spawner: IPtySpawner,\n private readonly logFile: ILogFile,\n private readonly processControl: IProcessControl,\n private readonly clock: IClock,\n private readonly environment: Readonly<Record<string, string | undefined>> = process.env,\n private readonly settings?: RunnerSettings,\n ) {}\n\n async launch(request: PtyLaunchRequest): Promise<PtyRun> {\n const cols = request.cols ?? DEFAULT_COLS;\n const rows = request.rows ?? DEFAULT_ROWS;\n const writer = await this.logFile.open(request.id);\n const terminal = new HeadlessTerminal({ cols, rows, scrollback: SCROLLBACK, allowProposedApi: true });\n const memoryLimit = getResultMaxBytes(this.environment, this.settings) * MEMORY_BUFFER_FACTOR;\n\n const child = await this.spawner.spawn({\n command: request.command,\n cwd: request.cwd,\n env: { ...this.environment, ...NO_TERMINAL_INPUT_ENV, [PI_SESSION_ID_ENV]: request.sessionId },\n cols,\n rows,\n });\n\n let buffer = '';\n let buffering = true;\n let settled = false;\n let resolveCompletion: (result: ExitResult) => void = () => undefined;\n let rejectCompletion: (error: unknown) => void = () => undefined;\n const completion = new Promise<ExitResult>((resolve, reject) => {\n resolveCompletion = resolve;\n rejectCompletion = reject;\n });\n const dataHandlers = new Set<(data: string) => void>();\n\n child.onData((data) => {\n // The terminal keeps the screen; the log keeps text worth grepping.\n terminal.write(data);\n const scrubbed = scrubTerminalOutput(data);\n void writer.append(scrubbed).catch((error: unknown) => process.emitWarning(String(error)));\n if (buffering) {\n buffer += scrubbed;\n if (buffer.length > memoryLimit) buffer = buffer.slice(-memoryLimit);\n }\n for (const handler of dataHandlers) handler(data);\n });\n\n child.onExit(async (result) => {\n if (settled) return;\n settled = true;\n this.runs.delete(request.name);\n try {\n await writer.close();\n resolveCompletion({ code: result.exitCode, signal: null });\n } catch (error) {\n rejectCompletion(error);\n }\n });\n\n const run: PtyRun = {\n id: request.id,\n name: request.name,\n pid: child.pid,\n logPath: writer.path,\n backend: 'native',\n output: async () => buffer,\n completion: () => completion,\n detach: () => {\n buffering = false;\n buffer = '';\n },\n stop: () => this.stopChild(child, request.name),\n write: (text) => child.write(text),\n screen: () => renderScreen(terminal),\n onData: (handler) => {\n dataHandlers.add(handler);\n return () => dataHandlers.delete(handler);\n },\n resize: (nextCols, nextRows) => {\n child.resize(nextCols, nextRows);\n terminal.resize(nextCols, nextRows);\n },\n };\n\n this.runs.set(request.name, run);\n return run;\n }\n\n get(name: string): PtyRun | undefined {\n return this.runs.get(name);\n }\n\n write(name: string, text: string): boolean {\n const run = this.runs.get(name);\n if (!run) return false;\n run.write(text.endsWith(NEWLINE) ? text : `${text}${NEWLINE}`);\n return true;\n }\n\n list(): PtyRun[] {\n return [...this.runs.values()];\n }\n\n async disposeAll(): Promise<void> {\n const runs = [...this.runs.values()];\n this.runs.clear();\n for (const run of runs) await run.stop();\n }\n\n private async stopChild(child: PtyProcess, name: string): Promise<boolean> {\n this.runs.delete(name);\n if (!this.processControl.isAlive(child.pid)) return false;\n\n child.kill('SIGTERM');\n const deadline = this.clock.now() + TERM_GRACE_MS;\n while (this.clock.now() < deadline) {\n if (!this.processControl.isAlive(child.pid)) return true;\n await new Promise<void>((resolve) => {\n this.clock.after(LIVENESS_POLL_MS, resolve);\n });\n }\n child.kill('SIGKILL');\n return true;\n }\n}\n\n/** The visible rows, trimmed of trailing blanks, as plain text. */\nfunction renderScreen(terminal: Terminal): string {\n const buffer = terminal.buffer.active;\n const lines: string[] = [];\n for (let row = buffer.baseY; row < buffer.baseY + terminal.rows; row += 1) {\n lines.push(buffer.getLine(row)?.translateToString(true) ?? '');\n }\n while (lines.length > 0 && lines.at(-1) === '') lines.pop();\n return lines.join(NEWLINE);\n}\n","import { execFile } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\n\nimport {\n COMMAND_SIDECAR_SUFFIX,\n EXIT_SIDECAR_SUFFIX,\n JSON_EXTENSION,\n NOT_FOUND_ERROR_CODE,\n REGISTRY_PATH_ENV,\n RUNNER_TAG,\n SERVICE_PREFIX,\n SERVICE_TYPE,\n} from '../../constants/runnerRegistry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n RunnerTerminationIntent,\n} from '../../types/runnerRegistry';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { isRunnerRecord } from '../runnerRecord';\nimport { resolveRootSessionId } from '../session';\n\n/** The slice of the shared registry service this wrapper depends on. */\nexport type ProcessRegistryPort = Pick<\n ProcessRegistryService,\n 'registerProcess' | 'releaseProcess' | 'listProcesses' | 'close'\n>;\n\n/** Opens the SQLite-backed registry the rest of the monorepo already shares. */\nexport function createDefaultProcessRegistry(\n env: Readonly<Record<string, string | undefined>> = process.env,\n): ProcessRegistryPort {\n return createProcessRegistryService(env[REGISTRY_PATH_ENV]);\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction readString(record: ProcessRegistryRecord, key: string): string {\n const value = record.metadata?.[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction readNumber(record: ProcessRegistryRecord, key: string): number {\n const value = record.metadata?.[key];\n return typeof value === 'number' ? value : 0;\n}\n\nconst runCommand = promisify(execFile);\n\n/**\n * The process start time the registry stores to prove a pid is still the\n * process it registered.\n *\n * The registry resolves it itself when the caller omits it, and it does so with\n * `execFileSync`, which stops this process for the length of a `ps` on every\n * runner that starts. Reading it here costs the same subprocess and blocks\n * nothing: the command is already running by the time a runner is registered.\n *\n * The value has to match what the registry would have read, so the command and\n * the trimming are deliberately identical to its own.\n */\nasync function processStartedAt(pid: number): Promise<string | undefined> {\n if (process.platform === 'win32') return undefined;\n try {\n const { stdout } = await runCommand('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8' });\n return stdout.trim() || undefined;\n } catch {\n return undefined;\n }\n}\n\ntype ListenerRegistration = Readonly<{ key: string; listener: () => void }>;\n\n// Server plugin replacement can leave an in-flight tool and its publisher on different registry instances.\nconst listenersBySession = new Map<string, Set<() => void>>();\n\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly subscriptions = new Set<ListenerRegistration>();\n private recordWrites: Promise<void> = Promise.resolve();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n private readonly environment: Readonly<Record<string, string | undefined>>,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId, this.environment);\n const identity = await processStartedAt(input.pid);\n const response = await this.registry.registerProcess({\n repositoryPath: await this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${input.id}`,\n serviceType: SERVICE_TYPE,\n pid: input.pid,\n command: input.command,\n ...(identity === undefined ? {} : { processStartedAt: identity }),\n tags: resolveProcessTags([RUNNER_TAG, `runner:${input.id}`, sessionTag(input.sessionId)]),\n metadata: {\n id: input.id,\n name: input.name,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n startedAt,\n hostPid: process.pid,\n },\n // The caller has already resolved name collisions against live records,\n // so anything still holding this name is stale.\n force: true,\n });\n if (!response.success) throw new Error(response.error ?? `Failed to register runner ${input.name}`);\n const record: RunnerRecord = {\n id: input.id,\n name: input.name,\n pid: input.pid,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n startedAt,\n state: 'running',\n promoted: false,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n hostPid: process.pid,\n };\n await this.writeRecord(record);\n this.notify(record.sessionId);\n return record;\n }\n\n async list(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n repositoryPath: await this.repositoryPath(),\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 await this.paths.ensureDirectories(sessionId);\n const records: RunnerRecord[] = [];\n const directory = this.paths.stateDirectory(sessionId);\n for (const entry of await fs.readdir(directory)) {\n if (!isPrimaryMetadataEntry(entry)) continue;\n try {\n const parsed: unknown = JSON.parse(await fs.readFile(`${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(await fs.readFile(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 await this.writeRecord(promoted);\n this.notify(promoted.sessionId);\n return promoted;\n }\n\n async requestTermination(\n id: string,\n intent: RunnerTerminationIntent | undefined,\n sessionId?: string,\n ): Promise<RunnerRecord | undefined> {\n const record = await this.get(id, sessionId);\n if (!record) return undefined;\n if (record.state === 'completed') return record;\n if (intent !== undefined && record.terminationIntent !== undefined) return record;\n const { terminationIntent: _previous, ...rest } = record;\n const updated: RunnerRecord = { ...rest, ...(intent ? { terminationIntent: intent } : {}) };\n await this.writeRecord(updated);\n return updated;\n }\n\n async complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined> {\n const persisted = await this.get(id, sessionId);\n if (persisted?.state === 'completed') {\n await this.release(id, persisted.sessionId);\n return persisted;\n }\n const active = await this.list();\n const record =\n persisted ??\n active.find((candidate) => candidate.id === id && (sessionId === undefined || candidate.sessionId === sessionId));\n if (!record) {\n const belongsToAnotherSession =\n sessionId !== undefined && active.some((candidate) => candidate.id === id && candidate.sessionId !== sessionId);\n if (!belongsToAnotherSession) await this.release(id, sessionId);\n return undefined;\n }\n const completed: RunnerRecord = {\n ...record,\n state: 'completed',\n exit: {\n ...outcome,\n ...(outcome.signal !== null && outcome.terminationReason === undefined && record.terminationIntent === undefined\n ? { terminationReason: 'external_signal' as const }\n : {}),\n ...record.terminationIntent,\n finishedAt: new Date().toISOString(),\n },\n };\n await this.writeRecord(completed);\n await this.release(id, completed.sessionId);\n return completed;\n }\n\n async release(id: string, sessionId?: string): Promise<void> {\n const activeSessionId = sessionId ?? (await this.list()).find((record) => record.id === id)?.sessionId;\n const ownedSessionId = activeSessionId ?? (await this.get(id))?.sessionId;\n const response = await this.registry.releaseProcess({\n repositoryPath: await this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${id}`,\n serviceType: SERVICE_TYPE,\n kill: false,\n releasePort: false,\n force: true,\n });\n if (!response.success && !response.error?.includes('No matching process entry')) {\n throw new Error(response.error ?? `Failed to release runner ${id}`);\n }\n if (ownedSessionId !== undefined) this.notify(ownedSessionId);\n }\n\n async pruneDead(): Promise<string[]> {\n const dead = (await this.list()).filter((record) => !this.processControl.isAlive(record.pid));\n for (const record of dead) {\n await this.complete(record.id, { reason: 'backend_lost', code: null, signal: null }, record.sessionId);\n }\n return dead.map((record) => record.id);\n }\n\n subscribe(listener: () => void, sessionId: string): () => void {\n const subscribed = (): void => listener();\n const registration = { key: this.subscriptionKey(sessionId), listener: subscribed };\n const listeners = listenersBySession.get(registration.key) ?? new Set<() => void>();\n listeners.add(subscribed);\n listenersBySession.set(registration.key, listeners);\n this.subscriptions.add(registration);\n return () => this.removeSubscription(registration);\n }\n\n close(): void {\n for (const registration of this.subscriptions) this.removeSubscription(registration);\n this.registry.close();\n }\n\n private notify(sessionId: string): void {\n for (const listener of listenersBySession.get(this.subscriptionKey(sessionId)) ?? []) {\n try {\n listener();\n } catch (error) {\n process.emitWarning(`Runner registry listener failed: ${String(error)}`);\n }\n }\n }\n\n private removeSubscription(registration: ListenerRegistration): void {\n const listeners = listenersBySession.get(registration.key);\n listeners?.delete(registration.listener);\n if (listeners?.size === 0) listenersBySession.delete(registration.key);\n this.subscriptions.delete(registration);\n }\n\n private subscriptionKey(sessionId: string): string {\n return JSON.stringify([path.resolve(this.paths.stateDirectory(sessionId)), sessionId]);\n }\n\n private async repositoryPath(): Promise<string> {\n return normalizeRepositoryPath(await this.paths.repositoryPath());\n }\n\n private writeRecord(record: RunnerRecord): Promise<void> {\n const write = this.recordWrites.then(async () => {\n await this.paths.ensureDirectories(record.sessionId);\n const target = this.paths.statePathFor(record.id, record.sessionId);\n const temporary = `${target}.${process.pid}.tmp`;\n await fs.writeFile(temporary, `${JSON.stringify(record, null, 2)}\\n`, 'utf8');\n await fs.rename(temporary, target);\n });\n // Keep later writes ordered even after a failed write, which its caller observes.\n this.recordWrites = write.catch(() => undefined);\n return write;\n }\n}\n\n/** Every field is read back from metadata this class wrote on registration. */\nfunction toRunnerRecord(record: ProcessRegistryRecord): RunnerRecord {\n return {\n id: readString(record, 'id'),\n name: readString(record, 'name'),\n pid: record.pid,\n command: readString(record, 'command'),\n cwd: readString(record, 'cwd'),\n logPath: readString(record, 'logPath'),\n interactive: record.metadata?.interactive === true,\n sessionId: readString(record, 'sessionId'),\n ...(readString(record, 'rootSessionId') ? { rootSessionId: readString(record, 'rootSessionId') } : {}),\n startedAt: readString(record, 'startedAt'),\n state: 'running',\n promoted: false,\n backend: readBackend(record.metadata?.backend),\n ...(readString(record, 'backendTarget') ? { backendTarget: readString(record, 'backendTarget') } : {}),\n hostPid: readNumber(record, 'hostPid'),\n };\n}\n\nfunction readBackend(value: unknown): RunnerRecord['backend'] {\n return value === 'rmux' || value === 'tmux' ? value : 'native';\n}\n\nfunction isPrimaryMetadataEntry(entry: string): boolean {\n return (\n entry.endsWith(JSON_EXTENSION) && !entry.endsWith(COMMAND_SIDECAR_SUFFIX) && !entry.endsWith(EXIT_SIDECAR_SUFFIX)\n );\n}\n","import fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport { BACKGROUND_CONTEXT, readTextIfExists, type FileSystem } from '@agimon-ai/po-core/platform';\nimport { CONFIG_DIR_NAME } from '@earendil-works/pi-coding-agent';\n\nimport {\n MAX_ERROR_ENTRIES_CEILING,\n MAX_PATTERNS,\n MAX_PATTERN_LENGTH,\n MAX_RESULT_BYTES_CEILING,\n MAX_RESULT_LINES_CEILING,\n MAX_RESULT_TOKENS_CEILING,\n RATIO_KEYS,\n RUNNER_SETTINGS_FILE,\n} from '../../constants/runnerSettings';\nimport type { IRunnerSettingsLoader, RunnerSettings, RunnerSettingsLoad } from '../../types/runnerSettings';\n\n/** Pi's convention is one JSON file per extension under the config directory. */\n\nconst EMPTY: RunnerSettingsLoad = { settings: {}, issues: [] };\nconst COUNT_KEYS = [\n 'maxResultBytes',\n 'maxResultLines',\n 'successMaxResultBytes',\n 'maxResultTokens',\n 'successMaxResultTokens',\n 'errorMaxEntries',\n 'errorMaxVariantsJoined',\n] as const;\nconst KNOWN_KEYS = new Set<string>([...RATIO_KEYS, ...COUNT_KEYS, 'errorPatterns']);\n/** Ceilings so a project file cannot flood the model's context. */\nconst CEILINGS: Readonly<Record<string, number>> = {\n maxResultBytes: MAX_RESULT_BYTES_CEILING,\n successMaxResultBytes: MAX_RESULT_BYTES_CEILING,\n maxResultTokens: MAX_RESULT_TOKENS_CEILING,\n successMaxResultTokens: MAX_RESULT_TOKENS_CEILING,\n maxResultLines: MAX_RESULT_LINES_CEILING,\n errorMaxEntries: MAX_ERROR_ENTRIES_CEILING,\n errorMaxVariantsJoined: MAX_ERROR_ENTRIES_CEILING,\n};\n\nexport class RunnerSettingsLoader implements IRunnerSettingsLoader {\n constructor(private readonly files?: FileSystem) {}\n\n async load(cwd: string, trusted: boolean): Promise<RunnerSettingsLoad> {\n // Project-local configuration is untrusted input until the project is.\n if (!trusted) return EMPTY;\n\n let raw: string;\n try {\n const target = path.join(cwd, CONFIG_DIR_NAME, RUNNER_SETTINGS_FILE);\n const text = this.files\n ? await readTextIfExists(this.files, target, BACKGROUND_CONTEXT)\n : await fs.readFile(target, 'utf8');\n if (text === undefined) return EMPTY;\n raw = text;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return EMPTY;\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} could not be read`] };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} is not valid JSON`] };\n }\n return validate(parsed);\n }\n}\n\nfunction validate(parsed: unknown): RunnerSettingsLoad {\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} must contain a JSON object`] };\n }\n\n const source = parsed as Record<string, unknown>;\n const settings: Record<string, unknown> = {};\n const issues: string[] = [];\n // A rejected key is reported rather than dropped: a silent typo reads as a\n // setting that simply does not work.\n for (const key of Object.keys(source)) if (!KNOWN_KEYS.has(key)) issues.push(`unknown key ${key}`);\n\n for (const key of COUNT_KEYS) {\n const value = source[key];\n if (value === undefined) continue;\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n issues.push(`${key} must be a positive integer`);\n continue;\n }\n settings[key] = Math.min(value, CEILINGS[key] ?? value);\n }\n\n for (const key of RATIO_KEYS) {\n const value = source[key];\n if (value === undefined) continue;\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value >= 1) {\n issues.push(`${key} must be a number above 0 and below 1`);\n continue;\n }\n settings[key] = value;\n }\n\n const patterns = source.errorPatterns;\n if (patterns !== undefined) {\n if (!Array.isArray(patterns) || patterns.some((entry) => typeof entry !== 'string')) {\n issues.push('errorPatterns must be an array of strings');\n } else {\n const accepted: string[] = [];\n for (const pattern of patterns.slice(0, MAX_PATTERNS) as string[]) {\n if (pattern.length > MAX_PATTERN_LENGTH) {\n issues.push('errorPatterns entry is too long');\n continue;\n }\n try {\n new RegExp(pattern, 'iu');\n accepted.push(pattern);\n } catch {\n issues.push(`errorPatterns entry is not a valid regular expression: ${pattern}`);\n }\n }\n if (accepted.length > 0) settings.errorPatterns = accepted;\n }\n }\n\n return { settings: settings as RunnerSettings, issues };\n}\n","import { PO_CODEMODE_SERVICE, readPoCodemode } from '@agimon-ai/po-core/codemode';\nimport { readPoFs, PO_SESSION_TEMP_DIR } from '@agimon-ai/po-core/platform';\nimport type { PoServerPluginContext } from '@agimon-ai/po-core/serverPlugin';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { REGISTRY_PATH_ENV } from '../../../../../constants/runnerRegistry';\nimport {\n HISTORY_SWEEP_INTERVAL_MS,\n RUNNER_FINISHED_MESSAGE,\n RUNNER_STATUS_POLL_MS,\n} from '../../../../../constants/runnerRuntime';\nimport { RUNNER_SETTINGS_FILE } from '../../../../../constants/runnerSettings';\nimport { SystemClock } from '../../../../../services/clock';\nimport { createHeadlessBashTool, createHeadlessRunnersCommand } from '../../../../../services/headless';\nimport { LifelineObserver } from '../../../../../services/lifeline';\nimport { summarizeLog } from '../../../../../services/logReader';\nimport { createRunnerDependencies } from '../../../../../services/runnerDependencies';\nimport { createRunnerHost } from '../../../../../services/runnerHost';\nimport { createRunnerNamespace } from '../../../../../services/runnerNamespace';\nimport { createRunnerOperations } from '../../../../../services/runnerOperations';\nimport { createDefaultProcessRegistry } from '../../../../../services/runnerRegistry';\nimport { createRunnerSequenceService } from '../../../../../services/runnerSequence';\nimport { createRunnerSequenceStore } from '../../../../../services/runnerSequenceStore';\nimport { createRunnerServerRuntime } from '../../../../../services/runnerServerRuntime';\nimport { RunnerSettingsLoader } from '../../../../../services/runnerSettings';\n\nexport const RUNNER_SERVER_SCOPE_SERVICE = 'po/runner-server-scope';\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n 'po/runner-server-scope': RunnerServerScope;\n }\n}\n\nexport const createRunnerServerRoot = async ({ context, host, agent, signal, resources }: PoServerPluginContext) => {\n if (!host.context.directEvents) throw new Error('Runner server plugin requires the session direct event bus.');\n if (!host.context.cwd || !host.context.environment) {\n throw new Error('Runner server plugin requires session cwd and environment.');\n }\n // Inherit the current headless Pi bridge's isProjectTrusted: () => true.\n // This preserves runner configuration parity, not a new general trust policy.\n const fs = readPoFs(context);\n const loaded = await new RunnerSettingsLoader(fs).load(host.context.cwd, true);\n const settings = Object.freeze({\n ...loaded.settings,\n ...(loaded.settings.errorPatterns ? { errorPatterns: Object.freeze([...loaded.settings.errorPatterns]) } : {}),\n });\n for (const issue of loaded.issues) process.emitWarning(`${RUNNER_SETTINGS_FILE}: ${issue}`);\n const environment = host.context.environment;\n if (!environment[PO_SESSION_TEMP_DIR])\n throw new Error('Runner requires the host-admitted session temporary directory.');\n // One per registry for this build in the hub: every session shares the connection and the loop that\n // keeps house for it, and the last session to close disposes them.\n const runnerHost = await resources.acquire(\n ['runner-host', environment[REGISTRY_PATH_ENV] ?? null],\n () =>\n createRunnerHost({\n processRegistry: createDefaultProcessRegistry(environment),\n clock: new SystemClock(),\n statusPollMs: RUNNER_STATUS_POLL_MS,\n historySweepMs: HISTORY_SWEEP_INTERVAL_MS,\n onWarning: (message) => process.emitWarning(message),\n }),\n (shared) => shared.dispose(),\n );\n // One observer process cleans up after every session of the registry, rather than one each.\n const lifelineObserver = await resources.acquire(\n ['runner-lifeline-observer', environment[REGISTRY_PATH_ENV] ?? null],\n () => new LifelineObserver(environment),\n (shared) => shared.close(),\n );\n const dependencies = createRunnerDependencies({\n settings,\n cwd: host.context.cwd,\n environment,\n processRegistry: runnerHost.processRegistry,\n lifelineObserver,\n ...(host.context.executionBudget === undefined ? {} : { executionBudget: host.context.executionBudget }),\n });\n const execution = agent?.context;\n const wakeAgent =\n execution &&\n (async (content: string, requestId: string, notice: string): Promise<void> => {\n try {\n // 'steer' joins a running turn and starts one on an idle session.\n // Reuse completion identity across retries and acknowledge admission, not settlement.\n // A custom message, not a prompt: the model reads it as input, while the\n // transcript shows the notice as an event rather than as the person's words.\n if (!execution.session.admitMessage) throw new Error('The session cannot admit a runner notification.');\n await execution.session.admitMessage(\n { type: RUNNER_FINISHED_MESSAGE, text: content, display: true, details: { notice } },\n 'steer',\n requestId,\n );\n } catch (error) {\n // Keep completion ownership with the watcher until admission succeeds.\n await Promise.resolve()\n .then(() => execution.client.notify({ body: String(error), level: 'warning' }))\n .catch(() => undefined);\n throw error;\n }\n });\n const runtime = createRunnerServerRuntime(dependencies, runnerHost, host.context.directEvents, wakeAgent, execution);\n const sequences =\n fs && execution\n ? createRunnerSequenceService({\n dependencies,\n store: createRunnerSequenceStore(fs, dependencies.paths, execution.sessionId),\n sessionId: execution.sessionId,\n signal,\n summarizeLog: (path, lines, bytes, tokens) => summarizeLog(path, lines, bytes, tokens, settings),\n onChange: () => runtime.sequenceChanged(),\n onWarning: (message) => process.emitWarning(message),\n onComplete: async (record) => {\n if (!record.result) return;\n await wakeAgent?.(\n record.result.content.flatMap((part) => (part.type === 'text' ? [part.text] : [])).join('\\n'),\n `runner-sequence-finished:${JSON.stringify([record.sessionId, record.id])}`,\n `Command sequence ${record.name} finished: ${record.outcome ?? record.state}`,\n );\n },\n })\n : undefined;\n if (sequences) runtime.setSequences(sequences);\n const bash = (wakeOnExit: boolean) =>\n createHeadlessBashTool(\n sequences\n ? async (params, context, toolCallId, callSignal, onUpdate) => {\n await runtime.ensureSession(context.sessionId);\n return sequences.run(params, context, toolCallId, callSignal, onUpdate, wakeOnExit);\n }\n : undefined,\n dependencies.environment,\n );\n const value = {\n sequences,\n tool: bash(true),\n // Remote MCP runners must not wake the local agent; the remote caller polls instead.\n remoteTool: bash(false),\n command: createHeadlessRunnersCommand(dependencies, {\n // Without the platform fs a missing folder still fails, only at spawn and less plainly.\n fs,\n sequences,\n onStart: (startedSessionId, failure) => runtime.reportStart(startedSessionId, failure),\n }),\n recoverCompaction: runtime.recoverCompaction,\n };\n let controlsDisposed = false;\n if (fs && execution) {\n const namespace = createRunnerNamespace({\n sessionId: execution.sessionId,\n operations: createRunnerOperations({\n ...dependencies,\n registry: dependencies.runnerRegistry,\n fs,\n sessionId: execution.sessionId,\n }),\n sequences,\n assertAvailable: () => {\n signal?.throwIfAborted();\n if (controlsDisposed) throw new Error('Runner session is disposed.');\n },\n });\n context.inject([PO_CODEMODE_SERVICE], (scoped) => {\n const registration = readPoCodemode(scoped)?.register('@agimon-ai/po-runner', namespace);\n return () => registration?.dispose();\n });\n }\n return {\n value,\n services: [\n runtime.backgroundWorkPlugin,\n (context: Context) => {\n context.plugin((providerContext) => providerContext.provide(RUNNER_SERVER_SCOPE_SERVICE, value));\n },\n ],\n activities: [runtime.activity],\n // Package install is on the session-create path; bash awaits supervision itself.\n onStart: execution\n ? () => {\n void runtime\n .ensureSession(execution.sessionId)\n .catch((error: unknown) =>\n process.emitWarning(`Could not initialize headless runner supervision: ${String(error)}`),\n );\n }\n : undefined,\n onStop: async () => {\n controlsDisposed = true;\n await runtime.dispose();\n },\n };\n};\n\nexport type RunnerServerScope = Awaited<ReturnType<typeof createRunnerServerRoot>>['value'];\n","import { defineMcpTool, type PoMcpPluginContext } from '@agimon-ai/po-core/mcpFacet';\n\nimport { RUNNER_SERVER_SCOPE_SERVICE, type RunnerServerScope } from '../_lib/serverRoot';\n\nconst pollHint = (id: string): string =>\n `You will not be notified when this runner ends. Check it with \\`po-runner status ${id}\\` and read output with \\`po-runner logs ${id} --lines 100\\`, both through this tool, until State is completed.`;\n\nexport default defineMcpTool((context: PoMcpPluginContext) => {\n const scope = context.services.get<RunnerServerScope>(RUNNER_SERVER_SCOPE_SERVICE);\n if (!scope) throw new Error('Runner session service is unavailable.');\n return {\n ...scope.remoteTool,\n description:\n \"Run an array of shell commands (bash, terminal, CLI) in the Po session's repository on the host machine, not on your local machine. Use a one-item array for one command. Commands run in order and stop on first failure. Only the last command may be interactive. Do not pipe or redirect output to a file, head, or tail: full logs are saved automatically and returned output is bounded. Long-running commands return a sequence id. No completion notice reaches you, so check with `po-runner status <id>` and `po-runner logs <id> --lines 100` instead of relaunching.\",\n async execute(toolCallId, parameters, signal, onUpdate, execution) {\n const result = await scope.remoteTool.execute(\n toolCallId,\n parameters,\n AbortSignal.any([context.signal, signal ?? context.signal]),\n onUpdate,\n execution,\n );\n const details = result.details as { promoted?: unknown; id?: unknown } | undefined;\n if (details?.promoted !== true || typeof details.id !== 'string') return result;\n return { ...result, content: [...result.content, { type: 'text', text: pollHint(details.id) }] };\n },\n };\n});\n","// Generated by @agimon-ai/po-build. Do not edit by hand.\nimport { defineMcpPlugin, type PoMcpSessionPlugin } from '@agimon-ai/po-core/mcpFacet';\n\nimport skillHelpHowToRunner from '../src/extensions/workspaces/sessions/(backend)/skill/help-how-to-runner.mcp';\nimport toolBash from '../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nconst withWidget = <T>(key: string, value: T): T => value === undefined ? value : ({\n ...value, _meta: { ...(value as { _meta?: object })._meta, [\"po/widget\"]: key },\n});\n\nexport const mcp = defineMcpPlugin({\n name: '@agimon-ai/po-runner',\n session: (context) => ({\n get skills(): PoMcpSessionPlugin['skills'] { return [at(skillHelpHowToRunner, context)]; },\n get tools(): PoMcpSessionPlugin['tools'] { return [withWidget(\"@agimon-ai/po-runner/bash\", via({ name: 'bash' }, at(toolBash, context)))]; },\n }) satisfies PoMcpSessionPlugin,\n});\n\nexport default mcp;\n"],"mappings":"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"}
|
package/dist/extensions/mcp.mjs
CHANGED
|
@@ -9,9 +9,9 @@ import headless from "@xterm/headless";
|
|
|
9
9
|
import "@rmux/sdk";
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import "@agimon-ai/foundation-process-registry";
|
|
12
|
-
import "@agimon-ai/po-core/childProcess";
|
|
13
12
|
import "typebox/value";
|
|
14
13
|
import "@agimon-ai/po-core/backgroundWork";
|
|
14
|
+
import "@agimon-ai/po-core/childProcess";
|
|
15
15
|
import "@agimon-ai/po-core/extensionFile";
|
|
16
16
|
import "@agimon-ai/po-core/telemetry";
|
|
17
17
|
import "@earendil-works/pi-coding-agent";
|