@trim21/personal-pi-extensions 0.0.133 → 0.0.135

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 +79 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.133",
3
+ "version": "0.0.135",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -43,11 +43,13 @@ interface GhResult {
43
43
  code: number;
44
44
  killed: boolean;
45
45
  combined: string;
46
+ /** Why the process was killed, when `killed` is true. */
47
+ reason?: "timeout" | "abort";
46
48
  }
47
49
 
48
50
  // ── helpers ──────────────────────────────────────────────────────────────────
49
51
 
50
- function runGh(
52
+ export function runGh(
51
53
  args: string[],
52
54
  ctx: { cwd?: string; signal?: AbortSignal; timeout?: number },
53
55
  ): Promise<GhResult> {
@@ -63,11 +65,14 @@ function runGh(
63
65
  let stderr = "";
64
66
  const combined: string[] = [];
65
67
  let killed = false;
68
+ let killReason: "timeout" | "abort" | undefined;
66
69
  let timeoutId: ReturnType<typeof setTimeout> | undefined;
70
+ let onAbort: (() => void) | undefined;
67
71
 
68
- const killProcess = () => {
72
+ const killProcess = (reason: "timeout" | "abort") => {
69
73
  if (!killed) {
70
74
  killed = true;
75
+ killReason = reason;
71
76
  proc.kill("SIGTERM");
72
77
  setTimeout(() => {
73
78
  if (!proc.killed) proc.kill("SIGKILL");
@@ -76,16 +81,21 @@ function runGh(
76
81
  };
77
82
 
78
83
  if (ctx.signal) {
84
+ onAbort = () => killProcess("abort");
79
85
  if (ctx.signal.aborted) {
80
- killProcess();
86
+ killProcess("abort");
81
87
  } else {
82
- ctx.signal.addEventListener("abort", killProcess, { once: true });
88
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
83
89
  }
84
90
  }
85
91
 
86
- const timeout = ctx.timeout ?? 30_000;
92
+ // Default timeout: 10 minutes. Long operations like downloading a CI job's
93
+ // full log routinely take well over 30s, so a short default would kill them
94
+ // mid-transfer; combined with `code ?? 0` that would silently cache a
95
+ // truncated log as success. A killed process must never look successful.
96
+ const timeout = ctx.timeout ?? 600_000;
87
97
  if (timeout > 0) {
88
- timeoutId = setTimeout(killProcess, timeout);
98
+ timeoutId = setTimeout(() => killProcess("timeout"), timeout);
89
99
  }
90
100
 
91
101
  proc.stdout?.on("data", (data: Buffer) => {
@@ -101,18 +111,29 @@ function runGh(
101
111
 
102
112
  proc.on("close", (code) => {
103
113
  if (timeoutId) clearTimeout(timeoutId);
104
- if (ctx.signal) {
105
- ctx.signal.removeEventListener("abort", killProcess);
114
+ if (ctx.signal && onAbort) {
115
+ ctx.signal.removeEventListener("abort", onAbort);
106
116
  }
107
- resolve({ stdout, stderr, code: code ?? 0, killed, combined: combined.join("") });
117
+ resolve({
118
+ stdout,
119
+ stderr,
120
+ // When killed by a signal the close event's code is null; report the
121
+ // process as failed instead of pretending it succeeded. -1 is a
122
+ // sentinel for "did not exit normally" — distinct from a real gh
123
+ // failure exit code (1), which is always in 0-255.
124
+ code: code ?? (killed ? -1 : 0),
125
+ killed,
126
+ combined: combined.join(""),
127
+ reason: killReason,
128
+ });
108
129
  });
109
130
 
110
131
  proc.on("error", () => {
111
132
  if (timeoutId) clearTimeout(timeoutId);
112
- if (ctx.signal) {
113
- ctx.signal.removeEventListener("abort", killProcess);
133
+ if (ctx.signal && onAbort) {
134
+ ctx.signal.removeEventListener("abort", onAbort);
114
135
  }
115
- resolve({ stdout, stderr, code: 1, killed, combined: combined.join("") });
136
+ resolve({ stdout, stderr, code: 1, killed, combined: combined.join(""), reason: killReason });
116
137
  });
117
138
  });
118
139
  }
@@ -131,7 +152,16 @@ export class GhError extends Error {
131
152
 
132
153
  constructor(args: string[], result: GhResult, input?: unknown) {
133
154
  const inputText = input === undefined ? "" : `<input>${JSON.stringify(input)}<input>\n`;
134
- super(`${inputText}<output>${result.combined.trim() || `exit code ${result.code}`}<output>`);
155
+ const killedText = result.killed
156
+ ? result.reason === "timeout"
157
+ ? " (command timed out)"
158
+ : result.reason === "abort"
159
+ ? " (command aborted)"
160
+ : ""
161
+ : "";
162
+ super(
163
+ `${inputText}<output>${result.combined.trim() || `exit code ${result.code}`}${killedText}<output>`,
164
+ );
135
165
  this.name = "GhError";
136
166
  this.args = args;
137
167
  this.code = result.code;
@@ -196,7 +226,7 @@ export async function pollChecksResult<R extends { code: number; stdout: string
196
226
  }
197
227
 
198
228
  /** Run `gh` and return stdout. On non-zero exit, throws a `GhError` carrying the toolcall input and raw command. */
199
- async function ghExec(
229
+ export async function ghExec(
200
230
  args: string[],
201
231
  ctx: { cwd?: string; signal?: AbortSignal; input?: unknown },
202
232
  ): Promise<string> {
@@ -258,12 +288,18 @@ function truncate(
258
288
  * Format a successful gh invocation's stdout into a tool result.
259
289
  * Failures are thrown by `ghExec` as `GhError`, so only the success path lives here.
260
290
  */
261
- function toToolResult(stdout: string): {
291
+ function toToolResult(
292
+ stdout: string,
293
+ input?: unknown,
294
+ ): {
262
295
  content: Array<{ type: "text"; text: string }>;
263
296
  details: Record<string, unknown>;
264
297
  } {
265
298
  const { text, truncated } = truncate(stdout);
266
- return { content: [{ type: "text", text }], details: { truncated } };
299
+ return {
300
+ content: [{ type: "text", text }],
301
+ details: { ...(input !== undefined ? { input } : {}), truncated },
302
+ };
267
303
  }
268
304
 
269
305
  interface ListFilters {
@@ -883,6 +919,7 @@ export default function (pi: ExtensionAPI) {
883
919
  ],
884
920
  { cwd: ctx.cwd, signal, input: params },
885
921
  ),
922
+ params,
886
923
  );
887
924
  },
888
925
  });
@@ -907,6 +944,7 @@ export default function (pi: ExtensionAPI) {
907
944
  async execute(_id, params, signal, _onUpdate, ctx) {
908
945
  return toToolResult(
909
946
  await listGithub("issue", params, { cwd: ctx.cwd, signal, input: params }),
947
+ params,
910
948
  );
911
949
  },
912
950
  });
@@ -935,6 +973,7 @@ export default function (pi: ExtensionAPI) {
935
973
  ],
936
974
  { cwd: ctx.cwd, signal, input: params },
937
975
  ),
976
+ params,
938
977
  );
939
978
  },
940
979
  });
@@ -959,7 +998,10 @@ export default function (pi: ExtensionAPI) {
959
998
  limit: Type.Optional(Type.Number({ description: "Max results (default 30)" })),
960
999
  }),
961
1000
  async execute(_id, params, signal, _onUpdate, ctx) {
962
- return toToolResult(await listGithub("pr", params, { cwd: ctx.cwd, signal, input: params }));
1001
+ return toToolResult(
1002
+ await listGithub("pr", params, { cwd: ctx.cwd, signal, input: params }),
1003
+ params,
1004
+ );
963
1005
  },
964
1006
  });
965
1007
 
@@ -976,7 +1018,7 @@ export default function (pi: ExtensionAPI) {
976
1018
  async execute(_id, params, signal, _onUpdate, ctx) {
977
1019
  const { number, repo } = params;
978
1020
  const args = ["pr", "diff", String(number), ...repoArgs(repo)];
979
- return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
1021
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }), params);
980
1022
  },
981
1023
  });
