@trim21/personal-pi-extensions 0.1.690 → 0.1.692
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 +11 -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/codemode/tool.ts +15 -6
- 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/index.ts +3 -0
- 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/tool-bus.ts +30 -2
- package/src/lib/tool-registration.ts +6 -0
- package/src/lib/tools-config.ts +24 -7
- 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
|
@@ -93,6 +93,8 @@
|
|
|
93
93
|
// 工具可用性规则:条目是工具名模式,或 { tools, models }
|
|
94
94
|
"disabledTools": ["talk-*", { "tools": ["web_*"], "models": ["gpt-*"] }],
|
|
95
95
|
"enabledTools": [{ "tools": ["web_search"], "models": ["glm-*"] }],
|
|
96
|
+
// 只进 codemode、不交给模型的工具(须声明结构化输出才会生效)
|
|
97
|
+
"codemodeOnlyTools": [{ "tools": ["read-github-*"], "models": ["deepseek-*"] }],
|
|
96
98
|
},
|
|
97
99
|
}
|
|
98
100
|
```
|
|
@@ -101,6 +103,12 @@
|
|
|
101
103
|
条目只在当前模型命中时参与判定。未配置该 section 时用 `fileIo: "claude-code"` 且不
|
|
102
104
|
禁用任何工具。
|
|
103
105
|
|
|
106
|
+
`codemodeOnlyTools` 把工具从模型可见列表移进 `codemode`:命中它、未被禁用、且声明了
|
|
107
|
+
`structuredSchema` 的工具只登记在工具总线上、不交给 pi,模型只能在 codemode 脚本里用
|
|
108
|
+
`call(...)` 调用它(`codemode` 的工具描述里仍有它的参数与返回类型声明)。没有结构化输出
|
|
109
|
+
的工具进不了 codemode,因此会被忽略、保持直接可用,并在会话启动时给出一条 warning——
|
|
110
|
+
想真正移除工具用 `disabledTools`。
|
|
111
|
+
|
|
104
112
|
**判定时机是每个会话启动**:pi 在启动 / `/new` / `resume` / `/fork` / `/reload` 时重建
|
|
105
113
|
扩展,注册就发生在 `session_start` 里(模型在扩展加载期读不到),因此新会话按自己的
|
|
106
114
|
模型重新判定;**同一会话内 `/model` 切换不会改变工具集合**,换模型请开新会话。
|
|
@@ -461,20 +469,20 @@ pi -e ./src/aft/index.ts
|
|
|
461
469
|
|
|
462
470
|
## gh-readonly
|
|
463
471
|
|
|
464
|
-
GitHub
|
|
472
|
+
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
473
|
|
|
466
474
|
- **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
475
|
- **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
476
|
- **仓库 / 发布**:`read-github-repo`、`list-github-releases`、`read-github-release`、`download-github-release-assets`
|
|
469
477
|
|
|
470
|
-
`list-github-issues` / `list-github-prs` 带 `keywords` 时走 GitHub search API
|
|
478
|
+
`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
479
|
|
|
472
480
|
出网代理:`~/.pi/agent/proxy.json`(`{ "proxy": "http://127.0.0.1:7890", "noProxy": "localhost" }`),缺省字段回退 `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` / `NO_PROXY` 环境变量。
|
|
473
481
|
|
|
474
482
|
### 使用
|
|
475
483
|
|
|
476
484
|
```bash
|
|
477
|
-
pi -e ./src/gh
|
|
485
|
+
pi -e ./src/gh/index.ts
|
|
478
486
|
```
|
|
479
487
|
|
|
480
488
|
---
|
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/codemode/tool.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 可调用集合:总线上实际注册的、**声明了 `structuredSchema`** 的工具,执行时再与 active
|
|
9
9
|
* 列表求交——pi 自己的 `defaultTools` / `--tools` / 子代理白名单的排除因此同样生效。
|
|
10
|
+
* codemode-only 工具(`codemodeOnlyTools` 命中且声明了 schema,因而没有交给 pi 注册)不在
|
|
11
|
+
* active 列表里是常态,直接进集合、不求交。
|
|
10
12
|
*
|
|
11
13
|
* wasm 在注册这个工具时编译一次(`createCodemodeSandbox`),worker 复用编译结果。
|
|
12
14
|
*/
|
|
@@ -36,11 +38,15 @@ export const CODEMODE_TOOL_NAME = "codemode";
|
|
|
36
38
|
*
|
|
37
39
|
* 脚本拿到的返回值必须有确定的形状——只给文本的工具在脚本里既没法当数据用(得解析文本),
|
|
38
40
|
* 也没有返回类型能写进声明。用「有没有结构化输出」当门槛,这条规则自己会维持一致:不给工具
|
|
39
|
-
* 加 schema
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* 加 schema 就不进集合,不需要维护一份会随工具增减而漂移的黑名单。
|
|
42
|
+
*
|
|
43
|
+
* active 求交只作用于直接工具;命中了 `codemodeOnlyTools` 的工具(声明了 schema、因而没有
|
|
44
|
+
* 交给 pi 注册)不在 active 列表里是常态,直接进集合。
|
|
45
|
+
*
|
|
46
|
+
* 下列工具天然不在集合里,理由各自成立:`codemode` 自身与 `spawn-agent`(没有 schema)、
|
|
47
|
+
* 文件读写工具(脚本用 `fs.read` / `fs.write`,不重复一套为 LLM 上下文设计的行号/锚点
|
|
48
|
+
* 语义)、搜索工具(脚本用 `call("Bash", { command })` 跑 `rg`,退出码可用、能拼管道)、
|
|
49
|
+
* `lsp-rename`(写工具)以及 talk / 会话工具(会把执行时间交给外部输入)。
|
|
44
50
|
*/
|
|
45
51
|
/** 估计 token 用的字符数(与 pi 一致)。 */
|
|
46
52
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -82,7 +88,10 @@ function collectTools(bus: ToolBus, allowed: Set<string> | undefined): CallableT
|
|
|
82
88
|
return bus
|
|
83
89
|
.list()
|
|
84
90
|
.filter((definition) => definition.structuredSchema !== undefined)
|
|
85
|
-
.filter(
|
|
91
|
+
.filter(
|
|
92
|
+
(definition) =>
|
|
93
|
+
definition.codemodeOnly === true || allowed === undefined || allowed.has(definition.name),
|
|
94
|
+
)
|
|
86
95
|
.map((definition) => ({
|
|
87
96
|
name: definition.name,
|
|
88
97
|
description: definition.description,
|
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 });
|