@trim21/personal-pi-extensions 0.1.685 → 0.1.688
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 +2 -2
- package/package.json +1 -1
- package/src/gh/base.ts +115 -60
- package/src/gh/render.ts +65 -65
- package/src/gh/schemas.ts +102 -129
- package/src/gh/tools/download-release-assets.ts +58 -80
- package/src/gh/tools/list-issues.ts +10 -6
- package/src/gh/tools/list-prs.ts +10 -6
- package/src/gh/tools/list-releases.ts +14 -13
- package/src/gh/tools/list-workflow-runs.ts +7 -19
- package/src/gh/tools/read-ci-logs.ts +6 -19
- package/src/gh/tools/read-issue-comments.ts +10 -7
- package/src/gh/tools/read-issue.ts +10 -14
- package/src/gh/tools/read-pr-comments.ts +19 -38
- package/src/gh/tools/read-pr-diff.ts +9 -8
- package/src/gh/tools/read-pr.ts +7 -15
- package/src/gh/tools/read-release.ts +7 -23
- package/src/gh/tools/read-repo.ts +7 -32
- package/src/gh/tools/wait-pr-checks.ts +6 -7
- package/src/gh/tools/watch-run.ts +35 -25
- package/src/lib/github-reads.ts +405 -0
- package/src/lib/github.ts +74 -57
package/src/gh/schemas.ts
CHANGED
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
* `structuredResult.value`。因此 schema 必须**允许额外字段**(GitHub 的 JSON 只多不少,
|
|
6
6
|
* 多出来的字段不该让工具失败),只对工具本身已经依赖的字段设 required。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
/** 评论条目:
|
|
35
|
+
/** 评论条目:REST 的 issue/PR 评论与行内评审评论共用(`diff_hunk` 只有后者有)。 */
|
|
37
36
|
export const ghCommentSchema = Type.Object({
|
|
38
|
-
id: Type.Optional(Type.
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
/** 评审条目:
|
|
48
|
+
/** 评审条目:REST 的 `/pulls/:n/reviews`。 */
|
|
50
49
|
export const ghReviewSchema = Type.Object({
|
|
51
|
-
id: Type.Optional(Type.
|
|
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
|
-
|
|
57
|
-
|
|
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`:`
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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`:`
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
111
|
+
changed_files: Type.Optional(Type.Number()),
|
|
113
112
|
labels: Type.Optional(Type.Array(ghLabelSchema)),
|
|
114
|
-
assignees: Type.Optional(Type.Array(ghActorSchema)),
|
|
115
|
-
|
|
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
|
-
/**
|
|
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
|
-
/** `
|
|
262
|
+
/** `repos.listReleases` 的一行(REST 字段名)。 */
|
|
267
263
|
export const ghReleaseSummarySchema = Type.Object(
|
|
268
264
|
{
|
|
269
|
-
|
|
270
|
-
name: Type.Optional(Type.String()),
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
/** `
|
|
283
|
+
/** `actions.listWorkflowRuns*` / `getWorkflowRun` 的共同字段(REST 字段名)。 */
|
|
289
284
|
export const ghRunSummarySchema = Type.Object(
|
|
290
285
|
{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
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
|
-
/** `
|
|
318
|
+
/** `repos.get` 里我们渲染与脚本可能用到的字段(REST 字段名)。 */
|
|
324
319
|
export const ghRepoViewSchema = Type.Object(
|
|
325
320
|
{
|
|
326
321
|
name: Type.Optional(Type.String()),
|
|
327
|
-
|
|
328
|
-
description: Type.Optional(Type.String()),
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
name: Type.Optional(Type.String()),
|
|
387
|
-
body: Type.Optional(Type.String()),
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
402
|
-
|
|
365
|
+
download_count: Type.Optional(Type.Number()),
|
|
366
|
+
content_type: Type.Optional(Type.String()),
|
|
403
367
|
state: Type.Optional(Type.String()),
|
|
404
|
-
|
|
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
|
-
/** `
|
|
30
|
+
/** `repos.getReleaseByTag` / `getLatestRelease` 里本工具真正读取的字段。 */
|
|
34
31
|
const releaseViewSchema = Type.Object({
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
* `
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
-
*
|
|
122
|
-
*
|
|
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
|
-
|
|
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.
|
|
125
|
+
const dir = releaseAssetDir(effectiveRepo, view.tag_name);
|
|
156
126
|
await mkdir(dir, { recursive: true });
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
159
|
+
const payload = { repo: effectiveRepo, tag: view.tag_name, dir, files };
|
|
182
160
|
const pendant: ToolPendant | undefined = subtitlePendant(
|
|
183
|
-
{ repo: effectiveRepo, tag: view.
|
|
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.
|
|
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(
|
|
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
|
|
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`
|
|
56
|
-
//
|
|
57
|
-
// 两条传输都支持,因此本仓库不要再实现一次展开。
|
|
55
|
+
// `@me` 只在关键词搜索分支可用:搜索 API 自己把它解析成当前登录用户。浏览分支走
|
|
56
|
+
// REST 列表端点,`@me` 按字面量转发,不会匹配到当前用户(本仓库不做展开)。
|
|
58
57
|
author: Type.Optional(
|
|
59
|
-
Type.String({
|
|
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({
|
|
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)" })),
|
package/src/gh/tools/list-prs.ts
CHANGED
|
@@ -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`
|
|
56
|
-
//
|
|
57
|
-
// 两条传输都支持,因此本仓库不要再实现一次展开。
|
|
55
|
+
// `@me` 只在关键词搜索分支可用:搜索 API 自己把它解析成当前登录用户。浏览分支走
|
|
56
|
+
// REST 列表端点,`@me` 按字面量转发,不会匹配到当前用户(本仓库不做展开)。
|
|
58
57
|
author: Type.Optional(
|
|
59
|
-
Type.String({
|
|
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({
|
|
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)" })),
|