@trim21/personal-pi-extensions 0.0.123 → 0.0.125
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 +11 -5
- package/src/bwrap/index.ts +19 -30
- package/src/gh-readonly.ts +146 -187
- package/src/opencode-edit.ts +3 -1
- package/src/opencode-read.ts +8 -16
- package/src/opencode-write.ts +2 -5
- package/src/todo-pendant.ts +6 -2
- package/src/workspace-guard.ts +10 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.125",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"check": "tsc --noEmit && prettier --check .",
|
|
22
|
+
"lint": "eslint .",
|
|
22
23
|
"format": "prettier --write .",
|
|
23
|
-
"build:cli": "esbuild bin/cli.ts --bundle --platform=node --format=esm --outfile=dist/cli.js --external:@earendil-works/pi-coding-agent --external:typebox --external:node:*",
|
|
24
24
|
"test": "vitest run",
|
|
25
25
|
"prepare": "husky"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@earendil-works/pi-ai": "*",
|
|
29
28
|
"@earendil-works/pi-agent-core": "*",
|
|
29
|
+
"@earendil-works/pi-ai": "*",
|
|
30
30
|
"@earendil-works/pi-coding-agent": "*",
|
|
31
31
|
"@earendil-works/pi-tui": "*",
|
|
32
32
|
"typebox": "*"
|
|
@@ -36,11 +36,13 @@
|
|
|
36
36
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
37
37
|
"@types/node": "^24.0.0",
|
|
38
38
|
"esbuild": "^0.28.1",
|
|
39
|
+
"eslint": "^10.8.0",
|
|
39
40
|
"husky": "^9.1.7",
|
|
40
41
|
"lint-staged": "^17.0.8",
|
|
41
42
|
"prettier": "^3.6.0",
|
|
42
43
|
"typebox": "1.3.1",
|
|
43
|
-
"typescript": "^
|
|
44
|
+
"typescript": "^6.0.3",
|
|
45
|
+
"typescript-eslint": "^8.65.0",
|
|
44
46
|
"vitest": "^4.1.10"
|
|
45
47
|
},
|
|
46
48
|
"pi": {
|
|
@@ -55,7 +57,11 @@
|
|
|
55
57
|
]
|
|
56
58
|
},
|
|
57
59
|
"lint-staged": {
|
|
58
|
-
"*.{ts,tsx,js,jsx
|
|
60
|
+
"*.{ts,tsx,js,jsx}": [
|
|
61
|
+
"eslint --fix",
|
|
62
|
+
"prettier --write"
|
|
63
|
+
],
|
|
64
|
+
"*.{json,md,yaml,yml}": [
|
|
59
65
|
"prettier --write"
|
|
60
66
|
]
|
|
61
67
|
},
|
package/src/bwrap/index.ts
CHANGED
|
@@ -62,7 +62,12 @@ import { fileURLToPath } from "node:url";
|
|
|
62
62
|
import { Type } from "typebox";
|
|
63
63
|
import { Value } from "typebox/value";
|
|
64
64
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
65
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
type BashOperations,
|
|
67
|
+
createBashTool,
|
|
68
|
+
getAgentDir,
|
|
69
|
+
Theme,
|
|
70
|
+
} from "@earendil-works/pi-coding-agent";
|
|
66
71
|
|
|
67
72
|
const SANDBOX_PROMPT = `
|
|
68
73
|
## Command Execution
|
|
@@ -166,10 +171,10 @@ function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
|
166
171
|
const base = {
|
|
167
172
|
mode: config.mode,
|
|
168
173
|
bwrapPath: config.bwrapPath,
|
|
169
|
-
writablePaths: config.writablePaths ??
|
|
174
|
+
writablePaths: config.writablePaths ?? [".", "/tmp"],
|
|
170
175
|
extraWritablePaths: config.extraWritablePaths,
|
|
171
|
-
tmpfsPaths: config.tmpfsPaths ??
|
|
172
|
-
extraArgs: config.extraArgs ??
|
|
176
|
+
tmpfsPaths: config.tmpfsPaths ?? [],
|
|
177
|
+
extraArgs: config.extraArgs ?? [],
|
|
173
178
|
};
|
|
174
179
|
switch (config.mode) {
|
|
175
180
|
case "allow-all":
|
|
@@ -229,7 +234,7 @@ function loadConfig(cwd: string): BwrapConfig {
|
|
|
229
234
|
try {
|
|
230
235
|
Object.assign(target, JSON.parse(readFileSync(path, "utf-8")));
|
|
231
236
|
} catch (e) {
|
|
232
|
-
console.error(`Warning: Could not parse ${path}: ${e}`);
|
|
237
|
+
console.error(`Warning: Could not parse ${path}: ${String(e)}`);
|
|
233
238
|
}
|
|
234
239
|
}
|
|
235
240
|
}
|
|
@@ -507,13 +512,6 @@ const sandboxedBashSchema = Type.Object({
|
|
|
507
512
|
),
|
|
508
513
|
});
|
|
509
514
|
|
|
510
|
-
interface SandboxedBashInput {
|
|
511
|
-
command: string;
|
|
512
|
-
timeout?: number;
|
|
513
|
-
request_full_access?: boolean;
|
|
514
|
-
request_full_access_reason?: string;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
515
|
export default function (pi: ExtensionAPI) {
|
|
518
516
|
pi.registerFlag("no-bwrap", {
|
|
519
517
|
description: "Disable bwrap sandboxing for bash commands",
|
|
@@ -525,7 +523,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
525
523
|
const localBash = createBashTool(localCwd);
|
|
526
524
|
|
|
527
525
|
let resolved: ResolvedBwrap | null = null;
|
|
528
|
-
let manuallyDisabled = false;
|
|
529
526
|
|
|
530
527
|
function getResolved(): ResolvedBwrap {
|
|
531
528
|
if (!resolved) {
|
|
@@ -534,10 +531,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
534
531
|
return resolved;
|
|
535
532
|
}
|
|
536
533
|
|
|
537
|
-
function isEnabled() {
|
|
538
|
-
return !manuallyDisabled && getResolved().bwrapEnabled;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
534
|
function setMode(mode: BwrapMode) {
|
|
542
535
|
const config = loadConfig(localCwd);
|
|
543
536
|
config.mode = mode;
|
|
@@ -607,24 +600,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
607
600
|
},
|
|
608
601
|
});
|
|
609
602
|
|
|
610
|
-
pi.on("session_start",
|
|
611
|
-
const noBwrap = pi.getFlag("no-bwrap")
|
|
603
|
+
pi.on("session_start", (_event, ctx) => {
|
|
604
|
+
const noBwrap = pi.getFlag("no-bwrap") === true;
|
|
612
605
|
|
|
613
606
|
if (noBwrap) {
|
|
614
|
-
manuallyDisabled = true;
|
|
615
607
|
resolved = null;
|
|
616
608
|
ctx.ui.notify("bwrap sandbox disabled via --no-bwrap", "warning");
|
|
617
609
|
return;
|
|
618
610
|
}
|
|
619
611
|
|
|
620
612
|
if (!process.env.HOME) {
|
|
621
|
-
manuallyDisabled = true;
|
|
622
613
|
ctx.ui.notify("bwrap requires HOME environment variable", "error");
|
|
623
614
|
return;
|
|
624
615
|
}
|
|
625
616
|
|
|
626
617
|
if (process.platform !== "linux") {
|
|
627
|
-
manuallyDisabled = true;
|
|
628
618
|
ctx.ui.notify("bwrap sandbox requires Linux", "warning");
|
|
629
619
|
return;
|
|
630
620
|
}
|
|
@@ -637,7 +627,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
637
627
|
findBwrap(resolved.bwrapPath);
|
|
638
628
|
} catch (err) {
|
|
639
629
|
resolved = null;
|
|
640
|
-
manuallyDisabled = true;
|
|
641
630
|
ctx.ui.notify(err instanceof Error ? err.message : "bwrap not found", "error");
|
|
642
631
|
return;
|
|
643
632
|
}
|
|
@@ -657,7 +646,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
657
646
|
|
|
658
647
|
pi.on("session_shutdown", () => {
|
|
659
648
|
resolved = null;
|
|
660
|
-
manuallyDisabled = false;
|
|
661
649
|
});
|
|
662
650
|
|
|
663
651
|
pi.on("before_agent_start", (event) => {
|
|
@@ -671,11 +659,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
671
659
|
|
|
672
660
|
pi.registerCommand("bwrap", {
|
|
673
661
|
description: "Show bwrap sandbox configuration",
|
|
674
|
-
handler:
|
|
662
|
+
handler: (_args, ctx) => {
|
|
675
663
|
const r = getResolved();
|
|
676
664
|
if (!r.bwrapEnabled) {
|
|
677
665
|
ctx.ui.notify(`bwrap disabled (mode: ${r.mode})`, "info");
|
|
678
|
-
return;
|
|
666
|
+
return Promise.resolve();
|
|
679
667
|
}
|
|
680
668
|
|
|
681
669
|
const net = r.network ? "net" : "no-net";
|
|
@@ -686,6 +674,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
686
674
|
`bwrap ${r.mode} ${net} write:[${w.join(", ")}] tmpfs:[${t.join(", ") || "-"}]`,
|
|
687
675
|
"info",
|
|
688
676
|
);
|
|
677
|
+
return Promise.resolve();
|
|
689
678
|
},
|
|
690
679
|
});
|
|
691
680
|
|
|
@@ -694,7 +683,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
694
683
|
ctx: {
|
|
695
684
|
ui: {
|
|
696
685
|
notify: (m: string, t?: "info" | "warning" | "error") => void;
|
|
697
|
-
theme:
|
|
686
|
+
theme: Theme;
|
|
698
687
|
setStatus: (k: string, t: string | undefined) => void;
|
|
699
688
|
};
|
|
700
689
|
},
|
|
@@ -718,16 +707,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
718
707
|
|
|
719
708
|
pi.registerCommand("bwrap-allow-all", {
|
|
720
709
|
description: "Disable bwrap sandbox, full access",
|
|
721
|
-
handler:
|
|
710
|
+
handler: (_args, ctx) => Promise.resolve(switchMode("allow-all", ctx)),
|
|
722
711
|
});
|
|
723
712
|
|
|
724
713
|
pi.registerCommand("bwrap-workspace-write", {
|
|
725
714
|
description: "Sandbox on, network off, workspace writable",
|
|
726
|
-
handler:
|
|
715
|
+
handler: (_args, ctx) => Promise.resolve(switchMode("workspace-write", ctx)),
|
|
727
716
|
});
|
|
728
717
|
|
|
729
718
|
pi.registerCommand("bwrap-readonly", {
|
|
730
719
|
description: "Sandbox on, network off, no writes",
|
|
731
|
-
handler:
|
|
720
|
+
handler: (_args, ctx) => Promise.resolve(switchMode("readonly", ctx)),
|
|
732
721
|
});
|
|
733
722
|
}
|
package/src/gh-readonly.ts
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
34
34
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
35
35
|
import { Type } from "typebox";
|
|
36
|
+
import { Value } from "typebox/value";
|
|
36
37
|
import { spawn } from "node:child_process";
|
|
37
38
|
import { homedir } from "node:os";
|
|
38
39
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -48,8 +49,7 @@ interface GhResult {
|
|
|
48
49
|
|
|
49
50
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
50
51
|
|
|
51
|
-
function
|
|
52
|
-
pi: ExtensionAPI,
|
|
52
|
+
function runGh(
|
|
53
53
|
args: string[],
|
|
54
54
|
ctx: { cwd?: string; signal?: AbortSignal; timeout?: number },
|
|
55
55
|
): Promise<GhResult> {
|
|
@@ -109,7 +109,7 @@ function execGh(
|
|
|
109
109
|
resolve({ stdout, stderr, code: code ?? 0, killed, combined: combined.join("") });
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
proc.on("error", (
|
|
112
|
+
proc.on("error", () => {
|
|
113
113
|
if (timeoutId) clearTimeout(timeoutId);
|
|
114
114
|
if (ctx.signal) {
|
|
115
115
|
ctx.signal.removeEventListener("abort", killProcess);
|
|
@@ -119,35 +119,67 @@ function execGh(
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Error thrown by `ghExec` when the `gh` invocation exits non-zero.
|
|
124
|
+
* The message carries the toolcall input (JSON) wrapped in `<input>` markers,
|
|
125
|
+
* and the command output wrapped in `<output>` markers.
|
|
126
|
+
*/
|
|
127
|
+
export class GhError extends Error {
|
|
128
|
+
readonly args: string[];
|
|
129
|
+
readonly code: number;
|
|
130
|
+
readonly stdout: string;
|
|
131
|
+
readonly stderr: string;
|
|
132
|
+
readonly input?: unknown;
|
|
133
|
+
|
|
134
|
+
constructor(args: string[], result: GhResult, input?: unknown) {
|
|
135
|
+
const inputText = input === undefined ? "" : `<input>${JSON.stringify(input)}<input>\n`;
|
|
136
|
+
super(`${inputText}<output>${result.combined.trim() || `exit code ${result.code}`}<output>`);
|
|
137
|
+
this.name = "GhError";
|
|
138
|
+
this.args = args;
|
|
139
|
+
this.code = result.code;
|
|
140
|
+
this.stdout = result.stdout;
|
|
141
|
+
this.stderr = result.stderr;
|
|
142
|
+
this.input = input;
|
|
143
|
+
}
|
|
128
144
|
}
|
|
129
145
|
|
|
146
|
+
/** Run `gh` and return stdout. On non-zero exit, throws a `GhError` carrying the toolcall input and raw command. */
|
|
130
147
|
async function ghExec(
|
|
131
|
-
pi: ExtensionAPI,
|
|
132
148
|
args: string[],
|
|
133
|
-
ctx: { cwd?: string; signal?: AbortSignal },
|
|
134
|
-
): Promise<
|
|
135
|
-
const result = await
|
|
149
|
+
ctx: { cwd?: string; signal?: AbortSignal; input?: unknown },
|
|
150
|
+
): Promise<string> {
|
|
151
|
+
const result = await runGh(args, ctx);
|
|
136
152
|
if (result.code !== 0) {
|
|
137
|
-
|
|
138
|
-
ok: false,
|
|
139
|
-
stdout: "",
|
|
140
|
-
error: result.combined.trim() || `exit code ${result.code}`,
|
|
141
|
-
args,
|
|
142
|
-
};
|
|
153
|
+
throw new GhError(args, result, ctx.input);
|
|
143
154
|
}
|
|
144
|
-
return
|
|
155
|
+
return result.stdout;
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
function repoArgs(repo?: string): string[] {
|
|
148
159
|
return repo ? ["--repo", repo] : [];
|
|
149
160
|
}
|
|
150
161
|
|
|
162
|
+
// ── runtime validation schemas for JSON.parse results ───────────────────────
|
|
163
|
+
|
|
164
|
+
const repoViewSchema = Type.Object({ nameWithOwner: Type.String() });
|
|
165
|
+
|
|
166
|
+
const stepSchema = Type.Object({
|
|
167
|
+
name: Type.String(),
|
|
168
|
+
number: Type.Number(),
|
|
169
|
+
status: Type.String(),
|
|
170
|
+
conclusion: Type.Union([Type.String(), Type.Null()]),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const jobRunSchema = Type.Object({
|
|
174
|
+
id: Type.Number(),
|
|
175
|
+
name: Type.String(),
|
|
176
|
+
status: Type.String(),
|
|
177
|
+
conclusion: Type.Union([Type.String(), Type.Null()]),
|
|
178
|
+
steps: Type.Array(stepSchema),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const jobsResponseSchema = Type.Object({ jobs: Type.Array(jobRunSchema) });
|
|
182
|
+
|
|
151
183
|
function truncate(
|
|
152
184
|
text: string,
|
|
153
185
|
maxLines = 2000,
|
|
@@ -171,22 +203,14 @@ function truncate(
|
|
|
171
203
|
}
|
|
172
204
|
|
|
173
205
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* On failure, returns the error text as content and includes the raw command
|
|
177
|
-
* input (`args`) in `details` so the user can debug what the model actually ran.
|
|
206
|
+
* Format a successful gh invocation's stdout into a tool result.
|
|
207
|
+
* Failures are thrown by `ghExec` as `GhError`, so only the success path lives here.
|
|
178
208
|
*/
|
|
179
|
-
function toToolResult(
|
|
209
|
+
function toToolResult(stdout: string): {
|
|
180
210
|
content: Array<{ type: "text"; text: string }>;
|
|
181
211
|
details: Record<string, unknown>;
|
|
182
212
|
} {
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
content: [{ type: "text", text: `gh ${res.args.join(" ")} failed: ${res.error}` }],
|
|
186
|
-
details: { error: res.error, args: res.args },
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
const { text, truncated } = truncate(res.stdout);
|
|
213
|
+
const { text, truncated } = truncate(stdout);
|
|
190
214
|
return { content: [{ type: "text", text }], details: { truncated } };
|
|
191
215
|
}
|
|
192
216
|
|
|
@@ -220,16 +244,16 @@ export function stepsDetail(
|
|
|
220
244
|
}
|
|
221
245
|
|
|
222
246
|
/** In-flight dedup map to avoid concurrent fetches of the same log. */
|
|
223
|
-
const inflightLogs = new Map<string, Promise<
|
|
247
|
+
const inflightLogs = new Map<string, Promise<string>>();
|
|
224
248
|
|
|
225
249
|
async function getJobLog(
|
|
226
|
-
pi: ExtensionAPI,
|
|
227
250
|
runId: string,
|
|
228
251
|
jobId: number,
|
|
229
252
|
effectiveRepo: string,
|
|
230
253
|
signal: AbortSignal | undefined,
|
|
231
254
|
cwd: string | undefined,
|
|
232
|
-
|
|
255
|
+
input?: unknown,
|
|
256
|
+
): Promise<string> {
|
|
233
257
|
const cacheDir = join(homedir(), ".cache", "pi", "ci-logs", runId);
|
|
234
258
|
const cacheFile = join(cacheDir, `${jobId}.log`);
|
|
235
259
|
const key = `${runId}:${jobId}`;
|
|
@@ -238,28 +262,27 @@ async function getJobLog(
|
|
|
238
262
|
const inflight = inflightLogs.get(key);
|
|
239
263
|
if (inflight) return inflight;
|
|
240
264
|
|
|
241
|
-
const fetchAndCache = async (): Promise<
|
|
265
|
+
const fetchAndCache = async (): Promise<string> => {
|
|
242
266
|
// Check file cache
|
|
243
267
|
try {
|
|
244
|
-
|
|
245
|
-
return { ok: true, stdout: cached, error: null, args: [], log: cached };
|
|
268
|
+
return await readFile(cacheFile, "utf-8");
|
|
246
269
|
} catch {
|
|
247
270
|
// Not cached, fetch from GitHub
|
|
248
271
|
}
|
|
249
272
|
|
|
250
|
-
const
|
|
273
|
+
const log = await ghExec(["api", `/repos/${effectiveRepo}/actions/jobs/${jobId}/logs`], {
|
|
251
274
|
cwd,
|
|
252
275
|
signal,
|
|
276
|
+
input,
|
|
253
277
|
});
|
|
254
|
-
if (!res.ok) return res;
|
|
255
278
|
|
|
256
279
|
// Write to cache
|
|
257
280
|
await mkdir(cacheDir, { recursive: true });
|
|
258
281
|
await withFileMutationQueue(cacheFile, async () => {
|
|
259
|
-
await writeFile(cacheFile,
|
|
282
|
+
await writeFile(cacheFile, log);
|
|
260
283
|
});
|
|
261
284
|
|
|
262
|
-
return
|
|
285
|
+
return log;
|
|
263
286
|
};
|
|
264
287
|
|
|
265
288
|
const promise = fetchAndCache();
|
|
@@ -272,15 +295,15 @@ async function getJobLog(
|
|
|
272
295
|
}
|
|
273
296
|
|
|
274
297
|
async function resolveRepo(
|
|
275
|
-
pi: ExtensionAPI,
|
|
276
298
|
repo: string | undefined,
|
|
277
299
|
signal: AbortSignal | undefined,
|
|
278
300
|
cwd: string | undefined,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
301
|
+
input?: unknown,
|
|
302
|
+
): Promise<string> {
|
|
303
|
+
if (repo) return repo;
|
|
304
|
+
const stdout = await ghExec(["repo", "view", "--json", "nameWithOwner"], { cwd, signal, input });
|
|
305
|
+
const { nameWithOwner } = Value.Parse(repoViewSchema, JSON.parse(stdout));
|
|
306
|
+
return nameWithOwner;
|
|
284
307
|
}
|
|
285
308
|
|
|
286
309
|
export function statusIcon(conclusion: string | null): string {
|
|
@@ -400,10 +423,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
400
423
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
|
|
401
424
|
}),
|
|
402
425
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
403
|
-
const { number, repo } = params
|
|
426
|
+
const { number, repo } = params;
|
|
404
427
|
return toToolResult(
|
|
405
428
|
await ghExec(
|
|
406
|
-
pi,
|
|
407
429
|
[
|
|
408
430
|
"issue",
|
|
409
431
|
"view",
|
|
@@ -412,7 +434,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
412
434
|
"--json",
|
|
413
435
|
"title,state,body,author,createdAt,updatedAt,closedAt,url,labels,assignees,comments,milestone,number",
|
|
414
436
|
],
|
|
415
|
-
{ cwd: ctx.cwd, signal },
|
|
437
|
+
{ cwd: ctx.cwd, signal, input: params },
|
|
416
438
|
),
|
|
417
439
|
);
|
|
418
440
|
},
|
|
@@ -430,11 +452,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
430
452
|
limit: Type.Optional(Type.Number({ description: "Max results (default 30)" })),
|
|
431
453
|
}),
|
|
432
454
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
433
|
-
const { repo, state, limit } = params
|
|
455
|
+
const { repo, state, limit } = params;
|
|
434
456
|
const args = ["issue", "list", ...repoArgs(repo)];
|
|
435
457
|
if (state) args.push("--state", state);
|
|
436
458
|
if (limit) args.push("--limit", String(limit));
|
|
437
|
-
return toToolResult(await ghExec(
|
|
459
|
+
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
438
460
|
},
|
|
439
461
|
});
|
|
440
462
|
|
|
@@ -449,10 +471,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
449
471
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
450
472
|
}),
|
|
451
473
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
452
|
-
const { number, repo } = params
|
|
474
|
+
const { number, repo } = params;
|
|
453
475
|
return toToolResult(
|
|
454
476
|
await ghExec(
|
|
455
|
-
pi,
|
|
456
477
|
[
|
|
457
478
|
"pr",
|
|
458
479
|
"view",
|
|
@@ -461,7 +482,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
461
482
|
"--json",
|
|
462
483
|
"title,state,body,author,createdAt,updatedAt,mergedAt,mergedBy,headRefName,baseRefName,url,additions,deletions,changedFiles,labels,assignees,reviewRequests,reviews,comments,number",
|
|
463
484
|
],
|
|
464
|
-
{ cwd: ctx.cwd, signal },
|
|
485
|
+
{ cwd: ctx.cwd, signal, input: params },
|
|
465
486
|
),
|
|
466
487
|
);
|
|
467
488
|
},
|
|
@@ -481,11 +502,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
481
502
|
limit: Type.Optional(Type.Number({ description: "Max results (default 30)" })),
|
|
482
503
|
}),
|
|
483
504
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
484
|
-
const { repo, state, limit } = params
|
|
505
|
+
const { repo, state, limit } = params;
|
|
485
506
|
const args = ["pr", "list", ...repoArgs(repo)];
|
|
486
507
|
if (state) args.push("--state", state);
|
|
487
508
|
if (limit) args.push("--limit", String(limit));
|
|
488
|
-
return toToolResult(await ghExec(
|
|
509
|
+
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
489
510
|
},
|
|
490
511
|
});
|
|
491
512
|
|
|
@@ -500,12 +521,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
500
521
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
501
522
|
}),
|
|
502
523
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
503
|
-
const { number, repo } = params
|
|
504
|
-
number: number | string;
|
|
505
|
-
repo?: string;
|
|
506
|
-
};
|
|
524
|
+
const { number, repo } = params;
|
|
507
525
|
const args = ["pr", "diff", String(number), ...repoArgs(repo)];
|
|
508
|
-
return toToolResult(await ghExec(
|
|
526
|
+
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
509
527
|
},
|
|
510
528
|
});
|
|
511
529
|
|
|
@@ -520,11 +538,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
520
538
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
521
539
|
}),
|
|
522
540
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
523
|
-
const { number, repo } = params
|
|
541
|
+
const { number, repo } = params;
|
|
524
542
|
return toToolResult(
|
|
525
|
-
await ghExec(
|
|
543
|
+
await ghExec(["pr", "checks", String(number), ...repoArgs(repo)], {
|
|
526
544
|
cwd: ctx.cwd,
|
|
527
545
|
signal,
|
|
546
|
+
input: params,
|
|
528
547
|
}),
|
|
529
548
|
);
|
|
530
549
|
},
|
|
@@ -548,31 +567,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
548
567
|
),
|
|
549
568
|
}),
|
|
550
569
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
551
|
-
const { number, repo, reviews } = params
|
|
552
|
-
number: number | string;
|
|
553
|
-
repo?: string;
|
|
554
|
-
reviews?: boolean;
|
|
555
|
-
};
|
|
570
|
+
const { number, repo, reviews } = params;
|
|
556
571
|
let out: string;
|
|
557
572
|
if (reviews) {
|
|
558
|
-
const
|
|
559
|
-
if (!resolved.ok) return toToolResult(resolved);
|
|
573
|
+
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
560
574
|
|
|
561
|
-
const [
|
|
562
|
-
ghExec(
|
|
575
|
+
const [comments, reviewsOut] = await Promise.all([
|
|
576
|
+
ghExec(["api", `/repos/${effectiveRepo}/pulls/${String(number)}/comments`], {
|
|
563
577
|
cwd: ctx.cwd,
|
|
564
578
|
signal,
|
|
579
|
+
input: params,
|
|
565
580
|
}),
|
|
566
|
-
ghExec(
|
|
581
|
+
ghExec(["api", `/repos/${effectiveRepo}/pulls/${String(number)}/reviews`], {
|
|
567
582
|
cwd: ctx.cwd,
|
|
568
583
|
signal,
|
|
584
|
+
input: params,
|
|
569
585
|
}),
|
|
570
586
|
]);
|
|
571
|
-
if (!commentsRes.ok) return toToolResult(commentsRes);
|
|
572
|
-
if (!reviewsRes.ok) return toToolResult(reviewsRes);
|
|
573
587
|
|
|
574
|
-
const reviewComments = JSON.parse(
|
|
575
|
-
const reviewSummaries = JSON.parse(
|
|
588
|
+
const reviewComments = Value.Parse(Type.Array(Type.Unknown()), JSON.parse(comments));
|
|
589
|
+
const reviewSummaries = Value.Parse(Type.Array(Type.Unknown()), JSON.parse(reviewsOut));
|
|
576
590
|
|
|
577
591
|
out = JSON.stringify(
|
|
578
592
|
{
|
|
@@ -583,16 +597,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
583
597
|
2,
|
|
584
598
|
);
|
|
585
599
|
} else {
|
|
586
|
-
|
|
587
|
-
pi,
|
|
600
|
+
out = await ghExec(
|
|
588
601
|
["pr", "view", String(number), ...repoArgs(repo), "--json", "comments"],
|
|
589
602
|
{
|
|
590
603
|
cwd: ctx.cwd,
|
|
591
604
|
signal,
|
|
605
|
+
input: params,
|
|
592
606
|
},
|
|
593
607
|
);
|
|
594
|
-
if (!res.ok) return toToolResult(res);
|
|
595
|
-
out = res.stdout;
|
|
596
608
|
}
|
|
597
609
|
const { text, truncated } = truncate(out);
|
|
598
610
|
return {
|
|
@@ -613,13 +625,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
613
625
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
614
626
|
}),
|
|
615
627
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
616
|
-
const { number, repo } = params
|
|
628
|
+
const { number, repo } = params;
|
|
617
629
|
return toToolResult(
|
|
618
|
-
await ghExec(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
),
|
|
630
|
+
await ghExec(["issue", "view", String(number), ...repoArgs(repo), "--json", "comments"], {
|
|
631
|
+
cwd: ctx.cwd,
|
|
632
|
+
signal,
|
|
633
|
+
input: params,
|
|
634
|
+
}),
|
|
623
635
|
);
|
|
624
636
|
},
|
|
625
637
|
});
|
|
@@ -639,17 +651,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
639
651
|
workflow: Type.Optional(Type.String({ description: "Filter by workflow name or file" })),
|
|
640
652
|
}),
|
|
641
653
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
642
|
-
const { repo, limit, status, workflow } = params
|
|
643
|
-
repo?: string;
|
|
644
|
-
limit?: number;
|
|
645
|
-
status?: string;
|
|
646
|
-
workflow?: string;
|
|
647
|
-
};
|
|
654
|
+
const { repo, limit, status, workflow } = params;
|
|
648
655
|
const args = ["run", "list", ...repoArgs(repo)];
|
|
649
656
|
if (limit) args.push("--limit", String(limit));
|
|
650
657
|
if (status) args.push("--status", status);
|
|
651
658
|
if (workflow) args.push("--workflow", workflow);
|
|
652
|
-
return toToolResult(await ghExec(
|
|
659
|
+
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
653
660
|
},
|
|
654
661
|
});
|
|
655
662
|
|
|
@@ -689,40 +696,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
689
696
|
),
|
|
690
697
|
}),
|
|
691
698
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
692
|
-
const { run_id, repo, job, step, offset, limit } = params
|
|
693
|
-
run_id: number | string;
|
|
694
|
-
repo?: string;
|
|
695
|
-
job?: string;
|
|
696
|
-
step?: string;
|
|
697
|
-
offset?: number;
|
|
698
|
-
limit?: number;
|
|
699
|
-
};
|
|
699
|
+
const { run_id, repo, job, step, offset, limit } = params;
|
|
700
700
|
|
|
701
701
|
// ── Fetch specific step logs ───────────────────────────────────────
|
|
702
702
|
if (step !== undefined && step !== null) {
|
|
703
|
-
const
|
|
704
|
-
if (!resolved.ok) return toToolResult(resolved);
|
|
703
|
+
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
705
704
|
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
{ cwd: ctx.cwd, signal },
|
|
705
|
+
const jobsOut = await ghExec(
|
|
706
|
+
["api", `/repos/${effectiveRepo}/actions/runs/${run_id}/jobs`],
|
|
707
|
+
{ cwd: ctx.cwd, signal, input: params },
|
|
710
708
|
);
|
|
711
|
-
|
|
712
|
-
const { jobs } = JSON.parse(jobsRes.stdout) as {
|
|
713
|
-
jobs: Array<{
|
|
714
|
-
id: number;
|
|
715
|
-
name: string;
|
|
716
|
-
status: string;
|
|
717
|
-
conclusion: string | null;
|
|
718
|
-
steps: Array<{
|
|
719
|
-
name: string;
|
|
720
|
-
number: number;
|
|
721
|
-
status: string;
|
|
722
|
-
conclusion: string | null;
|
|
723
|
-
}>;
|
|
724
|
-
}>;
|
|
725
|
-
};
|
|
709
|
+
const { jobs } = Value.Parse(jobsResponseSchema, JSON.parse(jobsOut));
|
|
726
710
|
|
|
727
711
|
if (!jobs || jobs.length === 0) {
|
|
728
712
|
return {
|
|
@@ -804,16 +788,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
804
788
|
details: {},
|
|
805
789
|
});
|
|
806
790
|
|
|
807
|
-
const
|
|
808
|
-
pi,
|
|
791
|
+
const rawLog = await getJobLog(
|
|
809
792
|
String(run_id),
|
|
810
793
|
targetJob.id,
|
|
811
|
-
|
|
794
|
+
effectiveRepo,
|
|
812
795
|
signal,
|
|
813
796
|
ctx.cwd,
|
|
797
|
+
params,
|
|
814
798
|
);
|
|
815
|
-
if (!logRes.ok) return toToolResult(logRes);
|
|
816
|
-
const rawLog = logRes.log!;
|
|
817
799
|
|
|
818
800
|
const stepLog = extractStepFromLog(rawLog, stepNum, targetJob.steps);
|
|
819
801
|
if (stepLog === null) {
|
|
@@ -891,29 +873,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
891
873
|
details: {},
|
|
892
874
|
});
|
|
893
875
|
|
|
894
|
-
const
|
|
895
|
-
if (!resolved.ok) return toToolResult(resolved);
|
|
876
|
+
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
896
877
|
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
);
|
|
902
|
-
|
|
903
|
-
const { jobs } = JSON.parse(jobsRes.stdout) as {
|
|
904
|
-
jobs: Array<{
|
|
905
|
-
id: number;
|
|
906
|
-
name: string;
|
|
907
|
-
status: string;
|
|
908
|
-
conclusion: string | null;
|
|
909
|
-
steps: Array<{
|
|
910
|
-
name: string;
|
|
911
|
-
number: number;
|
|
912
|
-
status: string;
|
|
913
|
-
conclusion: string | null;
|
|
914
|
-
}>;
|
|
915
|
-
}>;
|
|
916
|
-
};
|
|
878
|
+
const jobsOut = await ghExec(["api", `/repos/${effectiveRepo}/actions/runs/${run_id}/jobs`], {
|
|
879
|
+
cwd: ctx.cwd,
|
|
880
|
+
signal,
|
|
881
|
+
input: params,
|
|
882
|
+
});
|
|
883
|
+
const { jobs } = Value.Parse(jobsResponseSchema, JSON.parse(jobsOut));
|
|
917
884
|
|
|
918
885
|
if (!jobs || jobs.length === 0) {
|
|
919
886
|
return {
|
|
@@ -974,15 +941,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
974
941
|
if (failedSteps.length === 0) continue;
|
|
975
942
|
|
|
976
943
|
try {
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const rawLog = logRes.log!;
|
|
944
|
+
const rawLog = await getJobLog(
|
|
945
|
+
String(run_id),
|
|
946
|
+
j.id,
|
|
947
|
+
effectiveRepo,
|
|
948
|
+
signal,
|
|
949
|
+
ctx.cwd,
|
|
950
|
+
params,
|
|
951
|
+
);
|
|
986
952
|
|
|
987
953
|
for (const fs of failedSteps) {
|
|
988
954
|
if (fetchedCount >= maxFailed) break;
|
|
@@ -1046,13 +1012,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1046
1012
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1047
1013
|
}),
|
|
1048
1014
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1049
|
-
const { run_id, repo } = params
|
|
1050
|
-
const
|
|
1051
|
-
if (!resolved.ok) return toToolResult(resolved);
|
|
1015
|
+
const { run_id, repo } = params;
|
|
1016
|
+
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1052
1017
|
return toToolResult(
|
|
1053
|
-
await ghExec(
|
|
1018
|
+
await ghExec(["api", `/repos/${effectiveRepo}/actions/runs/${run_id}/jobs`], {
|
|
1054
1019
|
cwd: ctx.cwd,
|
|
1055
1020
|
signal,
|
|
1021
|
+
input: params,
|
|
1056
1022
|
}),
|
|
1057
1023
|
);
|
|
1058
1024
|
},
|
|
@@ -1068,10 +1034,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1068
1034
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1069
1035
|
}),
|
|
1070
1036
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1071
|
-
const { repo } = params
|
|
1037
|
+
const { repo } = params;
|
|
1072
1038
|
const args = ["repo", "view"];
|
|
1073
1039
|
if (repo) args.push(repo);
|
|
1074
|
-
return toToolResult(await ghExec(
|
|
1040
|
+
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
1075
1041
|
},
|
|
1076
1042
|
});
|
|
1077
1043
|
|
|
@@ -1086,10 +1052,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1086
1052
|
limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
|
|
1087
1053
|
}),
|
|
1088
1054
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1089
|
-
const { repo, limit } = params
|
|
1055
|
+
const { repo, limit } = params;
|
|
1090
1056
|
const args = ["release", "list", ...repoArgs(repo)];
|
|
1091
1057
|
if (limit) args.push("--limit", String(limit));
|
|
1092
|
-
return toToolResult(await ghExec(
|
|
1058
|
+
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
1093
1059
|
},
|
|
1094
1060
|
});
|
|
1095
1061
|
|
|
@@ -1104,11 +1070,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1104
1070
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1105
1071
|
}),
|
|
1106
1072
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1107
|
-
const { tag, repo } = params
|
|
1073
|
+
const { tag, repo } = params;
|
|
1108
1074
|
return toToolResult(
|
|
1109
|
-
await ghExec(
|
|
1075
|
+
await ghExec(["release", "view", tag, ...repoArgs(repo)], {
|
|
1110
1076
|
cwd: ctx.cwd,
|
|
1111
1077
|
signal,
|
|
1078
|
+
input: params,
|
|
1112
1079
|
}),
|
|
1113
1080
|
);
|
|
1114
1081
|
},
|
|
@@ -1130,11 +1097,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1130
1097
|
),
|
|
1131
1098
|
}),
|
|
1132
1099
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1133
|
-
const { number, repo, fail_fast } = params
|
|
1134
|
-
number: number | string;
|
|
1135
|
-
repo?: string;
|
|
1136
|
-
fail_fast?: boolean;
|
|
1137
|
-
};
|
|
1100
|
+
const { number, repo, fail_fast } = params;
|
|
1138
1101
|
|
|
1139
1102
|
onUpdate?.({
|
|
1140
1103
|
content: [{ type: "text", text: `Watching CI checks for PR #${number}...` }],
|
|
@@ -1144,7 +1107,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1144
1107
|
const args = ["pr", "checks", String(number), ...repoArgs(repo), "--watch"];
|
|
1145
1108
|
if (fail_fast) args.push("--fail-fast");
|
|
1146
1109
|
|
|
1147
|
-
const result = await
|
|
1110
|
+
const result = await runGh(args, { cwd: ctx.cwd, signal, timeout: 600_000 });
|
|
1148
1111
|
|
|
1149
1112
|
const exitCode = result.code;
|
|
1150
1113
|
const stdout = result.stdout;
|
|
@@ -1184,14 +1147,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1184
1147
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1185
1148
|
}),
|
|
1186
1149
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1187
|
-
const { run_id, repo } = params
|
|
1150
|
+
const { run_id, repo } = params;
|
|
1188
1151
|
|
|
1189
1152
|
onUpdate?.({
|
|
1190
1153
|
content: [{ type: "text", text: `Watching workflow run ${run_id}...` }],
|
|
1191
1154
|
details: {},
|
|
1192
1155
|
});
|
|
1193
1156
|
|
|
1194
|
-
const result = await
|
|
1157
|
+
const result = await runGh(["run", "watch", String(run_id), ...repoArgs(repo)], {
|
|
1195
1158
|
cwd: ctx.cwd,
|
|
1196
1159
|
signal,
|
|
1197
1160
|
timeout: 600_000,
|
|
@@ -1229,15 +1192,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1229
1192
|
limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
|
|
1230
1193
|
}),
|
|
1231
1194
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1232
|
-
const { query, include_prs, limit } = params
|
|
1233
|
-
query: string;
|
|
1234
|
-
include_prs?: boolean;
|
|
1235
|
-
limit?: number;
|
|
1236
|
-
};
|
|
1195
|
+
const { query, include_prs, limit } = params;
|
|
1237
1196
|
const args = ["search", "issues", query];
|
|
1238
1197
|
if (include_prs) args.push("--include-prs");
|
|
1239
1198
|
if (limit) args.push("--limit", String(limit));
|
|
1240
|
-
return toToolResult(await ghExec(
|
|
1199
|
+
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
1241
1200
|
},
|
|
1242
1201
|
});
|
|
1243
1202
|
|
|
@@ -1255,10 +1214,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1255
1214
|
limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
|
|
1256
1215
|
}),
|
|
1257
1216
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1258
|
-
const { query, limit } = params
|
|
1217
|
+
const { query, limit } = params;
|
|
1259
1218
|
const args = ["search", "prs", query];
|
|
1260
1219
|
if (limit) args.push("--limit", String(limit));
|
|
1261
|
-
return toToolResult(await ghExec(
|
|
1220
|
+
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
1262
1221
|
},
|
|
1263
1222
|
});
|
|
1264
1223
|
}
|
package/src/opencode-edit.ts
CHANGED
|
@@ -517,7 +517,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
517
517
|
} catch (error: unknown) {
|
|
518
518
|
throwIfAborted();
|
|
519
519
|
const msg =
|
|
520
|
-
error instanceof Error && "code" in error
|
|
520
|
+
error instanceof Error && "code" in error && typeof error.code === "string"
|
|
521
|
+
? `Error code: ${error.code}`
|
|
522
|
+
: String(error);
|
|
521
523
|
throw new Error(`Could not edit file: ${filePath}. ${msg}.`);
|
|
522
524
|
}
|
|
523
525
|
throwIfAborted();
|
package/src/opencode-read.ts
CHANGED
|
@@ -317,11 +317,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
317
317
|
),
|
|
318
318
|
}),
|
|
319
319
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
320
|
-
const {
|
|
321
|
-
filePath: rawPath,
|
|
322
|
-
offset,
|
|
323
|
-
limit,
|
|
324
|
-
} = params as { filePath: string; offset?: number; limit?: number };
|
|
320
|
+
const { filePath: rawPath, offset, limit } = params;
|
|
325
321
|
|
|
326
322
|
const absolutePath = isAbsolute(rawPath) ? rawPath : resolvePath(ctx.cwd, rawPath);
|
|
327
323
|
|
|
@@ -336,9 +332,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
336
332
|
} catch {
|
|
337
333
|
const suggestion = await didYouMean(absolutePath);
|
|
338
334
|
return {
|
|
339
|
-
content: [
|
|
340
|
-
{ type: "text", text: `File not found: ${absolutePath}${suggestion}` },
|
|
341
|
-
] as TextContent[],
|
|
335
|
+
content: [{ type: "text", text: `File not found: ${absolutePath}${suggestion}` }],
|
|
342
336
|
details: undefined,
|
|
343
337
|
};
|
|
344
338
|
}
|
|
@@ -366,7 +360,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
366
360
|
output += `\n</entries>`;
|
|
367
361
|
|
|
368
362
|
return {
|
|
369
|
-
content: [{ type: "text", text: output }]
|
|
363
|
+
content: [{ type: "text", text: output }],
|
|
370
364
|
details: undefined,
|
|
371
365
|
};
|
|
372
366
|
}
|
|
@@ -380,7 +374,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
380
374
|
await access(absolutePath, constants.R_OK);
|
|
381
375
|
} catch {
|
|
382
376
|
return {
|
|
383
|
-
content: [{ type: "text", text: `File not readable: ${absolutePath}` }]
|
|
377
|
+
content: [{ type: "text", text: `File not readable: ${absolutePath}` }],
|
|
384
378
|
details: undefined,
|
|
385
379
|
};
|
|
386
380
|
}
|
|
@@ -392,7 +386,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
392
386
|
const base64 = buffer.toString("base64");
|
|
393
387
|
content = [
|
|
394
388
|
{ type: "text", text: `[Image: ${mimeType}, ${formatSize(buffer.length)}]` },
|
|
395
|
-
{ type: "image", data: base64, mimeType }
|
|
389
|
+
{ type: "image", data: base64, mimeType },
|
|
396
390
|
];
|
|
397
391
|
return { content, details: undefined };
|
|
398
392
|
}
|
|
@@ -404,9 +398,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
404
398
|
// Binary file detection
|
|
405
399
|
if (isBinaryExtension(absolutePath) || isBinaryFileBySample(sample)) {
|
|
406
400
|
return {
|
|
407
|
-
content: [
|
|
408
|
-
{ type: "text", text: `Cannot read binary file: ${absolutePath}` },
|
|
409
|
-
] as TextContent[],
|
|
401
|
+
content: [{ type: "text", text: `Cannot read binary file: ${absolutePath}` }],
|
|
410
402
|
details: undefined,
|
|
411
403
|
};
|
|
412
404
|
}
|
|
@@ -426,7 +418,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
426
418
|
type: "text",
|
|
427
419
|
text: `Offset ${offset} is beyond end of file (${allLines.length} lines total)`,
|
|
428
420
|
},
|
|
429
|
-
]
|
|
421
|
+
],
|
|
430
422
|
details: undefined,
|
|
431
423
|
};
|
|
432
424
|
}
|
|
@@ -477,7 +469,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
477
469
|
}
|
|
478
470
|
}
|
|
479
471
|
|
|
480
|
-
content = [{ type: "text", text: outputText }]
|
|
472
|
+
content = [{ type: "text", text: outputText }];
|
|
481
473
|
|
|
482
474
|
return { content, details };
|
|
483
475
|
},
|
package/src/opencode-write.ts
CHANGED
|
@@ -35,7 +35,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
35
35
|
content: Type.String({ description: "The content to write to the file" }),
|
|
36
36
|
}),
|
|
37
37
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
38
|
-
const { filePath: rawPath, content } = params
|
|
38
|
+
const { filePath: rawPath, content } = params;
|
|
39
39
|
const absolutePath = resolvePath(ctx.cwd, rawPath);
|
|
40
40
|
const dir = dirname(absolutePath);
|
|
41
41
|
|
|
@@ -51,10 +51,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
51
51
|
throwIfAborted();
|
|
52
52
|
|
|
53
53
|
return {
|
|
54
|
-
content: [{ type: "text", text: `Wrote file successfully: ${absolutePath}` }]
|
|
55
|
-
type: "text";
|
|
56
|
-
text: string;
|
|
57
|
-
}>,
|
|
54
|
+
content: [{ type: "text", text: `Wrote file successfully: ${absolutePath}` }],
|
|
58
55
|
details: undefined,
|
|
59
56
|
};
|
|
60
57
|
});
|
package/src/todo-pendant.ts
CHANGED
|
@@ -36,6 +36,10 @@ interface TaskDetails {
|
|
|
36
36
|
error?: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function isTaskDetails(d: unknown): d is TaskDetails {
|
|
40
|
+
return typeof d === "object" && d !== null && "tasks" in d && Array.isArray(d.tasks);
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
// ---------------------------------------------------------------------------
|
|
40
44
|
// Helpers
|
|
41
45
|
// ---------------------------------------------------------------------------
|
|
@@ -65,8 +69,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
69
|
pi.on("tool_result", (event, ctx) => {
|
|
66
70
|
if (event.toolName !== "todo") return;
|
|
67
71
|
|
|
68
|
-
const details = event.details
|
|
69
|
-
if (!details
|
|
72
|
+
const details = event.details;
|
|
73
|
+
if (!isTaskDetails(details) || !details.tasks.length) {
|
|
70
74
|
ctx.ui.setWidget("todo-pendant", undefined);
|
|
71
75
|
return;
|
|
72
76
|
}
|
package/src/workspace-guard.ts
CHANGED
|
@@ -13,11 +13,18 @@
|
|
|
13
13
|
|
|
14
14
|
import { isAbsolute, join, resolve, relative, sep } from "node:path";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { ExtensionAPI, ToolCallEvent } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
|
|
18
18
|
const WRITE_TOOLS = new Set(["write", "edit"]);
|
|
19
19
|
const ALWAYS_ALLOW = ["/tmp"];
|
|
20
20
|
|
|
21
|
+
function getWriteTarget(input: ToolCallEvent["input"]): string | undefined {
|
|
22
|
+
if (typeof input !== "object" || input === null) return undefined;
|
|
23
|
+
if ("path" in input && typeof input.path === "string") return input.path;
|
|
24
|
+
if ("filePath" in input && typeof input.filePath === "string") return input.filePath;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
function resolvePath(filePath: string, cwd: string): string {
|
|
22
29
|
let p = filePath;
|
|
23
30
|
if (p.startsWith("~")) {
|
|
@@ -46,7 +53,7 @@ function isPathAllowed(resolvedPath: string, cwd: string): boolean {
|
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
export default function (pi: ExtensionAPI) {
|
|
49
|
-
pi.on("before_agent_start",
|
|
56
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
50
57
|
const currentCwd = ctx.cwd;
|
|
51
58
|
return {
|
|
52
59
|
systemPrompt:
|
|
@@ -60,8 +67,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
60
67
|
pi.on("tool_call", async (event, ctx) => {
|
|
61
68
|
if (!WRITE_TOOLS.has(event.toolName)) return;
|
|
62
69
|
|
|
63
|
-
const
|
|
64
|
-
const rawPath = input.path ?? input.filePath;
|
|
70
|
+
const rawPath = getWriteTarget(event.input);
|
|
65
71
|
if (!rawPath) return;
|
|
66
72
|
|
|
67
73
|
const resolved = resolvePath(rawPath, ctx.cwd);
|