@trim21/personal-pi-extensions 0.0.124 → 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 +47 -77
- 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";
|
|
@@ -108,7 +109,7 @@ function runGh(
|
|
|
108
109
|
resolve({ stdout, stderr, code: code ?? 0, killed, combined: combined.join("") });
|
|
109
110
|
});
|
|
110
111
|
|
|
111
|
-
proc.on("error", (
|
|
112
|
+
proc.on("error", () => {
|
|
112
113
|
if (timeoutId) clearTimeout(timeoutId);
|
|
113
114
|
if (ctx.signal) {
|
|
114
115
|
ctx.signal.removeEventListener("abort", killProcess);
|
|
@@ -158,6 +159,27 @@ function repoArgs(repo?: string): string[] {
|
|
|
158
159
|
return repo ? ["--repo", repo] : [];
|
|
159
160
|
}
|
|
160
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
|
+
|
|
161
183
|
function truncate(
|
|
162
184
|
text: string,
|
|
163
185
|
maxLines = 2000,
|
|
@@ -280,7 +302,8 @@ async function resolveRepo(
|
|
|
280
302
|
): Promise<string> {
|
|
281
303
|
if (repo) return repo;
|
|
282
304
|
const stdout = await ghExec(["repo", "view", "--json", "nameWithOwner"], { cwd, signal, input });
|
|
283
|
-
|
|
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,7 +423,7 @@ 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
429
|
[
|
|
@@ -429,7 +452,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
429
452
|
limit: Type.Optional(Type.Number({ description: "Max results (default 30)" })),
|
|
430
453
|
}),
|
|
431
454
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
432
|
-
const { repo, state, limit } = params
|
|
455
|
+
const { repo, state, limit } = params;
|
|
433
456
|
const args = ["issue", "list", ...repoArgs(repo)];
|
|
434
457
|
if (state) args.push("--state", state);
|
|
435
458
|
if (limit) args.push("--limit", String(limit));
|
|
@@ -448,7 +471,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
448
471
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
449
472
|
}),
|
|
450
473
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
451
|
-
const { number, repo } = params
|
|
474
|
+
const { number, repo } = params;
|
|
452
475
|
return toToolResult(
|
|
453
476
|
await ghExec(
|
|
454
477
|
[
|
|
@@ -479,7 +502,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
479
502
|
limit: Type.Optional(Type.Number({ description: "Max results (default 30)" })),
|
|
480
503
|
}),
|
|
481
504
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
482
|
-
const { repo, state, limit } = params
|
|
505
|
+
const { repo, state, limit } = params;
|
|
483
506
|
const args = ["pr", "list", ...repoArgs(repo)];
|
|
484
507
|
if (state) args.push("--state", state);
|
|
485
508
|
if (limit) args.push("--limit", String(limit));
|
|
@@ -498,10 +521,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
498
521
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
499
522
|
}),
|
|
500
523
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
501
|
-
const { number, repo } = params
|
|
502
|
-
number: number | string;
|
|
503
|
-
repo?: string;
|
|
504
|
-
};
|
|
524
|
+
const { number, repo } = params;
|
|
505
525
|
const args = ["pr", "diff", String(number), ...repoArgs(repo)];
|
|
506
526
|
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
507
527
|
},
|
|
@@ -518,7 +538,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
518
538
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
519
539
|
}),
|
|
520
540
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
521
|
-
const { number, repo } = params
|
|
541
|
+
const { number, repo } = params;
|
|
522
542
|
return toToolResult(
|
|
523
543
|
await ghExec(["pr", "checks", String(number), ...repoArgs(repo)], {
|
|
524
544
|
cwd: ctx.cwd,
|
|
@@ -547,11 +567,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
547
567
|
),
|
|
548
568
|
}),
|
|
549
569
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
550
|
-
const { number, repo, reviews } = params
|
|
551
|
-
number: number | string;
|
|
552
|
-
repo?: string;
|
|
553
|
-
reviews?: boolean;
|
|
554
|
-
};
|
|
570
|
+
const { number, repo, reviews } = params;
|
|
555
571
|
let out: string;
|
|
556
572
|
if (reviews) {
|
|
557
573
|
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
@@ -569,8 +585,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
569
585
|
}),
|
|
570
586
|
]);
|
|
571
587
|
|
|
572
|
-
const reviewComments = JSON.parse(comments);
|
|
573
|
-
const reviewSummaries = JSON.parse(reviewsOut);
|
|
588
|
+
const reviewComments = Value.Parse(Type.Array(Type.Unknown()), JSON.parse(comments));
|
|
589
|
+
const reviewSummaries = Value.Parse(Type.Array(Type.Unknown()), JSON.parse(reviewsOut));
|
|
574
590
|
|
|
575
591
|
out = JSON.stringify(
|
|
576
592
|
{
|
|
@@ -609,7 +625,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
609
625
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
610
626
|
}),
|
|
611
627
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
612
|
-
const { number, repo } = params
|
|
628
|
+
const { number, repo } = params;
|
|
613
629
|
return toToolResult(
|
|
614
630
|
await ghExec(["issue", "view", String(number), ...repoArgs(repo), "--json", "comments"], {
|
|
615
631
|
cwd: ctx.cwd,
|
|
@@ -635,12 +651,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
635
651
|
workflow: Type.Optional(Type.String({ description: "Filter by workflow name or file" })),
|
|
636
652
|
}),
|
|
637
653
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
638
|
-
const { repo, limit, status, workflow } = params
|
|
639
|
-
repo?: string;
|
|
640
|
-
limit?: number;
|
|
641
|
-
status?: string;
|
|
642
|
-
workflow?: string;
|
|
643
|
-
};
|
|
654
|
+
const { repo, limit, status, workflow } = params;
|
|
644
655
|
const args = ["run", "list", ...repoArgs(repo)];
|
|
645
656
|
if (limit) args.push("--limit", String(limit));
|
|
646
657
|
if (status) args.push("--status", status);
|
|
@@ -685,14 +696,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
685
696
|
),
|
|
686
697
|
}),
|
|
687
698
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
688
|
-
const { run_id, repo, job, step, offset, limit } = params
|
|
689
|
-
run_id: number | string;
|
|
690
|
-
repo?: string;
|
|
691
|
-
job?: string;
|
|
692
|
-
step?: string;
|
|
693
|
-
offset?: number;
|
|
694
|
-
limit?: number;
|
|
695
|
-
};
|
|
699
|
+
const { run_id, repo, job, step, offset, limit } = params;
|
|
696
700
|
|
|
697
701
|
// ── Fetch specific step logs ───────────────────────────────────────
|
|
698
702
|
if (step !== undefined && step !== null) {
|
|
@@ -702,20 +706,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
702
706
|
["api", `/repos/${effectiveRepo}/actions/runs/${run_id}/jobs`],
|
|
703
707
|
{ cwd: ctx.cwd, signal, input: params },
|
|
704
708
|
);
|
|
705
|
-
const { jobs } = JSON.parse(jobsOut)
|
|
706
|
-
jobs: Array<{
|
|
707
|
-
id: number;
|
|
708
|
-
name: string;
|
|
709
|
-
status: string;
|
|
710
|
-
conclusion: string | null;
|
|
711
|
-
steps: Array<{
|
|
712
|
-
name: string;
|
|
713
|
-
number: number;
|
|
714
|
-
status: string;
|
|
715
|
-
conclusion: string | null;
|
|
716
|
-
}>;
|
|
717
|
-
}>;
|
|
718
|
-
};
|
|
709
|
+
const { jobs } = Value.Parse(jobsResponseSchema, JSON.parse(jobsOut));
|
|
719
710
|
|
|
720
711
|
if (!jobs || jobs.length === 0) {
|
|
721
712
|
return {
|
|
@@ -889,20 +880,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
889
880
|
signal,
|
|
890
881
|
input: params,
|
|
891
882
|
});
|
|
892
|
-
const { jobs } = JSON.parse(jobsOut)
|
|
893
|
-
jobs: Array<{
|
|
894
|
-
id: number;
|
|
895
|
-
name: string;
|
|
896
|
-
status: string;
|
|
897
|
-
conclusion: string | null;
|
|
898
|
-
steps: Array<{
|
|
899
|
-
name: string;
|
|
900
|
-
number: number;
|
|
901
|
-
status: string;
|
|
902
|
-
conclusion: string | null;
|
|
903
|
-
}>;
|
|
904
|
-
}>;
|
|
905
|
-
};
|
|
883
|
+
const { jobs } = Value.Parse(jobsResponseSchema, JSON.parse(jobsOut));
|
|
906
884
|
|
|
907
885
|
if (!jobs || jobs.length === 0) {
|
|
908
886
|
return {
|
|
@@ -1034,7 +1012,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1034
1012
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1035
1013
|
}),
|
|
1036
1014
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1037
|
-
const { run_id, repo } = params
|
|
1015
|
+
const { run_id, repo } = params;
|
|
1038
1016
|
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
1039
1017
|
return toToolResult(
|
|
1040
1018
|
await ghExec(["api", `/repos/${effectiveRepo}/actions/runs/${run_id}/jobs`], {
|
|
@@ -1056,7 +1034,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1056
1034
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1057
1035
|
}),
|
|
1058
1036
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1059
|
-
const { repo } = params
|
|
1037
|
+
const { repo } = params;
|
|
1060
1038
|
const args = ["repo", "view"];
|
|
1061
1039
|
if (repo) args.push(repo);
|
|
1062
1040
|
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
@@ -1074,7 +1052,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1074
1052
|
limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
|
|
1075
1053
|
}),
|
|
1076
1054
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1077
|
-
const { repo, limit } = params
|
|
1055
|
+
const { repo, limit } = params;
|
|
1078
1056
|
const args = ["release", "list", ...repoArgs(repo)];
|
|
1079
1057
|
if (limit) args.push("--limit", String(limit));
|
|
1080
1058
|
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
|
@@ -1092,7 +1070,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1092
1070
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1093
1071
|
}),
|
|
1094
1072
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1095
|
-
const { tag, repo } = params
|
|
1073
|
+
const { tag, repo } = params;
|
|
1096
1074
|
return toToolResult(
|
|
1097
1075
|
await ghExec(["release", "view", tag, ...repoArgs(repo)], {
|
|
1098
1076
|
cwd: ctx.cwd,
|
|
@@ -1119,11 +1097,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1119
1097
|
),
|
|
1120
1098
|
}),
|
|
1121
1099
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1122
|
-
const { number, repo, fail_fast } = params
|
|
1123
|
-
number: number | string;
|
|
1124
|
-
repo?: string;
|
|
1125
|
-
fail_fast?: boolean;
|
|
1126
|
-
};
|
|
1100
|
+
const { number, repo, fail_fast } = params;
|
|
1127
1101
|
|
|
1128
1102
|
onUpdate?.({
|
|
1129
1103
|
content: [{ type: "text", text: `Watching CI checks for PR #${number}...` }],
|
|
@@ -1173,7 +1147,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1173
1147
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1174
1148
|
}),
|
|
1175
1149
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1176
|
-
const { run_id, repo } = params
|
|
1150
|
+
const { run_id, repo } = params;
|
|
1177
1151
|
|
|
1178
1152
|
onUpdate?.({
|
|
1179
1153
|
content: [{ type: "text", text: `Watching workflow run ${run_id}...` }],
|
|
@@ -1218,11 +1192,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1218
1192
|
limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
|
|
1219
1193
|
}),
|
|
1220
1194
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1221
|
-
const { query, include_prs, limit } = params
|
|
1222
|
-
query: string;
|
|
1223
|
-
include_prs?: boolean;
|
|
1224
|
-
limit?: number;
|
|
1225
|
-
};
|
|
1195
|
+
const { query, include_prs, limit } = params;
|
|
1226
1196
|
const args = ["search", "issues", query];
|
|
1227
1197
|
if (include_prs) args.push("--include-prs");
|
|
1228
1198
|
if (limit) args.push("--limit", String(limit));
|
|
@@ -1244,7 +1214,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1244
1214
|
limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
|
|
1245
1215
|
}),
|
|
1246
1216
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1247
|
-
const { query, limit } = params
|
|
1217
|
+
const { query, limit } = params;
|
|
1248
1218
|
const args = ["search", "prs", query];
|
|
1249
1219
|
if (limit) args.push("--limit", String(limit));
|
|
1250
1220
|
return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
|
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);
|