@trim21/personal-pi-extensions 0.1.557 → 0.1.558
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/base.ts +851 -0
- package/src/gh/index.ts +108 -0
- package/src/gh/tools/download-release-assets.ts +231 -0
- package/src/gh/tools/get-workflow-jobs.ts +48 -0
- package/src/gh/tools/list-issues.ts +61 -0
- package/src/gh/tools/list-prs.ts +61 -0
- package/src/gh/tools/list-releases.ts +28 -0
- package/src/gh/tools/list-workflow-runs.ts +34 -0
- package/src/gh/tools/read-ci-logs.ts +429 -0
- package/src/gh/tools/read-issue-comments.ts +30 -0
- package/src/gh/tools/read-issue.ts +36 -0
- package/src/gh/tools/read-pr-comments.ts +76 -0
- package/src/gh/tools/read-pr-diff.ts +27 -0
- package/src/gh/tools/read-pr-status.ts +70 -0
- package/src/gh/tools/read-pr.ts +36 -0
- package/src/gh/tools/read-release.ts +30 -0
- package/src/gh/tools/read-repo.ts +27 -0
- package/src/gh/tools/wait-commit-checks.ts +88 -0
- package/src/gh/tools/wait-pr-checks.ts +80 -0
- package/src/gh/tools/watch-run.ts +45 -0
- package/src/gh-readonly.ts +6 -2157
package/src/gh/index.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Read-Only Tools Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides individual read-only tools for GitHub operations using the system's `gh` CLI.
|
|
5
|
+
*
|
|
6
|
+
* Layout:
|
|
7
|
+
* - `base.ts` shared helpers (gh subprocess, result shaping, REST client, checks watch)
|
|
8
|
+
* - `tools/<name>.ts` one tool per file, each exporting `add<Name>Tool(gh, pi)`
|
|
9
|
+
* - `index.ts` this file: availability checks + tool registration
|
|
10
|
+
*
|
|
11
|
+
* Tools:
|
|
12
|
+
* - read-github-issue: Get issue details
|
|
13
|
+
* - list-github-issues: List or search issues
|
|
14
|
+
* - read-github-issue-comments: Get issue comments
|
|
15
|
+
* - read-github-pr: Get PR details
|
|
16
|
+
* - list-github-prs: List or search PRs
|
|
17
|
+
* - read-github-pr-diff: Get PR diff
|
|
18
|
+
* - read-github-pr-status: Get PR status checks
|
|
19
|
+
* - read-github-pr-comments: Get PR comments
|
|
20
|
+
* - read-github-ci-logs: Get CI workflow run logs
|
|
21
|
+
* - list-github-workflow-runs: List workflow runs
|
|
22
|
+
* - get-github-workflow-jobs: Get workflow run jobs
|
|
23
|
+
* - read-github-repo: Get repo info
|
|
24
|
+
* - list-github-releases: List releases
|
|
25
|
+
* - read-github-release: Get release details
|
|
26
|
+
* - download-github-release-assets: Download a release's assets with gh credentials
|
|
27
|
+
* - wait-github-pr-checks: Watch PR CI checks
|
|
28
|
+
* - wait-github-commit-checks: Watch CI checks of a commit (no PR required)
|
|
29
|
+
* - watch-github-run: Watch a workflow run
|
|
30
|
+
*
|
|
31
|
+
* Proxy (for the gh CLI and for the octokit-backed search/checks requests):
|
|
32
|
+
* ~/.pi/agent/proxy.json: { "proxy": "http://127.0.0.1:7890", "noProxy": "localhost" }
|
|
33
|
+
* HTTPS_PROXY / HTTP_PROXY / ALL_PROXY and NO_PROXY are used instead for the
|
|
34
|
+
* fields the config file leaves out. The config is read once per process.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
38
|
+
|
|
39
|
+
import { GhClient, isGhAvailable } from "./base.js";
|
|
40
|
+
import { addDownloadReleaseAssetsTool } from "./tools/download-release-assets.js";
|
|
41
|
+
import { addGetWorkflowJobsTool } from "./tools/get-workflow-jobs.js";
|
|
42
|
+
import { addListIssuesTool } from "./tools/list-issues.js";
|
|
43
|
+
import { addListPrsTool } from "./tools/list-prs.js";
|
|
44
|
+
import { addListReleasesTool } from "./tools/list-releases.js";
|
|
45
|
+
import { addListWorkflowRunsTool } from "./tools/list-workflow-runs.js";
|
|
46
|
+
import { addReadCiLogsTool } from "./tools/read-ci-logs.js";
|
|
47
|
+
import { addReadIssueTool } from "./tools/read-issue.js";
|
|
48
|
+
import { addReadIssueCommentsTool } from "./tools/read-issue-comments.js";
|
|
49
|
+
import { addReadPrTool } from "./tools/read-pr.js";
|
|
50
|
+
import { addReadPrCommentsTool } from "./tools/read-pr-comments.js";
|
|
51
|
+
import { addReadPrDiffTool } from "./tools/read-pr-diff.js";
|
|
52
|
+
import { addReadPrStatusTool } from "./tools/read-pr-status.js";
|
|
53
|
+
import { addReadReleaseTool } from "./tools/read-release.js";
|
|
54
|
+
import { addReadRepoTool } from "./tools/read-repo.js";
|
|
55
|
+
import { addWaitCommitChecksTool } from "./tools/wait-commit-checks.js";
|
|
56
|
+
import { addWaitPrChecksTool } from "./tools/wait-pr-checks.js";
|
|
57
|
+
import { addWatchRunTool } from "./tools/watch-run.js";
|
|
58
|
+
|
|
59
|
+
// Public API re-exports (tests import these from the extension entry).
|
|
60
|
+
export * from "./base.js";
|
|
61
|
+
export * from "./tools/download-release-assets.js";
|
|
62
|
+
export * from "./tools/read-ci-logs.js";
|
|
63
|
+
export * from "./tools/read-pr-status.js";
|
|
64
|
+
|
|
65
|
+
export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
66
|
+
// Windows 上禁用:gh 可执行文件的探测(无扩展名 + POSIX 路径)与进程
|
|
67
|
+
// 管理(SIGTERM 信号语义)都是 POSIX 假设,不做 Windows 适配。
|
|
68
|
+
if (process.platform === "win32") {
|
|
69
|
+
pi.on("session_start", (_event, ctx) => {
|
|
70
|
+
ctx.ui.notify("gh-readonly tools are disabled on Windows.", "warning");
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fail fast: the `gh` CLI is the only backend for these tools. Without it the
|
|
76
|
+
// extension registers nothing and reports the problem at session start, so
|
|
77
|
+
// the user gets one clear error instead of a dozen failing tool calls.
|
|
78
|
+
if (!isGhAvailable()) {
|
|
79
|
+
pi.on("session_start", (_event, ctx) => {
|
|
80
|
+
ctx.ui.notify(
|
|
81
|
+
"gh CLI not found in PATH: GitHub read-only tools are disabled. Install GitHub CLI (https://cli.github.com/) and reload the session.",
|
|
82
|
+
"error",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const gh = new GhClient();
|
|
89
|
+
|
|
90
|
+
addReadIssueTool(gh, pi);
|
|
91
|
+
addListIssuesTool(gh, pi);
|
|
92
|
+
addReadIssueCommentsTool(gh, pi);
|
|
93
|
+
addReadPrTool(gh, pi);
|
|
94
|
+
addListPrsTool(gh, pi);
|
|
95
|
+
addReadPrDiffTool(gh, pi);
|
|
96
|
+
addReadPrStatusTool(gh, pi);
|
|
97
|
+
addReadPrCommentsTool(gh, pi);
|
|
98
|
+
addReadCiLogsTool(gh, pi);
|
|
99
|
+
addListWorkflowRunsTool(gh, pi);
|
|
100
|
+
addGetWorkflowJobsTool(gh, pi);
|
|
101
|
+
addReadRepoTool(gh, pi);
|
|
102
|
+
addListReleasesTool(gh, pi);
|
|
103
|
+
addReadReleaseTool(gh, pi);
|
|
104
|
+
addDownloadReleaseAssetsTool(gh, pi);
|
|
105
|
+
addWaitPrChecksTool(gh, pi);
|
|
106
|
+
addWaitCommitChecksTool(gh, pi);
|
|
107
|
+
addWatchRunTool(gh, pi);
|
|
108
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { mkdir, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import { Value } from "typebox/value";
|
|
8
|
+
|
|
9
|
+
import { type ToolPendant } from "../../lib/pendant.js";
|
|
10
|
+
import {
|
|
11
|
+
type GhClient,
|
|
12
|
+
GhError,
|
|
13
|
+
ghExec,
|
|
14
|
+
repoArgs,
|
|
15
|
+
resolveRepo,
|
|
16
|
+
splitRepo,
|
|
17
|
+
subtitlePendant,
|
|
18
|
+
type ToolCall,
|
|
19
|
+
type ToolResult,
|
|
20
|
+
} from "../base.js";
|
|
21
|
+
|
|
22
|
+
interface ReleaseDownloadParams {
|
|
23
|
+
repo?: string;
|
|
24
|
+
tag?: string;
|
|
25
|
+
pattern?: string;
|
|
26
|
+
archive?: "zip" | "tar.gz";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `gh release view --json tagName,assets` 里本工具真正读取的字段。 */
|
|
30
|
+
const releaseViewSchema = Type.Object({
|
|
31
|
+
tagName: Type.String(),
|
|
32
|
+
assets: Type.Array(Type.Object({ name: Type.String(), size: Type.Number() })),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** One regular file in a release's download directory. */
|
|
36
|
+
interface ReleaseFile {
|
|
37
|
+
name: string;
|
|
38
|
+
path: string;
|
|
39
|
+
bytes: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Directory the release assets of one release are downloaded into:
|
|
44
|
+
* `~/.cache/pi/github/releases/<owner>/<repo>/<tag>/`.
|
|
45
|
+
*
|
|
46
|
+
* A tag is a git ref name and may contain `/`; only the characters that are safe
|
|
47
|
+
* in one path segment survive, so a tag can never escape its own directory.
|
|
48
|
+
*/
|
|
49
|
+
export function releaseAssetDir(repo: string, tag: string): string {
|
|
50
|
+
const { owner, repo: name } = splitRepo(repo);
|
|
51
|
+
const safeTag = tag.replaceAll(/[^A-Za-z0-9._+-]/g, "_").replace(/^\.+$/, "_");
|
|
52
|
+
return join(homedir(), ".cache", "pi", "github", "releases", owner, name, safeTag);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Split the comma-separated `pattern` toolcall parameter into gh pattern values. */
|
|
56
|
+
export function releasePatterns(pattern: string | undefined): string[] {
|
|
57
|
+
if (pattern === undefined) return [];
|
|
58
|
+
return pattern
|
|
59
|
+
.split(",")
|
|
60
|
+
.map((value) => value.trim())
|
|
61
|
+
.filter((value) => value !== "");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The `gh release download` argv. `--skip-existing` is always on: the download
|
|
66
|
+
* directory is a cache, and rewriting a file that is already there would pull
|
|
67
|
+
* the ground out from under anything reading it.
|
|
68
|
+
*/
|
|
69
|
+
export function releaseDownloadArgs(options: {
|
|
70
|
+
tag: string;
|
|
71
|
+
repo: string;
|
|
72
|
+
dir: string;
|
|
73
|
+
patterns: readonly string[];
|
|
74
|
+
archive?: "zip" | "tar.gz";
|
|
75
|
+
}): string[] {
|
|
76
|
+
const { tag, repo, dir, patterns, archive } = options;
|
|
77
|
+
const args = ["release", "download", tag, ...repoArgs(repo)];
|
|
78
|
+
if (archive !== undefined) args.push("--archive", archive);
|
|
79
|
+
for (const pattern of patterns) args.push("--pattern", pattern);
|
|
80
|
+
args.push("--dir", dir, "--skip-existing");
|
|
81
|
+
return args;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Regular files directly inside `dir`, with their sizes, sorted by name. */
|
|
85
|
+
async function listReleaseFiles(dir: string): Promise<ReleaseFile[]> {
|
|
86
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
87
|
+
const files: ReleaseFile[] = [];
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (!entry.isFile()) continue;
|
|
90
|
+
const path = join(dir, entry.name);
|
|
91
|
+
const info = await stat(path);
|
|
92
|
+
files.push({ name: entry.name, path, bytes: info.size });
|
|
93
|
+
}
|
|
94
|
+
return files.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `gh release download` answers this exact message when a `--pattern` matched no
|
|
99
|
+
* asset. It is the only signal the CLI offers, so the enrichment below degrades
|
|
100
|
+
* to gh's own error (still thrown) if the wording ever changes.
|
|
101
|
+
*/
|
|
102
|
+
const GH_NO_ASSET_MATCH = "no assets match the file pattern";
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `download-github-release-assets`: fetch a release's assets (or source archive)
|
|
106
|
+
* into `releaseAssetDir` with the gh credentials, so private repositories work
|
|
107
|
+
* and the shell sandbox's network limits do not apply.
|
|
108
|
+
*
|
|
109
|
+
* The tag is resolved through `gh release view` before downloading: a tag that
|
|
110
|
+
* does not exist and a pattern that matched nothing are different answers, and
|
|
111
|
+
* the release's own asset names are what the model needs to fix the second one.
|
|
112
|
+
*/
|
|
113
|
+
export async function downloadReleaseAssets(
|
|
114
|
+
call: ToolCall<ReleaseDownloadParams>,
|
|
115
|
+
): Promise<ToolResult> {
|
|
116
|
+
const { params, ctx, signal } = call;
|
|
117
|
+
const patterns = releasePatterns(params.pattern);
|
|
118
|
+
if (params.archive !== undefined && patterns.length > 0) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
"pattern and archive are mutually exclusive (pick asset globs or the source archive)",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const effectiveRepo = await resolveRepo(params.repo, signal, ctx.cwd, params);
|
|
125
|
+
const view = Value.Parse(
|
|
126
|
+
releaseViewSchema,
|
|
127
|
+
JSON.parse(
|
|
128
|
+
await ghExec(
|
|
129
|
+
[
|
|
130
|
+
"release",
|
|
131
|
+
"view",
|
|
132
|
+
...(params.tag === undefined ? [] : [params.tag]),
|
|
133
|
+
...repoArgs(effectiveRepo),
|
|
134
|
+
"--json",
|
|
135
|
+
"tagName,assets",
|
|
136
|
+
],
|
|
137
|
+
{ cwd: ctx.cwd, signal, input: params },
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const assetNames = view.assets.map((asset) => asset.name);
|
|
143
|
+
const dir = releaseAssetDir(effectiveRepo, view.tagName);
|
|
144
|
+
await mkdir(dir, { recursive: true });
|
|
145
|
+
try {
|
|
146
|
+
await ghExec(
|
|
147
|
+
releaseDownloadArgs({
|
|
148
|
+
tag: view.tagName,
|
|
149
|
+
repo: effectiveRepo,
|
|
150
|
+
dir,
|
|
151
|
+
patterns,
|
|
152
|
+
...(params.archive !== undefined && { archive: params.archive }),
|
|
153
|
+
}),
|
|
154
|
+
{ cwd: ctx.cwd, signal, input: params },
|
|
155
|
+
);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// gh names the fault but not the choices; the release's asset list turns a
|
|
158
|
+
// dead end into the next toolcall.
|
|
159
|
+
if (error instanceof GhError && error.stderr.includes(GH_NO_ASSET_MATCH)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`no asset of ${effectiveRepo}@${view.tagName} matched ${JSON.stringify(patterns)}; the release has: ${assetNames.join(", ") || "(no assets)"}`,
|
|
162
|
+
{ cause: error },
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const files = await listReleaseFiles(dir);
|
|
169
|
+
const payload = { repo: effectiveRepo, tag: view.tagName, dir, files };
|
|
170
|
+
const pendant: ToolPendant | undefined = subtitlePendant(
|
|
171
|
+
{ repo: effectiveRepo, tag: view.tagName },
|
|
172
|
+
"tag",
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (files.length === 0 && params.archive === undefined) {
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: `Nothing to download from ${effectiveRepo}@${view.tagName}: the release has no assets (try archive for the source tarball)`,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
details: {
|
|
184
|
+
...payload,
|
|
185
|
+
available_assets: assetNames,
|
|
186
|
+
input: params,
|
|
187
|
+
...(pendant && { pendant }),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
194
|
+
details: { ...payload, input: params, ...(pendant && { pendant }) },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function addDownloadReleaseAssetsTool(_gh: GhClient, pi: ExtensionAPI) {
|
|
199
|
+
pi.registerTool({
|
|
200
|
+
name: "download-github-release-assets",
|
|
201
|
+
label: "GitHub Release Download",
|
|
202
|
+
description:
|
|
203
|
+
"Download a GitHub release's assets (or its source archive) into " +
|
|
204
|
+
"~/.cache/pi/github/releases/<owner>/<repo>/<tag>/ using the gh CLI's credentials, " +
|
|
205
|
+
"so private repositories and large binaries work where a plain HTTP fetch cannot. " +
|
|
206
|
+
"Files already in that directory are kept, never re-fetched. The result is the JSON " +
|
|
207
|
+
"summary {repo, tag, dir, files:[{name, path, bytes}]} listing everything now in the " +
|
|
208
|
+
"directory; file contents are not echoed. Read the entries you need from `path`.",
|
|
209
|
+
promptSnippet: "Download GitHub release assets",
|
|
210
|
+
parameters: Type.Object({
|
|
211
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
|
|
212
|
+
tag: Type.Optional(
|
|
213
|
+
Type.String({ description: "Release tag (defaults to the latest release)" }),
|
|
214
|
+
),
|
|
215
|
+
pattern: Type.Optional(
|
|
216
|
+
Type.String({
|
|
217
|
+
description:
|
|
218
|
+
'Comma-separated glob patterns for asset names, e.g. "*.tar.gz,*.deb" (default: every asset)',
|
|
219
|
+
}),
|
|
220
|
+
),
|
|
221
|
+
archive: Type.Optional(
|
|
222
|
+
Type.Union([Type.Literal("zip"), Type.Literal("tar.gz")], {
|
|
223
|
+
description: "Download the release's source archive instead of its assets",
|
|
224
|
+
}),
|
|
225
|
+
),
|
|
226
|
+
}),
|
|
227
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
228
|
+
return downloadReleaseAssets({ params, ctx, signal });
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type GhClient,
|
|
6
|
+
resolveRepo,
|
|
7
|
+
splitRepo,
|
|
8
|
+
subtitlePendant,
|
|
9
|
+
type ToolCall,
|
|
10
|
+
type ToolResult,
|
|
11
|
+
toPositiveId,
|
|
12
|
+
toToolResult,
|
|
13
|
+
} from "../base.js";
|
|
14
|
+
|
|
15
|
+
interface RunIdParams {
|
|
16
|
+
run_id: number | string;
|
|
17
|
+
repo?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The toolcall handler behind `get-github-workflow-jobs`: every job of a run, all pages. */
|
|
21
|
+
async function workflowJobs(gh: GhClient, call: ToolCall<RunIdParams>): Promise<ToolResult> {
|
|
22
|
+
const { params, ctx, signal } = call;
|
|
23
|
+
const runId = toPositiveId(params.run_id, "run_id");
|
|
24
|
+
const effectiveRepo = await resolveRepo(params.repo, signal, ctx.cwd, params);
|
|
25
|
+
const { owner, repo: name } = splitRepo(effectiveRepo);
|
|
26
|
+
|
|
27
|
+
const jobs = await gh.checks.runJobs(owner, name, runId, signal);
|
|
28
|
+
const result = toToolResult(JSON.stringify({ total_count: jobs.length, jobs }), params);
|
|
29
|
+
result.details.pendant = subtitlePendant(params, "run_id");
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function addGetWorkflowJobsTool(gh: GhClient, pi: ExtensionAPI) {
|
|
34
|
+
pi.registerTool({
|
|
35
|
+
name: "get-github-workflow-jobs",
|
|
36
|
+
label: "GitHub Workflow Jobs",
|
|
37
|
+
description:
|
|
38
|
+
"Get every job of a workflow run as JSON {total_count, jobs:[{id, run_id, run_url, name, status, conclusion, html_url, steps:[{name, number, status, conclusion, started_at}]}]}. Paginated server-side, so runs with more than 30 jobs return all of them. Use the `id` with read-github-ci-logs after read-github-pr-status / wait-github-commit-checks did not already give you a job id.",
|
|
39
|
+
promptSnippet: "Get GitHub workflow run jobs",
|
|
40
|
+
parameters: Type.Object({
|
|
41
|
+
run_id: Type.Union([Type.Number(), Type.String()], { description: "Workflow run ID" }),
|
|
42
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
43
|
+
}),
|
|
44
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
45
|
+
return workflowJobs(gh, { params, ctx, signal, onUpdate });
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type GhClient,
|
|
6
|
+
type ListFilters,
|
|
7
|
+
listGithub,
|
|
8
|
+
searchList,
|
|
9
|
+
subtitlePendant,
|
|
10
|
+
type ToolCall,
|
|
11
|
+
type ToolResult,
|
|
12
|
+
toToolResult,
|
|
13
|
+
} from "../base.js";
|
|
14
|
+
|
|
15
|
+
async function listIssues(gh: GhClient, call: ToolCall<ListFilters>): Promise<ToolResult> {
|
|
16
|
+
const { params, ctx, signal } = call;
|
|
17
|
+
const result = toToolResult(
|
|
18
|
+
params.keywords
|
|
19
|
+
? await searchList("issue", params, gh.search)
|
|
20
|
+
: await listGithub("issue", params, { cwd: ctx.cwd, signal, input: params }),
|
|
21
|
+
params,
|
|
22
|
+
);
|
|
23
|
+
result.details.pendant = subtitlePendant(params);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function addListIssuesTool(gh: GhClient, pi: ExtensionAPI) {
|
|
28
|
+
pi.registerTool({
|
|
29
|
+
name: "list-github-issues",
|
|
30
|
+
label: "GitHub Issues List",
|
|
31
|
+
description:
|
|
32
|
+
'List GitHub issues with optional filters and keyword search. When repo is omitted, keyword search runs across GitHub. Keyword search defaults to open issues — pass state="all" to include closed ones. Set fields to choose the columns of each result row.',
|
|
33
|
+
promptSnippet: "List or search GitHub issues",
|
|
34
|
+
parameters: Type.Object({
|
|
35
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
|
|
36
|
+
keywords: Type.Optional(Type.String({ description: "Search keywords (free text)" })),
|
|
37
|
+
state: Type.Optional(
|
|
38
|
+
Type.String({
|
|
39
|
+
description:
|
|
40
|
+
"open, closed, all (default: open; all applies to keyword search and covers closed too)",
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
label: Type.Optional(Type.String({ description: "Filter by label" })),
|
|
44
|
+
author: Type.Optional(Type.String({ description: "Filter by author" })),
|
|
45
|
+
assignee: Type.Optional(
|
|
46
|
+
Type.String({ description: "Filter by assignee (@me for yourself)" }),
|
|
47
|
+
),
|
|
48
|
+
milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
|
|
49
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),
|
|
50
|
+
fields: Type.Optional(
|
|
51
|
+
Type.String({
|
|
52
|
+
description:
|
|
53
|
+
"Comma-separated columns for keyword-search rows (default: number,state,title,labels,updatedAt; adds repo when no repo given). Valid: number,state,title,url,author,labels,milestone,assignees,comments,repo,createdAt,updatedAt,closedAt",
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
}),
|
|
57
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
58
|
+
return listIssues(gh, { params, ctx, signal, onUpdate });
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type GhClient,
|
|
6
|
+
type ListFilters,
|
|
7
|
+
listGithub,
|
|
8
|
+
searchList,
|
|
9
|
+
subtitlePendant,
|
|
10
|
+
type ToolCall,
|
|
11
|
+
type ToolResult,
|
|
12
|
+
toToolResult,
|
|
13
|
+
} from "../base.js";
|
|
14
|
+
|
|
15
|
+
async function listPrs(gh: GhClient, call: ToolCall<ListFilters>): Promise<ToolResult> {
|
|
16
|
+
const { params, ctx, signal } = call;
|
|
17
|
+
const result = toToolResult(
|
|
18
|
+
params.keywords
|
|
19
|
+
? await searchList("pr", params, gh.search)
|
|
20
|
+
: await listGithub("pr", params, { cwd: ctx.cwd, signal, input: params }),
|
|
21
|
+
params,
|
|
22
|
+
);
|
|
23
|
+
result.details.pendant = subtitlePendant(params);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function addListPrsTool(gh: GhClient, pi: ExtensionAPI) {
|
|
28
|
+
pi.registerTool({
|
|
29
|
+
name: "list-github-prs",
|
|
30
|
+
label: "GitHub PRs List",
|
|
31
|
+
description:
|
|
32
|
+
'List GitHub pull requests with optional filters and keyword search. When repo is omitted, keyword search runs across GitHub. Keyword search defaults to open PRs — pass state="merged", state="closed" (merged excluded) or state="all" to broaden. Set fields to choose the columns of each result row.',
|
|
33
|
+
promptSnippet: "List or search GitHub PRs",
|
|
34
|
+
parameters: Type.Object({
|
|
35
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
|
|
36
|
+
keywords: Type.Optional(Type.String({ description: "Search keywords (free text)" })),
|
|
37
|
+
state: Type.Optional(
|
|
38
|
+
Type.String({
|
|
39
|
+
description:
|
|
40
|
+
"open, closed, merged, all (default: open; all applies to keyword search and covers open + closed + merged)",
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
label: Type.Optional(Type.String({ description: "Filter by label" })),
|
|
44
|
+
author: Type.Optional(Type.String({ description: "Filter by author" })),
|
|
45
|
+
assignee: Type.Optional(
|
|
46
|
+
Type.String({ description: "Filter by assignee (@me for yourself)" }),
|
|
47
|
+
),
|
|
48
|
+
milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
|
|
49
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),
|
|
50
|
+
fields: Type.Optional(
|
|
51
|
+
Type.String({
|
|
52
|
+
description:
|
|
53
|
+
"Comma-separated columns for keyword-search rows (default: number,state,title,labels,updatedAt; adds repo when no repo given). Valid: number,state,title,url,author,labels,milestone,assignees,comments,repo,createdAt,updatedAt,closedAt,mergedAt",
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
}),
|
|
57
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
58
|
+
return listPrs(gh, { params, ctx, signal, onUpdate });
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
import { type GhClient, ghExec, repoArgs, subtitlePendant, toToolResult } from "../base.js";
|
|
5
|
+
|
|
6
|
+
export function addListReleasesTool(_gh: GhClient, pi: ExtensionAPI) {
|
|
7
|
+
pi.registerTool({
|
|
8
|
+
name: "list-github-releases",
|
|
9
|
+
label: "GitHub Releases List",
|
|
10
|
+
description: "List GitHub releases.",
|
|
11
|
+
promptSnippet: "List GitHub releases",
|
|
12
|
+
parameters: Type.Object({
|
|
13
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
14
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
|
|
15
|
+
}),
|
|
16
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
17
|
+
const { repo, limit } = params;
|
|
18
|
+
const args = ["release", "list", ...repoArgs(repo)];
|
|
19
|
+
if (limit) args.push("--limit", String(limit));
|
|
20
|
+
const result = toToolResult(
|
|
21
|
+
await ghExec(args, { cwd: ctx.cwd, signal, input: params }),
|
|
22
|
+
params,
|
|
23
|
+
);
|
|
24
|
+
result.details.pendant = subtitlePendant(params);
|
|
25
|
+
return result;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
|
|
4
|
+
import { type GhClient, ghExec, repoArgs, subtitlePendant, toToolResult } from "../base.js";
|
|
5
|
+
|
|
6
|
+
export function addListWorkflowRunsTool(_gh: GhClient, pi: ExtensionAPI) {
|
|
7
|
+
pi.registerTool({
|
|
8
|
+
name: "list-github-workflow-runs",
|
|
9
|
+
label: "GitHub Workflow Runs",
|
|
10
|
+
description: "List GitHub Actions workflow runs.",
|
|
11
|
+
promptSnippet: "List GitHub workflow runs",
|
|
12
|
+
parameters: Type.Object({
|
|
13
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
14
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })),
|
|
15
|
+
status: Type.Optional(
|
|
16
|
+
Type.String({ description: "Filter by status: success, failure, cancelled, etc." }),
|
|
17
|
+
),
|
|
18
|
+
workflow: Type.Optional(Type.String({ description: "Filter by workflow name or file" })),
|
|
19
|
+
}),
|
|
20
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
21
|
+
const { repo, limit, status, workflow } = params;
|
|
22
|
+
const args = ["run", "list", ...repoArgs(repo)];
|
|
23
|
+
if (limit) args.push("--limit", String(limit));
|
|
24
|
+
if (status) args.push("--status", status);
|
|
25
|
+
if (workflow) args.push("--workflow", workflow);
|
|
26
|
+
const result = toToolResult(
|
|
27
|
+
await ghExec(args, { cwd: ctx.cwd, signal, input: params }),
|
|
28
|
+
params,
|
|
29
|
+
);
|
|
30
|
+
result.details.pendant = subtitlePendant(params);
|
|
31
|
+
return result;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|