@trim21/personal-pi-extensions 0.1.685 → 0.1.687

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/schemas.ts CHANGED
@@ -5,9 +5,8 @@
5
5
  * `structuredResult.value`。因此 schema 必须**允许额外字段**(GitHub 的 JSON 只多不少,
6
6
  * 多出来的字段不该让工具失败),只对工具本身已经依赖的字段设 required。
7
7
  *
8
- * 字段名取自 GitHub 的两种传输:`gh ... view --json`(GraphQL,actor 叫 `author` /
9
- * `user`)与 REST(`/pulls/:n/comments` 等,用了 `diff_hunk` / `submitted_at` 这类
10
- * 下划线命名)。评论与评审条目因此放宽成两者的并集。
8
+ * 字段名是 GitHub REST 的命名(`html_url` / `created_at` / `user.login`):gh 工具的取数
9
+ * 全部走 octokit 之后不再有 `gh ... --json` 的 GraphQL 形状。
11
10
  */
12
11
 
13
12
  import { Type } from "typebox";
@@ -33,29 +32,27 @@ export const ghMilestoneSchema = Type.Object({
33
32
  dueOn: Type.Optional(Type.Union([Type.String(), Type.Null()])),
34
33
  });
35
34
 
36
- /** 评论条目:GraphQL 的 `author`/`createdAt` 与 REST 的 `user`/`diff_hunk` 并集。 */
35
+ /** 评论条目:REST 的 issue/PR 评论与行内评审评论共用(`diff_hunk` 只有后者有)。 */
37
36
  export const ghCommentSchema = Type.Object({
38
- id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
39
- body: Type.Optional(Type.String()),
40
- author: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
37
+ id: Type.Optional(Type.Number()),
38
+ body: Type.Optional(Type.Union([Type.String(), Type.Null()])),
41
39
  user: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
42
40
  path: Type.Optional(Type.String()),
43
41
  line: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
44
42
  diff_hunk: Type.Optional(Type.String()),
45
- createdAt: Type.Optional(Type.String()),
46
- url: Type.Optional(Type.String()),
43
+ created_at: Type.Optional(Type.String()),
44
+ updated_at: Type.Optional(Type.String()),
45
+ html_url: Type.Optional(Type.String()),
47
46
  });
48
47
 
49
- /** 评审条目:GraphQL 的 `reviews` 与 REST 的 `/pulls/:n/reviews` 并集。 */
48
+ /** 评审条目:REST 的 `/pulls/:n/reviews`。 */
50
49
  export const ghReviewSchema = Type.Object({
51
- id: Type.Optional(Type.Union([Type.String(), Type.Number()])),
52
- body: Type.Optional(Type.String()),
53
- author: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
50
+ id: Type.Optional(Type.Number()),
51
+ body: Type.Optional(Type.Union([Type.String(), Type.Null()])),
54
52
  user: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
55
53
  state: Type.Optional(Type.String()),
56
- createdAt: Type.Optional(Type.String()),
57
- submitted_at: Type.Optional(Type.String()),
58
- url: Type.Optional(Type.String()),
54
+ submitted_at: Type.Optional(Type.Union([Type.String(), Type.Null()])),
55
+ html_url: Type.Optional(Type.String()),
59
56
  });
60
57
 
61
58
  /** 待评审人:可能是用户(login)也可能是团队(slug)。 */
