@trim21/personal-pi-extensions 0.0.125 → 0.0.127

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 +139 -75
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.125",
3
+ "version": "0.0.127",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -5,10 +5,10 @@
5
5
  *
6
6
  * Tools:
7
7
  * - read-github-issue: Get issue details
8
- * - list-github-issues: List issues
8
+ * - list-github-issues: List or search issues
9
9
  * - read-github-issue-comments: Get issue comments
10
10
  * - read-github-pr: Get PR details
11
- * - list-github-prs: List PRs
11
+ * - list-github-prs: List or search PRs
12
12
  * - read-github-pr-diff: Get PR diff
13
13
  * - read-github-pr-status: Get PR status checks
14
14
  * - read-github-pr-comments: Get PR comments
@@ -20,8 +20,6 @@
20
20
  * - read-github-release: Get release details
21
21
  * - wait-github-pr-checks: Watch PR CI checks
22
22
  * - watch-github-run: Watch a workflow run
23
- * - search-github-issues: Search GitHub issues
24
- * - search-github-prs: Search GitHub pull requests
25
23
  *
26
24
  * Install:
27
25
  * cp gh-readonly.ts ~/.pi/agent/extensions/
@@ -143,6 +141,60 @@ export class GhError extends Error {
143
141
  }
144
142
  }
145
143
 
