@tt-a1i/openpi 0.4.0 → 0.6.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 +116 -46
- package/SETUP.md +29 -7
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +155 -0
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +59 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +106 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1064 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
- package/extensions/ai-providers/cursor/provider.ts +1175 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +38 -3
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +484 -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 +47 -24
- package/extensions/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +12 -2
- 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/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +65 -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 +82 -6
- 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 +138 -130
- 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 +457 -21
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +83 -34
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/structured-output.ts +154 -0
- 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 +313 -62
- package/extensions/subagents/navigation.ts +34 -5
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +450 -70
- package/extensions/subagents/src/domain.ts +21 -1
- package/extensions/subagents/src/manager.ts +39 -2
- package/extensions/subagents/src/prompt.ts +49 -7
- package/extensions/subagents/src/result-artifact.ts +36 -0
- package/extensions/subagents/src/result-delivery.ts +39 -14
- 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 +16 -45
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +257 -0
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +459 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +175 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +501 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +79 -5
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +70 -16
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +214 -76
- package/extensions/workflows/retention.ts +599 -0
- package/extensions/workflows/runner.ts +389 -345
- 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 +34 -14
- package/skills/subagents/REFERENCE.md +190 -0
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +6 -4
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +664 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +1069 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +467 -0
- package/web/runtime/pi-runtime.ts +1206 -0
- package/web/runtime/types.ts +102 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1700 -0
- package/web/ui/index.html +142 -0
- package/web/ui/styles.css +680 -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,13 +1,11 @@
|
|
|
1
|
-
import { isAbsolute, relative } from "node:path";
|
|
2
|
-
import { stripVTControlCharacters } from "node:util";
|
|
3
1
|
import type {
|
|
4
2
|
AgentToolResult,
|
|
5
3
|
Theme,
|
|
6
4
|
ToolDefinition,
|
|
7
5
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
6
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
9
7
|
import type { TSchema } from "typebox";
|
|
10
|
-
import {
|
|
8
|
+
import { renderPaddedToolActivityLine } from "../shared/tool-activity.ts";
|
|
11
9
|
|
|
12
10
|
type ActivityStatus = "pending" | "success" | "error";
|
|
13
11
|
|
|
@@ -23,33 +21,11 @@ type ActivityRenderState<TDetails> = {
|
|
|
23
21
|
};
|
|
24
22
|
};
|
|
25
23
|
|
|
26
|
-
type ActivityRow = {
|
|
27
|
-
verb: string;
|
|
28
|
-
target: string;
|
|
29
|
-
detail?: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const HORIZONTAL_PADDING = " ";
|
|
33
|
-
|
|
34
24
|
const emptyComponent: Component = {
|
|
35
25
|
render: () => [],
|
|
36
26
|
invalidate() {},
|
|
37
27
|
};
|
|
38
28
|
|
|
39
|
-
function record(value: unknown): Record<string, unknown> {
|
|
40
|
-
return value !== null && typeof value === "object"
|
|
41
|
-
? (value as Record<string, unknown>)
|
|
42
|
-
: {};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function string(value: unknown) {
|
|
46
|
-
return typeof value === "string" ? value : "";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function number(value: unknown) {
|
|
50
|
-
return typeof value === "number" ? value : undefined;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
29
|
function textOutput(result: AgentToolResult<unknown> | undefined) {
|
|
54
30
|
return (
|
|
55
31
|
result?.content
|
|
@@ -59,228 +35,6 @@ function textOutput(result: AgentToolResult<unknown> | undefined) {
|
|
|
59
35
|
);
|
|
60
36
|
}
|
|
61
37
|
|
|
62
|
-
function resultCount(result: AgentToolResult<unknown> | undefined) {
|
|
63
|
-
return textOutput(result)
|
|
64
|
-
.split(/\r?\n/)
|
|
65
|
-
.filter((line) => line.trim().length > 0 && !line.trim().startsWith("["))
|
|
66
|
-
.length;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function grepMatchCount(result: AgentToolResult<unknown> | undefined) {
|
|
70
|
-
const output = textOutput(result).trim();
|
|
71
|
-
if (!output || output === "No matches found") return 0;
|
|
72
|
-
return output.split(/\r?\n/).filter((line) => /^.+:\d+:/.test(line)).length;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function itemCount(
|
|
76
|
-
result: AgentToolResult<unknown> | undefined,
|
|
77
|
-
emptyMessage: string,
|
|
78
|
-
) {
|
|
79
|
-
const output = textOutput(result).trim();
|
|
80
|
-
return !output || output === emptyMessage ? 0 : resultCount(result);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function plural(count: number, singular: string) {
|
|
84
|
-
const pluralForm =
|
|
85
|
-
singular === "match"
|
|
86
|
-
? "matches"
|
|
87
|
-
: singular === "entry"
|
|
88
|
-
? "entries"
|
|
89
|
-
: `${singular}s`;
|
|
90
|
-
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function editStats(details: unknown) {
|
|
94
|
-
const diff = string(record(details).diff);
|
|
95
|
-
if (!diff) return undefined;
|
|
96
|
-
let additions = 0;
|
|
97
|
-
let removals = 0;
|
|
98
|
-
for (const line of diff.split(/\r?\n/)) {
|
|
99
|
-
if (line.startsWith("+") && !line.startsWith("+++")) additions += 1;
|
|
100
|
-
if (line.startsWith("-") && !line.startsWith("---")) removals += 1;
|
|
101
|
-
}
|
|
102
|
-
return { additions, removals };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function range(args: Record<string, unknown>) {
|
|
106
|
-
const offset = number(args.offset);
|
|
107
|
-
const limit = number(args.limit);
|
|
108
|
-
if (offset === undefined && limit === undefined) return "";
|
|
109
|
-
const start = offset ?? 1;
|
|
110
|
-
return limit === undefined ? `:${start}-` : `:${start}-${start + limit - 1}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function displayPath(path: string, cwd: string) {
|
|
114
|
-
if (!isAbsolute(path)) return path;
|
|
115
|
-
const local = relative(cwd, path);
|
|
116
|
-
if (local === "") return ".";
|
|
117
|
-
return local.startsWith("..") || isAbsolute(local) ? path : local;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function activityRow(
|
|
121
|
-
name: string,
|
|
122
|
-
argsValue: unknown,
|
|
123
|
-
result: AgentToolResult<unknown> | undefined,
|
|
124
|
-
cwd: string,
|
|
125
|
-
): ActivityRow {
|
|
126
|
-
const args = record(argsValue);
|
|
127
|
-
const path = displayPath(string(args.path) || ".", cwd);
|
|
128
|
-
switch (name) {
|
|
129
|
-
case "read":
|
|
130
|
-
return { verb: "Read", target: `${path}${range(args)}` };
|
|
131
|
-
case "bash":
|
|
132
|
-
return {
|
|
133
|
-
verb: "Ran",
|
|
134
|
-
target: string(args.command).replace(/\s+/g, " ").trim(),
|
|
135
|
-
};
|
|
136
|
-
case "write": {
|
|
137
|
-
const content = string(args.content);
|
|
138
|
-
const lines =
|
|
139
|
-
content.length === 0
|
|
140
|
-
? 0
|
|
141
|
-
: content.replace(/\r?\n$/, "").split(/\r?\n/).length;
|
|
142
|
-
return { verb: "Wrote", target: path, detail: plural(lines, "line") };
|
|
143
|
-
}
|
|
144
|
-
case "edit":
|
|
145
|
-
return { verb: "Edited", target: path };
|
|
146
|
-
case "grep":
|
|
147
|
-
return {
|
|
148
|
-
verb: "Searched",
|
|
149
|
-
target: string(args.pattern),
|
|
150
|
-
detail: `in ${path} ${plural(grepMatchCount(result), "match")}`,
|
|
151
|
-
};
|
|
152
|
-
case "find":
|
|
153
|
-
return {
|
|
154
|
-
verb: "Searched",
|
|
155
|
-
target: string(args.pattern),
|
|
156
|
-
detail: `in ${path} ${plural(itemCount(result, "No files found matching pattern"), "result")}`,
|
|
157
|
-
};
|
|
158
|
-
case "ls":
|
|
159
|
-
return {
|
|
160
|
-
verb: "Listed",
|
|
161
|
-
target: path,
|
|
162
|
-
detail: plural(itemCount(result, "(empty directory)"), "entry"),
|
|
163
|
-
};
|
|
164
|
-
default:
|
|
165
|
-
return { verb: name, target: "" };
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function pendingVerb(name: string) {
|
|
170
|
-
switch (name) {
|
|
171
|
-
case "read":
|
|
172
|
-
return "Reading";
|
|
173
|
-
case "bash":
|
|
174
|
-
return "Running";
|
|
175
|
-
case "write":
|
|
176
|
-
return "Writing";
|
|
177
|
-
case "edit":
|
|
178
|
-
return "Editing";
|
|
179
|
-
case "grep":
|
|
180
|
-
case "find":
|
|
181
|
-
return "Searching";
|
|
182
|
-
case "ls":
|
|
183
|
-
return "Listing";
|
|
184
|
-
default:
|
|
185
|
-
return "Running";
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function activityIcon(name: string) {
|
|
190
|
-
switch (name) {
|
|
191
|
-
case "read":
|
|
192
|
-
return "\ueaa4"; // Nerd Fonts Codicon: book
|
|
193
|
-
case "bash":
|
|
194
|
-
return "\uea85"; // Nerd Fonts Codicon: terminal
|
|
195
|
-
case "write":
|
|
196
|
-
case "edit":
|
|
197
|
-
return "\uea73"; // Nerd Fonts Codicon: edit
|
|
198
|
-
case "grep":
|
|
199
|
-
case "find":
|
|
200
|
-
return "\uea6d"; // Nerd Fonts Codicon: search
|
|
201
|
-
case "ls":
|
|
202
|
-
return "\uea83"; // Nerd Fonts Codicon: folder
|
|
203
|
-
default:
|
|
204
|
-
return "✓";
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function errorSummary(result: AgentToolResult<unknown> | undefined) {
|
|
209
|
-
const lines = textOutput(result)
|
|
210
|
-
.split(/\r?\n/)
|
|
211
|
-
.map((line) => stripVTControlCharacters(line).trim())
|
|
212
|
-
.filter(Boolean);
|
|
213
|
-
return (
|
|
214
|
-
[...lines]
|
|
215
|
-
.reverse()
|
|
216
|
-
.find((line) =>
|
|
217
|
-
/(?:command (?:exited|timed out|aborted)|error|denied|failed)/i.test(
|
|
218
|
-
line,
|
|
219
|
-
),
|
|
220
|
-
) ?? lines[0]
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function duration(
|
|
225
|
-
state: NonNullable<ActivityRenderState<unknown>["openpiActivity"]>,
|
|
226
|
-
) {
|
|
227
|
-
if (state.startedAt === undefined) return undefined;
|
|
228
|
-
const seconds = Math.floor(
|
|
229
|
-
((state.endedAt ?? Date.now()) - state.startedAt) / 1000,
|
|
230
|
-
);
|
|
231
|
-
return seconds > 0 ? `${seconds}s` : undefined;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function activityText(
|
|
235
|
-
name: string,
|
|
236
|
-
args: unknown,
|
|
237
|
-
state: NonNullable<ActivityRenderState<unknown>["openpiActivity"]>,
|
|
238
|
-
theme: Theme,
|
|
239
|
-
cwd: string,
|
|
240
|
-
) {
|
|
241
|
-
const row = activityRow(name, args, state.result, cwd);
|
|
242
|
-
const elapsed = duration(state);
|
|
243
|
-
const verbText = (
|
|
244
|
-
state.status === "pending"
|
|
245
|
-
? pendingVerb(name)
|
|
246
|
-
: state.status === "error"
|
|
247
|
-
? "Failed"
|
|
248
|
-
: row.verb
|
|
249
|
-
).padEnd(8);
|
|
250
|
-
const verb = theme.fg(
|
|
251
|
-
state.status === "error"
|
|
252
|
-
? "error"
|
|
253
|
-
: state.status === "success"
|
|
254
|
-
? "muted"
|
|
255
|
-
: "toolTitle",
|
|
256
|
-
verbText,
|
|
257
|
-
);
|
|
258
|
-
if (state.status === "pending") {
|
|
259
|
-
const detail = elapsed ? ` · ${elapsed}` : "";
|
|
260
|
-
return `${theme.fg("warning", spinnerFrame(Date.now()))} ${verb} ${row.target}${theme.fg("dim", detail)}`;
|
|
261
|
-
}
|
|
262
|
-
if (state.status === "error") {
|
|
263
|
-
const summary = errorSummary(state.result);
|
|
264
|
-
const detail = [elapsed, summary].filter(Boolean).join(" · ");
|
|
265
|
-
return `${theme.fg("error", "✕")} ${verb} ${row.target}${detail ? theme.fg("dim", ` · ${detail}`) : ""}`;
|
|
266
|
-
}
|
|
267
|
-
const parts: string[] = [];
|
|
268
|
-
if (name === "edit") {
|
|
269
|
-
// Kimi-style diff stats: additions green, removals red.
|
|
270
|
-
const stats = editStats(state.result?.details);
|
|
271
|
-
if (stats) {
|
|
272
|
-
parts.push(
|
|
273
|
-
`${theme.fg("success", `+${stats.additions}`)} ${theme.fg("error", `-${stats.removals}`)}`,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
} else if (row.detail) {
|
|
277
|
-
parts.push(theme.fg("dim", row.detail));
|
|
278
|
-
}
|
|
279
|
-
if (elapsed) parts.push(theme.fg("dim", elapsed));
|
|
280
|
-
const detail = parts.join(theme.fg("dim", " · "));
|
|
281
|
-
return `${theme.fg("dim", activityIcon(name))} ${verb} ${theme.fg("muted", row.target)}${detail ? ` ${detail}` : ""}`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
38
|
function activityComponent(
|
|
285
39
|
name: string,
|
|
286
40
|
args: unknown,
|
|
@@ -290,15 +44,21 @@ function activityComponent(
|
|
|
290
44
|
): Component {
|
|
291
45
|
return {
|
|
292
46
|
render(width) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
47
|
+
const line = renderPaddedToolActivityLine(
|
|
48
|
+
{
|
|
49
|
+
name,
|
|
50
|
+
args,
|
|
51
|
+
output: textOutput(state.result),
|
|
52
|
+
details: state.result?.details,
|
|
53
|
+
status: state.status,
|
|
54
|
+
cwd,
|
|
55
|
+
startedAt: state.startedAt,
|
|
56
|
+
endedAt: state.endedAt,
|
|
57
|
+
},
|
|
58
|
+
theme,
|
|
59
|
+
width,
|
|
60
|
+
);
|
|
61
|
+
return line ? [line] : [];
|
|
302
62
|
},
|
|
303
63
|
invalidate() {},
|
|
304
64
|
};
|
|
@@ -20,7 +20,15 @@ import { execFile } from "node:child_process";
|
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { dirname, join } from "node:path";
|
|
22
22
|
import { promisify } from "node:util";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
Crypto,
|
|
25
|
+
Data,
|
|
26
|
+
Effect,
|
|
27
|
+
Encoding,
|
|
28
|
+
FileSystem,
|
|
29
|
+
Layer,
|
|
30
|
+
Stream,
|
|
31
|
+
} from "effect";
|
|
24
32
|
import { FetchHttpClient, HttpClient } from "effect/unstable/http";
|
|
25
33
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
26
34
|
|
|
@@ -161,6 +169,12 @@ export class InstallError extends Data.TaggedError("InstallError")<{
|
|
|
161
169
|
readonly cause?: unknown;
|
|
162
170
|
}> {}
|
|
163
171
|
|
|
172
|
+
export class BinaryDownloadError extends Data.TaggedError(
|
|
173
|
+
"BinaryDownloadError",
|
|
174
|
+
)<{
|
|
175
|
+
readonly message: string;
|
|
176
|
+
}> {}
|
|
177
|
+
|
|
164
178
|
export interface BinaryEnv {
|
|
165
179
|
/** True when the executable runs and supports the flags this tool requires. */
|
|
166
180
|
readonly probe: (command: string, tool: ToolName) => Effect.Effect<boolean>;
|
|
@@ -237,9 +251,9 @@ export function readBoundedResponse<E, R>(
|
|
|
237
251
|
return Effect.gen(function* () {
|
|
238
252
|
const declaredLength = Number(response.headers["content-length"]);
|
|
239
253
|
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
240
|
-
return yield*
|
|
241
|
-
|
|
242
|
-
);
|
|
254
|
+
return yield* new BinaryDownloadError({
|
|
255
|
+
message: `download exceeds the ${maxBytes}-byte size limit`,
|
|
256
|
+
});
|
|
243
257
|
}
|
|
244
258
|
|
|
245
259
|
const result = yield* Stream.runFoldEffect(
|
|
@@ -249,7 +263,9 @@ export function readBoundedResponse<E, R>(
|
|
|
249
263
|
const totalBytes = accumulator.totalBytes + chunk.byteLength;
|
|
250
264
|
if (totalBytes > maxBytes) {
|
|
251
265
|
return Effect.fail(
|
|
252
|
-
new
|
|
266
|
+
new BinaryDownloadError({
|
|
267
|
+
message: `download exceeds the ${maxBytes}-byte size limit`,
|
|
268
|
+
}),
|
|
253
269
|
);
|
|
254
270
|
}
|
|
255
271
|
return Effect.sync(() => {
|
|
@@ -272,9 +288,9 @@ function downloadAsset(client: HttpClient.HttpClient, initialUrl: URL) {
|
|
|
272
288
|
|
|
273
289
|
for (let redirects = 0; redirects <= MAX_DOWNLOAD_REDIRECTS; redirects++) {
|
|
274
290
|
if (url.protocol !== "https:") {
|
|
275
|
-
return yield*
|
|
276
|
-
|
|
277
|
-
);
|
|
291
|
+
return yield* new BinaryDownloadError({
|
|
292
|
+
message: `refusing non-HTTPS download URL: ${url.href}`,
|
|
293
|
+
});
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
const result = yield* Effect.scoped(
|
|
@@ -283,31 +299,27 @@ function downloadAsset(client: HttpClient.HttpClient, initialUrl: URL) {
|
|
|
283
299
|
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
284
300
|
const location = response.headers.location;
|
|
285
301
|
if (!location) {
|
|
286
|
-
return yield*
|
|
287
|
-
|
|
288
|
-
);
|
|
302
|
+
return yield* new BinaryDownloadError({
|
|
303
|
+
message: `redirect from ${url.href} had no location header`,
|
|
304
|
+
});
|
|
289
305
|
}
|
|
290
306
|
if (redirects === MAX_DOWNLOAD_REDIRECTS) {
|
|
291
|
-
return yield*
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
),
|
|
295
|
-
);
|
|
307
|
+
return yield* new BinaryDownloadError({
|
|
308
|
+
message: `download exceeded ${MAX_DOWNLOAD_REDIRECTS} redirects`,
|
|
309
|
+
});
|
|
296
310
|
}
|
|
297
311
|
if (!URL.canParse(location, url)) {
|
|
298
|
-
return yield*
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
),
|
|
302
|
-
);
|
|
312
|
+
return yield* new BinaryDownloadError({
|
|
313
|
+
message: `download returned an invalid redirect URL: ${location}`,
|
|
314
|
+
});
|
|
303
315
|
}
|
|
304
316
|
return { _tag: "Redirect" as const, url: new URL(location, url) };
|
|
305
317
|
}
|
|
306
318
|
|
|
307
319
|
if (response.status < 200 || response.status >= 300) {
|
|
308
|
-
return yield*
|
|
309
|
-
|
|
310
|
-
);
|
|
320
|
+
return yield* new BinaryDownloadError({
|
|
321
|
+
message: `download failed with HTTP ${response.status}`,
|
|
322
|
+
});
|
|
311
323
|
}
|
|
312
324
|
return {
|
|
313
325
|
_tag: "Complete" as const,
|
|
@@ -320,7 +332,9 @@ function downloadAsset(client: HttpClient.HttpClient, initialUrl: URL) {
|
|
|
320
332
|
url = result.url;
|
|
321
333
|
}
|
|
322
334
|
|
|
323
|
-
return yield*
|
|
335
|
+
return yield* new BinaryDownloadError({
|
|
336
|
+
message: "download redirect handling failed",
|
|
337
|
+
});
|
|
324
338
|
});
|
|
325
339
|
}
|
|
326
340
|
|
|
@@ -344,9 +358,9 @@ export const liveBinaryEnv: BinaryEnv = {
|
|
|
344
358
|
install: (asset, destination) => {
|
|
345
359
|
const install = Effect.gen(function* () {
|
|
346
360
|
if (!URL.canParse(asset.url)) {
|
|
347
|
-
return yield*
|
|
348
|
-
|
|
349
|
-
);
|
|
361
|
+
return yield* new BinaryDownloadError({
|
|
362
|
+
message: `invalid download URL: ${asset.url}`,
|
|
363
|
+
});
|
|
350
364
|
}
|
|
351
365
|
|
|
352
366
|
const url = new URL(asset.url);
|
|
@@ -360,11 +374,9 @@ export const liveBinaryEnv: BinaryEnv = {
|
|
|
360
374
|
const digestBytes = yield* crypto.digest("SHA-256", bytes);
|
|
361
375
|
const digest = Encoding.encodeHex(digestBytes);
|
|
362
376
|
if (digest !== asset.sha256) {
|
|
363
|
-
return yield*
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
),
|
|
367
|
-
);
|
|
377
|
+
return yield* new BinaryDownloadError({
|
|
378
|
+
message: `SHA-256 mismatch for ${asset.fileName}: expected ${asset.sha256}, received ${digest}`,
|
|
379
|
+
});
|
|
368
380
|
}
|
|
369
381
|
|
|
370
382
|
const workDir = yield* fs.makeTempDirectoryScoped({
|
|
@@ -384,9 +396,9 @@ export const liveBinaryEnv: BinaryEnv = {
|
|
|
384
396
|
}),
|
|
385
397
|
).pipe(Effect.timeout(60_000));
|
|
386
398
|
if (tarExitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
387
|
-
return yield*
|
|
388
|
-
|
|
389
|
-
);
|
|
399
|
+
return yield* new BinaryDownloadError({
|
|
400
|
+
message: `tar failed with exit code ${tarExitCode}`,
|
|
401
|
+
});
|
|
390
402
|
}
|
|
391
403
|
|
|
392
404
|
const extracted = join(workDir, asset.archiveDir, asset.binaryName);
|
|
@@ -401,13 +413,17 @@ export const liveBinaryEnv: BinaryEnv = {
|
|
|
401
413
|
yield* fs.rename(stagedDestination, destination);
|
|
402
414
|
});
|
|
403
415
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
Effect.provideService(FetchHttpClient.RequestInit, {
|
|
416
|
+
const InstallLayer = Layer.mergeAll(
|
|
417
|
+
NodeServices.layer,
|
|
418
|
+
NodeHttpClient.layerFetch,
|
|
419
|
+
Layer.succeed(FetchHttpClient.RequestInit, {
|
|
409
420
|
redirect: "manual",
|
|
410
421
|
}),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
return install.pipe(
|
|
425
|
+
Effect.scoped,
|
|
426
|
+
Effect.provide(InstallLayer),
|
|
411
427
|
Effect.mapError(
|
|
412
428
|
(cause) =>
|
|
413
429
|
new InstallError({
|
|
@@ -13,15 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
17
16
|
import type {
|
|
18
17
|
AgentToolResult,
|
|
19
18
|
ExtensionAPI,
|
|
20
|
-
ExtensionContext,
|
|
21
19
|
} from "@earendil-works/pi-coding-agent";
|
|
22
20
|
import { Text } from "@earendil-works/pi-tui";
|
|
23
21
|
import { Cause, Effect, Exit } from "effect";
|
|
24
|
-
import {
|
|
22
|
+
import { Type } from "typebox";
|
|
25
23
|
import { formatCapturedOutput } from "../file-search/src/output.ts";
|
|
26
24
|
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
|
|
27
25
|
import {
|