@@ -75,58 +72,57 @@ export const checkBucketSchema = Type.Union([
75
72
 
76
73
  // ── 各工具的输出 ─────────────────────────────────────────────────────────────
77
74
 
78
- /** `read-github-issue`:`gh issue view --json` 的输出。 */
75
+ /** `read-github-issue`:`issues.get` 的响应(REST 字段名,未读字段不在这里出现也不影响校验)。 */
79
76
  export const issueViewSchema = Type.Object({
80
77
  number: Type.Number(),
81
78
  title: Type.String(),
82
79
  state: Type.String(),
83
- body: Type.Optional(Type.String()),
84
- author: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
85
- createdAt: Type.Optional(Type.String()),
86
- updatedAt: Type.Optional(Type.String()),
87
- closedAt: Type.Optional(Type.Union([Type.String(), Type.Null()])),
88
- url: Type.Optional(Type.String()),
80
+ body: Type.Optional(Type.Union([Type.String(), Type.Null()])),
81
+ user: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
82
+ created_at: Type.Optional(Type.String()),
83
+ updated_at: Type.Optional(Type.String()),
84
+ closed_at: Type.Optional(Type.Union([Type.String(), Type.Null()])),
85
+ html_url: Type.Optional(Type.String()),
89
86
  labels: Type.Optional(Type.Array(ghLabelSchema)),
90
- assignees: Type.Optional(Type.Array(ghActorSchema)),
91
- comments: Type.Optional(Type.Array(ghCommentSchema)),
87
+ assignees: Type.Optional(Type.Union([Type.Array(ghActorSchema), Type.Null()])),
88
+ /** REST 这里给的是评论**数量**(`gh issue view --json comments` 给的是数组)。 */
89
+ comments: Type.Optional(Type.Number()),
92
90
  milestone: Type.Optional(Type.Union([ghMilestoneSchema, Type.Null()])),
93
91
  });
94
92
 
95
- /** `read-github-pr`:`gh pr view --json` 的输出,比 issue 多出合并与 diff 统计。 */
93
+ /** `read-github-pr`:`pulls.get` 的响应,比 issue 多出合并信息与 diff 统计。 */
96
94
  export const prViewSchema = Type.Object({
97
95
  number: Type.Number(),
98
96
  title: Type.String(),
99
97
  state: Type.String(),
100
- body: Type.Optional(Type.String()),
101
- author: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
102
- createdAt: Type.Optional(Type.String()),
103
- updatedAt: Type.Optional(Type.String()),
104
- closedAt: Type.Optional(Type.Union([Type.String(), Type.Null()])),
105
- mergedAt: Type.Optional(Type.Union([Type.String(), Type.Null()])),
106
- mergedBy: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
107
- headRefName: Type.Optional(Type.String()),
108
- baseRefName: Type.Optional(Type.String()),
109
- url: Type.Optional(Type.String()),
98
+ body: Type.Optional(Type.Union([Type.String(), Type.Null()])),
99
+ user: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
100
+ created_at: Type.Optional(Type.String()),
101
+ updated_at: Type.Optional(Type.String()),
102
+ closed_at: Type.Optional(Type.Union([Type.String(), Type.Null()])),
103
+ merged_at: Type.Optional(Type.Union([Type.String(), Type.Null()])),
104
+ merged: Type.Optional(Type.Boolean()),
105
+ merged_by: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
106
+ head: Type.Optional(Type.Object({ ref: Type.Optional(Type.String()) })),
107
+ base: Type.Optional(Type.Object({ ref: Type.Optional(Type.String()) })),
108
+ html_url: Type.Optional(Type.String()),
110
109
  additions: Type.Optional(Type.Number()),
111
110
  deletions: Type.Optional(Type.Number()),
112
- changedFiles: Type.Optional(Type.Number()),
111
+ changed_files: Type.Optional(Type.Number()),
113
112
  labels: Type.Optional(Type.Array(ghLabelSchema)),
114
- assignees: Type.Optional(Type.Array(ghActorSchema)),
115
- reviewRequests: Type.Optional(Type.Array(ghReviewRequestSchema)),
116
- reviews: Type.Optional(Type.Array(ghReviewSchema)),
117
- comments: Type.Optional(Type.Array(ghCommentSchema)),
113
+ assignees: Type.Optional(Type.Union([Type.Array(ghActorSchema), Type.Null()])),
114
+ requested_reviewers: Type.Optional(Type.Union([Type.Array(ghReviewRequestSchema), Type.Null()])),
118
115
  milestone: Type.Optional(Type.Union([ghMilestoneSchema, Type.Null()])),
119
116
  });
120
117
 
121
- /** `read-github-issue-comments`:`gh issue view --json comments`。 */
118
+ /**
119
+ * 两个评论工具共用:`issues.listComments` 或 `pulls.listReviewComments` 的数组。
120
+ * `reviews: true`(只对 PR)时额外带 `pulls.listReviews` 的结果。
121
+ */
122
122
  export const issueCommentsSchema = Type.Object({
123
- comments: Type.Array(ghCommentSchema),
123
+ comments: Type.Array(ghCommentSchema, { description: "该 issue / PR 的评论(分页取全)" }),
124
124
  });
125
125
 
126
- /**
127
- * `read-github-pr-comments`:默认模式是 `gh pr view --json comments`(只有 comments),
128
- * `reviews: true` 时走 REST 同时给出 reviews 与行内评论。两个字段都可选。
129
- */
130
126
  export const prCommentsSchema = Type.Object({
131
127
  comments: Type.Optional(Type.Array(ghCommentSchema)),
132
128
  reviews: Type.Optional(Type.Array(ghReviewSchema)),
@@ -263,16 +259,15 @@ export const ghHitListPayloadSchema = Type.Object(
263
259
  { additionalProperties: false },
264
260
  );
265
261
 
266
- /** `gh release list --json` 的一行。 */
262
+ /** `repos.listReleases` 的一行(REST 字段名)。 */
267
263
  export const ghReleaseSummarySchema = Type.Object(
268
264
  {
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()),
265
+ tag_name: Type.String(),
266
+ name: Type.Optional(Type.Union([Type.String(), Type.Null()])),
267
+ draft: Type.Optional(Type.Boolean()),
268
+ prerelease: Type.Optional(Type.Boolean()),
269
+ published_at: Type.Optional(Type.Union([Type.String(), Type.Null()])),
270
+ created_at: Type.Optional(Type.String()),
276
271
  },
277
272
  { additionalProperties: true },
278
273
  );
@@ -285,20 +280,20 @@ export const ghReleaseListPayloadSchema = Type.Object(
285
280
  { additionalProperties: false },
286
281
  );
287
282
 
288
- /** `gh run list` / `gh run view --json` 的共同字段。 */
283
+ /** `actions.listWorkflowRuns*` / `getWorkflowRun` 的共同字段(REST 字段名)。 */
289
284
  export const ghRunSummarySchema = Type.Object(
290
285
  {
291
- databaseId: Type.Number(),
292
- displayTitle: Type.Optional(Type.String()),
293
- status: Type.Optional(Type.String()),
286
+ id: Type.Number(),
287
+ name: Type.Optional(Type.Union([Type.String(), Type.Null()])),
288
+ display_title: Type.Optional(Type.String()),
289
+ status: Type.Optional(Type.Union([Type.String(), Type.Null()])),
294
290
  conclusion: Type.Optional(Type.Union([Type.String(), Type.Null()])),
295
- workflowName: Type.Optional(Type.String()),
296
- headBranch: Type.Optional(Type.String()),
291
+ head_branch: Type.Optional(Type.Union([Type.String(), Type.Null()])),
297
292
  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()),
293
+ created_at: Type.Optional(Type.String()),
294
+ updated_at: Type.Optional(Type.String()),
295
+ run_started_at: Type.Optional(Type.String()),
296
+ html_url: Type.Optional(Type.String()),
302
297
  },
303
298
  { additionalProperties: true },
304
299
  );
@@ -311,7 +306,7 @@ export const ghRunListPayloadSchema = Type.Object(
311
306
  { additionalProperties: false },
312
307
  );
313
308
 
314
- /** `watch-github-run`:文本是 `gh run watch` 的输出,载荷是收尾时查到的运行状态。 */
309
+ /** `watch-github-run`:文本是轮询进度与完成报告,载荷是收尾时查到的运行状态。 */
315
310
  export const ghRunPayloadSchema = Type.Object(
316
311
  {
317
312
  text: Type.String(),
@@ -320,88 +315,57 @@ export const ghRunPayloadSchema = Type.Object(
320
315
  { additionalProperties: false },
321
316
  );
322
317
 
323
- /** `gh repo view --json` 里我们渲染与脚本可能用到的字段。 */
318
+ /** `repos.get` 里我们渲染与脚本可能用到的字段(REST 字段名)。 */
324
319
  export const ghRepoViewSchema = Type.Object(
325
320
  {
326
321
  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()),
322
+ full_name: Type.Optional(Type.String()),
323
+ description: Type.Optional(Type.Union([Type.String(), Type.Null()])),
324
+ html_url: Type.Optional(Type.String()),
325
+ homepage: Type.Optional(Type.Union([Type.String(), Type.Null()])),
326
+ private: Type.Optional(Type.Boolean()),
327
+ fork: Type.Optional(Type.Boolean()),
328
+ archived: Type.Optional(Type.Boolean()),
331
329
  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
- ]),
330
+ language: Type.Optional(Type.Union([Type.String(), Type.Null()])),
331
+ default_branch: Type.Optional(Type.String()),
332
+ stargazers_count: Type.Optional(Type.Number()),
333
+ forks_count: Type.Optional(Type.Number()),
334
+ open_issues_count: Type.Optional(Type.Number()),
335
+ license: Type.Optional(
336
+ Type.Union([Type.Object({ name: Type.Optional(Type.String()) }), Type.Null()]),
354
337
  ),
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()),
338
+ pushed_at: Type.Optional(Type.String()),
339
+ created_at: Type.Optional(Type.String()),
340
+ updated_at: Type.Optional(Type.String()),
370
341
  },
371
342
  { additionalProperties: true },
372
343
  );
