@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.
- package/package.json +1 -1
- package/src/gh-readonly.ts +139 -75
package/package.json
CHANGED
package/src/gh-readonly.ts
CHANGED
|
@@ -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:
|
|
448
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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:
|
|
496
|
-
|
|
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
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
}
|