@tt-a1i/openpi 0.4.0 → 0.5.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/README.md +102 -40
- package/SETUP.md +22 -6
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- package/extensions/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +153 -86
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +123 -127
- package/extensions/shared/activity-status.ts +30 -0
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +437 -21
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +73 -33
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +270 -59
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/manager.ts +34 -2
- package/extensions/subagents/src/prompt.ts +32 -4
- package/extensions/subagents/src/result-artifact.ts +4 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/ui-customization/footer.ts +0 -40
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +1 -1
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +167 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +488 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +74 -4
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +66 -6
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +128 -64
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +25 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +1 -1
- package/skills/workflows/REFERENCE.md +4 -2
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -303
|
@@ -1,603 +0,0 @@
|
|
|
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
|
-
}
|