373
344
 
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` 里我们渲染与脚本可能用到的字段。 */
345
+ /** `repos.getReleaseByTag` / `getLatestRelease` 里我们渲染与脚本可能用到的字段。 */
383
346
  export const ghReleaseViewSchema = Type.Object(
384
347
  {
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()),
348
+ tag_name: Type.Optional(Type.String()),
349
+ name: Type.Optional(Type.Union([Type.String(), Type.Null()])),
350
+ body: Type.Optional(Type.Union([Type.String(), Type.Null()])),
351
+ html_url: Type.Optional(Type.String()),
352
+ draft: Type.Optional(Type.Boolean()),
353
+ prerelease: Type.Optional(Type.Boolean()),
354
+ created_at: Type.Optional(Type.String()),
355
+ published_at: Type.Optional(Type.Union([Type.String(), Type.Null()])),
356
+ target_commitish: Type.Optional(Type.String()),
394
357
  author: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
395
358
  assets: Type.Optional(
396
359
  Type.Array(
397
360
  Type.Object(
398
361
  {
362
+ id: Type.Optional(Type.Number()),
399
363
  name: Type.Optional(Type.String()),
400
364
  size: Type.Optional(Type.Number()),
401
- downloadCount: Type.Optional(Type.Number()),
402
- contentType: Type.Optional(Type.String()),
365
+ download_count: Type.Optional(Type.Number()),
366
+ content_type: Type.Optional(Type.String()),
403
367
  state: Type.Optional(Type.String()),
404
- url: Type.Optional(Type.String()),
368
+ browser_download_url: Type.Optional(Type.String()),
405
369
  },
406
370
  { additionalProperties: true },
407
371
  ),
@@ -419,6 +383,15 @@ export const ghReleaseViewPayloadSchema = Type.Object(
419
383
  { additionalProperties: false },
420
384
  );
421
385
 
386
+ /** `repos.get` 的载荷:文本 + REST 响应。 */
387
+ export const ghRepoPayloadSchema = Type.Object(
388
+ {
389
+ text: Type.String(),
390
+ repo: ghRepoViewSchema,
391
+ },
392
+ { additionalProperties: false },
393
+ );
394
+
422
395
  /** `read-github-pr-diff`:文本是原始 diff,载荷是从它解析出的变更统计。 */
423
396
  export const ghDiffPayloadSchema = Type.Object(
424
397
  {
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readdir, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
- import { join } from "node:path";
3
+ import { join, matchesGlob } from "node:path";
4
4
 
5
5
  import { Type } from "typebox";
6
6
  import { Value } from "typebox/value";
@@ -9,9 +9,6 @@ import { type ToolPendant } from "../../lib/pendant.js";
9
9
  import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
10
10
  import {
11
11
  type GhClient,
12
- GhError,
13
- ghExec,
14
- repoArgs,
15
12
  resolveRepo,
16
13
  splitRepo,
17
14
  structuredFailure,
@@ -30,10 +27,10 @@ interface ReleaseDownloadParams {
30
27
  archive?: "zip" | "tar.gz";
31
28
  }
32
29
 
33
- /** `gh release view --json tagName,assets` 里本工具真正读取的字段。 */
30
+ /** `repos.getReleaseByTag` / `getLatestRelease` 里本工具真正读取的字段。 */
34
31
  const releaseViewSchema = Type.Object({
35
- tagName: Type.String(),
36
- assets: Type.Array(Type.Object({ name: Type.String(), size: Type.Number() })),
32
+ tag_name: Type.String(),
33
+ assets: Type.Array(Type.Object({ id: Type.Number(), name: Type.String(), size: Type.Number() })),
37
34
  });
38
35
 
39
36
  /** One regular file in a release's download directory. */
@@ -56,7 +53,7 @@ export function releaseAssetDir(repo: string, tag: string): string {
56
53
  return join(homedir(), ".cache", "pi", "github", "releases", owner, name, safeTag);
57
54
  }
58
55
 
59
- /** Split the comma-separated `pattern` toolcall parameter into gh pattern values. */
56
+ /** Split the comma-separated `pattern` toolcall parameter into glob values. */
60
57
  export function releasePatterns(pattern: string | undefined): string[] {
61
58
  if (pattern === undefined) {
62
59
  return [];
@@ -67,28 +64,19 @@ export function releasePatterns(pattern: string | undefined): string[] {
67
64
  .filter((value) => value !== "");
68
65
  }
69
66
 
70
- /**
71
- * The `gh release download` argv. `--skip-existing` is always on: the download
72
- * directory is a cache, and rewriting a file that is already there would pull
73
- * the ground out from under anything reading it.
74
- */
75
- export function releaseDownloadArgs(options: {
76
- tag: string;
77
- repo: string;
78
- dir: string;
79
- patterns: readonly string[];
80
- archive?: "zip" | "tar.gz";
81
- }): string[] {
82
- const { tag, repo, dir, patterns, archive } = options;
83
- const args = ["release", "download", tag, ...repoArgs(repo)];
84
- if (archive !== undefined) {
85
- args.push("--archive", archive);
86
- }
87
- for (const pattern of patterns) {
88
- args.push("--pattern", pattern);
67
+ /** 资产名是否命中任意一个 glob(`*` / `?` / `[...]` 都支持;无 glob 时取全部资产)。 */
68
+ export function matchAsset(name: string, patterns: readonly string[]): boolean {
69
+ return patterns.length === 0 || patterns.some((pattern) => matchesGlob(name, pattern));
70
+ }
71
+
72
+ /** 文件大小;不存在(或读不到)时给 undefined,用来判断是否已下载完整。 */
73
+ async function fileSize(path: string): Promise<number | undefined> {
74
+ try {
75
+ const info = await stat(path);
76
+ return info.size;
77
+ } catch {
78
+ return undefined;
89
79
  }
90
- args.push("--dir", dir, "--skip-existing");
91
- return args;
92
80
  }
93
81
 
94
82
  /** Regular files directly inside `dir`, with their sizes, sorted by name. */
@@ -107,22 +95,15 @@ async function listReleaseFiles(dir: string): Promise<ReleaseFile[]> {
107
95
  }
108
96
 
109
97
  /**
110
- * `gh release download` answers this exact message when a `--pattern` matched no
111
- * asset. It is the only signal the CLI offers, so the enrichment below degrades
112
- * to gh's own error (still thrown) if the wording ever changes.
113
- */
114
- const GH_NO_ASSET_MATCH = "no assets match the file pattern";
115
-
116
- /**
117
- * `download-github-release-assets`: fetch a release's assets (or source archive)
118
- * into `releaseAssetDir` with the gh credentials, so private repositories work
119
- * and the shell sandbox's network limits do not apply.
98
+ * `download-github-release-assets`: 把一个 release 的资产(或源码归档)下载到
99
+ * `releaseAssetDir`,用 `gh auth token` 的凭据,因此私有仓库可用、也不受 shell 沙箱的网络
100
+ * 限制。
120
101
  *
121
- * The tag is resolved through `gh release view` before downloading: a tag that
122
- * does not exist and a pattern that matched nothing are different answers, and
123
- * the release's own asset names are what the model needs to fix the second one.
102
+ * 先解析 release(缺 tag 时取 latest):tag 不存在与「glob 没匹配到资产」是两种不同的答案,
103
+ * 后者要把该 release 实际的可选资产名列出来,模型才知道下一步怎么改。
124
104
  */
125
105
  export async function downloadReleaseAssets(
106
+ gh: GhClient,
126
107
  call: ToolCall<ReleaseDownloadParams>,
127
108
  ): Promise<StructuredResultOf<typeof releaseDownloadSchema> | StructuredFailureResult> {
128
109
  const { params, ctx, signal } = call;
@@ -134,58 +115,55 @@ export async function downloadReleaseAssets(
134
115
  }
135
116
 
136
117
  const effectiveRepo = await resolveRepo(params.repo, signal, ctx.cwd, params);
118
+ const { owner, repo: repoName } = splitRepo(effectiveRepo);
137
119
  const view = Value.Parse(
138
120
  releaseViewSchema,
139
- JSON.parse(
140
- await ghExec(
141
- [
142
- "release",
143
- "view",
144
- ...(params.tag === undefined ? [] : [params.tag]),
145
- ...repoArgs(effectiveRepo),
146
- "--json",
147
- "tagName,assets",
148
- ],
149
- { cwd: ctx.cwd, signal, input: params },
150
- ),
151
- ),
121
+ await gh.reads.release(owner, repoName, params.tag, signal),
152
122
  );
153
123
 
154
124
  const assetNames = view.assets.map((asset) => asset.name);
155
- const dir = releaseAssetDir(effectiveRepo, view.tagName);
125
+ const dir = releaseAssetDir(effectiveRepo, view.tag_name);
156
126
  await mkdir(dir, { recursive: true });
157
- try {
158
- await ghExec(
159
- releaseDownloadArgs({
160
- tag: view.tagName,
161
- repo: effectiveRepo,
162
- dir,
163
- patterns,
164
- ...(params.archive !== undefined && { archive: params.archive }),
165
- }),
166
- { cwd: ctx.cwd, signal, input: params },
167
- );
168
- } catch (error) {
169
- // gh names the fault but not the choices; the release's asset list turns a
170
- // dead end into the next toolcall.
171
- if (error instanceof GhError && error.stderr.includes(GH_NO_ASSET_MATCH)) {
127
+
128
+ if (params.archive === undefined) {
129
+ const selected = view.assets.filter((asset) => matchAsset(asset.name, patterns));
130
+ if (selected.length === 0 && patterns.length > 0) {
172
131
  throw new Error(
173
- `no asset of ${effectiveRepo}@${view.tagName} matched ${JSON.stringify(patterns)}; the release has: ${assetNames.join(", ") || "(no assets)"}`,
174
- { cause: error },
132
+ `no asset of ${effectiveRepo}@${view.tag_name} matched ${JSON.stringify(patterns)}; the release has: ${assetNames.join(", ") || "(no assets)"}`,
175
133
  );
176
134
  }
177
- throw error;
135
+ for (const asset of selected) {
136
+ const destPath = join(dir, asset.name);
137
+ // 同名且大小一致视为已完成(缓存目录里的半截文件不该被当成结果)
138
+ const existing = await fileSize(destPath);
139
+ if (existing === asset.size) {
140
+ continue;
141
+ }
142
+ await gh.reads.downloadAssetTo(owner, repoName, asset.id, destPath, signal);
143
+ }
144
+ } else {
145
+ const extension = params.archive === "zip" ? "zip" : "tar.gz";
146
+ // 归档是仓库源码的 tarball / zipball,名字由我们定(gh 用的是 `<owner>-<repo>-<tag>`)
147
+ const destPath = join(dir, `${repoName}-${view.tag_name}.${extension}`);
148
+ await gh.reads.downloadArchiveTo(
149
+ owner,
150
+ repoName,
151
+ params.archive,
152
+ view.tag_name,
153
+ destPath,
154
+ signal,
155
+ );
178
156
  }
179
157
 
180
158
  const files = await listReleaseFiles(dir);
181
- const payload = { repo: effectiveRepo, tag: view.tagName, dir, files };
159
+ const payload = { repo: effectiveRepo, tag: view.tag_name, dir, files };
182
160
  const pendant: ToolPendant | undefined = subtitlePendant(
183
- { repo: effectiveRepo, tag: view.tagName },
161
+ { repo: effectiveRepo, tag: view.tag_name },
184
162
  "tag",
185
163
  );
186
164
 
187
165
  if (files.length === 0 && params.archive === undefined) {
188
- const text = `Nothing to download from ${effectiveRepo}@${view.tagName}: the release has no assets (try archive for the source tarball)`;
166
+ const text = `Nothing to download from ${effectiveRepo}@${view.tag_name}: the release has no assets (try archive for the source tarball)`;
189
167
  return {
190
168
  content: [{ type: "text", text }],
191
169
  details: {
@@ -207,14 +185,14 @@ export async function downloadReleaseAssets(
207
185
  );
208
186
  }
209
187
 
210
- export function addDownloadReleaseAssetsTool(_gh: GhClient, bus: ToolBus) {
188
+ export function addDownloadReleaseAssetsTool(gh: GhClient, bus: ToolBus) {
211
189
  bus.register(
212
190
  defineStructuredTool({
213
191
  name: "download-github-release-assets",
214
192
  label: "GitHub Release Download",
215
193
  description:
216
194
  "Download a GitHub release's assets (or its source archive) into " +
217
- "~/.cache/pi/github/releases/<owner>/<repo>/<tag>/ using the gh CLI's credentials, " +
195
+ "~/.cache/pi/github/releases/<owner>/<repo>/<tag>/ using the GitHub credentials, " +
218
196
  "so private repositories and large binaries work where a plain HTTP fetch cannot. " +
219
197
  "Files already in that directory are kept, never re-fetched. The result is the JSON " +
220
198
  "summary {repo, tag, dir, files:[{name, path, bytes}]} listing everything now in the " +
@@ -239,7 +217,7 @@ export function addDownloadReleaseAssetsTool(_gh: GhClient, bus: ToolBus) {
239
217
  }),
240
218
  structuredSchema: releaseDownloadSchema,
241
219
  async execute(_id, params, signal, _onUpdate, ctx) {
242
- return downloadReleaseAssets({ params, ctx, signal });
220
+ return downloadReleaseAssets(gh, { params, ctx, signal });
243
221
  },
244
222
  }),
245
223
  );
@@ -26,7 +26,7 @@ async function listIssues(
26
26
  const { params, ctx, signal } = call;
27
27
  const { repo, hits } = params.keywords
28
28
  ? { repo: params.repo, hits: await gh.search.search("issue", params) }
29
- : await browseList("issue", params, { cwd: ctx.cwd, signal, input: params });
29
+ : await browseList(gh, "issue", params, { cwd: ctx.cwd, signal, input: params });
30
30
  const text = renderHitList("issue", hits, { repo, fields: params.fields });
31
31
  const result = toToolResult(text, params);
32
32
  result.details.pendant = subtitlePendant(params);
@@ -52,14 +52,18 @@ export function addListIssuesTool(gh: GhClient, bus: ToolBus) {
52
52
  }),
53
53
  ),
54
54
  label: Type.Optional(Type.String({ description: "Filter by label" })),
55
- // `@me` 由 GitHub 的搜索 / 过滤 API 解析:不带 keywords 时 gh 自行展开(GraphQL 过滤分支)
56
- // 或按字面量转发(搜索分支),带 keywords 时我们直接把 `assignee:@me` 交给搜索 API。
57
- // 两条传输都支持,因此本仓库不要再实现一次展开。
55
+ // `@me` 只在关键词搜索分支可用:搜索 API 自己把它解析成当前登录用户。浏览分支走
56
+ // REST 列表端点,`@me` 按字面量转发,不会匹配到当前用户(本仓库不做展开)。
58
57
  author: Type.Optional(
59
- Type.String({ description: "Filter by author ('@me' for yourself)" }),
58
+ Type.String({
59
+ description: "Filter by author ('@me' works with keywords; browse takes it literally)",
60
+ }),
60
61
  ),
61
62
  assignee: Type.Optional(
62
- Type.String({ description: "Filter by assignee ('@me' for yourself)" }),
63
+ Type.String({
64
+ description:
65
+ "Filter by assignee ('@me' works with keywords; browse takes it literally)",
66
+ }),
63
67
  ),
64
68
  milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
65
69
  limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),
@@ -26,7 +26,7 @@ async function listPrs(
26
26
  const { params, ctx, signal } = call;
27
27
  const { repo, hits } = params.keywords
28
28
  ? { repo: params.repo, hits: await gh.search.search("pr", params) }
29
- : await browseList("pr", params, { cwd: ctx.cwd, signal, input: params });
29
+ : await browseList(gh, "pr", params, { cwd: ctx.cwd, signal, input: params });
30
30
  const text = renderHitList("pr", hits, { repo, fields: params.fields });
31
31
  const result = toToolResult(text, params);
32
32
  result.details.pendant = subtitlePendant(params);
@@ -52,14 +52,18 @@ export function addListPrsTool(gh: GhClient, bus: ToolBus) {
52
52
  }),
53
53
  ),
54
54
  label: Type.Optional(Type.String({ description: "Filter by label" })),
55
- // `@me` 由 GitHub 的搜索 / 过滤 API 解析:不带 keywords 时 gh 自行展开(GraphQL 过滤分支)
56
- // 或按字面量转发(搜索分支),带 keywords 时我们直接把 `assignee:@me` 交给搜索 API。
57
- // 两条传输都支持,因此本仓库不要再实现一次展开。
55
+ // `@me` 只在关键词搜索分支可用:搜索 API 自己把它解析成当前登录用户。浏览分支走
56
+ // REST 列表端点,`@me` 按字面量转发,不会匹配到当前用户(本仓库不做展开)。
58
57
  author: Type.Optional(
59
- Type.String({ description: "Filter by author ('@me' for yourself)" }),
58
+ Type.String({
59
+ description: "Filter by author ('@me' works with keywords; browse takes it literally)",
60
+ }),
60
61
  ),
61
62
  assignee: Type.Optional(
62
- Type.String({ description: "Filter by assignee ('@me' for yourself)" }),
63
+ Type.String({
64
+ description:
65
+ "Filter by assignee ('@me' works with keywords; browse takes it literally)",
66
+ }),
63
67
  ),
64
68
  milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
65
69
  limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),