@trim21/personal-pi-extensions 0.1.678 → 0.1.679
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 +10 -4
- package/package.json +1 -1
- package/src/codemode/declarations.ts +28 -12
- package/src/codemode/prelude.ts +32 -16
- package/src/codemode/protocol.ts +5 -2
- package/src/codemode/tool.ts +10 -3
- package/src/codemode/worker.js +36 -29
- package/src/codemode/worker.ts +2 -10
- package/src/gh/base.ts +62 -12
- package/src/gh/schemas.ts +230 -0
- package/src/gh/tools/download-release-assets.ts +51 -44
- package/src/gh/tools/get-workflow-jobs.ts +29 -18
- package/src/gh/tools/read-ci-logs.ts +37 -23
- package/src/gh/tools/read-issue-comments.ts +31 -24
- package/src/gh/tools/read-issue.ts +30 -22
- package/src/gh/tools/read-pr-comments.ts +62 -58
- package/src/gh/tools/read-pr-status.ts +31 -20
- package/src/gh/tools/read-pr.ts +30 -22
- package/src/gh/tools/wait-commit-checks.ts +35 -31
- package/src/gh/tools/wait-pr-checks.ts +30 -23
- package/src/lib/tool-bus.ts +128 -8
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gh-readonly 工具的结构化结果 schema(TypeBox)。
|
|
3
|
+
*
|
|
4
|
+
* 两个用途:codemode 脚本侧据此渲染 `call()` 的返回类型;工具总线在运行期用它复核
|
|
5
|
+
* `structuredResult.value`。因此 schema 必须**允许额外字段**(GitHub 的 JSON 只多不少,
|
|
6
|
+
* 多出来的字段不该让工具失败),只对工具本身已经依赖的字段设 required。
|
|
7
|
+
*
|
|
8
|
+
* 字段名取自 GitHub 的两种传输:`gh ... view --json`(GraphQL,actor 叫 `author` /
|
|
9
|
+
* `user`)与 REST(`/pulls/:n/comments` 等,用了 `diff_hunk` / `submitted_at` 这类
|
|
10
|
+
* 下划线命名)。评论与评审条目因此放宽成两者的并集。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Type } from "typebox";
|
|
14
|
+
|
|
15
|
+
// ── 共享片段 ─────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** GitHub 的 actor(author / assignee / mergedBy / review 作者)。 */
|
|
18
|
+
export const ghActorSchema = Type.Object({
|
|
19
|
+
login: Type.Optional(Type.String()),
|
|
20
|
+
name: Type.Optional(Type.String()),
|
|
21
|
+
is_bot: Type.Optional(Type.Boolean()),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const ghLabelSchema = Type.Object({
|
|
25
|
+
name: Type.String(),
|
|
26
|
+
color: Type.Optional(Type.String()),
|
|
27
|
+
description: Type.Optional(Type.String()),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const ghMilestoneSchema = Type.Object({
|
|
31
|
+
title: Type.String(),
|
|
32
|
+
number: Type.Optional(Type.Number()),
|
|
33
|
+
dueOn: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** 评论条目:GraphQL 的 `author`/`createdAt` 与 REST 的 `user`/`diff_hunk` 并集。 */
|
|
37
|
+
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()])),
|
|
41
|
+
user: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
|
|
42
|
+
path: Type.Optional(Type.String()),
|
|
43
|
+
line: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
|
|
44
|
+
diff_hunk: Type.Optional(Type.String()),
|
|
45
|
+
createdAt: Type.Optional(Type.String()),
|
|
46
|
+
url: Type.Optional(Type.String()),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** 评审条目:GraphQL 的 `reviews` 与 REST 的 `/pulls/:n/reviews` 并集。 */
|
|
50
|
+
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()])),
|
|
54
|
+
user: Type.Optional(Type.Union([ghActorSchema, Type.Null()])),
|
|
55
|
+
state: Type.Optional(Type.String()),
|
|
56
|
+
createdAt: Type.Optional(Type.String()),
|
|
57
|
+
submitted_at: Type.Optional(Type.String()),
|
|
58
|
+
url: Type.Optional(Type.String()),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** 待评审人:可能是用户(login)也可能是团队(slug)。 */
|
|
62
|
+
export const ghReviewRequestSchema = Type.Object({
|
|
63
|
+
login: Type.Optional(Type.String()),
|
|
64
|
+
name: Type.Optional(Type.String()),
|
|
65
|
+
slug: Type.Optional(Type.String()),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** `read-github-pr-status` / 两个 wait 工具共用的检查桶。 */
|
|
69
|
+
export const checkBucketSchema = Type.Union([
|
|
70
|
+
Type.Literal("pass"),
|
|
71
|
+
Type.Literal("fail"),
|
|
72
|
+
Type.Literal("pending"),
|
|
73
|
+
Type.Literal("skipped"),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
// ── 各工具的输出 ─────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/** `read-github-issue`:`gh issue view --json` 的输出。 */
|
|
79
|
+
export const issueViewSchema = Type.Object({
|
|
80
|
+
number: Type.Number(),
|
|
81
|
+
title: Type.String(),
|
|
82
|
+
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()),
|
|
89
|
+
labels: Type.Optional(Type.Array(ghLabelSchema)),
|
|
90
|
+
assignees: Type.Optional(Type.Array(ghActorSchema)),
|
|
91
|
+
comments: Type.Optional(Type.Array(ghCommentSchema)),
|
|
92
|
+
milestone: Type.Optional(Type.Union([ghMilestoneSchema, Type.Null()])),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/** `read-github-pr`:`gh pr view --json` 的输出,比 issue 多出合并与 diff 统计。 */
|
|
96
|
+
export const prViewSchema = Type.Object({
|
|
97
|
+
number: Type.Number(),
|
|
98
|
+
title: Type.String(),
|
|
99
|
+
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()),
|
|
110
|
+
additions: Type.Optional(Type.Number()),
|
|
111
|
+
deletions: Type.Optional(Type.Number()),
|
|
112
|
+
changedFiles: Type.Optional(Type.Number()),
|
|
113
|
+
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)),
|
|
118
|
+
milestone: Type.Optional(Type.Union([ghMilestoneSchema, Type.Null()])),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** `read-github-issue-comments`:`gh issue view --json comments`。 */
|
|
122
|
+
export const issueCommentsSchema = Type.Object({
|
|
123
|
+
comments: Type.Array(ghCommentSchema),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* `read-github-pr-comments`:默认模式是 `gh pr view --json comments`(只有 comments),
|
|
128
|
+
* `reviews: true` 时走 REST 同时给出 reviews 与行内评论。两个字段都可选。
|
|
129
|
+
*/
|
|
130
|
+
export const prCommentsSchema = Type.Object({
|
|
131
|
+
comments: Type.Optional(Type.Array(ghCommentSchema)),
|
|
132
|
+
reviews: Type.Optional(Type.Array(ghReviewSchema)),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/** `read-github-pr-status`:工具自己拼的 `{pr, repo, head_sha, checks}`。 */
|
|
136
|
+
export const prStatusSchema = Type.Object({
|
|
137
|
+
pr: Type.Number(),
|
|
138
|
+
repo: Type.String(),
|
|
139
|
+
head_sha: Type.String(),
|
|
140
|
+
checks: Type.Array(
|
|
141
|
+
Type.Object({
|
|
142
|
+
name: Type.String(),
|
|
143
|
+
bucket: checkBucketSchema,
|
|
144
|
+
event: Type.Union([Type.String(), Type.Null()]),
|
|
145
|
+
run_id: Type.Union([Type.Number(), Type.Null()]),
|
|
146
|
+
job_id: Type.Union([Type.Number(), Type.Null()]),
|
|
147
|
+
url: Type.Union([Type.String(), Type.Null()]),
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const workflowJobsSchema = Type.Object({
|
|
153
|
+
total_count: Type.Number(),
|
|
154
|
+
jobs: Type.Array(
|
|
155
|
+
Type.Object({
|
|
156
|
+
id: Type.Number(),
|
|
157
|
+
run_id: Type.Number(),
|
|
158
|
+
run_url: Type.String(),
|
|
159
|
+
name: Type.String(),
|
|
160
|
+
status: Type.String(),
|
|
161
|
+
conclusion: Type.Union([Type.String(), Type.Null()]),
|
|
162
|
+
html_url: Type.Union([Type.String(), Type.Null()]),
|
|
163
|
+
steps: Type.Array(
|
|
164
|
+
Type.Object({
|
|
165
|
+
name: Type.String(),
|
|
166
|
+
number: Type.Number(),
|
|
167
|
+
status: Type.String(),
|
|
168
|
+
conclusion: Type.Union([Type.String(), Type.Null()]),
|
|
169
|
+
started_at: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
170
|
+
}),
|
|
171
|
+
),
|
|
172
|
+
}),
|
|
173
|
+
),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/** `read-github-ci-logs`:`CiLogsJobIndex`(步骤带它在原始日志里的行号范围)。 */
|
|
177
|
+
export const ciLogsSchema = Type.Object({
|
|
178
|
+
name: Type.String(),
|
|
179
|
+
id: Type.Number(),
|
|
180
|
+
status: Type.String(),
|
|
181
|
+
conclusion: Type.Union([Type.String(), Type.Null()]),
|
|
182
|
+
log_file: Type.String(),
|
|
183
|
+
steps: Type.Array(
|
|
184
|
+
Type.Object({
|
|
185
|
+
number: Type.Number(),
|
|
186
|
+
name: Type.String(),
|
|
187
|
+
conclusion: Type.Union([Type.String(), Type.Null()]),
|
|
188
|
+
start_line: Type.Optional(Type.Number()),
|
|
189
|
+
end_line: Type.Optional(Type.Number()),
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/** `download-github-release-assets`:下载目录里此刻的文件清单。 */
|
|
195
|
+
export const releaseDownloadSchema = Type.Object({
|
|
196
|
+
repo: Type.String(),
|
|
197
|
+
tag: Type.String(),
|
|
198
|
+
dir: Type.String(),
|
|
199
|
+
files: Type.Array(
|
|
200
|
+
Type.Object({ name: Type.String(), path: Type.String(), bytes: Type.Number() }),
|
|
201
|
+
),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/** 两个 wait 工具共享的判定结果(`waitChecksReport` 的 payload)。 */
|
|
205
|
+
export const checksVerdictSchema = Type.Object({
|
|
206
|
+
status: Type.Union([Type.Literal("success"), Type.Literal("failure"), Type.Literal("pending")]),
|
|
207
|
+
totalChecks: Type.Number(),
|
|
208
|
+
checks: Type.Array(
|
|
209
|
+
Type.Object({
|
|
210
|
+
name: Type.String(),
|
|
211
|
+
bucket: checkBucketSchema,
|
|
212
|
+
startedAt: Type.Union([Type.String(), Type.Null()]),
|
|
213
|
+
link: Type.Union([Type.String(), Type.Null()]),
|
|
214
|
+
event: Type.Union([Type.String(), Type.Null()]),
|
|
215
|
+
runId: Type.Union([Type.Number(), Type.Null()]),
|
|
216
|
+
jobId: Type.Union([Type.Number(), Type.Null()]),
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
219
|
+
failedJobs: Type.Array(
|
|
220
|
+
Type.Object({
|
|
221
|
+
runId: Type.Number(),
|
|
222
|
+
runName: Type.String(),
|
|
223
|
+
runUrl: Type.String(),
|
|
224
|
+
jobId: Type.Number(),
|
|
225
|
+
jobName: Type.String(),
|
|
226
|
+
conclusion: Type.String(),
|
|
227
|
+
jobUrl: Type.Optional(Type.String()),
|
|
228
|
+
}),
|
|
229
|
+
),
|
|
230
|
+
});
|
|
@@ -6,7 +6,7 @@ import { Type } from "typebox";
|
|
|
6
6
|
import { Value } from "typebox/value";
|
|
7
7
|
|
|
8
8
|
import { type ToolPendant } from "../../lib/pendant.js";
|
|
9
|
-
import type
|
|
9
|
+
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
10
10
|
import {
|
|
11
11
|
type GhClient,
|
|
12
12
|
GhError,
|
|
@@ -14,10 +14,14 @@ import {
|
|
|
14
14
|
repoArgs,
|
|
15
15
|
resolveRepo,
|
|
16
16
|
splitRepo,
|
|
17
|
+
structuredFailure,
|
|
18
|
+
type StructuredFailureResult,
|
|
19
|
+
type StructuredResultOf,
|
|
17
20
|
subtitlePendant,
|
|
18
21
|
type ToolCall,
|
|
19
|
-
|
|
22
|
+
withStructuredResult,
|
|
20
23
|
} from "../base.js";
|
|
24
|
+
import { releaseDownloadSchema } from "../schemas.js";
|
|
21
25
|
|
|
22
26
|
interface ReleaseDownloadParams {
|
|
23
27
|
repo?: string;
|
|
@@ -120,7 +124,7 @@ const GH_NO_ASSET_MATCH = "no assets match the file pattern";
|
|
|
120
124
|
*/
|
|
121
125
|
export async function downloadReleaseAssets(
|
|
122
126
|
call: ToolCall<ReleaseDownloadParams>,
|
|
123
|
-
): Promise<
|
|
127
|
+
): Promise<StructuredResultOf<typeof releaseDownloadSchema> | StructuredFailureResult> {
|
|
124
128
|
const { params, ctx, signal } = call;
|
|
125
129
|
const patterns = releasePatterns(params.pattern);
|
|
126
130
|
if (params.archive !== undefined && patterns.length > 0) {
|
|
@@ -181,59 +185,62 @@ export async function downloadReleaseAssets(
|
|
|
181
185
|
);
|
|
182
186
|
|
|
183
187
|
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)`;
|
|
184
189
|
return {
|
|
185
|
-
content: [
|
|
186
|
-
{
|
|
187
|
-
type: "text",
|
|
188
|
-
text: `Nothing to download from ${effectiveRepo}@${view.tagName}: the release has no assets (try archive for the source tarball)`,
|
|
189
|
-
},
|
|
190
|
-
],
|
|
190
|
+
content: [{ type: "text", text }],
|
|
191
191
|
details: {
|
|
192
192
|
...payload,
|
|
193
193
|
available_assets: assetNames,
|
|
194
194
|
input: params,
|
|
195
195
|
...(pendant && { pendant }),
|
|
196
196
|
},
|
|
197
|
+
structuredResult: structuredFailure(text),
|
|
197
198
|
};
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
return withStructuredResult(
|
|
202
|
+
{
|
|
203
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
204
|
+
details: { ...payload, input: params, ...(pendant && { pendant }) },
|
|
205
|
+
},
|
|
206
|
+
payload,
|
|
207
|
+
);
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
export function addDownloadReleaseAssetsTool(_gh: GhClient, bus: ToolBus) {
|
|
207
|
-
bus.register(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
Type.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
Type.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
211
|
+
bus.register(
|
|
212
|
+
defineStructuredTool({
|
|
213
|
+
name: "download-github-release-assets",
|
|
214
|
+
label: "GitHub Release Download",
|
|
215
|
+
description:
|
|
216
|
+
"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, " +
|
|
218
|
+
"so private repositories and large binaries work where a plain HTTP fetch cannot. " +
|
|
219
|
+
"Files already in that directory are kept, never re-fetched. The result is the JSON " +
|
|
220
|
+
"summary {repo, tag, dir, files:[{name, path, bytes}]} listing everything now in the " +
|
|
221
|
+
"directory; file contents are not echoed. Read the entries you need from `path`.",
|
|
222
|
+
promptSnippet: "Download GitHub release assets",
|
|
223
|
+
parameters: Type.Object({
|
|
224
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
|
|
225
|
+
tag: Type.Optional(
|
|
226
|
+
Type.String({ description: "Release tag (defaults to the latest release)" }),
|
|
227
|
+
),
|
|
228
|
+
pattern: Type.Optional(
|
|
229
|
+
Type.String({
|
|
230
|
+
description:
|
|
231
|
+
'Comma-separated glob patterns for asset names, e.g. "*.tar.gz,*.deb" (default: every asset)',
|
|
232
|
+
}),
|
|
233
|
+
),
|
|
234
|
+
archive: Type.Optional(
|
|
235
|
+
Type.Union([Type.Literal("zip"), Type.Literal("tar.gz")], {
|
|
236
|
+
description: "Download the release's source archive instead of its assets",
|
|
237
|
+
}),
|
|
238
|
+
),
|
|
239
|
+
}),
|
|
240
|
+
structuredSchema: releaseDownloadSchema,
|
|
241
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
242
|
+
return downloadReleaseAssets({ params, ctx, signal });
|
|
243
|
+
},
|
|
234
244
|
}),
|
|
235
|
-
|
|
236
|
-
return downloadReleaseAssets({ params, ctx, signal });
|
|
237
|
-
},
|
|
238
|
-
});
|
|
245
|
+
);
|
|
239
246
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
4
4
|
import {
|
|
5
5
|
type GhClient,
|
|
6
6
|
resolveRepo,
|
|
7
7
|
splitRepo,
|
|
8
|
+
type StructuredResultOf,
|
|
8
9
|
subtitlePendant,
|
|
9
10
|
type ToolCall,
|
|
10
|
-
type ToolResult,
|
|
11
11
|
toPositiveId,
|
|
12
|
-
|
|
12
|
+
toStructuredJsonResult,
|
|
13
13
|
} from "../base.js";
|
|
14
|
+
import { workflowJobsSchema } from "../schemas.js";
|
|
14
15
|
|
|
15
16
|
interface RunIdParams {
|
|
16
17
|
run_id: number | string;
|
|
@@ -18,31 +19,41 @@ interface RunIdParams {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/** The toolcall handler behind `get-github-workflow-jobs`: every job of a run, all pages. */
|
|
21
|
-
async function workflowJobs(
|
|
22
|
+
async function workflowJobs(
|
|
23
|
+
gh: GhClient,
|
|
24
|
+
call: ToolCall<RunIdParams>,
|
|
25
|
+
): Promise<StructuredResultOf<typeof workflowJobsSchema>> {
|
|
22
26
|
const { params, ctx, signal } = call;
|
|
23
27
|
const runId = toPositiveId(params.run_id, "run_id");
|
|
24
28
|
const effectiveRepo = await resolveRepo(params.repo, signal, ctx.cwd, params);
|
|
25
29
|
const { owner, repo: name } = splitRepo(effectiveRepo);
|
|
26
30
|
|
|
27
31
|
const jobs = await gh.checks.runJobs(owner, name, runId, signal);
|
|
28
|
-
const result =
|
|
32
|
+
const result = toStructuredJsonResult(
|
|
33
|
+
JSON.stringify({ total_count: jobs.length, jobs }),
|
|
34
|
+
params,
|
|
35
|
+
workflowJobsSchema,
|
|
36
|
+
);
|
|
29
37
|
result.details.pendant = subtitlePendant(params, "run_id");
|
|
30
38
|
return result;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
export function addGetWorkflowJobsTool(gh: GhClient, bus: ToolBus) {
|
|
34
|
-
bus.register(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
bus.register(
|
|
43
|
+
defineStructuredTool({
|
|
44
|
+
name: "get-github-workflow-jobs",
|
|
45
|
+
label: "GitHub Workflow Jobs",
|
|
46
|
+
description:
|
|
47
|
+
"Get every job of a workflow run as JSON {total_count, jobs:[{id, run_id, run_url, name, status, conclusion, html_url, steps:[{name, number, status, conclusion, started_at}]}]}. Paginated server-side, so runs with more than 30 jobs return all of them. Use the `id` with read-github-ci-logs after read-github-pr-status / wait-github-commit-checks did not already give you a job id.",
|
|
48
|
+
promptSnippet: "Get GitHub workflow run jobs",
|
|
49
|
+
parameters: Type.Object({
|
|
50
|
+
run_id: Type.Union([Type.Number(), Type.String()], { description: "Workflow run ID" }),
|
|
51
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
52
|
+
}),
|
|
53
|
+
structuredSchema: workflowJobsSchema,
|
|
54
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
55
|
+
return workflowJobs(gh, { params, ctx, signal, onUpdate });
|
|
56
|
+
},
|
|
43
57
|
}),
|
|
44
|
-
|
|
45
|
-
return workflowJobs(gh, { params, ctx, signal, onUpdate });
|
|
46
|
-
},
|
|
47
|
-
});
|
|
58
|
+
);
|
|
48
59
|
}
|
|
@@ -11,17 +11,21 @@ import { Type } from "typebox";
|
|
|
11
11
|
|
|
12
12
|
import type { RunJob } from "../../lib/github.js";
|
|
13
13
|
import { createSeqState } from "../../lib/seq-state.js";
|
|
14
|
-
import type
|
|
14
|
+
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
15
15
|
import {
|
|
16
16
|
type GhClient,
|
|
17
17
|
ghExec,
|
|
18
18
|
resolveRepo,
|
|
19
19
|
splitRepo,
|
|
20
|
+
structuredFailure,
|
|
21
|
+
type StructuredFailureResult,
|
|
22
|
+
type StructuredResultOf,
|
|
20
23
|
subtitlePendant,
|
|
21
24
|
type ToolCall,
|
|
22
|
-
type ToolResult,
|
|
23
25
|
toPositiveId,
|
|
26
|
+
withStructuredResult,
|
|
24
27
|
} from "../base.js";
|
|
28
|
+
import { ciLogsSchema } from "../schemas.js";
|
|
25
29
|
|
|
26
30
|
// 模块级串行状态:同一资源(如 CI 日志)的请求排队执行,配合函数内部的
|
|
27
31
|
// 缓存检查避免重复网络请求。闭包状态不与其他扩展共享,key 无需全局前缀。
|
|
@@ -373,8 +377,11 @@ interface JobIdParams {
|
|
|
373
377
|
repo?: string;
|
|
374
378
|
}
|
|
375
379
|
|
|
380
|
+
/** 「没找到 / 还没日志」是结构化失败,不是异常:脚本能按 ok === false 分辨。 */
|
|
381
|
+
type CiLogsResult = StructuredResultOf<typeof ciLogsSchema> | StructuredFailureResult;
|
|
382
|
+
|
|
376
383
|
/** The toolcall handler behind `read-github-ci-logs`. */
|
|
377
|
-
async function ciLogs(gh: GhClient, call: ToolCall<JobIdParams>): Promise<
|
|
384
|
+
async function ciLogs(gh: GhClient, call: ToolCall<JobIdParams>): Promise<CiLogsResult> {
|
|
378
385
|
const { params, ctx, signal, onUpdate } = call;
|
|
379
386
|
const { job_id, repo } = params;
|
|
380
387
|
const jobId = toPositiveId(job_id, "job_id");
|
|
@@ -383,9 +390,10 @@ async function ciLogs(gh: GhClient, call: ToolCall<JobIdParams>): Promise<ToolRe
|
|
|
383
390
|
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
384
391
|
const { owner, repo: name } = splitRepo(effectiveRepo);
|
|
385
392
|
|
|
386
|
-
const failure = (text: string):
|
|
393
|
+
const failure = (text: string): StructuredFailureResult => ({
|
|
387
394
|
content: [{ type: "text", text }],
|
|
388
395
|
details: { input: params, ...(pendant && { pendant }) },
|
|
396
|
+
structuredResult: structuredFailure(text),
|
|
389
397
|
});
|
|
390
398
|
|
|
391
399
|
let target: RunJob;
|
|
@@ -415,28 +423,34 @@ async function ciLogs(gh: GhClient, call: ToolCall<JobIdParams>): Promise<ToolRe
|
|
|
415
423
|
const rawLog = await getJobLog(target, signal, ctx.cwd, params);
|
|
416
424
|
const index = jobLogIndex(target, rawLog);
|
|
417
425
|
|
|
418
|
-
return
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
426
|
+
return withStructuredResult(
|
|
427
|
+
{
|
|
428
|
+
content: [{ type: "text", text: JSON.stringify(index, null, 2) }],
|
|
429
|
+
details: { ...index, input: params, ...(pendant && { pendant }) },
|
|
430
|
+
},
|
|
431
|
+
index,
|
|
432
|
+
);
|
|
422
433
|
}
|
|
423
434
|
|
|
424
435
|
export function addReadCiLogsTool(gh: GhClient, bus: ToolBus) {
|
|
425
|
-
bus.register(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
436
|
+
bus.register(
|
|
437
|
+
defineStructuredTool({
|
|
438
|
+
name: "read-github-ci-logs",
|
|
439
|
+
label: "GitHub CI Logs",
|
|
440
|
+
description:
|
|
441
|
+
"Download one GitHub Actions job's CI log by job ID and index its steps. Returns JSON {name, id, status, conclusion, log_file, steps:[{number, name, conclusion, start_line?, end_line?}]}: `log_file` is the job's complete raw log on disk (runner timestamps and ANSI kept, exactly as GitHub delivers it) and each step carries the 1-based inclusive line range of its block inside that file. Read the content out of the file yourself (read/grep with offset/limit) — it is not echoed back. Get the job IDs from get-github-workflow-jobs, then call this once per job you need." +
|
|
442
|
+
" Note: queued jobs have no logs yet; use watch-github-run to wait for completion.",
|
|
443
|
+
promptSnippet: "Read GitHub CI logs",
|
|
444
|
+
parameters: Type.Object({
|
|
445
|
+
job_id: Type.Union([Type.Number(), Type.String()], {
|
|
446
|
+
description: "Job ID, from get-github-workflow-jobs.",
|
|
447
|
+
}),
|
|
448
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
435
449
|
}),
|
|
436
|
-
|
|
450
|
+
structuredSchema: ciLogsSchema,
|
|
451
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
452
|
+
return ciLogs(gh, { params, ctx, signal, onUpdate });
|
|
453
|
+
},
|
|
437
454
|
}),
|
|
438
|
-
|
|
439
|
-
return ciLogs(gh, { params, ctx, signal, onUpdate });
|
|
440
|
-
},
|
|
441
|
-
});
|
|
455
|
+
);
|
|
442
456
|
}
|
|
@@ -1,30 +1,37 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
|
|
3
|
-
import type
|
|
4
|
-
import {
|
|
3
|
+
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
4
|
+
import {
|
|
5
|
+
type GhClient,
|
|
6
|
+
ghExec,
|
|
7
|
+
repoArgs,
|
|
8
|
+
subtitlePendant,
|
|
9
|
+
toStructuredJsonResult,
|
|
10
|
+
} from "../base.js";
|
|
11
|
+
import { issueCommentsSchema } from "../schemas.js";
|
|
5
12
|
|
|
6
13
|
export function addReadIssueCommentsTool(_gh: GhClient, bus: ToolBus) {
|
|
7
|
-
bus.register(
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
bus.register(
|
|
15
|
+
defineStructuredTool({
|
|
16
|
+
name: "read-github-issue-comments",
|
|
17
|
+
label: "GitHub Issue Comments",
|
|
18
|
+
description: "Get comments on a GitHub issue.",
|
|
19
|
+
promptSnippet: "Read GitHub issue comments",
|
|
20
|
+
parameters: Type.Object({
|
|
21
|
+
number: Type.Union([Type.Number(), Type.String()], { description: "Issue number" }),
|
|
22
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
23
|
+
}),
|
|
24
|
+
structuredSchema: issueCommentsSchema,
|
|
25
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
26
|
+
const { number, repo } = params;
|
|
27
|
+
const out = await ghExec(
|
|
28
|
+
["issue", "view", String(number), ...repoArgs(repo), "--json", "comments"],
|
|
29
|
+
{ cwd: ctx.cwd, signal, input: params },
|
|
30
|
+
);
|
|
31
|
+
const result = toStructuredJsonResult(out, params, issueCommentsSchema);
|
|
32
|
+
result.details.pendant = subtitlePendant(params, "number");
|
|
33
|
+
return result;
|
|
34
|
+
},
|
|
15
35
|
}),
|
|
16
|
-
|
|
17
|
-
const { number, repo } = params;
|
|
18
|
-
const result = toToolResultJson(
|
|
19
|
-
await ghExec(["issue", "view", String(number), ...repoArgs(repo), "--json", "comments"], {
|
|
20
|
-
cwd: ctx.cwd,
|
|
21
|
-
signal,
|
|
22
|
-
input: params,
|
|
23
|
-
}),
|
|
24
|
-
params,
|
|
25
|
-
);
|
|
26
|
-
result.details.pendant = subtitlePendant(params, "number");
|
|
27
|
-
return result;
|
|
28
|
-
},
|
|
29
|
-
});
|
|
36
|
+
);
|
|
30
37
|
}
|