982
1024
 
@@ -1003,7 +1045,7 @@ export default function (pi: ExtensionAPI) {
1003
1045
  // Anything else is a real error (cancelled, auth, network, ...)
1004
1046
  throw new GhError(args, final, params);
1005
1047
  }
1006
- return toToolResult(final.stdout);
1048
+ return toToolResult(final.stdout, params);
1007
1049
  },
1008
1050
  });
1009
1051
 
@@ -1067,7 +1109,7 @@ export default function (pi: ExtensionAPI) {
1067
1109
  const { text, truncated } = truncate(out);
1068
1110
  return {
1069
1111
  content: [{ type: "text", text }],
1070
- details: { truncated },
1112
+ details: { input: params, truncated },
1071
1113
  };
1072
1114
  },
1073
1115
  });
@@ -1090,6 +1132,7 @@ export default function (pi: ExtensionAPI) {
1090
1132
  signal,
1091
1133
  input: params,
1092
1134
  }),
1135
+ params,
1093
1136
  );
1094
1137
  },
1095
1138
  });
@@ -1114,7 +1157,7 @@ export default function (pi: ExtensionAPI) {
1114
1157
  if (limit) args.push("--limit", String(limit));
1115
1158
  if (status) args.push("--status", status);
1116
1159
  if (workflow) args.push("--workflow", workflow);
1117
- return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
1160
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }), params);
1118
1161
  },