144
+ /** How long `read-github-pr-status` waits for pending checks to resolve. */
145
+ const POLL_INTERVAL_MS = 30_000;
146
+ const POLL_TIMEOUT_MS = 30 * 60_000;
147
+
148
+ /** Sleep for `ms`, resolving early if `signal` is aborted. */
149
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
150
+ return new Promise((resolve) => {
151
+ if (signal?.aborted) {
152
+ resolve();
153
+ return;
154
+ }
155
+ const onAbort = () => {
156
+ clearTimeout(timer);
157
+ resolve();
158
+ };
159
+ const timer = setTimeout(() => {
160
+ signal?.removeEventListener("abort", onAbort);
161
+ resolve();
162
+ }, ms);
163
+ signal?.addEventListener("abort", onAbort, { once: true });
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Poll a `gh pr checks` query until it reaches a final state.
169
+ *
170
+ * `gh pr checks` exit codes: 0 = all passed, 1 = some failed, 8 = some pending.
171
+ * Pending (8) is polled every `intervalMs` until `timeoutMs` elapses, at which
172
+ * point the current result is returned as-is. Any other code is returned
173
+ * immediately; the caller decides whether it is an error.
174
+ */
175
+ export async function pollChecksResult<R extends { code: number; stdout: string }>(
176
+ query: () => Promise<R>,
177
+ opts: { signal?: AbortSignal; intervalMs?: number; timeoutMs?: number } = {},
178
+ ): Promise<R> {
179
+ const { signal, intervalMs = POLL_INTERVAL_MS, timeoutMs = POLL_TIMEOUT_MS } = opts;
180
+ const deadline = Date.now() + timeoutMs;
181
+ for (;;) {
182
+ if (signal?.aborted) {
183
+ throw new Error("read-github-pr-status aborted");
184
+ }
185
+ const result = await query();
186
+ if (result.code !== 8) {
187
+ // 0 = all passed, 1 = some failed, anything else is a real error
188
+ return result;
189
+ }
190
+ // Still pending — keep waiting unless the overall timeout expired
191
+ if (Date.now() >= deadline) {
192
+ return result;
193
+ }
194
+ await sleep(intervalMs, signal);
195
+ }
196
+ }
197
+
146
198
  /** Run `gh` and return stdout. On non-zero exit, throws a `GhError` carrying the toolcall input and raw command. */
147
199
  async function ghExec(
148
200
  args: string[],
@@ -214,6 +266,55 @@ function toToolResult(stdout: string): {
214
266
  return { content: [{ type: "text", text }], details: { truncated } };
215
267
  }
216
268
 
269
+ interface ListFilters {
270
+ repo?: string;
271
+ keywords?: string;
272
+ state?: string;
273
+ label?: string;
274
+ author?: string;
275
+ assignee?: string;
276
+ milestone?: string;
277
+ limit?: number;
278
+ }
279
+
280
+ /**
281
+ * List or search issues/PRs with structured filters.
282
+ *
283
+ * `gh issue list` / `gh pr list` are used when a repo is available (repo param or
284
+ * current directory), with keywords passed via `--search`. When no repo is given
285
+ * and keywords are present, falls back to `gh search issues` / `gh search prs`
286
+ * with plain keywords — never embedding a `repo:` qualifier in the query string,
287
+ * because `gh` mis-parses `repo:` values followed by spaces.
288
+ */
289
+ async function listGithub(
290
+ kind: "issue" | "pr",
291
+ params: ListFilters,
292
+ ctx: { cwd?: string; signal?: AbortSignal; input?: unknown },
293
+ ): Promise<string> {
294
+ const { repo, keywords, state, label, author, assignee, milestone, limit } = params;
295
+
296
+ if (!repo && keywords) {
297
+ const args = ["search", kind === "issue" ? "issues" : "prs", keywords];
298
+ if (state && state !== "all") args.push("--state", state);
299
+ if (label) args.push("--label", label);
300
+ if (author) args.push("--author", author);
301
+ if (assignee) args.push("--assignee", assignee);
302
+ if (milestone) args.push("--milestone", milestone);
303
+ if (limit) args.push("--limit", String(limit));
304
+ return ghExec(args, ctx);
305
+ }
306
+
307
+ const args = [kind, "list", ...repoArgs(repo)];
308
+ if (state) args.push("--state", state);
309
+ if (keywords) args.push("--search", keywords);
310
+ if (label) args.push("--label", label);
311
+ if (author) args.push("--author", author);
312
+ if (assignee) args.push("--assignee", assignee);
313
+ if (milestone) args.push("--milestone", milestone);
314
+ if (limit) args.push("--limit", String(limit));
315
+ return ghExec(args, ctx);
316
+ }
317
+
217
318
  // ── CI helpers ───────────────────────────────────────────────────────────────
218
319
 
219
320
  export interface StepInfo {
@@ -444,19 +545,23 @@ export default function (pi: ExtensionAPI) {
444
545
  pi.registerTool({
445
546
  name: "list-github-issues",
446
547
  label: "GitHub Issues List",
447
- description: "List GitHub issues with optional filters.",
448
- promptSnippet: "List GitHub issues",
548
+ description:
549
+ "List GitHub issues with optional filters and keyword search. When repo is omitted, searches across GitHub using keywords.",
550
+ promptSnippet: "List or search GitHub issues",
449
551
  parameters: Type.Object({
450
- repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
552
+ repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
553
+ keywords: Type.Optional(Type.String({ description: "Search keywords (free text)" })),
451
554
  state: Type.Optional(Type.String({ description: "open, closed, all (default: open)" })),
555
+ label: Type.Optional(Type.String({ description: "Filter by label" })),
556
+ author: Type.Optional(Type.String({ description: "Filter by author" })),
557
+ assignee: Type.Optional(Type.String({ description: "Filter by assignee" })),
558
+ milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
452
559
  limit: Type.Optional(Type.Number({ description: "Max results (default 30)" })),
453
560
  }),
454
561
  async execute(_id, params, signal, _onUpdate, ctx) {
455
- const { repo, state, limit } = params;
456
- const args = ["issue", "list", ...repoArgs(repo)];
457
- if (state) args.push("--state", state);
458
- if (limit) args.push("--limit", String(limit));
459
- return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
562
+ return toToolResult(
563
+ await listGithub("issue", params, { cwd: ctx.cwd, signal, input: params }),
564
+ );
460
565
  },
461
566
  });
462
567
 
@@ -492,21 +597,23 @@ export default function (pi: ExtensionAPI) {
492
597
  pi.registerTool({
493
598
  name: "list-github-prs",
494
599
  label: "GitHub PRs List",
495
- description: "List GitHub pull requests with optional filters.",
496
- promptSnippet: "List GitHub PRs",
600
+ description:
601
+ "List GitHub pull requests with optional filters and keyword search. When repo is omitted, searches across GitHub using keywords.",
602
+ promptSnippet: "List or search GitHub PRs",
497
603
  parameters: Type.Object({
498
- repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
604
+ repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
605
+ keywords: Type.Optional(Type.String({ description: "Search keywords (free text)" })),
499
606
  state: Type.Optional(
500
607
  Type.String({ description: "open, closed, merged, all (default: open)" }),
501
608
  ),
609
+ label: Type.Optional(Type.String({ description: "Filter by label" })),
610
+ author: Type.Optional(Type.String({ description: "Filter by author" })),
611
+ assignee: Type.Optional(Type.String({ description: "Filter by assignee" })),
612
+ milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
502
613
  limit: Type.Optional(Type.Number({ description: "Max results (default 30)" })),
503
614
  }),
504
615
  async execute(_id, params, signal, _onUpdate, ctx) {
505
- const { repo, state, limit } = params;
506
- const args = ["pr", "list", ...repoArgs(repo)];
507
- if (state) args.push("--state", state);
508
- if (limit) args.push("--limit", String(limit));
509
- return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
616
+ return toToolResult(await listGithub("pr", params, { cwd: ctx.cwd, signal, input: params }));
510
617
  },
511
618
  });
512
619
 
@@ -539,13 +646,18 @@ export default function (pi: ExtensionAPI) {
539
646
  }),
540
647
  async execute(_id, params, signal, _onUpdate, ctx) {
541
648
  const { number, repo } = params;
542
- return toToolResult(
543
- await ghExec(["pr", "checks", String(number), ...repoArgs(repo)], {
544
- cwd: ctx.cwd,
545
- signal,
546
- input: params,
547
- }),
548
- );
649
+ const args = ["pr", "checks", String(number), ...repoArgs(repo)];
650
+
651
+ // Pending is not an error — poll until checks fail or all pass.
652
+ const final = await pollChecksResult(() => runGh(args, { cwd: ctx.cwd, signal }), {
653
+ signal,
654
+ });
655
+
656
+ if (final.code !== 0 && final.code !== 1 && final.code !== 8) {
657
+ // Anything else is a real error (cancelled, auth, network, ...)
658
+ throw new GhError(args, final, params);
659
+ }
660
+ return toToolResult(final.stdout);
549
661
  },
550
662
  });
551
663
 
@@ -1172,52 +1284,4 @@ export default function (pi: ExtensionAPI) {
1172
1284
  };
1173
1285
  },
1174
1286
  });
