@narumitw/pi-subagents 0.43.1 → 0.46.0
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/package.json +1 -1
- package/src/pi-invocation.ts +168 -0
- package/src/runner.ts +17 -22
package/package.json
CHANGED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getPackageDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
const CORE_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
6
|
+
const MAX_DISPLAY_PATH_LENGTH = 500;
|
|
7
|
+
|
|
8
|
+
export interface PiInvocation {
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PiInvocationRuntime {
|
|
14
|
+
execPath: string;
|
|
15
|
+
packageDir: string;
|
|
16
|
+
runtimeKind: "node" | "bun" | "unsupported";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class PiInvocationError extends Error {
|
|
20
|
+
constructor(message: string) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "PiInvocationError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function displayPath(value: string): string {
|
|
27
|
+
const suffix = value.length > MAX_DISPLAY_PATH_LENGTH ? "…" : "";
|
|
28
|
+
return JSON.stringify(`${value.slice(0, MAX_DISPLAY_PATH_LENGTH)}${suffix}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolutionError(packageDir: string, reason: string): PiInvocationError {
|
|
32
|
+
return new PiInvocationError(
|
|
33
|
+
`Unable to resolve the Pi CLI from the loaded ${CORE_PACKAGE_NAME} package at ${displayPath(packageDir)}: ${reason}. Reinstall the matching Pi core package before using the subprocess transport.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function currentRuntime(): PiInvocationRuntime {
|
|
38
|
+
let packageDir: string;
|
|
39
|
+
try {
|
|
40
|
+
packageDir = getPackageDir();
|
|
41
|
+
} catch {
|
|
42
|
+
throw resolutionError("<unavailable>", "Pi core did not provide its package directory");
|
|
43
|
+
}
|
|
44
|
+
const runtimeKind = process.versions.bun
|
|
45
|
+
? "bun"
|
|
46
|
+
: process.release.name === "node" && !process.versions.electron
|
|
47
|
+
? "node"
|
|
48
|
+
: "unsupported";
|
|
49
|
+
return {
|
|
50
|
+
execPath: process.execPath,
|
|
51
|
+
packageDir,
|
|
52
|
+
runtimeKind,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
57
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isWithinDirectory(parent: string, candidate: string): boolean {
|
|
61
|
+
const relative = path.relative(parent, candidate);
|
|
62
|
+
return (
|
|
63
|
+
relative === "" ||
|
|
64
|
+
(!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readCoreManifest(packageDir: string): Record<string, unknown> {
|
|
69
|
+
const manifestPath = path.join(packageDir, "package.json");
|
|
70
|
+
let source: string;
|
|
71
|
+
try {
|
|
72
|
+
source = fs.readFileSync(manifestPath, "utf8");
|
|
73
|
+
} catch {
|
|
74
|
+
throw resolutionError(packageDir, "the package manifest is unavailable");
|
|
75
|
+
}
|
|
76
|
+
let manifest: unknown;
|
|
77
|
+
try {
|
|
78
|
+
manifest = JSON.parse(source);
|
|
79
|
+
} catch {
|
|
80
|
+
throw resolutionError(packageDir, "the package manifest is invalid JSON");
|
|
81
|
+
}
|
|
82
|
+
if (!isRecord(manifest)) {
|
|
83
|
+
throw resolutionError(packageDir, "the package manifest is invalid");
|
|
84
|
+
}
|
|
85
|
+
if (manifest.name !== CORE_PACKAGE_NAME) {
|
|
86
|
+
throw resolutionError(packageDir, "the package manifest has an unexpected package name");
|
|
87
|
+
}
|
|
88
|
+
return manifest;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveDeclaredBin(packageDir: string, manifest: Record<string, unknown>): string {
|
|
92
|
+
const bin = manifest.bin;
|
|
93
|
+
const piBin = isRecord(bin) ? bin.pi : undefined;
|
|
94
|
+
if (typeof piBin !== "string" || !piBin.trim()) {
|
|
95
|
+
throw resolutionError(packageDir, "package.json bin.pi must be a non-empty string");
|
|
96
|
+
}
|
|
97
|
+
const candidate = path.resolve(packageDir, piBin);
|
|
98
|
+
if (path.isAbsolute(piBin) || !isWithinDirectory(packageDir, candidate)) {
|
|
99
|
+
throw resolutionError(packageDir, "the declared bin.pi target escapes the package directory");
|
|
100
|
+
}
|
|
101
|
+
return candidate;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveExistingFile(packageDir: string, candidate: string, reason: string): string {
|
|
105
|
+
let resolved: string;
|
|
106
|
+
try {
|
|
107
|
+
resolved = fs.realpathSync(candidate);
|
|
108
|
+
if (!fs.statSync(resolved).isFile()) throw new Error("not a file");
|
|
109
|
+
} catch {
|
|
110
|
+
throw resolutionError(packageDir, reason);
|
|
111
|
+
}
|
|
112
|
+
if (!isWithinDirectory(packageDir, resolved)) {
|
|
113
|
+
throw resolutionError(packageDir, "the declared bin.pi target escapes the package directory");
|
|
114
|
+
}
|
|
115
|
+
return resolved;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveStandaloneExecutable(
|
|
119
|
+
packageDir: string,
|
|
120
|
+
runtime: PiInvocationRuntime,
|
|
121
|
+
): string | undefined {
|
|
122
|
+
if (runtime.runtimeKind !== "bun") return undefined;
|
|
123
|
+
const { execPath } = runtime;
|
|
124
|
+
if (!/^pi(?:\.exe)?$/i.test(path.basename(execPath))) return undefined;
|
|
125
|
+
let resolved: string;
|
|
126
|
+
let mode: number;
|
|
127
|
+
try {
|
|
128
|
+
resolved = fs.realpathSync(execPath);
|
|
129
|
+
const stat = fs.statSync(resolved);
|
|
130
|
+
if (!stat.isFile()) throw new Error("not a file");
|
|
131
|
+
mode = stat.mode;
|
|
132
|
+
} catch {
|
|
133
|
+
throw resolutionError(packageDir, "the standalone Pi executable is unavailable");
|
|
134
|
+
}
|
|
135
|
+
if (path.dirname(resolved) !== packageDir) return undefined;
|
|
136
|
+
if (process.platform !== "win32" && (mode & 0o111) === 0) {
|
|
137
|
+
throw resolutionError(packageDir, "the standalone Pi executable is not executable");
|
|
138
|
+
}
|
|
139
|
+
return resolved;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function resolvePiInvocation(
|
|
143
|
+
args: string[],
|
|
144
|
+
runtime: PiInvocationRuntime = currentRuntime(),
|
|
145
|
+
): PiInvocation {
|
|
146
|
+
let packageDir: string;
|
|
147
|
+
try {
|
|
148
|
+
packageDir = fs.realpathSync(runtime.packageDir);
|
|
149
|
+
if (!fs.statSync(packageDir).isDirectory()) throw new Error("not a directory");
|
|
150
|
+
} catch {
|
|
151
|
+
throw resolutionError(runtime.packageDir, "the package directory is unavailable");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const manifest = readCoreManifest(packageDir);
|
|
155
|
+
const declaredBin = resolveDeclaredBin(packageDir, manifest);
|
|
156
|
+
const standalone = resolveStandaloneExecutable(packageDir, runtime);
|
|
157
|
+
if (standalone) return { command: standalone, args: [...args] };
|
|
158
|
+
|
|
159
|
+
if (runtime.runtimeKind !== "node" && runtime.runtimeKind !== "bun") {
|
|
160
|
+
throw resolutionError(packageDir, "the host does not provide a supported Node or Bun runtime");
|
|
161
|
+
}
|
|
162
|
+
const cliPath = resolveExistingFile(
|
|
163
|
+
packageDir,
|
|
164
|
+
declaredBin,
|
|
165
|
+
"the declared bin.pi target is unavailable",
|
|
166
|
+
);
|
|
167
|
+
return { command: runtime.execPath, args: [cliPath, ...args] };
|
|
168
|
+
}
|
package/src/runner.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
MAX_SUBAGENT_TIMEOUT_MS,
|
|
17
17
|
truncateUtf8,
|
|
18
18
|
} from "./limits.js";
|
|
19
|
+
import { resolvePiInvocation } from "./pi-invocation.js";
|
|
19
20
|
import { JsonLineDecoder } from "./protocol.js";
|
|
20
21
|
|
|
21
22
|
export const KILL_GRACE_MS = 5000;
|
|
@@ -353,22 +354,6 @@ export function buildPiArgs(options: PiArgsOptions): string[] {
|
|
|
353
354
|
return args;
|
|
354
355
|
}
|
|
355
356
|
|
|
356
|
-
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
357
|
-
const currentScript = process.argv[1];
|
|
358
|
-
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
359
|
-
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
360
|
-
return { command: process.execPath, args: [currentScript, ...args] };
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const execName = path.basename(process.execPath).toLowerCase();
|
|
364
|
-
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
365
|
-
if (!isGenericRuntime) {
|
|
366
|
-
return { command: process.execPath, args };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return { command: "pi", args };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
357
|
function signalProcess(proc: ReturnType<typeof spawn>, signal: NodeJS.Signals): void {
|
|
373
358
|
if (process.platform !== "win32" && proc.pid) {
|
|
374
359
|
try {
|
|
@@ -576,16 +561,26 @@ export async function runSingleAgent(
|
|
|
576
561
|
systemPromptPath: tmpPromptPath ?? undefined,
|
|
577
562
|
task,
|
|
578
563
|
});
|
|
579
|
-
let
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const exitCode = await new Promise<number>((resolve) => {
|
|
583
|
-
const invocation = invocationOverride
|
|
564
|
+
let invocation: { command: string; args: string[] };
|
|
565
|
+
try {
|
|
566
|
+
invocation = invocationOverride
|
|
584
567
|
? {
|
|
585
568
|
command: invocationOverride.command,
|
|
586
569
|
args: [...(invocationOverride.argsPrefix ?? []), ...args],
|
|
587
570
|
}
|
|
588
|
-
:
|
|
571
|
+
: resolvePiInvocation(args);
|
|
572
|
+
} catch (error) {
|
|
573
|
+
currentResult.launchFailed = true;
|
|
574
|
+
currentResult.exitCode = 1;
|
|
575
|
+
currentResult.stderr = setErrorMessage(
|
|
576
|
+
error instanceof Error ? error.message : String(error),
|
|
577
|
+
);
|
|
578
|
+
return currentResult;
|
|
579
|
+
}
|
|
580
|
+
let wasAborted = false;
|
|
581
|
+
let timedOut = false;
|
|
582
|
+
|
|
583
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
589
584
|
let settled = false;
|
|
590
585
|
let cleanupTermination: (() => void) | undefined;
|
|
591
586
|
let timeout: NodeJS.Timeout | undefined;
|