1119
1162
  });
1120
1163
 
@@ -1172,12 +1215,13 @@ export default function (pi: ExtensionAPI) {
1172
1215
  content: [{ type: "text", text: `Fetching job list...` }],
1173
1216
  details: {},
1174
1217
  });
1175
- return renderStepLog(
1218
+ const stepResult = await renderStepLog(
1176
1219
  { runId: String(run_id), job, step, offset, limit },
1177
1220
  jobs,
1178
1221
  fetchJobLog,
1179
1222
  onUpdate,
1180
1223
  );
1224
+ return { ...stepResult, details: { ...stepResult.details, input: params } };
1181
1225
  }
1182
1226
 
1183
1227
  // ── List jobs/steps, with failed step logs expanded ────────────────
@@ -1185,7 +1229,12 @@ export default function (pi: ExtensionAPI) {
1185
1229
  content: [{ type: "text", text: `Fetching job list...` }],
1186
1230
  details: {},
1187
1231
  });
1188
- return renderJobLogs({ runId: String(run_id), job, offset, limit }, jobs, fetchJobLog);
1232
+ const jobsResult = await renderJobLogs(
1233
+ { runId: String(run_id), job, offset, limit },
1234
+ jobs,
1235
+ fetchJobLog,
1236
+ );
1237
+ return { ...jobsResult, details: { ...jobsResult.details, input: params } };
1189
1238
  },
1190
1239
  });
1191
1240
 
@@ -1209,6 +1258,7 @@ export default function (pi: ExtensionAPI) {
1209
1258
  signal,
1210
1259
  input: params,
1211
1260
  }),
1261
+ params,
1212
1262
  );
1213
1263
  },
1214
1264
  });
@@ -1226,7 +1276,7 @@ export default function (pi: ExtensionAPI) {
1226
1276
  const { repo } = params;
1227
1277
  const args = ["repo", "view"];
1228
1278
  if (repo) args.push(repo);
1229
- return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
1279
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }), params);
1230
1280
  },
1231
1281
  });
1232
1282
 
@@ -1244,7 +1294,7 @@ export default function (pi: ExtensionAPI) {
1244
1294
  const { repo, limit } = params;
1245
1295
  const args = ["release", "list", ...repoArgs(repo)];
1246
1296
  if (limit) args.push("--limit", String(limit));
1247
- return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
1297
+ return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }), params);
1248
1298
  },
1249
1299
  });
1250
1300
 
@@ -1266,6 +1316,7 @@ export default function (pi: ExtensionAPI) {
1266
1316
  signal,
1267
1317
  input: params,
1268
1318
  }),
1319
+ params,
1269
1320
  );
1270
1321
  },
1271
1322
  });
@@ -1308,7 +1359,7 @@ export default function (pi: ExtensionAPI) {
1308
1359
  content: [
1309
1360
  { type: "text", text: `## PR #${number} CI Checks - FAILED\n\n${stdout}\n${stderr}` },
1310
1361
  ],
1311
- details: { status: "failure", exitCode },
1362
+ details: { status: "failure", exitCode, input: params },
1312
1363
  };
1313
1364
  }
1314
1365
 
@@ -1318,7 +1369,7 @@ export default function (pi: ExtensionAPI) {
1318
1369
 
1319
1370
  return {
1320
1371
  content: [{ type: "text", text: `## PR #${number} CI Checks - PASSED\n\n${stdout}` }],
1321
- details: { status: "success", exitCode: 0 },
1372
+ details: { status: "success", exitCode: 0, input: params },
1322
1373
  };
1323
1374
  },
1324
1375
  });
@@ -1357,7 +1408,7 @@ export default function (pi: ExtensionAPI) {
1357
1408
  content: [
1358
1409
  { type: "text", text: `## Workflow Run ${run_id} Completed\n\n${result.stdout}` },
1359
1410
  ],
1360
- details: { exitCode: 0 },
1411
+ details: { exitCode: 0, input: params },
1361
1412
  };
1362
1413
  },
1363
1414
  });