1175
-
1176
- // ── search-github-issues ───────────────────────────────────────────────────
1177
- pi.registerTool({
1178
- name: "search-github-issues",
1179
- label: "GitHub Issue Search",
1180
- description: "Search GitHub issues using GitHub search syntax.",
1181
- promptSnippet: "Search GitHub issues",
1182
- parameters: Type.Object({
1183
- query: Type.String({
1184
- description:
1185
- "GitHub search syntax (e.g. 'repo:owner/name keyword', 'is:open label:bug'). Do NOT include 'type:issue' or 'type:pr' qualifiers.",
1186
- }),
1187
- include_prs: Type.Optional(
1188
- Type.Boolean({
1189
- description: "Whether to include pull requests in results (default: false)",
1190
- }),
1191
- ),
1192
- limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
1193
- }),
1194
- async execute(_id, params, signal, _onUpdate, ctx) {
1195
- const { query, include_prs, limit } = params;
1196
- const args = ["search", "issues", query];
1197
- if (include_prs) args.push("--include-prs");
1198
- if (limit) args.push("--limit", String(limit));
1199
- return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
1200
- },
1201
- });
1202
-
1203
- // ── search-github-prs ──────────────────────────────────────────────────────
1204
- pi.registerTool({
1205
- name: "search-github-prs",
1206
- label: "GitHub PR Search",
1207
- description: "Search GitHub pull requests using GitHub search syntax.",
1208
- promptSnippet: "Search GitHub PRs",
1209
- parameters: Type.Object({
1210
- query: Type.String({
1211
- description:
1212
- "GitHub search syntax (e.g. 'repo:owner/name keyword', 'is:open label:bug'). Do NOT include 'type:issue' or 'type:pr' qualifiers.",
1213
- }),
1214
- limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
1215
- }),
1216
- async execute(_id, params, signal, _onUpdate, ctx) {
1217
- const { query, limit } = params;
1218
- const args = ["search", "prs", query];
1219
- if (limit) args.push("--limit", String(limit));
1220
- return toToolResult(await ghExec(args, { cwd: ctx.cwd, signal, input: params }));
1221
- },
1222
- });
1223
1287
  }