@trim21/personal-pi-extensions 0.0.122 → 0.0.124

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/gh-readonly.ts +105 -116
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.122",
3
+ "version": "0.0.124",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -48,8 +48,7 @@ interface GhResult {
48
48
 
49
49
  // ── helpers ──────────────────────────────────────────────────────────────────
50
50
 
51
- function execGh(
52
- pi: ExtensionAPI,
51
+ function runGh(
53
52
  args: string[],
54
53
  ctx: { cwd?: string; signal?: AbortSignal; timeout?: number },
55
54
  ): Promise<GhResult> {
@@ -119,29 +118,40 @@ function execGh(
119
118
  });
120
119
  }
121
120
 
122
- /** Result of a gh invocation. On failure, `args` carries the full raw command input for debugging. */
123
- export interface GhExecResult {
124
- ok: boolean;
125
- stdout: string;
126
- error: string | null;
127
- args: string[];
121
+ /**
122
+ * Error thrown by `ghExec` when the `gh` invocation exits non-zero.
123
+ * The message carries the toolcall input (JSON) wrapped in `<input>` markers,
124
+ * and the command output wrapped in `<output>` markers.
125
+ */
126
+ export class GhError extends Error {
127
+ readonly args: string[];
128
+ readonly code: number;
129
+ readonly stdout: string;
130
+ readonly stderr: string;
131
+ readonly input?: unknown;
132
+
133
+ constructor(args: string[], result: GhResult, input?: unknown) {
134
+ const inputText = input === undefined ? "" : `<input>${JSON.stringify(input)}<input>\n`;
135
+ super(`${inputText}<output>${result.combined.trim() || `exit code ${result.code}`}<output>`);
136
+ this.name = "GhError";
137
+ this.args = args;
138
+ this.code = result.code;
139
+ this.stdout = result.stdout;
140
+ this.stderr = result.stderr;
141
+ this.input = input;
142
+ }
128
143
  }
129
144
 
145
+ /** Run `gh` and return stdout. On non-zero exit, throws a `GhError` carrying the toolcall input and raw command. */
130
146
  async function ghExec(
131
- pi: ExtensionAPI,
132
147
  args: string[],
133
- ctx: { cwd?: string; signal?: AbortSignal },
134
- ): Promise<GhExecResult> {
135
- const result = await execGh(pi, args, ctx);
148
+ ctx: { cwd?: string; signal?: AbortSignal; input?: unknown },
149
+ ): Promise<string> {
150
+ const result = await runGh(args, ctx);
136
151
  if (result.code !== 0) {
137
- return {
138
- ok: false,
139
- stdout: "",
140
- error: result.combined.trim() || `exit code ${result.code}`,
141
- args,
142
- };
152
+ throw new GhError(args, result, ctx.input);
143
153
  }
144
- return { ok: true, stdout: result.stdout, error: null, args };
154
+ return result.stdout;
145
155
  }
146
156
 
147
157
  function repoArgs(repo?: string): string[] {
@@ -171,22 +181,14 @@ function truncate(
171
181
  }
172
182
 
173
183
  /**
174
- * Convert a gh invocation result into a tool result.
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.
184
+ * Format a successful gh invocation's stdout into a tool result.
185
+ * Failures are thrown by `ghExec` as `GhError`, so only the success path lives here.
178
186
  */
179
- function toToolResult(res: GhExecResult): {
187
+ function toToolResult(stdout: string): {
180
188
  content: Array<{ type: "text"; text: string }>;
181
189
  details: Record<string, unknown>;
182
190
  } {
183
- if (!res.ok) {
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);
191
+ const { text, truncated } = truncate(stdout);
190
192
  return { content: [{ type: "text", text }], details: { truncated } };
191
193
  }
192
194
 
@@ -220,16 +222,16 @@ export function stepsDetail(
220
222
  }
221
223
 
222
224
  /** In-flight dedup map to avoid concurrent fetches of the same log. */
223
- const inflightLogs = new Map<string, Promise<GhExecResult & { log?: string }>>();
225
+ const inflightLogs = new Map<string, Promise<string>>();
224
226
 
225
227
  async function getJobLog(
226
- pi: ExtensionAPI,
227
228
  runId: string,
228
229
  jobId: number,
229
230
  effectiveRepo: string,
230
231
  signal: AbortSignal | undefined,
231
232
  cwd: string | undefined,
232
- ): Promise<GhExecResult & { log?: string }> {
233
+ input?: unknown,
234
+ ): Promise<string> {
233
235
  const cacheDir = join(homedir(), ".cache", "pi", "ci-logs", runId);
234
236
  const cacheFile = join(cacheDir, `${jobId}.log`);
235
237
  const key = `${runId}:${jobId}`;
@@ -238,28 +240,27 @@ async function getJobLog(
238
240
  const inflight = inflightLogs.get(key);
239
241
  if (inflight) return inflight;
240
242
 
241
- const fetchAndCache = async (): Promise<GhExecResult & { log?: string }> => {
243
+ const fetchAndCache = async (): Promise<string> => {
242
244
  // Check file cache
243
245
  try {
244
- const cached = await readFile(cacheFile, "utf-8");
245
- return { ok: true, stdout: cached, error: null, args: [], log: cached };
246
+ return await readFile(cacheFile, "utf-8");
246
247
  } catch {
247
248
  // Not cached, fetch from GitHub
248
249
  }
249
250
 
250
- const res = await ghExec(pi, ["api", `/repos/${effectiveRepo}/actions/jobs/${jobId}/logs`], {
251
+ const log = await ghExec(["api", `/repos/${effectiveRepo}/actions/jobs/${jobId}/logs`], {
251
252
  cwd,
252
253
  signal,
254
+ input,
253
255
  });
254
- if (!res.ok) return res;
255
256
 
256
257
  // Write to cache
257
258
  await mkdir(cacheDir, { recursive: true });
258
259
  await withFileMutationQueue(cacheFile, async () => {
259
- await writeFile(cacheFile, res.stdout);
260
+ await writeFile(cacheFile, log);
260
261
  });
261
262
 
262
- return { ...res, log: res.stdout };
263
+ return log;
263
264
  };
264
265
 
265
266
  const promise = fetchAndCache();
@@ -272,15 +273,14 @@ async function getJobLog(
272
273
  }
273
274
 
274
275
  async function resolveRepo(
275
- pi: ExtensionAPI,
276
276
  repo: string | undefined,
277
277
  signal: AbortSignal | undefined,
278
278
  cwd: string | undefined,
279
- ): Promise<GhExecResult & { repo?: string }> {
280
- if (repo) return { ok: true, stdout: "", error: null, args: [], repo };
281
- const res = await ghExec(pi, ["repo", "view", "--json", "nameWithOwner"], { cwd, signal });
282
- if (!res.ok) return res;
283
- return { ...res, repo: JSON.parse(res.stdout).nameWithOwner };
279
+ input?: unknown,
280
+ ): Promise<string> {
281
+ if (repo) return repo;
282
+ const stdout = await ghExec(["repo", "view", "--json", "nameWithOwner"], { cwd, signal, input });
283
+ return JSON.parse(stdout).nameWithOwner;
284
284
  }
285
285
 
286
286
  export function statusIcon(conclusion: string | null): string {
@@ -403,7 +403,6 @@ export default function (pi: ExtensionAPI) {
403
403
  const { number, repo } = params as { number: number | string; repo?: string };
404
404
  return toToolResult(
405
405
  await ghExec(
406
- pi,
407
406
  [
408
407
  "issue",
409
408
  "view",
@@ -412,7 +411,7 @@ export default function (pi: ExtensionAPI) {
412
411
  "--json",
413
412
  "title,state,body,author,createdAt,updatedAt,closedAt,url,labels,assignees,comments,milestone,number",
414
413
  ],
415
- { cwd: ctx.cwd, signal },
414
+ { cwd: ctx.cwd, signal, input: params },
416
415
  ),
417
416
  );
418
417
  },
@@ -434,7 +433,7 @@ export default function (pi: ExtensionAPI) {
434
433
  const args = ["issue", "list", ...repoArgs(repo)];
435
434
  if (state) args.push("--state", state);
436
435
  if (limit) args.push("--limit", String(limit));
437
- return toToolResult(await ghExec(pi, args, { cwd: ctx.cwd, signal }));
436
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
438
437
  },
439
438
  });
440
439
 
@@ -452,7 +451,6 @@ export default function (pi: ExtensionAPI) {
452
451
  const { number, repo } = params as { number: number | string; repo?: string };
453
452
  return toToolResult(
454
453
  await ghExec(
455
- pi,
456
454
  [
457
455
  "pr",
458
456
  "view",
@@ -461,7 +459,7 @@ export default function (pi: ExtensionAPI) {
461
459
  "--json",
462
460
  "title,state,body,author,createdAt,updatedAt,mergedAt,mergedBy,headRefName,baseRefName,url,additions,deletions,changedFiles,labels,assignees,reviewRequests,reviews,comments,number",
463
461
  ],
464
- { cwd: ctx.cwd, signal },
462
+ { cwd: ctx.cwd, signal, input: params },
465
463
  ),
466
464
  );
467
465
  },
@@ -485,7 +483,7 @@ export default function (pi: ExtensionAPI) {
485
483
  const args = ["pr", "list", ...repoArgs(repo)];
486
484
  if (state) args.push("--state", state);
487
485
  if (limit) args.push("--limit", String(limit));
488
- return toToolResult(await ghExec(pi, args, { cwd: ctx.cwd, signal }));
486
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
489
487
  },
490
488
  });
491
489
 
@@ -505,7 +503,7 @@ export default function (pi: ExtensionAPI) {
505
503
  repo?: string;
506
504
  };
507
505
  const args = ["pr", "diff", String(number), ...repoArgs(repo)];
508
- return toToolResult(await ghExec(pi, args, { cwd: ctx.cwd, signal }));
506
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
509
507
  },
510
508
  });
511
509
 
@@ -522,9 +520,10 @@ export default function (pi: ExtensionAPI) {
522
520
  async execute(_id, params, signal, _onUpdate, ctx) {
523
521
  const { number, repo } = params as { number: number | string; repo?: string };
524
522
  return toToolResult(
525
- await ghExec(pi, ["pr", "checks", String(number), ...repoArgs(repo)], {
523
+ await ghExec(["pr", "checks", String(number), ...repoArgs(repo)], {
526
524
  cwd: ctx.cwd,
527
525
  signal,
526
+ input: params,
528
527
  }),
529
528
  );
530
529
  },
@@ -555,24 +554,23 @@ export default function (pi: ExtensionAPI) {
555
554
  };
556
555
  let out: string;
557
556
  if (reviews) {
558
- const resolved = await resolveRepo(pi, repo, signal, ctx.cwd);
559
- if (!resolved.ok) return toToolResult(resolved);
557
+ const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
560
558
 
561
- const [commentsRes, reviewsRes] = await Promise.all([
562
- ghExec(pi, ["api", `/repos/${resolved.repo!}/pulls/${String(number)}/comments`], {
559
+ const [comments, reviewsOut] = await Promise.all([
560
+ ghExec(["api", `/repos/${effectiveRepo}/pulls/${String(number)}/comments`], {
563
561
  cwd: ctx.cwd,
564
562
  signal,
563
+ input: params,
565
564
  }),
566
- ghExec(pi, ["api", `/repos/${resolved.repo!}/pulls/${String(number)}/reviews`], {
565
+ ghExec(["api", `/repos/${effectiveRepo}/pulls/${String(number)}/reviews`], {
567
566
  cwd: ctx.cwd,
568
567
  signal,
568
+ input: params,
569
569
  }),
570
570
  ]);
571
- if (!commentsRes.ok) return toToolResult(commentsRes);
572
- if (!reviewsRes.ok) return toToolResult(reviewsRes);
573
571
 
574
- const reviewComments = JSON.parse(commentsRes.stdout);
575
- const reviewSummaries = JSON.parse(reviewsRes.stdout);
572
+ const reviewComments = JSON.parse(comments);
573
+ const reviewSummaries = JSON.parse(reviewsOut);
576
574
 
577
575
  out = JSON.stringify(
578
576
  {
@@ -583,16 +581,14 @@ export default function (pi: ExtensionAPI) {
583
581
  2,
584
582
  );
585
583
  } else {
586
- const res = await ghExec(
587
- pi,
584
+ out = await ghExec(
588
585
  ["pr", "view", String(number), ...repoArgs(repo), "--json", "comments"],
589
586
  {
590
587
  cwd: ctx.cwd,
591
588
  signal,
589
+ input: params,
592
590
  },
593
591
  );
594
- if (!res.ok) return toToolResult(res);
595
- out = res.stdout;
596
592
  }
597
593
  const { text, truncated } = truncate(out);
598
594
  return {
@@ -615,11 +611,11 @@ export default function (pi: ExtensionAPI) {
615
611
  async execute(_id, params, signal, _onUpdate, ctx) {
616
612
  const { number, repo } = params as { number: number | string; repo?: string };
617
613
  return toToolResult(
618
- await ghExec(
619
- pi,
620
- ["issue", "view", String(number), ...repoArgs(repo), "--json", "comments"],
621
- { cwd: ctx.cwd, signal },
622
- ),
614
+ await ghExec(["issue", "view", String(number), ...repoArgs(repo), "--json", "comments"], {
615
+ cwd: ctx.cwd,
616
+ signal,
617
+ input: params,
618
+ }),
623
619
  );
624
620
  },
625
621
  });
@@ -649,7 +645,7 @@ export default function (pi: ExtensionAPI) {
649
645
  if (limit) args.push("--limit", String(limit));
650
646
  if (status) args.push("--status", status);
651
647
  if (workflow) args.push("--workflow", workflow);
652
- return toToolResult(await ghExec(pi, args, { cwd: ctx.cwd, signal }));
648
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
653
649
  },
654
650
  });
655
651
 
@@ -700,16 +696,13 @@ export default function (pi: ExtensionAPI) {
700
696
 
701
697
  // ── Fetch specific step logs ───────────────────────────────────────
702
698
  if (step !== undefined && step !== null) {
703
- const resolved = await resolveRepo(pi, repo, signal, ctx.cwd);
704
- if (!resolved.ok) return toToolResult(resolved);
699
+ const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
705
700
 
706
- const jobsRes = await ghExec(
707
- pi,
708
- ["api", `/repos/${resolved.repo!}/actions/runs/${run_id}/jobs`],
709
- { cwd: ctx.cwd, signal },
701
+ const jobsOut = await ghExec(
702
+ ["api", `/repos/${effectiveRepo}/actions/runs/${run_id}/jobs`],
703
+ { cwd: ctx.cwd, signal, input: params },
710
704
  );
711
- if (!jobsRes.ok) return toToolResult(jobsRes);
712
- const { jobs } = JSON.parse(jobsRes.stdout) as {
705
+ const { jobs } = JSON.parse(jobsOut) as {
713
706
  jobs: Array<{
714
707
  id: number;
715
708
  name: string;
@@ -804,16 +797,14 @@ export default function (pi: ExtensionAPI) {
804
797
  details: {},
805
798
  });
806
799
 
807
- const logRes = await getJobLog(
808
- pi,
800
+ const rawLog = await getJobLog(
809
801
  String(run_id),
810
802
  targetJob.id,
811
- resolved.repo!,
803
+ effectiveRepo,
812
804
  signal,
813
805
  ctx.cwd,
806
+ params,
814
807
  );
815
- if (!logRes.ok) return toToolResult(logRes);
816
- const rawLog = logRes.log!;
817
808
 
818
809
  const stepLog = extractStepFromLog(rawLog, stepNum, targetJob.steps);
819
810
  if (stepLog === null) {
@@ -891,16 +882,14 @@ export default function (pi: ExtensionAPI) {
891
882
  details: {},
892
883
  });
893
884
 
894
- const resolved = await resolveRepo(pi, repo, signal, ctx.cwd);
895
- if (!resolved.ok) return toToolResult(resolved);
885
+ const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
896
886
 
897
- const jobsRes = await ghExec(
898
- pi,
899
- ["api", `/repos/${resolved.repo!}/actions/runs/${run_id}/jobs`],
900
- { cwd: ctx.cwd, signal },
901
- );
902
- if (!jobsRes.ok) return toToolResult(jobsRes);
903
- const { jobs } = JSON.parse(jobsRes.stdout) as {
887
+ const jobsOut = await ghExec(["api", `/repos/${effectiveRepo}/actions/runs/${run_id}/jobs`], {
888
+ cwd: ctx.cwd,
889
+ signal,
890
+ input: params,
891
+ });
892
+ const { jobs } = JSON.parse(jobsOut) as {
904
893
  jobs: Array<{
905
894
  id: number;
906
895
  name: string;
@@ -974,15 +963,14 @@ export default function (pi: ExtensionAPI) {
974
963
  if (failedSteps.length === 0) continue;
975
964
 
976
965
  try {
977
- const logRes = await getJobLog(pi, String(run_id), j.id, resolved.repo!, signal, ctx.cwd);
978
- if (!logRes.ok) {
979
- contents.push({
980
- type: "text",
981
- text: `\n⚠️ Could not auto-fetch logs for ${j.name}: gh ${logRes.args.join(" ")} failed: ${logRes.error}`,
982
- });
983
- continue;
984
- }
985
- const rawLog = logRes.log!;
966
+ const rawLog = await getJobLog(
967
+ String(run_id),
968
+ j.id,
969
+ effectiveRepo,
970
+ signal,
971
+ ctx.cwd,
972
+ params,
973
+ );
986
974
 
987
975
  for (const fs of failedSteps) {
988
976
  if (fetchedCount >= maxFailed) break;
@@ -1047,12 +1035,12 @@ export default function (pi: ExtensionAPI) {
1047
1035
  }),
1048
1036
  async execute(_id, params, signal, _onUpdate, ctx) {
1049
1037
  const { run_id, repo } = params as { run_id: number | string; repo?: string };
1050
- const resolved = await resolveRepo(pi, repo, signal, ctx.cwd);
1051
- if (!resolved.ok) return toToolResult(resolved);
1038
+ const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
1052
1039
  return toToolResult(
1053
- await ghExec(pi, ["api", `/repos/${resolved.repo!}/actions/runs/${run_id}/jobs`], {
1040
+ await ghExec(["api", `/repos/${effectiveRepo}/actions/runs/${run_id}/jobs`], {
1054
1041
  cwd: ctx.cwd,
1055
1042
  signal,
1043
+ input: params,
1056
1044
  }),
1057
1045
  );
1058
1046
  },
@@ -1071,7 +1059,7 @@ export default function (pi: ExtensionAPI) {
1071
1059
  const { repo } = params as { repo?: string };
1072
1060
  const args = ["repo", "view"];
1073
1061
  if (repo) args.push(repo);
1074
- return toToolResult(await ghExec(pi, args, { cwd: ctx.cwd, signal }));
1062
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
1075
1063
  },
1076
1064
  });
1077
1065
 
@@ -1089,7 +1077,7 @@ export default function (pi: ExtensionAPI) {
1089
1077
  const { repo, limit } = params as { repo?: string; limit?: number };
1090
1078
  const args = ["release", "list", ...repoArgs(repo)];
1091
1079
  if (limit) args.push("--limit", String(limit));
1092
- return toToolResult(await ghExec(pi, args, { cwd: ctx.cwd, signal }));
1080
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
1093
1081
  },
1094
1082
  });
1095
1083
 
@@ -1106,9 +1094,10 @@ export default function (pi: ExtensionAPI) {
1106
1094
  async execute(_id, params, signal, _onUpdate, ctx) {
1107
1095
  const { tag, repo } = params as { tag: string; repo?: string };
1108
1096
  return toToolResult(
1109
- await ghExec(pi, ["release", "view", tag, ...repoArgs(repo)], {
1097
+ await ghExec(["release", "view", tag, ...repoArgs(repo)], {
1110
1098
  cwd: ctx.cwd,
1111
1099
  signal,
1100
+ input: params,
1112
1101
  }),
1113
1102
  );
1114
1103
  },
@@ -1144,7 +1133,7 @@ export default function (pi: ExtensionAPI) {
1144
1133
  const args = ["pr", "checks", String(number), ...repoArgs(repo), "--watch"];
1145
1134
  if (fail_fast) args.push("--fail-fast");
1146
1135
 
1147
- const result = await execGh(pi, args, { cwd: ctx.cwd, signal, timeout: 600_000 });
1136
+ const result = await runGh(args, { cwd: ctx.cwd, signal, timeout: 600_000 });
1148
1137
 
1149
1138
  const exitCode = result.code;
1150
1139
  const stdout = result.stdout;
@@ -1191,7 +1180,7 @@ export default function (pi: ExtensionAPI) {
1191
1180
  details: {},
1192
1181
  });
1193
1182
 
1194
- const result = await execGh(pi, ["run", "watch", String(run_id), ...repoArgs(repo)], {
1183
+ const result = await runGh(["run", "watch", String(run_id), ...repoArgs(repo)], {
1195
1184
  cwd: ctx.cwd,
1196
1185
  signal,
1197
1186
  timeout: 600_000,
@@ -1235,9 +1224,9 @@ export default function (pi: ExtensionAPI) {
1235
1224
  limit?: number;
1236
1225
  };
1237
1226
  const args = ["search", "issues", query];
1238
- if (!include_prs) args.push("--type", "issue");
1227
+ if (include_prs) args.push("--include-prs");
1239
1228
  if (limit) args.push("--limit", String(limit));
1240
- return toToolResult(await ghExec(pi, args, { cwd: ctx.cwd, signal }));
1229
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
1241
1230
  },
1242
1231
  });
1243
1232
 
@@ -1258,7 +1247,7 @@ export default function (pi: ExtensionAPI) {
1258
1247
  const { query, limit } = params as { query: string; limit?: number };
1259
1248
  const args = ["search", "prs", query];
1260
1249
  if (limit) args.push("--limit", String(limit));
1261
- return toToolResult(await ghExec(pi, args, { cwd: ctx.cwd, signal }));
1250
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
1262
1251
  },
1263
1252
  });
1264
1253
  }