@trim21/personal-pi-extensions 0.1.690 → 0.1.691
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 +3 -3
- package/package.json +1 -1
- package/src/bwrap/dcg-scan.ts +2 -8
- package/src/bwrap/html.ts +13 -0
- package/src/bwrap/runtime.ts +1 -8
- package/src/claude-code/shell.ts +12 -42
- package/src/codemode/declarations.ts +3 -8
- package/src/gh/base.ts +91 -23
- package/src/gh/index.ts +8 -5
- package/src/gh/tools/download-release-assets.ts +6 -3
- package/src/gh/tools/get-workflow-jobs.ts +4 -7
- package/src/gh/tools/list-issues.ts +6 -39
- package/src/gh/tools/list-prs.ts +6 -39
- package/src/gh/tools/list-releases.ts +6 -9
- package/src/gh/tools/list-workflow-runs.ts +6 -9
- package/src/gh/tools/read-ci-logs.ts +8 -19
- package/src/gh/tools/read-issue-comments.ts +4 -12
- package/src/gh/tools/read-issue.ts +4 -12
- package/src/gh/tools/read-pr-comments.ts +6 -14
- package/src/gh/tools/read-pr-diff.ts +8 -8
- package/src/gh/tools/read-pr-status.ts +6 -4
- package/src/gh/tools/read-pr.ts +4 -12
- package/src/gh/tools/read-release.ts +5 -8
- package/src/gh/tools/read-repo.ts +5 -9
- package/src/gh/tools/wait-commit-checks.ts +2 -4
- package/src/gh/tools/wait-pr-checks.ts +2 -4
- package/src/gh/tools/watch-run.ts +7 -4
- package/src/lib/abort.ts +10 -0
- package/src/lib/bash-tool.ts +48 -0
- package/src/lib/github-reads.ts +3 -23
- package/src/lib/github.ts +81 -41
- package/src/lib/model-call.ts +3 -7
- package/src/lib/tools-config.ts +2 -4
- package/src/opencode/bash.ts +11 -42
- package/src/opencode/glob.ts +1 -6
- package/src/opencode/grep.ts +1 -6
- package/src/opencode/ripgrep.ts +5 -0
- package/src/web/fetch.ts +2 -6
- package/src/web/search.ts +2 -6
- package/src/orphan-worker-cleaner.ts +0 -38
package/README.md
CHANGED
|
@@ -461,20 +461,20 @@ pi -e ./src/aft/index.ts
|
|
|
461
461
|
|
|
462
462
|
## gh-readonly
|
|
463
463
|
|
|
464
|
-
GitHub
|
|
464
|
+
GitHub 只读工具集。取数全部走 octokit REST(`src/lib/github-reads.ts` 的读取层 + `src/lib/github.ts` 的 accessor);只有「当前仓库」解析(`gh repo view`)与 token 获取(`gh auth token`)还用系统 [`gh`](https://cli.github.com/),因此 `gh` 不在 PATH 时整组不注册并在 session_start 报错;Windows 禁用。一个会话只 spawn 一次 `gh auth token`:三个子客户端(search / checks / reads)共用同一个 accessor。
|
|
465
465
|
|
|
466
466
|
- **Issue / PR**:`read-github-issue`、`list-github-issues`、`read-github-issue-comments`、`read-github-pr`、`list-github-prs`、`read-github-pr-diff`、`read-github-pr-status`、`read-github-pr-comments`
|
|
467
467
|
- **CI**:`read-github-ci-logs`、`list-github-workflow-runs`、`get-github-workflow-jobs`、`wait-github-pr-checks`、`wait-github-commit-checks`、`watch-github-run`
|
|
468
468
|
- **仓库 / 发布**:`read-github-repo`、`list-github-releases`、`read-github-release`、`download-github-release-assets`
|
|
469
469
|
|
|
470
|
-
`list-github-issues` / `list-github-prs` 带 `keywords` 时走 GitHub search API
|
|
470
|
+
`list-github-issues` / `list-github-prs` 带 `keywords` 时走 GitHub search API,不带 `keywords` 时走 REST 列表端点(state 缺省只搜 open,`state: "all"` 不加 state qualifier 覆盖 open + closed),输出 TSV,默认列 `number,state,title,labels,updatedAt`,可用 `fields` 白名单指定列。`wait-github-pr-checks` / `wait-github-commit-checks` 30 秒轮询、600 秒截止:任一 fail 即返回,全部 pass / skipped 才算通过,超时返回快照不抛错。
|
|
471
471
|
|
|
472
472
|
出网代理:`~/.pi/agent/proxy.json`(`{ "proxy": "http://127.0.0.1:7890", "noProxy": "localhost" }`),缺省字段回退 `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` / `NO_PROXY` 环境变量。
|
|
473
473
|
|
|
474
474
|
### 使用
|
|
475
475
|
|
|
476
476
|
```bash
|
|
477
|
-
pi -e ./src/gh
|
|
477
|
+
pi -e ./src/gh/index.ts
|
|
478
478
|
```
|
|
479
479
|
|
|
480
480
|
---
|
package/package.json
CHANGED
package/src/bwrap/dcg-scan.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
|
|
10
|
+
import { escapeHtml } from "./html.js";
|
|
11
|
+
|
|
10
12
|
/** `dcg test --format json` 输出中我们关心的字段。 */
|
|
11
13
|
interface DcgTestOutput {
|
|
12
14
|
decision?: "allow" | "deny" | "indeterminate";
|
|
@@ -30,14 +32,6 @@ export type DcgScanOutcome =
|
|
|
30
32
|
/** dcg 扫描的超时预算:超时视为无建议,不让审批弹窗被拖住。 */
|
|
31
33
|
const DCG_SCAN_TIMEOUT_MS = 2000;
|
|
32
34
|
|
|
33
|
-
function escapeHtml(text: string): string {
|
|
34
|
-
return text
|
|
35
|
-
.replaceAll("&", "&")
|
|
36
|
-
.replaceAll("<", "<")
|
|
37
|
-
.replaceAll(">", ">")
|
|
38
|
-
.replaceAll('"', """);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
35
|
/**
|
|
42
36
|
* 对命令做 dcg 扫描。返回三种结果:
|
|
43
37
|
* - `suggestion`:正常判定,携带建议文本
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI 文本的 HTML 转义。
|
|
3
|
+
*
|
|
4
|
+
* bwrap 的审批弹窗把外部文本(dcg 建议、命令、路径)直接拼进 HTML description,
|
|
5
|
+
* 这些文本必须先转义,否则会成为弹窗里的标签/属性。
|
|
6
|
+
*/
|
|
7
|
+
export function escapeHtml(text: string): string {
|
|
8
|
+
return text
|
|
9
|
+
.replaceAll("&", "&")
|
|
10
|
+
.replaceAll("<", "<")
|
|
11
|
+
.replaceAll(">", ">")
|
|
12
|
+
.replaceAll('"', """);
|
|
13
|
+
}
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type ResolvedBwrap,
|
|
44
44
|
} from "./core.js";
|
|
45
45
|
import { dcgSuggestion } from "./dcg-scan.js";
|
|
46
|
+
import { escapeHtml } from "./html.js";
|
|
46
47
|
import { loadSandboxConfig, runInSandbox } from "./sandbox.js";
|
|
47
48
|
|
|
48
49
|
/** 全权限审批对话框的选项 label(也作为 switch 匹配键与测试引用)。 */
|
|
@@ -155,14 +156,6 @@ export function formatElapsedSeconds(elapsedMs: number): string {
|
|
|
155
156
|
return `${(elapsedMs / 1000).toFixed(1)} seconds`;
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
function escapeHtml(text: string): string {
|
|
159
|
-
return text
|
|
160
|
-
.replaceAll("&", "&")
|
|
161
|
-
.replaceAll("<", "<")
|
|
162
|
-
.replaceAll(">", ">")
|
|
163
|
-
.replaceAll('"', """);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
159
|
/** 进度推送的节流间隔(对齐 pi 内置 bash 工具的 100ms)。 */
|
|
167
160
|
const BASH_UPDATE_THROTTLE_MS = 100;
|
|
168
161
|
/** 进度快照只保留尾部内容,避免大输出每 100ms 全量推给 TUI。 */
|
package/src/claude-code/shell.ts
CHANGED
|
@@ -16,12 +16,15 @@ import {
|
|
|
16
16
|
formatElapsedSeconds,
|
|
17
17
|
sandboxHintBlock,
|
|
18
18
|
} from "../bwrap/runtime.js";
|
|
19
|
+
import {
|
|
20
|
+
BASH_DEFAULT_TIMEOUT_MS,
|
|
21
|
+
BASH_MAX_TIMEOUT_MS,
|
|
22
|
+
bashStructuredSchema,
|
|
23
|
+
spilledOutput,
|
|
24
|
+
} from "../lib/bash-tool.js";
|
|
19
25
|
import { resolveWorkdir } from "../lib/path.js";
|
|
20
26
|
import { defineStructuredTool, type ToolBus } from "../lib/tool-bus.js";
|
|
21
27
|
|
|
22
|
-
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
23
|
-
const MAX_TIMEOUT_MS = 7_200_000;
|
|
24
|
-
|
|
25
28
|
/** 对齐 Claude Code formatError:错误文本超过该长度时头尾各保留一半。 */
|
|
26
29
|
const MAX_ERROR_CHARS = 10_000;
|
|
27
30
|
|
|
@@ -66,39 +69,6 @@ function appendTruncationNotice(
|
|
|
66
69
|
return `${text}\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(truncation.maxBytes)} limit). Full output: ${fullOutputPath}]`;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
/**
|
|
70
|
-
* `Bash` 的结构化结果:只有退出码与完整输出——命令非零退出不是失败,所以载荷里没有成败
|
|
71
|
-
* 标志,脚本直接读 `exitCode` 分支;超时/中止时 `exitCode` 为 `null`(原因在模型侧文本里)。
|
|
72
|
-
*/
|
|
73
|
-
const bashStructuredSchema = Type.Object({
|
|
74
|
-
exitCode: Type.Union([Type.Number(), Type.Null()], {
|
|
75
|
-
description: "Exit code of the command; null when it was killed (timeout or abort)",
|
|
76
|
-
}),
|
|
77
|
-
output: Type.String({
|
|
78
|
-
description:
|
|
79
|
-
"Complete output of the command (stdout and stderr merged). Never truncated, never mixed with tool-added notices.",
|
|
80
|
-
}),
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 载荷里的输出:文本被截断时读回落盘的完整输出(读不到就退回那份截断文本),
|
|
85
|
-
* 因此脚本拿到的永远是命令真正输出的内容。
|
|
86
|
-
*/
|
|
87
|
-
async function fullOutput(
|
|
88
|
-
output: string,
|
|
89
|
-
truncation: TruncationResult,
|
|
90
|
-
spillPath: string | undefined,
|
|
91
|
-
): Promise<string> {
|
|
92
|
-
if (spillPath === undefined || !truncation.truncated) {
|
|
93
|
-
return output;
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
return await readFile(spillPath, "utf8");
|
|
97
|
-
} catch {
|
|
98
|
-
return output;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
72
|
/**
|
|
103
73
|
* 成功路径:消费 runtime 的截断结果(输出已由 runtime 截断并落盘),
|
|
104
74
|
* 截断时追加 `[Showing lines X-Y of N. Full output: path]` 提示。
|
|
@@ -158,9 +128,9 @@ export function registerShellTools(bus: ToolBus, pi: ExtensionAPI, runtime: Bwra
|
|
|
158
128
|
{ additionalProperties: false },
|
|
159
129
|
),
|
|
160
130
|
async execute(id, params, signal, onUpdate, ctx) {
|
|
161
|
-
const timeout = params.timeout ??
|
|
162
|
-
if (!Number.isFinite(timeout) || timeout <= 0 || timeout >
|
|
163
|
-
throw new Error(`timeout must be between 1 and ${
|
|
131
|
+
const timeout = params.timeout ?? BASH_DEFAULT_TIMEOUT_MS;
|
|
132
|
+
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > BASH_MAX_TIMEOUT_MS) {
|
|
133
|
+
throw new Error(`timeout must be between 1 and ${BASH_MAX_TIMEOUT_MS} milliseconds`);
|
|
164
134
|
}
|
|
165
135
|
|
|
166
136
|
const cwd = params.workdir ? await resolveWorkdir(params.workdir, ctx.cwd) : ctx.cwd;
|
|
@@ -203,7 +173,7 @@ export function registerShellTools(bus: ToolBus, pi: ExtensionAPI, runtime: Bwra
|
|
|
203
173
|
ok: true as const,
|
|
204
174
|
value: {
|
|
205
175
|
exitCode: null,
|
|
206
|
-
output: await
|
|
176
|
+
output: await spilledOutput(
|
|
207
177
|
error.partial.output,
|
|
208
178
|
error.partial.truncation,
|
|
209
179
|
error.partial.fullOutputPath,
|
|
@@ -226,7 +196,7 @@ export function registerShellTools(bus: ToolBus, pi: ExtensionAPI, runtime: Bwra
|
|
|
226
196
|
ok: true as const,
|
|
227
197
|
value: {
|
|
228
198
|
exitCode: null,
|
|
229
|
-
output: await
|
|
199
|
+
output: await spilledOutput(
|
|
230
200
|
error.partial.output,
|
|
231
201
|
error.partial.truncation,
|
|
232
202
|
error.partial.fullOutputPath,
|
|
@@ -264,7 +234,7 @@ export function registerShellTools(bus: ToolBus, pi: ExtensionAPI, runtime: Bwra
|
|
|
264
234
|
ok: true as const,
|
|
265
235
|
value: {
|
|
266
236
|
exitCode: result.exitCode,
|
|
267
|
-
output: await
|
|
237
|
+
output: await spilledOutput(result.output, result.truncation, result.fullOutputPath),
|
|
268
238
|
},
|
|
269
239
|
},
|
|
270
240
|
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* const / union / enum),其余退化成 `unknown`——宁可少给类型,也不要编出错类型。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { isRecord } from "../lib/narrow.js";
|
|
9
10
|
import type { ScriptTool } from "./protocol.js";
|
|
10
11
|
|
|
11
12
|
interface ToolLike {
|
|
@@ -16,12 +17,6 @@ interface ToolLike {
|
|
|
16
17
|
structuredSchema: unknown;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
type JsonSchema = Record<string, unknown>;
|
|
20
|
-
|
|
21
|
-
function isSchema(value: unknown): value is JsonSchema {
|
|
22
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
20
|
function literal(value: unknown): string {
|
|
26
21
|
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
27
22
|
}
|
|
@@ -32,7 +27,7 @@ function propertyName(name: string): string {
|
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
function renderType(schema: unknown, indent: string): string {
|
|
35
|
-
if (!
|
|
30
|
+
if (!isRecord(schema)) {
|
|
36
31
|
return "unknown";
|
|
37
32
|
}
|
|
38
33
|
// TypeBox 的 Type.Literal / StringEnum 产出 const,而不是 enum
|
|
@@ -64,7 +59,7 @@ function renderType(schema: unknown, indent: string): string {
|
|
|
64
59
|
return `Array<${renderType(schema.items, indent)}>`;
|
|
65
60
|
}
|
|
66
61
|
case "object": {
|
|
67
|
-
const properties =
|
|
62
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
68
63
|
const required = new Set(Array.isArray(schema.required) ? (schema.required as string[]) : []);
|
|
69
64
|
const entries = Object.entries(properties);
|
|
70
65
|
if (entries.length === 0) {
|
package/src/gh/base.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type ActionJob,
|
|
19
19
|
type CheckRun,
|
|
20
20
|
type CommitStatus,
|
|
21
|
+
createGithubApi,
|
|
21
22
|
createGithubChecks,
|
|
22
23
|
createGithubSearch,
|
|
23
24
|
type GithubChecksClient,
|
|
@@ -31,7 +32,7 @@ import { createGithubReads, type GithubReads } from "../lib/github-reads.js";
|
|
|
31
32
|
import { parseWithSchema } from "../lib/parse-with-schema.js";
|
|
32
33
|
import { type ToolPendant } from "../lib/pendant.js";
|
|
33
34
|
import { type StructuredResult } from "../lib/tool-bus.js";
|
|
34
|
-
import type
|
|
35
|
+
import { type checksVerdictSchema, ghRunSummarySchema } from "./schemas.js";
|
|
35
36
|
|
|
36
37
|
/** A tool result: what the model sees plus the structured details payload. */
|
|
37
38
|
export interface ToolResult {
|
|
@@ -178,6 +179,29 @@ export async function resolveRepo(
|
|
|
178
179
|
return nameWithOwner;
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
/** 工具操作的仓库:`OWNER/REPO` 全名加拆分后的两段。 */
|
|
183
|
+
export interface RepoTarget {
|
|
184
|
+
/** 规范化的 `OWNER/REPO`(参数缺省时来自当前目录解析)。 */
|
|
185
|
+
readonly fullName: string;
|
|
186
|
+
readonly owner: string;
|
|
187
|
+
readonly name: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 解析工具该操作的仓库并拆开 `OWNER/REPO`。取数工具都要走这一步,收成一个调用
|
|
192
|
+
* 后每个工具只剩一行,也不再需要各自 import `splitRepo`。
|
|
193
|
+
*/
|
|
194
|
+
export async function resolveRepoTarget(
|
|
195
|
+
repo: string | undefined,
|
|
196
|
+
signal: AbortSignal | undefined,
|
|
197
|
+
cwd: string | undefined,
|
|
198
|
+
input?: unknown,
|
|
199
|
+
): Promise<RepoTarget> {
|
|
200
|
+
const fullName = await resolveRepo(repo, signal, cwd, input);
|
|
201
|
+
const { owner, repo: name } = splitRepo(fullName);
|
|
202
|
+
return { fullName, owner, name };
|
|
203
|
+
}
|
|
204
|
+
|
|
181
205
|
export function truncate(
|
|
182
206
|
text: string,
|
|
183
207
|
maxLines = 2000,
|
|
@@ -294,6 +318,58 @@ export function subtitlePendant<IdKey extends string = never>(
|
|
|
294
318
|
return { subtitle: parts.join(" ") };
|
|
295
319
|
}
|
|
296
320
|
|
|
321
|
+
/**
|
|
322
|
+
* 给结果挂上 pendant subtitle 后原样返回:取数工具的固定收尾,省掉每个工具重复的
|
|
323
|
+
* `result.details.pendant = ...; return result;` 两行。
|
|
324
|
+
*/
|
|
325
|
+
export function withPendant<T extends ToolResult, IdKey extends string = never>(
|
|
326
|
+
result: T,
|
|
327
|
+
params: { repo?: string } & Partial<Record<IdKey, string | number>>,
|
|
328
|
+
idKey?: IdKey,
|
|
329
|
+
): T {
|
|
330
|
+
result.details.pendant = subtitlePendant(params, idKey);
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* `list-github-issues` / `list-github-prs` 共用的一套过滤参数,只有 `state` 与 `fields`
|
|
336
|
+
* 的说明文案按 kind 不同。
|
|
337
|
+
*/
|
|
338
|
+
export function listFilterParameters(kind: "issue" | "pr") {
|
|
339
|
+
const isPr = kind === "pr";
|
|
340
|
+
return Type.Object({
|
|
341
|
+
repo: Type.Optional(Type.String({ description: "OWNER/REPO (defaults to current repo)" })),
|
|
342
|
+
keywords: Type.Optional(Type.String({ description: "Search keywords (free text)" })),
|
|
343
|
+
state: Type.Optional(
|
|
344
|
+
Type.String({
|
|
345
|
+
description: isPr
|
|
346
|
+
? "open, closed, merged, all (default: open; all applies to keyword search and covers open + closed + merged)"
|
|
347
|
+
: "open, closed, all (default: open; all applies to keyword search and covers closed too)",
|
|
348
|
+
}),
|
|
349
|
+
),
|
|
350
|
+
label: Type.Optional(Type.String({ description: "Filter by label" })),
|
|
351
|
+
// `@me` 只在关键词搜索分支可用:搜索 API 自己把它解析成当前登录用户。浏览分支走
|
|
352
|
+
// REST 列表端点,`@me` 按字面量转发,不会匹配到当前用户(本仓库不做展开)。
|
|
353
|
+
author: Type.Optional(
|
|
354
|
+
Type.String({
|
|
355
|
+
description: "Filter by author ('@me' works with keywords; browse takes it literally)",
|
|
356
|
+
}),
|
|
357
|
+
),
|
|
358
|
+
assignee: Type.Optional(
|
|
359
|
+
Type.String({
|
|
360
|
+
description: "Filter by assignee ('@me' works with keywords; browse takes it literally)",
|
|
361
|
+
}),
|
|
362
|
+
),
|
|
363
|
+
milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
|
|
364
|
+
limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),
|
|
365
|
+
fields: Type.Optional(
|
|
366
|
+
Type.String({
|
|
367
|
+
description: `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${isPr ? ",mergedAt" : ""}`,
|
|
368
|
+
}),
|
|
369
|
+
),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
297
373
|
export interface ListFilters {
|
|
298
374
|
repo?: string;
|
|
299
375
|
keywords?: string;
|
|
@@ -317,8 +393,11 @@ export async function browseList(
|
|
|
317
393
|
params: ListFilters,
|
|
318
394
|
ctx: { cwd?: string; signal?: AbortSignal; input?: unknown },
|
|
319
395
|
): Promise<{ repo: string; hits: SearchHit[] }> {
|
|
320
|
-
const
|
|
321
|
-
|
|
396
|
+
const {
|
|
397
|
+
fullName: repo,
|
|
398
|
+
owner,
|
|
399
|
+
name,
|
|
400
|
+
} = await resolveRepoTarget(params.repo, ctx.signal, ctx.cwd, ctx.input);
|
|
322
401
|
const query = {
|
|
323
402
|
state: params.state,
|
|
324
403
|
label: params.label,
|
|
@@ -329,8 +408,8 @@ export async function browseList(
|
|
|
329
408
|
};
|
|
330
409
|
const items =
|
|
331
410
|
kind === "issue"
|
|
332
|
-
? await gh.reads.listIssues(owner,
|
|
333
|
-
: await gh.reads.listPulls(owner,
|
|
411
|
+
? await gh.reads.listIssues(owner, name, query, ctx.signal)
|
|
412
|
+
: await gh.reads.listPulls(owner, name, query, ctx.signal);
|
|
334
413
|
return { repo, hits: normalizeRestList(items, repo) };
|
|
335
414
|
}
|
|
336
415
|
|
|
@@ -367,9 +446,12 @@ export class GhClient {
|
|
|
367
446
|
options: Pick<GithubClientOptions, "token"> = {},
|
|
368
447
|
) {
|
|
369
448
|
this.fetch = fetchImpl;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
449
|
+
// 三个子客户端共用同一个 accessor,因此也共用同一份 octokit 与 token 缓存
|
|
450
|
+
// (各自 createGithubApi 会让一个会话最多 spawn 三次 `gh auth token`)。
|
|
451
|
+
const api = createGithubApi({ fetch: fetchImpl, ...options });
|
|
452
|
+
this.search = createGithubSearch(api);
|
|
453
|
+
this.checks = createGithubChecks(api);
|
|
454
|
+
this.reads = createGithubReads(api);
|
|
373
455
|
}
|
|
374
456
|
}
|
|
375
457
|
|
|
@@ -380,20 +462,6 @@ const CHECKS_POLL_INTERVAL_MS = 30_000;
|
|
|
380
462
|
const RUN_WATCH_INTERVAL_MS = 30_000;
|
|
381
463
|
const RUN_WATCH_DEADLINE_MS = 600_000;
|
|
382
464
|
|
|
383
|
-
/** 运行快照:只声明我们渲染与载荷用到的字段,其余由 REST 原样带过。 */
|
|
384
|
-
const runSnapshotSchema = Type.Object({
|
|
385
|
-
id: Type.Number(),
|
|
386
|
-
name: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
387
|
-
display_title: Type.Optional(Type.String()),
|
|
388
|
-
status: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
389
|
-
conclusion: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
390
|
-
head_branch: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
391
|
-
event: Type.Optional(Type.String()),
|
|
392
|
-
created_at: Type.Optional(Type.String()),
|
|
393
|
-
updated_at: Type.Optional(Type.String()),
|
|
394
|
-
run_started_at: Type.Optional(Type.String()),
|
|
395
|
-
html_url: Type.Optional(Type.String()),
|
|
396
|
-
});
|
|
397
465
|
const CHECKS_WATCH_DEADLINE_MS = 600_000;
|
|
398
466
|
|
|
399
467
|
export type CheckBucket = "pass" | "skipped" | "fail" | "pending";
|
|
@@ -688,7 +756,7 @@ export async function watchRun(options: WatchRunOptions): Promise<RunWatchResult
|
|
|
688
756
|
const watchStart = Date.now();
|
|
689
757
|
for (;;) {
|
|
690
758
|
signal.throwIfAborted();
|
|
691
|
-
const run = parseWithSchema(
|
|
759
|
+
const run = parseWithSchema(ghRunSummarySchema, await reads.run(owner, repo, runId, signal));
|
|
692
760
|
const elapsedMs = Date.now() - watchStart;
|
|
693
761
|
onUpdate?.({
|
|
694
762
|
content: [
|
package/src/gh/index.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GitHub Read-Only Tools Extension
|
|
3
3
|
*
|
|
4
|
-
* Provides individual read-only tools for GitHub operations
|
|
4
|
+
* Provides individual read-only tools for GitHub operations. 取数走 octokit REST;
|
|
5
|
+
* 只有「当前仓库」解析(`gh repo view`)与 token 获取(`gh auth token`)用系统 `gh`。
|
|
5
6
|
*
|
|
6
7
|
* Layout:
|
|
7
|
-
* - `base.ts` shared helpers (
|
|
8
|
-
* - `
|
|
8
|
+
* - `base.ts` shared helpers (result shaping, GhClient, repo resolution, checks watch)
|
|
9
|
+
* - `schemas.ts` structured-output schemas
|
|
10
|
+
* - `render.ts` text rendering of REST responses
|
|
11
|
+
* - `tools/<name>.ts` one tool per file, each exporting `add<Name>Tool(gh, bus)`
|
|
9
12
|
* - `index.ts` this file: availability checks + tool registration
|
|
10
13
|
*
|
|
11
14
|
* Tools:
|
|
@@ -28,10 +31,10 @@
|
|
|
28
31
|
* - wait-github-commit-checks: Watch CI checks of a commit (no PR required)
|
|
29
32
|
* - watch-github-run: Watch a workflow run
|
|
30
33
|
*
|
|
31
|
-
* Proxy (
|
|
34
|
+
* Proxy (shared by the `gh` subprocess and the octokit requests):
|
|
32
35
|
* ~/.pi/agent/proxy.json: { "proxy": "http://127.0.0.1:7890", "noProxy": "localhost" }
|
|
33
36
|
* HTTPS_PROXY / HTTP_PROXY / ALL_PROXY and NO_PROXY are used instead for the
|
|
34
|
-
* fields the config file leaves out
|
|
37
|
+
* fields the config file leaves out; the config is read through `src/lib/egress.ts`.
|
|
35
38
|
*/
|
|
36
39
|
|
|
37
40
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -9,7 +9,7 @@ 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
|
-
|
|
12
|
+
resolveRepoTarget,
|
|
13
13
|
splitRepo,
|
|
14
14
|
structuredFailure,
|
|
15
15
|
type StructuredFailureResult,
|
|
@@ -114,8 +114,11 @@ export async function downloadReleaseAssets(
|
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
const
|
|
118
|
-
|
|
117
|
+
const {
|
|
118
|
+
fullName: effectiveRepo,
|
|
119
|
+
owner,
|
|
120
|
+
name: repoName,
|
|
121
|
+
} = await resolveRepoTarget(params.repo, signal, ctx.cwd, params);
|
|
119
122
|
const view = Value.Parse(
|
|
120
123
|
releaseViewSchema,
|
|
121
124
|
await gh.reads.release(owner, repoName, params.tag, signal),
|
|
@@ -3,13 +3,12 @@ import { Type } from "typebox";
|
|
|
3
3
|
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
4
4
|
import {
|
|
5
5
|
type GhClient,
|
|
6
|
-
|
|
7
|
-
splitRepo,
|
|
6
|
+
resolveRepoTarget,
|
|
8
7
|
type StructuredResultOf,
|
|
9
|
-
subtitlePendant,
|
|
10
8
|
type ToolCall,
|
|
11
9
|
toPositiveId,
|
|
12
10
|
toStructuredJsonResult,
|
|
11
|
+
withPendant,
|
|
13
12
|
} from "../base.js";
|
|
14
13
|
import { workflowJobsSchema } from "../schemas.js";
|
|
15
14
|
|
|
@@ -25,8 +24,7 @@ async function workflowJobs(
|
|
|
25
24
|
): Promise<StructuredResultOf<typeof workflowJobsSchema>> {
|
|
26
25
|
const { params, ctx, signal } = call;
|
|
27
26
|
const runId = toPositiveId(params.run_id, "run_id");
|
|
28
|
-
const
|
|
29
|
-
const { owner, repo: name } = splitRepo(effectiveRepo);
|
|
27
|
+
const { owner, name } = await resolveRepoTarget(params.repo, signal, ctx.cwd, params);
|
|
30
28
|
|
|
31
29
|
const jobs = await gh.checks.runJobs(owner, name, runId, signal);
|
|
32
30
|
const result = toStructuredJsonResult(
|
|
@@ -34,8 +32,7 @@ async function workflowJobs(
|
|
|
34
32
|
params,
|
|
35
33
|
workflowJobsSchema,
|
|
36
34
|
);
|
|
37
|
-
result
|
|
38
|
-
return result;
|
|
35
|
+
return withPendant(result, params, "run_id");
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
export function addGetWorkflowJobsTool(gh: GhClient, bus: ToolBus) {
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `list-github-issues`:两条取数分支(关键词搜索走 octokit
|
|
3
|
-
*
|
|
2
|
+
* `list-github-issues`:两条取数分支(关键词搜索走 octokit 的 `/search/issues`,浏览走
|
|
3
|
+
* REST 列表端点)归一成同一套 `SearchHit`,因此文本渲染与结构化载荷只有一份实现。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Type } from "typebox";
|
|
7
|
-
|
|
8
6
|
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
9
7
|
import {
|
|
10
8
|
browseList,
|
|
11
9
|
type GhClient,
|
|
10
|
+
listFilterParameters,
|
|
12
11
|
type ListFilters,
|
|
13
12
|
renderHitList,
|
|
14
13
|
type StructuredResultOf,
|
|
15
|
-
subtitlePendant,
|
|
16
14
|
type ToolCall,
|
|
17
15
|
toToolResult,
|
|
16
|
+
withPendant,
|
|
18
17
|
withStructuredResult,
|
|
19
18
|
} from "../base.js";
|
|
20
19
|
import { ghHitListPayloadSchema } from "../schemas.js";
|
|
@@ -29,9 +28,8 @@ async function listIssues(
|
|
|
29
28
|
: await browseList(gh, "issue", params, { cwd: ctx.cwd, signal, input: params });
|
|
30
29
|
const text = renderHitList("issue", hits, { repo, fields: params.fields });
|
|
31
30
|
const result = toToolResult(text, params);
|
|
32
|
-
result.details.pendant = subtitlePendant(params);
|
|
33
31
|
// 文本可能被行数/字节预算截断,载荷给全部命中
|
|
34
|
-
return withStructuredResult(result, { text, items: hits });
|
|
32
|
+
return withStructuredResult(withPendant(result, params), { text, items: hits });
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
export function addListIssuesTool(gh: GhClient, bus: ToolBus) {
|
|
@@ -42,38 +40,7 @@ export function addListIssuesTool(gh: GhClient, bus: ToolBus) {
|
|
|
42
40
|
description:
|
|
43
41
|
'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
42
|
promptSnippet: "List or search GitHub issues",
|
|
45
|
-
parameters:
|
|
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` 只在关键词搜索分支可用:搜索 API 自己把它解析成当前登录用户。浏览分支走
|
|
56
|
-
// REST 列表端点,`@me` 按字面量转发,不会匹配到当前用户(本仓库不做展开)。
|
|
57
|
-
author: Type.Optional(
|
|
58
|
-
Type.String({
|
|
59
|
-
description: "Filter by author ('@me' works with keywords; browse takes it literally)",
|
|
60
|
-
}),
|
|
61
|
-
),
|
|
62
|
-
assignee: Type.Optional(
|
|
63
|
-
Type.String({
|
|
64
|
-
description:
|
|
65
|
-
"Filter by assignee ('@me' works with keywords; browse takes it literally)",
|
|
66
|
-
}),
|
|
67
|
-
),
|
|
68
|
-
milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
|
|
69
|
-
limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),
|
|
70
|
-
fields: Type.Optional(
|
|
71
|
-
Type.String({
|
|
72
|
-
description:
|
|
73
|
-
"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",
|
|
74
|
-
}),
|
|
75
|
-
),
|
|
76
|
-
}),
|
|
43
|
+
parameters: listFilterParameters("issue"),
|
|
77
44
|
structuredSchema: ghHitListPayloadSchema,
|
|
78
45
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
79
46
|
return listIssues(gh, { params, ctx, signal, onUpdate });
|
package/src/gh/tools/list-prs.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `list-github-prs`:与 `list-github-issues` 同一套两条分支(关键词搜索走 octokit
|
|
3
|
-
*
|
|
2
|
+
* `list-github-prs`:与 `list-github-issues` 同一套两条分支(关键词搜索走 octokit 的
|
|
3
|
+
* `/search/issues`,浏览走 REST 列表端点),归一成同一套 `SearchHit`。
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Type } from "typebox";
|
|
7
|
-
|
|
8
6
|
import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
|
|
9
7
|
import {
|
|
10
8
|
browseList,
|
|
11
9
|
type GhClient,
|
|
10
|
+
listFilterParameters,
|
|
12
11
|
type ListFilters,
|
|
13
12
|
renderHitList,
|
|
14
13
|
type StructuredResultOf,
|
|
15
|
-
subtitlePendant,
|
|
16
14
|
type ToolCall,
|
|
17
15
|
toToolResult,
|
|
16
|
+
withPendant,
|
|
18
17
|
withStructuredResult,
|
|
19
18
|
} from "../base.js";
|
|
20
19
|
import { ghHitListPayloadSchema } from "../schemas.js";
|
|
@@ -29,9 +28,8 @@ async function listPrs(
|
|
|
29
28
|
: await browseList(gh, "pr", params, { cwd: ctx.cwd, signal, input: params });
|
|
30
29
|
const text = renderHitList("pr", hits, { repo, fields: params.fields });
|
|
31
30
|
const result = toToolResult(text, params);
|
|
32
|
-
result.details.pendant = subtitlePendant(params);
|
|
33
31
|
// 文本可能被行数/字节预算截断,载荷给全部命中
|
|
34
|
-
return withStructuredResult(result, { text, items: hits });
|
|
32
|
+
return withStructuredResult(withPendant(result, params), { text, items: hits });
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
export function addListPrsTool(gh: GhClient, bus: ToolBus) {
|
|
@@ -42,38 +40,7 @@ export function addListPrsTool(gh: GhClient, bus: ToolBus) {
|
|
|
42
40
|
description:
|
|
43
41
|
'List GitHub pull requests with optional filters and keyword search. When repo is omitted, keyword search runs across GitHub. Keyword search defaults to open PRs — pass state="merged", state="closed" (merged excluded) or state="all" to broaden. Set fields to choose the columns of each result row.',
|
|
44
42
|
promptSnippet: "List or search GitHub PRs",
|
|
45
|
-
parameters:
|
|
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, merged, all (default: open; all applies to keyword search and covers open + closed + merged)",
|
|
52
|
-
}),
|
|
53
|
-
),
|
|
54
|
-
label: Type.Optional(Type.String({ description: "Filter by label" })),
|
|
55
|
-
// `@me` 只在关键词搜索分支可用:搜索 API 自己把它解析成当前登录用户。浏览分支走
|
|
56
|
-
// REST 列表端点,`@me` 按字面量转发,不会匹配到当前用户(本仓库不做展开)。
|
|
57
|
-
author: Type.Optional(
|
|
58
|
-
Type.String({
|
|
59
|
-
description: "Filter by author ('@me' works with keywords; browse takes it literally)",
|
|
60
|
-
}),
|
|
61
|
-
),
|
|
62
|
-
assignee: Type.Optional(
|
|
63
|
-
Type.String({
|
|
64
|
-
description:
|
|
65
|
-
"Filter by assignee ('@me' works with keywords; browse takes it literally)",
|
|
66
|
-
}),
|
|
67
|
-
),
|
|
68
|
-
milestone: Type.Optional(Type.String({ description: "Filter by milestone" })),
|
|
69
|
-
limit: Type.Optional(Type.Number({ description: "Max results (default 30, max 100)" })),
|
|
70
|
-
fields: Type.Optional(
|
|
71
|
-
Type.String({
|
|
72
|
-
description:
|
|
73
|
-
"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,mergedAt",
|
|
74
|
-
}),
|
|
75
|
-
),
|
|
76
|
-
}),
|
|
43
|
+
parameters: listFilterParameters("pr"),
|
|
77
44
|
structuredSchema: ghHitListPayloadSchema,
|
|
78
45
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
79
46
|
return listPrs(gh, { params, ctx, signal, onUpdate });
|