@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/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
- export async function listGithub(
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
- return ghExec(listGithubArgs(kind, params), ctx);
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
- /** Run a keyword search through the octokit client and render the rows. */
368
- export async function searchList(
378
+ /** 行列表的文本:两条分支共用一套 TSV 渲染;空结果是成功结果。 */
379
+ export function renderHitList(
369
380
  kind: "issue" | "pr",
370
- params: ListFilters,
371
- githubSearch: GithubSearch,
372
- ): Promise<string> {
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, { repo: params.repo, fields: params.fields });
387
+ return renderHits(hits, options);
378
388
  }
379
389
 
380
390
  /**
@@ -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 { ToolBus } from "../../lib/tool-bus.js";
8
+ import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
4
9
  import {
10
+ browseList,
5
11
  type GhClient,
6
12
  type ListFilters,
7
- listGithub,
8
- searchList,
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(gh: GhClient, call: ToolCall<ListFilters>): Promise<ToolResult> {
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 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
- );
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
- return result;
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
- 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
- // `@me` 由 GitHub 的搜索 / 过滤 API 解析:不带 keywords 时 gh 自行展开(GraphQL 过滤分支)
45
- // 或按字面量转发(搜索分支),带 keywords 时我们直接把 `assignee:@me` 交给搜索 API。
46
- // 两条传输都支持,因此本仓库不要再实现一次展开。
47
- author: Type.Optional(Type.String({ description: "Filter by author ('@me' for yourself)" })),
48
- assignee: Type.Optional(
49
- Type.String({ description: "Filter by assignee ('@me' for yourself)" }),
50
- ),
51
- milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
52
- limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),
53
- fields: Type.Optional(
54
- Type.String({
55
- description:
56
- "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",
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
- async execute(_id, params, signal, onUpdate, ctx) {
61
- return listIssues(gh, { params, ctx, signal, onUpdate });
62
- },
63
- });
78
+ );
64
79
  }