@tt-a1i/openpi 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +295 -389
- package/SETUP.md +24 -22
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/assets/readme-hero-mobile.svg +2 -2
- package/assets/readme-hero.svg +10 -10
- package/extensions/ask-user/handoff.ts +5 -1
- package/extensions/ask-user/index.ts +44 -0
- package/extensions/background-terminals/index.ts +118 -29
- package/extensions/background-terminals/src/domain.ts +5 -1
- package/extensions/background-terminals/src/manager.ts +2 -1
- package/extensions/background-terminals/src/prompt.ts +35 -0
- package/extensions/background-terminals/src/result-delivery.ts +76 -3
- package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
- package/extensions/capabilities/index.ts +198 -0
- package/extensions/context-pivot/index.ts +21 -0
- package/extensions/cron/index.ts +42 -15
- package/extensions/execution-convergence/active-evidence.ts +129 -0
- package/extensions/execution-convergence/index.ts +442 -0
- package/extensions/execution-convergence/workspace-provenance.ts +338 -0
- package/extensions/file-search/index.ts +8 -1
- package/extensions/file-search/src/binaries.ts +2 -1
- package/extensions/git-info/src/runtime.ts +1 -1
- package/extensions/goal/controller.ts +2 -1
- package/extensions/goal/index.ts +20 -1
- package/extensions/plan-mode/index.ts +12 -0
- package/extensions/setup/index.ts +241 -45
- package/extensions/setup/intercom-fs-helper.cjs +130 -0
- package/extensions/setup/intercom.ts +603 -0
- package/extensions/shared/child-session.ts +42 -5
- package/extensions/shared/setup-config.ts +27 -1
- package/extensions/shared/setup-episode-state.ts +7 -0
- package/extensions/shared/tool-surface.ts +435 -0
- package/extensions/subagents/index.ts +16 -1
- package/extensions/subagents/src/manager.ts +13 -11
- package/extensions/subagents/src/prompt.ts +1 -1
- package/extensions/tasks/index.ts +39 -12
- package/extensions/ui-customization/footer.ts +6 -1
- package/extensions/workflows/artifacts.ts +6 -1
- package/extensions/workflows/dashboard.ts +138 -27
- package/extensions/workflows/graph-projection.ts +240 -0
- package/extensions/workflows/handoff.ts +194 -0
- package/extensions/workflows/index.ts +258 -56
- package/extensions/workflows/invocation-ledger.ts +368 -0
- package/extensions/workflows/model.ts +57 -1
- package/extensions/workflows/operator.ts +131 -0
- package/extensions/workflows/prompt.ts +10 -38
- package/extensions/workflows/replay-safety.ts +9 -8
- package/extensions/workflows/runner.ts +10 -2
- package/extensions/workflows/sandbox.ts +5 -0
- package/package.json +15 -15
- package/skills/subagents/SKILL.md +6 -0
- package/skills/workflows/EXAMPLES.md +58 -0
- package/skills/workflows/REFERENCE.md +44 -0
- package/skills/workflows/SKILL.md +39 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { constants, lstatSync, readFileSync } from "node:fs";
|
|
4
|
+
import {
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
open,
|
|
8
|
+
readFile,
|
|
9
|
+
type FileHandle,
|
|
10
|
+
} from "node:fs/promises";
|
|
11
|
+
import { basename, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import {
|
|
14
|
+
DefaultPackageManager,
|
|
15
|
+
getAgentDir,
|
|
16
|
+
SettingsManager,
|
|
17
|
+
type ProgressEvent,
|
|
18
|
+
} from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
20
|
+
|
|
21
|
+
export const PI_INTERCOM_SOURCE = "npm:pi-intercom";
|
|
22
|
+
const PI_INTERCOM_CONFIG_LOCK = "config.json.openpi-install.lock";
|
|
23
|
+
const PI_INTERCOM_FS_HELPER = fileURLToPath(
|
|
24
|
+
new URL("./intercom-fs-helper.cjs", import.meta.url),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export interface PiIntercomStatus {
|
|
28
|
+
readonly configured: boolean;
|
|
29
|
+
readonly installed: boolean;
|
|
30
|
+
readonly active: boolean;
|
|
31
|
+
readonly version?: string;
|
|
32
|
+
readonly confirmSend?: boolean;
|
|
33
|
+
readonly inboundTrigger?: "always" | "replies" | "never";
|
|
34
|
+
readonly diagnostic?: string;
|
|
35
|
+
readonly reloadRequired?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface OptionalFile {
|
|
39
|
+
readonly exists: boolean;
|
|
40
|
+
readonly text?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface PreparedIntercomConfig {
|
|
44
|
+
readonly path: string;
|
|
45
|
+
readonly nextText: string;
|
|
46
|
+
readonly changed: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface IntercomDirectoryGuard {
|
|
50
|
+
readonly directory: string;
|
|
51
|
+
readonly handle?: FileHandle;
|
|
52
|
+
readonly dev: number | bigint;
|
|
53
|
+
readonly ino: number | bigint;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface PiIntercomInstallOptions {
|
|
57
|
+
readonly agentDir?: string;
|
|
58
|
+
/** Download/repair package files without making the package active. */
|
|
59
|
+
readonly install: (source: typeof PI_INTERCOM_SOURCE) => Promise<void>;
|
|
60
|
+
/** Persist the package source only after the safe config commit. */
|
|
61
|
+
readonly persist?: (source: typeof PI_INTERCOM_SOURCE) => Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
65
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
|
|
67
|
+
const isErrno = (error: unknown, code: string) =>
|
|
68
|
+
error instanceof Error && "code" in error && error.code === code;
|
|
69
|
+
|
|
70
|
+
const boundedError = (error: unknown) =>
|
|
71
|
+
sanitizeTerminalText(error instanceof Error ? error.message : String(error))
|
|
72
|
+
.replace(/\s+/gu, " ")
|
|
73
|
+
.trim()
|
|
74
|
+
.slice(0, 2_000);
|
|
75
|
+
|
|
76
|
+
export function isPiIntercomPackageSource(source: string) {
|
|
77
|
+
return /^npm:pi-intercom(?:@[^/\s]+)?$/.test(source.trim());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function intercomDirectory(agentDir: string) {
|
|
81
|
+
return join(agentDir, "intercom");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function intercomConfigPath(agentDir: string) {
|
|
85
|
+
return join(intercomDirectory(agentDir), "config.json");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function ensureIntercomDirectory(agentDir: string) {
|
|
89
|
+
const directory = intercomDirectory(agentDir);
|
|
90
|
+
try {
|
|
91
|
+
const metadata = await lstat(directory);
|
|
92
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Refusing non-directory or symlinked pi-intercom path at ${directory}.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!isErrno(error, "ENOENT")) throw error;
|
|
99
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
100
|
+
const metadata = await lstat(directory);
|
|
101
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Refusing non-directory or symlinked pi-intercom path at ${directory}.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return directory;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sameIdentity = (
|
|
111
|
+
left: { dev: number | bigint; ino: number | bigint },
|
|
112
|
+
right: { dev: number | bigint; ino: number | bigint },
|
|
113
|
+
) => left.dev === right.dev && left.ino === right.ino;
|
|
114
|
+
|
|
115
|
+
async function openIntercomDirectoryGuard(agentDir: string) {
|
|
116
|
+
const directory = await ensureIntercomDirectory(agentDir);
|
|
117
|
+
const current = await lstat(directory, { bigint: true });
|
|
118
|
+
if (process.platform === "win32") {
|
|
119
|
+
return {
|
|
120
|
+
directory,
|
|
121
|
+
dev: current.dev,
|
|
122
|
+
ino: current.ino,
|
|
123
|
+
} satisfies IntercomDirectoryGuard;
|
|
124
|
+
}
|
|
125
|
+
const flags =
|
|
126
|
+
constants.O_RDONLY |
|
|
127
|
+
(constants.O_DIRECTORY ?? 0) |
|
|
128
|
+
(constants.O_NOFOLLOW ?? 0);
|
|
129
|
+
const handle = await open(directory, flags);
|
|
130
|
+
try {
|
|
131
|
+
const identity = await handle.stat({ bigint: true });
|
|
132
|
+
const latest = await lstat(directory, { bigint: true });
|
|
133
|
+
if (
|
|
134
|
+
!identity.isDirectory() ||
|
|
135
|
+
!latest.isDirectory() ||
|
|
136
|
+
latest.isSymbolicLink() ||
|
|
137
|
+
!sameIdentity(identity, latest)
|
|
138
|
+
) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`pi-intercom directory identity changed while opening ${directory}.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
directory,
|
|
145
|
+
handle,
|
|
146
|
+
dev: identity.dev,
|
|
147
|
+
ino: identity.ino,
|
|
148
|
+
} satisfies IntercomDirectoryGuard;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
await handle.close().catch(() => undefined);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function assertIntercomDirectoryIdentity(guard: IntercomDirectoryGuard) {
|
|
156
|
+
const current = await lstat(guard.directory, { bigint: true });
|
|
157
|
+
const held = guard.handle
|
|
158
|
+
? await guard.handle.stat({ bigint: true })
|
|
159
|
+
: { dev: guard.dev, ino: guard.ino, isDirectory: () => true };
|
|
160
|
+
if (
|
|
161
|
+
!held.isDirectory() ||
|
|
162
|
+
!current.isDirectory() ||
|
|
163
|
+
current.isSymbolicLink() ||
|
|
164
|
+
!sameIdentity(held, current) ||
|
|
165
|
+
held.dev !== guard.dev ||
|
|
166
|
+
held.ino !== guard.ino
|
|
167
|
+
) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`pi-intercom directory identity changed during installation at ${guard.directory}.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function readOptionalFile(path: string): Promise<OptionalFile> {
|
|
175
|
+
try {
|
|
176
|
+
const [text, metadata] = await Promise.all([
|
|
177
|
+
readFile(path, "utf8"),
|
|
178
|
+
lstat(path),
|
|
179
|
+
]);
|
|
180
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Refusing non-regular pi-intercom config path at ${path}.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return { exists: true, text };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (isErrno(error, "ENOENT")) return { exists: false };
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseIntercomConfig(file: OptionalFile) {
|
|
193
|
+
if (!file.exists) return {};
|
|
194
|
+
|
|
195
|
+
let value: unknown;
|
|
196
|
+
try {
|
|
197
|
+
value = JSON.parse(file.text!);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Refusing to overwrite invalid pi-intercom config (${boundedError(error)}).`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
if (!isRecord(value)) {
|
|
204
|
+
throw new Error("Refusing to overwrite non-object pi-intercom config.");
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
Object.hasOwn(value, "confirmSend") &&
|
|
208
|
+
typeof value.confirmSend !== "boolean"
|
|
209
|
+
) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
'Refusing to overwrite pi-intercom config: "confirmSend" must be boolean.',
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (
|
|
215
|
+
Object.hasOwn(value, "inboundTrigger") &&
|
|
216
|
+
value.inboundTrigger !== "always" &&
|
|
217
|
+
value.inboundTrigger !== "replies" &&
|
|
218
|
+
value.inboundTrigger !== "never"
|
|
219
|
+
) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
'Refusing to overwrite pi-intercom config: "inboundTrigger" must be "always", "replies", or "never".',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function preparePiIntercomSafeDefaults(
|
|
228
|
+
agentDir = getAgentDir(),
|
|
229
|
+
guard?: IntercomDirectoryGuard,
|
|
230
|
+
): Promise<PreparedIntercomConfig> {
|
|
231
|
+
if (guard) await assertIntercomDirectoryIdentity(guard);
|
|
232
|
+
else await ensureIntercomDirectory(agentDir);
|
|
233
|
+
const path = intercomConfigPath(agentDir);
|
|
234
|
+
const original = await readOptionalFile(path);
|
|
235
|
+
if (guard) await assertIntercomDirectoryIdentity(guard);
|
|
236
|
+
const current = parseIntercomConfig(original);
|
|
237
|
+
if (original.exists) {
|
|
238
|
+
const missing = [
|
|
239
|
+
...(!Object.hasOwn(current, "confirmSend") ? ["confirmSend"] : []),
|
|
240
|
+
...(!Object.hasOwn(current, "inboundTrigger") ? ["inboundTrigger"] : []),
|
|
241
|
+
];
|
|
242
|
+
if (missing.length > 0) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Existing pi-intercom config is missing ${missing.join(" and ")}; OpenPI will not rewrite an existing preference file. Add confirmSend=true and inboundTrigger=\"replies\", then retry.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return { path, nextText: original.text!, changed: false };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
path,
|
|
252
|
+
nextText: `${JSON.stringify(
|
|
253
|
+
{ confirmSend: true, inboundTrigger: "replies" },
|
|
254
|
+
null,
|
|
255
|
+
2,
|
|
256
|
+
)}\n`,
|
|
257
|
+
changed: true,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function helperRuntime() {
|
|
262
|
+
const executable = basename(process.execPath).toLowerCase();
|
|
263
|
+
return executable === "node" ||
|
|
264
|
+
executable === "node.exe" ||
|
|
265
|
+
executable === "bun" ||
|
|
266
|
+
executable === "bun.exe"
|
|
267
|
+
? process.execPath
|
|
268
|
+
: "node";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function runIntercomDirectoryHelper(options: {
|
|
272
|
+
readonly guard: IntercomDirectoryGuard;
|
|
273
|
+
readonly operation: "create" | "remove-owned";
|
|
274
|
+
readonly name: "config.json" | typeof PI_INTERCOM_CONFIG_LOCK;
|
|
275
|
+
readonly payload: string;
|
|
276
|
+
}) {
|
|
277
|
+
const encoded = Buffer.from(options.payload, "utf8").toString("base64");
|
|
278
|
+
return new Promise<void>((resolve, reject) => {
|
|
279
|
+
execFile(
|
|
280
|
+
helperRuntime(),
|
|
281
|
+
[
|
|
282
|
+
PI_INTERCOM_FS_HELPER,
|
|
283
|
+
options.operation,
|
|
284
|
+
String(options.guard.dev),
|
|
285
|
+
String(options.guard.ino),
|
|
286
|
+
options.name,
|
|
287
|
+
encoded,
|
|
288
|
+
],
|
|
289
|
+
{
|
|
290
|
+
cwd: options.guard.directory,
|
|
291
|
+
encoding: "utf8",
|
|
292
|
+
env: { PATH: process.env.PATH ?? "" },
|
|
293
|
+
maxBuffer: 16 * 1_024,
|
|
294
|
+
timeout: 5_000,
|
|
295
|
+
windowsHide: true,
|
|
296
|
+
},
|
|
297
|
+
(error, _stdout, stderr) => {
|
|
298
|
+
if (!error) {
|
|
299
|
+
resolve();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const marker = stderr.match(/OPENPI:([A-Z]+):([^\r\n]*)/u);
|
|
303
|
+
const failure = new Error(
|
|
304
|
+
marker
|
|
305
|
+
? `pi-intercom filesystem helper refused ${options.operation}: ${boundedError(marker[2])}`
|
|
306
|
+
: `pi-intercom filesystem helper failed: ${boundedError(error)}`,
|
|
307
|
+
{ cause: error },
|
|
308
|
+
);
|
|
309
|
+
if (marker?.[1] === "EEXIST") {
|
|
310
|
+
Object.assign(failure, { code: "EEXIST" });
|
|
311
|
+
}
|
|
312
|
+
reject(failure);
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function commitPreparedConfig(
|
|
319
|
+
prepared: PreparedIntercomConfig,
|
|
320
|
+
guard: IntercomDirectoryGuard,
|
|
321
|
+
) {
|
|
322
|
+
if (!prepared.changed) return false;
|
|
323
|
+
try {
|
|
324
|
+
if (prepared.path !== join(guard.directory, "config.json")) {
|
|
325
|
+
throw new Error("Refusing unexpected pi-intercom config path.");
|
|
326
|
+
}
|
|
327
|
+
await assertIntercomDirectoryIdentity(guard);
|
|
328
|
+
await runIntercomDirectoryHelper({
|
|
329
|
+
guard,
|
|
330
|
+
operation: "create",
|
|
331
|
+
name: "config.json",
|
|
332
|
+
payload: prepared.nextText,
|
|
333
|
+
});
|
|
334
|
+
await assertIntercomDirectoryIdentity(guard);
|
|
335
|
+
return true;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (isErrno(error, "EEXIST")) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
"pi-intercom config appeared while OpenPI was preparing the installation; retry instead of overwriting it.",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function withPiIntercomConfigLock<A>(
|
|
347
|
+
agentDir: string,
|
|
348
|
+
action: (guard: IntercomDirectoryGuard) => Promise<A>,
|
|
349
|
+
) {
|
|
350
|
+
const guard = await openIntercomDirectoryGuard(agentDir);
|
|
351
|
+
const lockPath = join(guard.directory, PI_INTERCOM_CONFIG_LOCK);
|
|
352
|
+
const token = `${process.pid}:${randomUUID()}\n`;
|
|
353
|
+
try {
|
|
354
|
+
await assertIntercomDirectoryIdentity(guard);
|
|
355
|
+
await runIntercomDirectoryHelper({
|
|
356
|
+
guard,
|
|
357
|
+
operation: "create",
|
|
358
|
+
name: PI_INTERCOM_CONFIG_LOCK,
|
|
359
|
+
payload: token,
|
|
360
|
+
});
|
|
361
|
+
await assertIntercomDirectoryIdentity(guard);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
await guard.handle?.close().catch(() => undefined);
|
|
364
|
+
if (isErrno(error, "EEXIST")) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Another OpenPI pi-intercom installation is active, or a prior process left ${lockPath}. Retry after the active setup finishes; remove a stale lock only after confirming no setup is running.`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
try {
|
|
374
|
+
return await action(guard);
|
|
375
|
+
} finally {
|
|
376
|
+
await assertIntercomDirectoryIdentity(guard);
|
|
377
|
+
try {
|
|
378
|
+
await runIntercomDirectoryHelper({
|
|
379
|
+
guard,
|
|
380
|
+
operation: "remove-owned",
|
|
381
|
+
name: PI_INTERCOM_CONFIG_LOCK,
|
|
382
|
+
payload: token,
|
|
383
|
+
});
|
|
384
|
+
} catch (error) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
`Refusing uncertain pi-intercom install-lock cleanup at ${lockPath}: ${boundedError(error)}`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} finally {
|
|
391
|
+
await guard.handle?.close().catch(() => undefined);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function installPiIntercomSafely(
|
|
396
|
+
options: PiIntercomInstallOptions,
|
|
397
|
+
) {
|
|
398
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
399
|
+
return withPiIntercomConfigLock(agentDir, async (guard) => {
|
|
400
|
+
// Validate before spending network/disk work, but re-read after the package
|
|
401
|
+
// download so a concurrent manual edit is never replaced from a stale
|
|
402
|
+
// pre-download snapshot.
|
|
403
|
+
await preparePiIntercomSafeDefaults(agentDir, guard);
|
|
404
|
+
try {
|
|
405
|
+
await options.install(PI_INTERCOM_SOURCE);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
throw new Error(boundedError(error), { cause: error });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const prepared = await preparePiIntercomSafeDefaults(agentDir, guard);
|
|
411
|
+
const committed = await commitPreparedConfig(prepared, guard);
|
|
412
|
+
try {
|
|
413
|
+
await options.persist?.(PI_INTERCOM_SOURCE);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
const retained = committed
|
|
416
|
+
? " Safe defaults were retained because package activation may have reached disk."
|
|
417
|
+
: " Existing pi-intercom preferences were preserved.";
|
|
418
|
+
throw new Error(`${boundedError(error)}${retained}`, { cause: error });
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function inspectInstalledPackage(installedPath: string | undefined) {
|
|
424
|
+
if (!installedPath) return { installed: false as const };
|
|
425
|
+
try {
|
|
426
|
+
const directoryMetadata = lstatSync(installedPath);
|
|
427
|
+
if (
|
|
428
|
+
!directoryMetadata.isDirectory() ||
|
|
429
|
+
directoryMetadata.isSymbolicLink()
|
|
430
|
+
) {
|
|
431
|
+
throw new Error("installed package directory is not a regular directory");
|
|
432
|
+
}
|
|
433
|
+
const manifestPath = join(installedPath, "package.json");
|
|
434
|
+
const manifestMetadata = lstatSync(manifestPath);
|
|
435
|
+
if (!manifestMetadata.isFile() || manifestMetadata.isSymbolicLink()) {
|
|
436
|
+
throw new Error("installed package manifest is not a regular file");
|
|
437
|
+
}
|
|
438
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as unknown;
|
|
439
|
+
if (!isRecord(manifest) || manifest.name !== "pi-intercom") {
|
|
440
|
+
return {
|
|
441
|
+
installed: false as const,
|
|
442
|
+
diagnostic: `Installed package identity mismatch at ${installedPath}.`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
installed: true as const,
|
|
447
|
+
...(typeof manifest.version === "string"
|
|
448
|
+
? { version: manifest.version }
|
|
449
|
+
: {}),
|
|
450
|
+
};
|
|
451
|
+
} catch (error) {
|
|
452
|
+
return {
|
|
453
|
+
installed: false as const,
|
|
454
|
+
diagnostic: `Cannot verify installed pi-intercom package at ${installedPath}: ${boundedError(error)}`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function readEffectiveSafety(agentDir: string):
|
|
460
|
+
| Pick<PiIntercomStatus, "confirmSend" | "inboundTrigger">
|
|
461
|
+
| {
|
|
462
|
+
diagnostic: string;
|
|
463
|
+
} {
|
|
464
|
+
try {
|
|
465
|
+
const directoryMetadata = lstatSync(intercomDirectory(agentDir));
|
|
466
|
+
if (
|
|
467
|
+
!directoryMetadata.isDirectory() ||
|
|
468
|
+
directoryMetadata.isSymbolicLink()
|
|
469
|
+
) {
|
|
470
|
+
throw new Error("Refusing symlinked pi-intercom config directory.");
|
|
471
|
+
}
|
|
472
|
+
const path = intercomConfigPath(agentDir);
|
|
473
|
+
const metadata = lstatSync(path);
|
|
474
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
475
|
+
throw new Error("Refusing non-regular pi-intercom config file.");
|
|
476
|
+
}
|
|
477
|
+
const value = parseIntercomConfig({
|
|
478
|
+
exists: true,
|
|
479
|
+
text: readFileSync(path, "utf8"),
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
confirmSend:
|
|
483
|
+
typeof value.confirmSend === "boolean" ? value.confirmSend : false,
|
|
484
|
+
inboundTrigger:
|
|
485
|
+
value.inboundTrigger === "always" ||
|
|
486
|
+
value.inboundTrigger === "replies" ||
|
|
487
|
+
value.inboundTrigger === "never"
|
|
488
|
+
? value.inboundTrigger
|
|
489
|
+
: ("always" as const),
|
|
490
|
+
};
|
|
491
|
+
} catch (error) {
|
|
492
|
+
return isErrno(error, "ENOENT")
|
|
493
|
+
? { confirmSend: false, inboundTrigger: "always" as const }
|
|
494
|
+
: { diagnostic: boundedError(error) };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function settingsErrorsMessage(
|
|
499
|
+
errors: ReturnType<SettingsManager["drainErrors"]>,
|
|
500
|
+
) {
|
|
501
|
+
return errors
|
|
502
|
+
.map(({ scope, error }) => `${scope}: ${boundedError(error)}`)
|
|
503
|
+
.join("; ")
|
|
504
|
+
.slice(0, 2_000);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function inspectPiIntercom(options: {
|
|
508
|
+
readonly cwd: string;
|
|
509
|
+
readonly active: boolean;
|
|
510
|
+
readonly agentDir?: string;
|
|
511
|
+
}): PiIntercomStatus {
|
|
512
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
513
|
+
const settingsManager = SettingsManager.create(options.cwd, agentDir, {
|
|
514
|
+
projectTrusted: false,
|
|
515
|
+
});
|
|
516
|
+
const settingsErrors = settingsManager.drainErrors();
|
|
517
|
+
if (settingsErrors.length > 0) {
|
|
518
|
+
return {
|
|
519
|
+
configured: false,
|
|
520
|
+
installed: false,
|
|
521
|
+
active: options.active,
|
|
522
|
+
diagnostic: settingsErrorsMessage(settingsErrors),
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const packageManager = new DefaultPackageManager({
|
|
527
|
+
cwd: options.cwd,
|
|
528
|
+
agentDir,
|
|
529
|
+
settingsManager,
|
|
530
|
+
});
|
|
531
|
+
const configuredPackage = packageManager
|
|
532
|
+
.listConfiguredPackages()
|
|
533
|
+
.find(({ source }) => isPiIntercomPackageSource(source));
|
|
534
|
+
const safety = readEffectiveSafety(agentDir);
|
|
535
|
+
const installed = inspectInstalledPackage(configuredPackage?.installedPath);
|
|
536
|
+
return {
|
|
537
|
+
configured: Boolean(configuredPackage),
|
|
538
|
+
active: options.active,
|
|
539
|
+
...installed,
|
|
540
|
+
...safety,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function installPiIntercom(options: {
|
|
545
|
+
readonly cwd: string;
|
|
546
|
+
readonly agentDir?: string;
|
|
547
|
+
readonly onProgress?: (event: ProgressEvent) => void;
|
|
548
|
+
}) {
|
|
549
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
550
|
+
const settingsManager = SettingsManager.create(options.cwd, agentDir, {
|
|
551
|
+
projectTrusted: false,
|
|
552
|
+
});
|
|
553
|
+
const initialErrors = settingsManager.drainErrors();
|
|
554
|
+
if (initialErrors.length > 0) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
`Cannot install pi-intercom while Pi settings are invalid: ${settingsErrorsMessage(initialErrors)}`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const packageManager = new DefaultPackageManager({
|
|
561
|
+
cwd: options.cwd,
|
|
562
|
+
agentDir,
|
|
563
|
+
settingsManager,
|
|
564
|
+
});
|
|
565
|
+
packageManager.setProgressCallback(options.onProgress);
|
|
566
|
+
await installPiIntercomSafely({
|
|
567
|
+
agentDir,
|
|
568
|
+
install: (source) => packageManager.install(source),
|
|
569
|
+
persist: async (source) => {
|
|
570
|
+
packageManager.addSourceToSettings(source);
|
|
571
|
+
await settingsManager.flush();
|
|
572
|
+
const errors = settingsManager.drainErrors();
|
|
573
|
+
if (errors.length > 0) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`Pi could not persist the package setting: ${settingsErrorsMessage(errors)}`,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function formatPiIntercomStatus(status: PiIntercomStatus) {
|
|
583
|
+
if (status.diagnostic) {
|
|
584
|
+
return `Intercom: unavailable (${boundedError(status.diagnostic)})`;
|
|
585
|
+
}
|
|
586
|
+
if (!status.configured && !status.active) {
|
|
587
|
+
return "Intercom: not installed · optional setup component";
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const state = status.active
|
|
591
|
+
? "active"
|
|
592
|
+
: status.installed
|
|
593
|
+
? status.reloadRequired
|
|
594
|
+
? "installed · /reload required"
|
|
595
|
+
: "installed · inactive or filtered"
|
|
596
|
+
: "configured · package files missing";
|
|
597
|
+
const version = status.version ? ` ${status.version}` : "";
|
|
598
|
+
const safety =
|
|
599
|
+
status.confirmSend === undefined || status.inboundTrigger === undefined
|
|
600
|
+
? ""
|
|
601
|
+
: ` · confirmSend ${status.confirmSend ? "on" : "off"} · inboundTrigger ${status.inboundTrigger}`;
|
|
602
|
+
return `Intercom:${version} · ${state} · parent-only${safety}`;
|
|
603
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import {
|
|
3
|
+
type AgentSession,
|
|
3
4
|
DefaultResourceLoader,
|
|
4
5
|
getAgentDir,
|
|
5
6
|
ProjectTrustStore,
|
|
6
|
-
SettingsManager,
|
|
7
|
-
type AgentSession,
|
|
8
7
|
type SessionShutdownEvent,
|
|
8
|
+
SettingsManager,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
|
|
11
11
|
export const CHILD_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
@@ -51,6 +51,8 @@ function isPiIntercomNpmResource(resource: {
|
|
|
51
51
|
* drift test in child-session.test.ts).
|
|
52
52
|
*/
|
|
53
53
|
export const CHILD_EXCLUDED_TOOL_NAMES = [
|
|
54
|
+
// capability discovery mutates the parent model-facing tool surface
|
|
55
|
+
"openpi_load_tools",
|
|
54
56
|
// subagents — children cannot spawn/observe more agents
|
|
55
57
|
"subagent_spawn",
|
|
56
58
|
"subagent_wait",
|
|
@@ -185,7 +187,9 @@ export function resolveStandaloneChildProjectTrust(options: {
|
|
|
185
187
|
|
|
186
188
|
interface ChildSessionStartup {
|
|
187
189
|
bindExtensions(bindings: { mode: "print" }): Promise<void>;
|
|
188
|
-
getActiveToolNames(): string[];
|
|
190
|
+
getActiveToolNames?(): string[];
|
|
191
|
+
getAllTools?(): { name: string }[];
|
|
192
|
+
setActiveToolsByName?(toolNames: string[]): void;
|
|
189
193
|
}
|
|
190
194
|
|
|
191
195
|
function boundedToolNames(names: readonly string[]) {
|
|
@@ -208,9 +212,41 @@ export async function bindChildSessionExtensions(
|
|
|
208
212
|
) {
|
|
209
213
|
await session.bindExtensions({ mode: "print" });
|
|
210
214
|
const requested = effectiveChildToolAllowlist(requestedTools);
|
|
215
|
+
let active: Set<string> | undefined;
|
|
216
|
+
if (
|
|
217
|
+
session.getActiveToolNames &&
|
|
218
|
+
session.getAllTools &&
|
|
219
|
+
session.setActiveToolsByName
|
|
220
|
+
) {
|
|
221
|
+
const requestedSet = requested ? new Set(requested) : undefined;
|
|
222
|
+
const available = new Set(session.getAllTools().map(({ name }) => name));
|
|
223
|
+
const activeNames = session.getActiveToolNames();
|
|
224
|
+
active = new Set(activeNames);
|
|
225
|
+
for (const name of CHILD_SAFE_PACKAGE_TOOL_NAMES) {
|
|
226
|
+
if (
|
|
227
|
+
available.has(name) &&
|
|
228
|
+
!active.has(name) &&
|
|
229
|
+
(requestedSet === undefined || requestedSet.has(name))
|
|
230
|
+
) {
|
|
231
|
+
activeNames.push(name);
|
|
232
|
+
active.add(name);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (activeNames.length !== session.getActiveToolNames().length) {
|
|
236
|
+
session.setActiveToolsByName(activeNames);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
211
239
|
if (!requested) return;
|
|
212
240
|
|
|
213
|
-
|
|
241
|
+
if (!active && session.getActiveToolNames) {
|
|
242
|
+
active = new Set(session.getActiveToolNames());
|
|
243
|
+
}
|
|
244
|
+
if (!active) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
"Child tool preflight failed: the bound child session does not expose active-tool introspection.",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
214
250
|
const missing = [...new Set(requested)].filter((name) => !active.has(name));
|
|
215
251
|
if (missing.length === 0) return;
|
|
216
252
|
|
|
@@ -267,11 +303,12 @@ async function waitUntil(
|
|
|
267
303
|
const remaining = Math.max(0, deadline - Date.now());
|
|
268
304
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
269
305
|
const timeout = new Promise<{ error: string; timedOut: true }>((resolve) => {
|
|
306
|
+
// This timer owns the awaited deadline contract. Keep it referenced so a
|
|
307
|
+
// short-lived Node 22 process cannot exit with the promise still pending.
|
|
270
308
|
timer = setTimeout(
|
|
271
309
|
() => resolve({ error: `${label} timed out`, timedOut: true }),
|
|
272
310
|
remaining,
|
|
273
311
|
);
|
|
274
|
-
timer.unref?.();
|
|
275
312
|
});
|
|
276
313
|
const completed = operation.then(
|
|
277
314
|
() => ({ timedOut: false as const }),
|