@trim21/personal-pi-extensions 0.1.683 → 0.1.685
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/README.md +8 -6
- package/package.json +1 -1
- package/src/aft/schemas.ts +246 -0
- package/src/aft/tools.ts +275 -235
- package/src/codemode/declarations.ts +3 -3
- package/src/codemode/tool.ts +13 -26
- package/src/gh/base.ts +20 -10
- package/src/gh/render.ts +259 -0
- package/src/gh/schemas.ts +213 -0
- package/src/gh/tools/list-issues.ts +62 -47
- package/src/gh/tools/list-prs.ts +62 -47
- package/src/gh/tools/list-releases.ts +41 -24
- package/src/gh/tools/list-workflow-runs.ts +52 -34
- package/src/gh/tools/read-pr-diff.ts +32 -21
- package/src/gh/tools/read-release.ts +52 -24
- package/src/gh/tools/read-repo.ts +59 -23
- package/src/gh/tools/watch-run.ts +55 -39
- package/src/lib/github.ts +75 -0
- package/src/lib/lsp/inspect-schemas.ts +44 -0
- package/src/lib/lsp/inspect-tool.ts +188 -137
package/src/gh/base.ts
CHANGED
|
@@ -20,10 +20,13 @@ import {
|
|
|
20
20
|
type CommitStatus,
|
|
21
21
|
createGithubChecks,
|
|
22
22
|
createGithubSearch,
|
|
23
|
+
GH_LIST_FIELDS,
|
|
23
24
|
type GithubChecksClient,
|
|
24
25
|
type GithubClientOptions,
|
|
25
26
|
type GithubSearch,
|
|
27
|
+
normalizeGhList,
|
|
26
28
|
renderHits,
|
|
29
|
+
type SearchHit,
|
|
27
30
|
} from "../lib/github.js";
|
|
28
31
|
import { parseWithSchema } from "../lib/parse-with-schema.js";
|
|
29
32
|
import { type ToolPendant } from "../lib/pendant.js";
|
|
@@ -356,25 +359,32 @@ export function listGithubArgs(kind: "issue" | "pr", params: ListFilters): strin
|
|
|
356
359
|
return args;
|
|
357
360
|
}
|
|
358
361
|
|
|
359
|
-
|
|
362
|
+
/**
|
|
363
|
+
* 浏览分支(`gh issue list` / `gh pr list`,不带 keywords):直接要 `--json`,再归一到与
|
|
364
|
+
* 搜索分支同一套 `SearchHit`。repo 缺省时先解析当前仓库——文本要据此决定是否带 repo 列,
|
|
365
|
+
* 载荷里也必须有值。
|
|
366
|
+
*/
|
|
367
|
+
export async function browseList(
|
|
360
368
|
kind: "issue" | "pr",
|
|
361
369
|
params: ListFilters,
|
|
362
370
|
ctx: { cwd?: string; signal?: AbortSignal; input?: unknown },
|
|
363
|
-
): Promise<string> {
|
|
364
|
-
|
|
371
|
+
): Promise<{ repo: string; hits: SearchHit[] }> {
|
|
372
|
+
const repo = params.repo ?? (await resolveRepo(undefined, ctx.signal, ctx.cwd, ctx.input));
|
|
373
|
+
const args = [...listGithubArgs(kind, params), "--json", GH_LIST_FIELDS];
|
|
374
|
+
const stdout = await ghExec(args, ctx);
|
|
375
|
+
return { repo, hits: normalizeGhList(stdout, repo) };
|
|
365
376
|
}
|
|
366
377
|
|
|
367
|
-
/**
|
|
368
|
-
export
|
|
378
|
+
/** 行列表的文本:两条分支共用一套 TSV 渲染;空结果是成功结果。 */
|
|
379
|
+
export function renderHitList(
|
|
369
380
|
kind: "issue" | "pr",
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
):
|
|
373
|
-
const hits = await githubSearch.search(kind, params);
|
|
381
|
+
hits: SearchHit[],
|
|
382
|
+
options: { repo?: string; fields?: string },
|
|
383
|
+
): string {
|
|
374
384
|
if (hits.length === 0) {
|
|
375
385
|
return `(no matching ${kind === "issue" ? "issues" : "pull requests"})`;
|
|
376
386
|
}
|
|
377
|
-
return renderHits(hits,
|
|
387
|
+
return renderHits(hits, options);
|
|
378
388
|
}
|
|
379
389
|
|
|
380
390
|
/**
|
package/src/gh/render.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文本类 gh 工具改走 `--json` 之后的渲染器:同一份 JSON 既渲染成给模型看的文本,也原样
|
|
3
|
+
* 进结构化载荷。文本格式是我们自己的(不再是 gh 的表格),因此这些函数要稳定、好读,
|
|
4
|
+
* 且只依赖 schema 里声明过的字段。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { dateOnly } from "../lib/github.js";
|
|
8
|
+
|
|
9
|
+
/** 一行一个 release:`tag 标记 发布日期 标题`。 */
|
|
10
|
+
export function renderReleaseList(
|
|
11
|
+
releases: {
|
|
12
|
+
tagName: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
isLatest?: boolean;
|
|
15
|
+
isPrerelease?: boolean;
|
|
16
|
+
isDraft?: boolean;
|
|
17
|
+
publishedAt?: string | null;
|
|
18
|
+
}[],
|
|
19
|
+
): string {
|
|
20
|
+
if (releases.length === 0) {
|
|
21
|
+
return "(no releases)";
|
|
22
|
+
}
|
|
23
|
+
return releases
|
|
24
|
+
.map((release) => {
|
|
25
|
+
const flags = [
|
|
26
|
+
release.isLatest ? "latest" : "",
|
|
27
|
+
release.isPrerelease ? "prerelease" : "",
|
|
28
|
+
release.isDraft ? "draft" : "",
|
|
29
|
+
].filter(Boolean);
|
|
30
|
+
return [
|
|
31
|
+
release.tagName,
|
|
32
|
+
flags.join(","),
|
|
33
|
+
dateOnly(release.publishedAt),
|
|
34
|
+
release.name ?? "",
|
|
35
|
+
].join("\t");
|
|
36
|
+
})
|
|
37
|
+
.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 一行一个运行:`id 状态 结论 工作流 分支 事件 创建时间 链接`。 */
|
|
41
|
+
export function renderRunList(
|
|
42
|
+
runs: {
|
|
43
|
+
databaseId: number;
|
|
44
|
+
status?: string;
|
|
45
|
+
conclusion?: string | null;
|
|
46
|
+
workflowName?: string;
|
|
47
|
+
headBranch?: string;
|
|
48
|
+
event?: string;
|
|
49
|
+
createdAt?: string;
|
|
50
|
+
url?: string;
|
|
51
|
+
}[],
|
|
52
|
+
): string {
|
|
53
|
+
if (runs.length === 0) {
|
|
54
|
+
return "(no workflow runs)";
|
|
55
|
+
}
|
|
56
|
+
return runs
|
|
57
|
+
.map((run) =>
|
|
58
|
+
[
|
|
59
|
+
String(run.databaseId),
|
|
60
|
+
run.status ?? "",
|
|
61
|
+
run.conclusion ?? "",
|
|
62
|
+
run.workflowName ?? "",
|
|
63
|
+
run.headBranch ?? "",
|
|
64
|
+
run.event ?? "",
|
|
65
|
+
dateOnly(run.createdAt),
|
|
66
|
+
run.url ?? "",
|
|
67
|
+
].join("\t"),
|
|
68
|
+
)
|
|
69
|
+
.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 仓库概览:标题行 + 一行事实 + 一行链接 + 一行日期/许可。 */
|
|
73
|
+
export function renderRepoView(repo: {
|
|
74
|
+
nameWithOwner?: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
url?: string;
|
|
77
|
+
visibility?: string;
|
|
78
|
+
primaryLanguage?: { name: string } | null;
|
|
79
|
+
defaultBranchRef?: { name: string } | null;
|
|
80
|
+
stargazerCount?: number;
|
|
81
|
+
forkCount?: number;
|
|
82
|
+
issues?: { totalCount: number } | null;
|
|
83
|
+
pullRequests?: { totalCount: number } | null;
|
|
84
|
+
licenseInfo?: { name: string } | null;
|
|
85
|
+
pushedAt?: string;
|
|
86
|
+
createdAt?: string;
|
|
87
|
+
}): string {
|
|
88
|
+
const facts = [
|
|
89
|
+
repo.visibility?.toLowerCase(),
|
|
90
|
+
repo.primaryLanguage?.name,
|
|
91
|
+
repo.defaultBranchRef && `default branch ${repo.defaultBranchRef.name}`,
|
|
92
|
+
repo.stargazerCount !== undefined && `stars ${repo.stargazerCount}`,
|
|
93
|
+
repo.forkCount !== undefined && `forks ${repo.forkCount}`,
|
|
94
|
+
repo.issues && `open issues ${repo.issues.totalCount}`,
|
|
95
|
+
repo.pullRequests && `open PRs ${repo.pullRequests.totalCount}`,
|
|
96
|
+
].filter(Boolean);
|
|
97
|
+
return [
|
|
98
|
+
[repo.nameWithOwner, repo.description].filter(Boolean).join(" — "),
|
|
99
|
+
facts.join(" · "),
|
|
100
|
+
repo.url ?? "",
|
|
101
|
+
[
|
|
102
|
+
repo.pushedAt && `pushed ${dateOnly(repo.pushedAt)}`,
|
|
103
|
+
repo.createdAt && `created ${dateOnly(repo.createdAt)}`,
|
|
104
|
+
repo.licenseInfo?.name,
|
|
105
|
+
]
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.join(" · "),
|
|
108
|
+
]
|
|
109
|
+
.filter((line) => line !== "")
|
|
110
|
+
.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** release 详情:标题、事实、资产清单、正文。 */
|
|
114
|
+
export function renderReleaseView(release: {
|
|
115
|
+
tagName?: string;
|
|
116
|
+
name?: string;
|
|
117
|
+
url?: string;
|
|
118
|
+
isLatest?: boolean;
|
|
119
|
+
isPrerelease?: boolean;
|
|
120
|
+
isDraft?: boolean;
|
|
121
|
+
publishedAt?: string | null;
|
|
122
|
+
createdAt?: string;
|
|
123
|
+
author?: { login?: string } | null;
|
|
124
|
+
assets?: { name?: string; size?: number; downloadCount?: number }[];
|
|
125
|
+
body?: string;
|
|
126
|
+
}): string {
|
|
127
|
+
const flags = [
|
|
128
|
+
release.isLatest && "latest",
|
|
129
|
+
release.isPrerelease && "prerelease",
|
|
130
|
+
release.isDraft && "draft",
|
|
131
|
+
]
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join(",");
|
|
134
|
+
const assets = release.assets ?? [];
|
|
135
|
+
const lines = [
|
|
136
|
+
[release.tagName, release.name].filter(Boolean).join(" — "),
|
|
137
|
+
[
|
|
138
|
+
`published ${dateOnly(release.publishedAt ?? release.createdAt)}`,
|
|
139
|
+
flags,
|
|
140
|
+
release.author?.login && `by ${release.author.login}`,
|
|
141
|
+
release.url,
|
|
142
|
+
]
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.join(" · "),
|
|
145
|
+
];
|
|
146
|
+
if (assets.length > 0) {
|
|
147
|
+
lines.push(`assets (${assets.length}):`);
|
|
148
|
+
for (const asset of assets) {
|
|
149
|
+
lines.push(
|
|
150
|
+
`- ${asset.name ?? ""} ${asset.size ?? 0} bytes, ${asset.downloadCount ?? 0} downloads`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const body = release.body?.trim() ?? "";
|
|
155
|
+
if (body !== "") {
|
|
156
|
+
lines.push("", body);
|
|
157
|
+
}
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface DiffFileStat {
|
|
162
|
+
path: string;
|
|
163
|
+
additions: number;
|
|
164
|
+
deletions: number;
|
|
165
|
+
oldPath?: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface DiffStats {
|
|
169
|
+
files: DiffFileStat[];
|
|
170
|
+
additions: number;
|
|
171
|
+
deletions: number;
|
|
172
|
+
changedFiles: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** diff 里的路径:去掉 `a/` / `b/` 前缀与 git 给带空格路径加的双引号;`/dev/null` 给空串。 */
|
|
176
|
+
function stripDiffPrefix(raw: string): string {
|
|
177
|
+
const trimmed = raw.trimEnd();
|
|
178
|
+
if (trimmed === "/dev/null") {
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
return trimmed
|
|
182
|
+
.replace(/^[ab]\//, "")
|
|
183
|
+
.replace(/^"/, "")
|
|
184
|
+
.replace(/"$/, "");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** `diff --git a/x b/y` 的两个路径(路径带空格时 git 会加双引号)。 */
|
|
188
|
+
function parseDiffHeader(rest: string): { oldPath: string; newPath: string } | undefined {
|
|
189
|
+
const match = /^(?:"a\/(.+)"|a\/(\S+)) (?:"b\/(.+)"|b\/(\S+))$/.exec(rest.trim());
|
|
190
|
+
if (!match) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
// 两组可选路径各有一个未参与匹配的捕获组(运行期是 undefined,类型上是 string),用 `||`
|
|
194
|
+
return { oldPath: match[1] || match[2], newPath: match[3] || match[4] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 从 unified diff 文本里解析变更统计。
|
|
199
|
+
*
|
|
200
|
+
* `gh pr diff` 没有 `--json`,因此载荷只能从这里来。解析的边界都是 diff 的结构标记
|
|
201
|
+
* (`diff --git` / `---` / `+++` / `@@`),不依赖具体语言或文件内容:二进制文件与纯
|
|
202
|
+
* 模式变更(没有 `---` / `+++`)按 0 行变更计,路径取自 `diff --git` 头。
|
|
203
|
+
*/
|
|
204
|
+
export function parseDiffStats(diff: string): DiffStats {
|
|
205
|
+
const files: DiffFileStat[] = [];
|
|
206
|
+
let current: DiffFileStat | undefined;
|
|
207
|
+
/** `--- ` 给出的旧路径,等 `+++ ` 到了再决定它是不是重命名。 */
|
|
208
|
+
let oldPathFromHunk = "";
|
|
209
|
+
let inHunk = false;
|
|
210
|
+
|
|
211
|
+
for (const line of diff.split("\n")) {
|
|
212
|
+
if (line.startsWith("diff --git ")) {
|
|
213
|
+
if (current) {
|
|
214
|
+
files.push(current);
|
|
215
|
+
}
|
|
216
|
+
const header = parseDiffHeader(line.slice("diff --git ".length));
|
|
217
|
+
current = { path: header?.newPath ?? "", additions: 0, deletions: 0 };
|
|
218
|
+
oldPathFromHunk = header?.oldPath ?? "";
|
|
219
|
+
inHunk = false;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (current === undefined) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (line.startsWith("--- ")) {
|
|
226
|
+
oldPathFromHunk = stripDiffPrefix(line.slice(4));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (line.startsWith("+++ ")) {
|
|
230
|
+
const newPath = stripDiffPrefix(line.slice(4));
|
|
231
|
+
// 删除的文件新路径是 /dev/null,此时用旧路径当 path
|
|
232
|
+
current.path = newPath === "" ? oldPathFromHunk : newPath;
|
|
233
|
+
if (oldPathFromHunk !== "" && oldPathFromHunk !== current.path) {
|
|
234
|
+
current.oldPath = oldPathFromHunk;
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (line.startsWith("@@")) {
|
|
239
|
+
inHunk = true;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!inHunk) {
|
|
243
|
+
// 文件头(index / mode / rename / Binary files …)不算变更行
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (line.startsWith("+")) {
|
|
247
|
+
current.additions += 1;
|
|
248
|
+
} else if (line.startsWith("-")) {
|
|
249
|
+
current.deletions += 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (current) {
|
|
253
|
+
files.push(current);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const additions = files.reduce((sum, file) => sum + file.additions, 0);
|
|
257
|
+
const deletions = files.reduce((sum, file) => sum + file.deletions, 0);
|
|
258
|
+
return { files, additions, deletions, changedFiles: files.length };
|
|
259
|
+
}
|
package/src/gh/schemas.ts
CHANGED
|
@@ -228,3 +228,216 @@ export const checksVerdictSchema = Type.Object({
|
|
|
228
228
|
}),
|
|
229
229
|
),
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
// ── 文本类工具改成 --json 之后的载荷 ─────────────────────────────────────────
|
|
233
|
+
//
|
|
234
|
+
// 这批工具的文本由我们自己从 JSON 渲染,因此载荷是 gh 的 JSON(字段名就是 gh 的)加一个
|
|
235
|
+
// `text`(与工具输出一致)。与上面十个透传工具一样,gh 的 JSON 允许额外字段。
|
|
236
|
+
|
|
237
|
+
/** `list-github-issues` / `list-github-prs` 的一行:浏览与搜索两条分支归一后的记录。 */
|
|
238
|
+
export const ghHitSchema = Type.Object(
|
|
239
|
+
{
|
|
240
|
+
number: Type.Number(),
|
|
241
|
+
state: Type.Union([Type.Literal("open"), Type.Literal("closed"), Type.Literal("merged")]),
|
|
242
|
+
title: Type.String(),
|
|
243
|
+
url: Type.String(),
|
|
244
|
+
repo: Type.String({ description: "OWNER/REPO;浏览分支取参数或当前仓库" }),
|
|
245
|
+
author: Type.String(),
|
|
246
|
+
labels: Type.Array(Type.String()),
|
|
247
|
+
milestone: Type.String(),
|
|
248
|
+
assignees: Type.Array(Type.String()),
|
|
249
|
+
comments: Type.Number({ description: "评论数(浏览分支是评论数组的长度)" }),
|
|
250
|
+
createdAt: Type.String({ description: "YYYY-MM-DD" }),
|
|
251
|
+
updatedAt: Type.String({ description: "YYYY-MM-DD" }),
|
|
252
|
+
closedAt: Type.String(),
|
|
253
|
+
mergedAt: Type.String(),
|
|
254
|
+
},
|
|
255
|
+
{ additionalProperties: false },
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
export const ghHitListPayloadSchema = Type.Object(
|
|
259
|
+
{
|
|
260
|
+
text: Type.String({ description: "与工具输出一致的行列表(可能被行数/字节预算截断)" }),
|
|
261
|
+
items: Type.Array(ghHitSchema, { description: "全部命中,不受文本截断影响" }),
|
|
262
|
+
},
|
|
263
|
+
{ additionalProperties: false },
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
/** `gh release list --json` 的一行。 */
|
|
267
|
+
export const ghReleaseSummarySchema = Type.Object(
|
|
268
|
+
{
|
|
269
|
+
tagName: Type.String(),
|
|
270
|
+
name: Type.Optional(Type.String()),
|
|
271
|
+
isLatest: Type.Optional(Type.Boolean()),
|
|
272
|
+
isPrerelease: Type.Optional(Type.Boolean()),
|
|
273
|
+
isDraft: Type.Optional(Type.Boolean()),
|
|
274
|
+
publishedAt: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
275
|
+
createdAt: Type.Optional(Type.String()),
|
|
276
|
+
},
|
|
277
|
+
{ additionalProperties: true },
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
export const ghReleaseListPayloadSchema = Type.Object(
|
|
281
|
+
{
|
|
282
|
+
text: Type.String(),
|
|
283
|
+
releases: Type.Array(ghReleaseSummarySchema),
|
|
284
|
+
},
|
|
285
|
+
{ additionalProperties: false },
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
/** `gh run list` / `gh run view --json` 的共同字段。 */
|
|
289
|
+
export const ghRunSummarySchema = Type.Object(
|
|
290
|
+
{
|
|
291
|
+
databaseId: Type.Number(),
|
|
292
|
+
displayTitle: Type.Optional(Type.String()),
|
|
293
|
+
status: Type.Optional(Type.String()),
|
|
294
|
+
conclusion: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
295
|
+
workflowName: Type.Optional(Type.String()),
|
|
296
|
+
headBranch: Type.Optional(Type.String()),
|
|
297
|
+
event: Type.Optional(Type.String()),
|
|
298
|
+
createdAt: Type.Optional(Type.String()),
|
|
299
|
+
updatedAt: Type.Optional(Type.String()),
|
|
300
|
+
startedAt: Type.Optional(Type.String()),
|
|
301
|
+
url: Type.Optional(Type.String()),
|
|
302
|
+
},
|
|
303
|
+
{ additionalProperties: true },
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
export const ghRunListPayloadSchema = Type.Object(
|
|
307
|
+
{
|
|
308
|
+
text: Type.String(),
|
|
309
|
+
runs: Type.Array(ghRunSummarySchema),
|
|
310
|
+
},
|
|
311
|
+
{ additionalProperties: false },
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
/** `watch-github-run`:文本是 `gh run watch` 的输出,载荷是收尾时查到的运行状态。 */
|
|
315
|
+
export const ghRunPayloadSchema = Type.Object(
|
|
316
|
+
{
|
|
317
|
+
text: Type.String(),
|
|
318
|
+
run: ghRunSummarySchema,
|
|
319
|
+
},
|
|
320
|
+
{ additionalProperties: false },
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
/** `gh repo view --json` 里我们渲染与脚本可能用到的字段。 */
|
|
324
|
+
export const ghRepoViewSchema = Type.Object(
|
|
325
|
+
{
|
|
326
|
+
name: Type.Optional(Type.String()),
|
|
327
|
+
nameWithOwner: Type.Optional(Type.String()),
|
|
328
|
+
description: Type.Optional(Type.String()),
|
|
329
|
+
url: Type.Optional(Type.String()),
|
|
330
|
+
homepageUrl: Type.Optional(Type.String()),
|
|
331
|
+
visibility: Type.Optional(Type.String()),
|
|
332
|
+
isPrivate: Type.Optional(Type.Boolean()),
|
|
333
|
+
isFork: Type.Optional(Type.Boolean()),
|
|
334
|
+
isArchived: Type.Optional(Type.Boolean()),
|
|
335
|
+
stargazerCount: Type.Optional(Type.Number()),
|
|
336
|
+
forkCount: Type.Optional(Type.Number()),
|
|
337
|
+
primaryLanguage: Type.Optional(
|
|
338
|
+
Type.Union([
|
|
339
|
+
Type.Object({ name: Type.String() }, { additionalProperties: true }),
|
|
340
|
+
Type.Null(),
|
|
341
|
+
]),
|
|
342
|
+
),
|
|
343
|
+
defaultBranchRef: Type.Optional(
|
|
344
|
+
Type.Union([
|
|
345
|
+
Type.Object({ name: Type.String() }, { additionalProperties: true }),
|
|
346
|
+
Type.Null(),
|
|
347
|
+
]),
|
|
348
|
+
),
|
|
349
|
+
licenseInfo: Type.Optional(
|
|
350
|
+
Type.Union([
|
|
351
|
+
Type.Object({ name: Type.String() }, { additionalProperties: true }),
|
|
352
|
+
Type.Null(),
|
|
353
|
+
]),
|
|
354
|
+
),
|
|
355
|
+
issues: Type.Optional(
|
|
356
|
+
Type.Union([
|
|
357
|
+
Type.Object({ totalCount: Type.Number() }, { additionalProperties: true }),
|
|
358
|
+
Type.Null(),
|
|
359
|
+
]),
|
|
360
|
+
),
|
|
361
|
+
pullRequests: Type.Optional(
|
|
362
|
+
Type.Union([
|
|
363
|
+
Type.Object({ totalCount: Type.Number() }, { additionalProperties: true }),
|
|
364
|
+
Type.Null(),
|
|
365
|
+
]),
|
|
366
|
+
),
|
|
367
|
+
pushedAt: Type.Optional(Type.String()),
|
|
368
|
+
createdAt: Type.Optional(Type.String()),
|
|
369
|
+
updatedAt: Type.Optional(Type.String()),
|
|
370
|
+
},
|
|
371
|
+
{ additionalProperties: true },
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
export const ghRepoPayloadSchema = Type.Object(
|
|
375
|
+
{
|
|
376
|
+
text: Type.String(),
|
|
377
|
+
repo: ghRepoViewSchema,
|
|
378
|
+
},
|
|
379
|
+
{ additionalProperties: false },
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
/** `gh release view --json` 里我们渲染与脚本可能用到的字段。 */
|
|
383
|
+
export const ghReleaseViewSchema = Type.Object(
|
|
384
|
+
{
|
|
385
|
+
tagName: Type.Optional(Type.String()),
|
|
386
|
+
name: Type.Optional(Type.String()),
|
|
387
|
+
body: Type.Optional(Type.String()),
|
|
388
|
+
url: Type.Optional(Type.String()),
|
|
389
|
+
isDraft: Type.Optional(Type.Boolean()),
|
|
390
|
+
isPrerelease: Type.Optional(Type.Boolean()),
|
|
391
|
+
createdAt: Type.Optional(Type.String()),
|
|
392
|
+
publishedAt: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
393
|
+
targetCommitish: Type.Optional(Type.String()),
|
|
394
|
+
author: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
|
|
395
|
+
assets: Type.Optional(
|
|
396
|
+
Type.Array(
|
|
397
|
+
Type.Object(
|
|
398
|
+
{
|
|
399
|
+
name: Type.Optional(Type.String()),
|
|
400
|
+
size: Type.Optional(Type.Number()),
|
|
401
|
+
downloadCount: Type.Optional(Type.Number()),
|
|
402
|
+
contentType: Type.Optional(Type.String()),
|
|
403
|
+
state: Type.Optional(Type.String()),
|
|
404
|
+
url: Type.Optional(Type.String()),
|
|
405
|
+
},
|
|
406
|
+
{ additionalProperties: true },
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
),
|
|
410
|
+
},
|
|
411
|
+
{ additionalProperties: true },
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
export const ghReleaseViewPayloadSchema = Type.Object(
|
|
415
|
+
{
|
|
416
|
+
text: Type.String(),
|
|
417
|
+
release: ghReleaseViewSchema,
|
|
418
|
+
},
|
|
419
|
+
{ additionalProperties: false },
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
/** `read-github-pr-diff`:文本是原始 diff,载荷是从它解析出的变更统计。 */
|
|
423
|
+
export const ghDiffPayloadSchema = Type.Object(
|
|
424
|
+
{
|
|
425
|
+
text: Type.String(),
|
|
426
|
+
files: Type.Array(
|
|
427
|
+
Type.Object(
|
|
428
|
+
{
|
|
429
|
+
path: Type.String(),
|
|
430
|
+
additions: Type.Number(),
|
|
431
|
+
deletions: Type.Number(),
|
|
432
|
+
/** diff 里的旧路径,重命名/复制时与 `path` 不同。 */
|
|
433
|
+
oldPath: Type.Optional(Type.String()),
|
|
434
|
+
},
|
|
435
|
+
{ additionalProperties: false },
|
|
436
|
+
),
|
|
437
|
+
),
|
|
438
|
+
additions: Type.Number(),
|
|
439
|
+
deletions: Type.Number(),
|
|
440
|
+
changedFiles: Type.Number(),
|
|
441
|
+
},
|
|
442
|
+
{ additionalProperties: false },
|
|
443
|
+
);
|
|
@@ -1,64 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `list-github-issues`:两条取数分支(关键词搜索走 octokit,浏览走 `gh issue list --json`)
|
|
3
|
+
* 归一成同一套 `SearchHit`,因此文本渲染与结构化载荷只有一份实现。
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
import { Type } from "typebox";
|
|
2
7
|
|
|
3
|
-
import type
|
|
8
|
+
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
4
9
|
import {
|
|
10
|
+
browseList,
|
|
5
11
|
type GhClient,
|
|
6
12
|
type ListFilters,
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
renderHitList,
|
|
14
|
+
type StructuredResultOf,
|
|
9
15
|
subtitlePendant,
|
|
10
16
|
type ToolCall,
|
|
11
|
-
type ToolResult,
|
|
12
17
|
toToolResult,
|
|
18
|
+
withStructuredResult,
|
|
13
19
|
} from "../base.js";
|
|
20
|
+
import { ghHitListPayloadSchema } from "../schemas.js";
|
|
14
21
|
|
|
15
|
-
async function listIssues(
|
|
22
|
+
async function listIssues(
|
|
23
|
+
gh: GhClient,
|
|
24
|
+
call: ToolCall<ListFilters>,
|
|
25
|
+
): Promise<StructuredResultOf<typeof ghHitListPayloadSchema>> {
|
|
16
26
|
const { params, ctx, signal } = call;
|
|
17
|
-
const
|
|
18
|
-
params.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
);
|
|
27
|
+
const { repo, hits } = params.keywords
|
|
28
|
+
? { repo: params.repo, hits: await gh.search.search("issue", params) }
|
|
29
|
+
: await browseList("issue", params, { cwd: ctx.cwd, signal, input: params });
|
|
30
|
+
const text = renderHitList("issue", hits, { repo, fields: params.fields });
|
|
31
|
+
const result = toToolResult(text, params);
|
|
23
32
|
result.details.pendant = subtitlePendant(params);
|
|
24
|
-
|
|
33
|
+
// 文本可能被行数/字节预算截断,载荷给全部命中
|
|
34
|
+
return withStructuredResult(result, { text, items: hits });
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
export function addListIssuesTool(gh: GhClient, bus: ToolBus) {
|
|
28
|
-
bus.register(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Type.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Type.String({
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
38
|
+
bus.register(
|
|
39
|
+
defineStructuredTool({
|
|
40
|
+
name: "list-github-issues",
|
|
41
|
+
label: "GitHub Issues List",
|
|
42
|
+
description:
|
|
43
|
+
'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.',
|
|
44
|
+
promptSnippet: "List or search GitHub issues",
|
|
45
|
+
parameters: Type.Object({
|
|
46
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
|
|
47
|
+
keywords: Type.Optional(Type.String({ description: "Search keywords (free text)" })),
|
|
48
|
+
state: Type.Optional(
|
|
49
|
+
Type.String({
|
|
50
|
+
description:
|
|
51
|
+
"open, closed, all (default: open; all applies to keyword search and covers closed too)",
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
label: Type.Optional(Type.String({ description: "Filter by label" })),
|
|
55
|
+
// `@me` 由 GitHub 的搜索 / 过滤 API 解析:不带 keywords 时 gh 自行展开(GraphQL 过滤分支)
|
|
56
|
+
// 或按字面量转发(搜索分支),带 keywords 时我们直接把 `assignee:@me` 交给搜索 API。
|
|
57
|
+
// 两条传输都支持,因此本仓库不要再实现一次展开。
|
|
58
|
+
author: Type.Optional(
|
|
59
|
+
Type.String({ description: "Filter by author ('@me' for yourself)" }),
|
|
60
|
+
),
|
|
61
|
+
assignee: Type.Optional(
|
|
62
|
+
Type.String({ description: "Filter by assignee ('@me' for yourself)" }),
|
|
63
|
+
),
|
|
64
|
+
milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
|
|
65
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),
|
|
66
|
+
fields: Type.Optional(
|
|
67
|
+
Type.String({
|
|
68
|
+
description:
|
|
69
|
+
"Comma-separated columns for the 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",
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
}),
|
|
73
|
+
structuredSchema: ghHitListPayloadSchema,
|
|
74
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
75
|
+
return listIssues(gh, { params, ctx, signal, onUpdate });
|
|
76
|
+
},
|
|
59
77
|
}),
|
|
60
|
-
|
|
61
|
-
return listIssues(gh, { params, ctx, signal, onUpdate });
|
|
62
|
-
},
|
|
63
|
-
});
|
|
78
|
+
);
|
|
64
79
|
}
|