@trim21/personal-pi-extensions 0.1.684 → 0.1.685

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.
@@ -1,29 +1,65 @@
1
1
  import { Type } from "typebox";
2
2
 
3
- import type { ToolBus } from "../../lib/tool-bus.js";
4
- import { type GhClient, ghExec, subtitlePendant, toToolResult } from "../base.js";
3
+ import { parseWithSchema } from "../../lib/parse-with-schema.js";
4
+ import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
5
+ import {
6
+ type GhClient,
7
+ ghExec,
8
+ subtitlePendant,
9
+ toToolResult,
10
+ withStructuredResult,
11
+ } from "../base.js";
12
+ import { renderRepoView } from "../render.js";
13
+ import { ghRepoPayloadSchema, ghRepoViewSchema } from "../schemas.js";
14
+
15
+ /** 我们向 gh 要的仓库字段,与渲染和载荷一一对应。 */
16
+ const REPO_FIELDS = [
17
+ "name",
18
+ "nameWithOwner",
19
+ "description",
20
+ "url",
21
+ "homepageUrl",
22
+ "visibility",
23
+ "isPrivate",
24
+ "isFork",
25
+ "isArchived",
26
+ "stargazerCount",
27
+ "forkCount",
28
+ "primaryLanguage",
29
+ "defaultBranchRef",
30
+ "licenseInfo",
31
+ "issues",
32
+ "pullRequests",
33
+ "pushedAt",
34
+ "createdAt",
35
+ "updatedAt",
36
+ ].join(",");
5
37
 
6
38
  export function addReadRepoTool(_gh: GhClient, bus: ToolBus) {
7
- bus.register({
8
- name: "read-github-repo",
9
- label: "GitHub Repo",
10
- description: "Get repository information.",
11
- promptSnippet: "Read GitHub repo info",
12
- parameters: Type.Object({
13
- repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
39
+ bus.register(
40
+ defineStructuredTool({
41
+ name: "read-github-repo",
42
+ label: "GitHub Repo",
43
+ description: "Get repository information.",
44
+ promptSnippet: "Read GitHub repo info",
45
+ parameters: Type.Object({
46
+ repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
47
+ }),
48
+ structuredSchema: ghRepoPayloadSchema,
49
+ async execute(_id, params, signal, _onUpdate, ctx) {
50
+ const { repo } = params;
51
+ const args = ["repo", "view"];
52
+ if (repo) {
53
+ args.push(repo);
54
+ }
55
+ args.push("--json", REPO_FIELDS);
56
+ const stdout = await ghExec(args, { cwd: ctx.cwd, signal, input: params });
57
+ const view = parseWithSchema(ghRepoViewSchema, JSON.parse(stdout));
58
+ const text = renderRepoView(view);
59
+ const result = toToolResult(text, params);
60
+ result.details.pendant = subtitlePendant(params);
61
+ return withStructuredResult(result, { text, repo: view });
62
+ },
14
63
  }),
15
- async execute(_id, params, signal, _onUpdate, ctx) {
16
- const { repo } = params;
17
- const args = ["repo", "view"];
18
- if (repo) {
19
- args.push(repo);
20
- }
21
- const result = toToolResult(
22
- await ghExec(args, { cwd: ctx.cwd, signal, input: params }),
23
- params,
24
- );
25
- result.details.pendant = subtitlePendant(params);
26
- return result;
27
- },
28
- });
64
+ );
29
65
  }
@@ -1,46 +1,62 @@
1
1
  import { Type } from "typebox";
2
2
 
3
3
  import { runGh } from "../../lib/gh-process.js";
4
- import type { ToolBus } from "../../lib/tool-bus.js";
5
- import { type GhClient, repoArgs, subtitlePendant } from "../base.js";
4
+ import { parseWithSchema } from "../../lib/parse-with-schema.js";
5
+ import { defineStructuredTool, type ToolBus } from "../../lib/tool-bus.js";
6
+ import { type GhClient, ghExec, repoArgs, subtitlePendant } from "../base.js";
7
+ import { ghRunPayloadSchema, ghRunSummarySchema } from "../schemas.js";
8
+
9
+ /** 收尾时查运行状态要的字段(`gh run watch` 自己没有 `--json`)。 */
10
+ const RUN_FIELDS =
11
+ "databaseId,displayTitle,status,conclusion,workflowName,headBranch,event,createdAt,updatedAt,startedAt,url";
6
12
 
7
13
  export function addWatchRunTool(_gh: GhClient, bus: ToolBus) {
8
- bus.register({
9
- name: "watch-github-run",
10
- label: "Watch GitHub Workflow Run",
11
- description:
12
- "Watch a GitHub Actions workflow run until it completes. " +
13
- "Blocks until the run finishes and shows the final status.",
14
- promptSnippet: "Watch and wait for a GitHub Actions run to complete",
15
- parameters: Type.Object({
16
- run_id: Type.Union([Type.Number(), Type.String()], { description: "Workflow run ID" }),
17
- repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
14
+ bus.register(
15
+ defineStructuredTool({
16
+ name: "watch-github-run",
17
+ label: "Watch GitHub Workflow Run",
18
+ description:
19
+ "Watch a GitHub Actions workflow run until it completes. " +
20
+ "Blocks until the run finishes and shows the final status.",
21
+ promptSnippet: "Watch and wait for a GitHub Actions run to complete",
22
+ parameters: Type.Object({
23
+ run_id: Type.Union([Type.Number(), Type.String()], { description: "Workflow run ID" }),
24
+ repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
25
+ }),
26
+ structuredSchema: ghRunPayloadSchema,
27
+ async execute(_id, params, signal, onUpdate, ctx) {
28
+ const { run_id, repo } = params;
29
+
30
+ const pendant = subtitlePendant(params, "run_id");
31
+ onUpdate?.({
32
+ content: [{ type: "text", text: `Watching workflow run ${run_id}...` }],
33
+ details: {},
34
+ });
35
+
36
+ const result = await runGh(["run", "watch", String(run_id), ...repoArgs(repo)], {
37
+ cwd: ctx.cwd,
38
+ signal,
39
+ timeout: 600_000,
40
+ });
41
+
42
+ if (result.code !== 0) {
43
+ throw new Error(`gh run watch failed: ${result.stderr || `exit code ${result.code}`}`);
44
+ }
45
+
46
+ const text = `## Workflow Run ${run_id} Completed\n\n${result.stdout}`;
47
+ // 监控的是状态变化,结论要另外查一次(watch 的文本里只有过程)
48
+ const stdout = await ghExec(
49
+ ["run", "view", String(run_id), ...repoArgs(repo), "--json", RUN_FIELDS],
50
+ { cwd: ctx.cwd, signal, input: params },
51
+ );
52
+ const run = parseWithSchema(ghRunSummarySchema, JSON.parse(stdout));
53
+
54
+ return {
55
+ content: [{ type: "text" as const, text }],
56
+ details: { exitCode: 0, input: params, ...(pendant && { pendant }) },
57
+ structuredResult: { ok: true as const, value: { text, run } },
58
+ };
59
+ },
18
60
  }),
19
- async execute(_id, params, signal, onUpdate, ctx) {
20
- const { run_id, repo } = params;
21
-
22
- const pendant = subtitlePendant(params, "run_id");
23
- onUpdate?.({
24
- content: [{ type: "text", text: `Watching workflow run ${run_id}...` }],
25
- details: {},
26
- });
27
-
28
- const result = await runGh(["run", "watch", String(run_id), ...repoArgs(repo)], {
29
- cwd: ctx.cwd,
30
- signal,
31
- timeout: 600_000,
32
- });
33
-
34
- if (result.code !== 0) {
35
- throw new Error(`gh run watch failed: ${result.stderr || `exit code ${result.code}`}`);
36
- }
37
-
38
- return {
39
- content: [
40
- { type: "text", text: `## Workflow Run ${run_id} Completed\n\n${result.stdout}` },
41
- ],
42
- details: { exitCode: 0, input: params, ...(pendant && { pendant }) },
43
- };
44
- },
45
- });
61
+ );
46
62
  }
package/src/lib/github.ts CHANGED
@@ -186,6 +186,11 @@ export function renderHits(hits: SearchHit[], options: { repo?: string; fields?:
186
186
  }
187
187
 
188
188
  function toDate(iso: string | null | undefined): string {
189
+ return dateOnly(iso);
190
+ }
191
+
192
+ /** ISO 时间戳取日期部分(`2026-10-03`);缺省或 null 给空串。 */
193
+ export function dateOnly(iso: string | null | undefined): string {
189
194
  return iso ? iso.slice(0, 10) : "";
190
195
  }
191
196
 
@@ -217,6 +222,76 @@ function normalize(raw: SearchItem): SearchHit {
217
222
  };
218
223
  }
219
224
 
225
+ /**
226
+ * `gh issue list --json …` / `gh pr list --json …` 的响应形状。gh 走 GraphQL,字段是
227
+ * camelCase,`state` 是大写的 `OPEN` / `CLOSED` / `MERGED`,`comments` 是整个评论数组
228
+ * (不是计数)——所以浏览分支要在归一化这一步把它压成计数,才能和搜索分支(REST,
229
+ * 下划线命名、state 小写、comments 是数字)共用同一套 SearchHit、渲染与载荷。
230
+ */
231
+ const ghListRecordSchema = Type.Object({
232
+ number: Type.Number(),
233
+ title: Type.String(),
234
+ state: Type.String(),
235
+ url: Type.String(),
236
+ labels: Type.Array(Type.Object({ name: Type.String() }, { additionalProperties: true })),
237
+ milestone: Type.Union([
238
+ Type.Object({ title: Type.String() }, { additionalProperties: true }),
239
+ Type.Null(),
240
+ ]),
241
+ assignees: Type.Array(Type.Object({ login: Type.String() }, { additionalProperties: true })),
242
+ author: Type.Union([
243
+ Type.Object({ login: Type.String() }, { additionalProperties: true }),
244
+ Type.Null(),
245
+ ]),
246
+ comments: Type.Array(Type.Unknown()),
247
+ createdAt: Type.String(),
248
+ updatedAt: Type.String(),
249
+ closedAt: Type.Union([Type.String(), Type.Null()]),
250
+ mergedAt: Type.Optional(Type.Union([Type.String(), Type.Null()])),
251
+ });
252
+
253
+ /** 浏览分支要 gh 给的字段,与 `SearchHit` 的列一一对应(`comments` 只用来数个数)。 */
254
+ export const GH_LIST_FIELDS = [
255
+ "number",
256
+ "title",
257
+ "state",
258
+ "url",
259
+ "labels",
260
+ "milestone",
261
+ "assignees",
262
+ "author",
263
+ "comments",
264
+ "createdAt",
265
+ "updatedAt",
266
+ "closedAt",
267
+ "mergedAt",
268
+ ].join(",");
269
+
270
+ /** 把 `gh issue list --json` / `gh pr list --json` 的输出归一到 `SearchHit`(repo 由调用方给)。 */
271
+ export function normalizeGhList(stdout: string, repo: string): SearchHit[] {
272
+ const items = parseWithSchema(Type.Array(ghListRecordSchema), JSON.parse(stdout));
273
+ return items.map((item) => {
274
+ const mergedAt = item.mergedAt ?? null;
275
+ return {
276
+ number: item.number,
277
+ // gh 给 MERGED 状态的 PR 与 OPEN/CLOSED 一样是 state 字段,merged 语义与搜索分支对齐
278
+ state: mergedAt ? "merged" : (item.state.toLowerCase() as SearchHit["state"]),
279
+ title: item.title,
280
+ url: item.url,
281
+ repo,
282
+ author: item.author?.login ?? "",
283
+ labels: item.labels.map((label) => label.name),
284
+ milestone: item.milestone?.title ?? "",
285
+ assignees: item.assignees.map((assignee) => assignee.login),
286
+ comments: item.comments.length,
287
+ createdAt: toDate(item.createdAt),
288
+ updatedAt: toDate(item.updatedAt),
289
+ closedAt: toDate(item.closedAt),
290
+ mergedAt: toDate(mergedAt),
291
+ };
292
+ });
293
+ }
294
+
220
295
  /**
221
296
  * Error thrown when the GitHub search API rejects the request. Carries the
222
297
  * original toolcall params so the model can see the exact input.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 三个只读 LSP 查询工具(`lsp-find-definition` / `lsp-find-reference` / `lsp-inspect`)的
3
+ * 结构化结果 schema。
4
+ *
5
+ * 载荷只有我们自己归一化过的数据(`service.inspect` 的产出),没有外部引擎字段,因此这里
6
+ * 不学 AFT 那样全线可选:字段都在,类型确定。位置的行列号是 **1-based**(与工具文本、
7
+ * 与这三个工具自己的 `line` / `character` 参数一致),脚本拿到就能直接回喂给另一次查询。
8
+ *
9
+ * `locations` 给的是查询到的**全部**位置:文本会为了模型上下文对引用列表做截断
10
+ * (每文件 10 条、最多 30 个文件),载荷不跟着截——它是给程序用的数据,截断是渲染的事。
11
+ */
12
+
13
+ import { Type } from "typebox";
14
+
15
+ /** 一个位置:绝对路径 + 1-based 行列号(与工具文本里的 `path:line:col` 同源)。 */
16
+ export const lspLocationSchema = Type.Object(
17
+ {
18
+ path: Type.String({ description: "文件的绝对路径" }),
19
+ line: Type.Integer({ minimum: 1, description: "1-based 行号" }),
20
+ character: Type.Integer({ minimum: 1, description: "1-based 列号" }),
21
+ },
22
+ { additionalProperties: false },
23
+ );
24
+
25
+ /** 回答这次查询的语言服务器 id,以及查询到的全部位置。 */
26
+ export const lspLocationsStructuredSchema = Type.Object(
27
+ {
28
+ text: Type.String({ description: "与工具输出一致的渲染文本(引用列表可能被截断)" }),
29
+ serverID: Type.String({ description: "回答这次查询的语言服务器 id" }),
30
+ locations: Type.Array(lspLocationSchema, {
31
+ description: "查询到的全部位置,1-based 行列号,顺序与文本一致",
32
+ }),
33
+ },
34
+ { additionalProperties: false },
35
+ );
36
+
37
+ /** hover 的内容是服务器给的异构结构,渲染成文本后原样给出。 */
38
+ export const lspHoverStructuredSchema = Type.Object(
39
+ {
40
+ text: Type.String({ description: "与工具输出一致的 hover 文本(服务器返回的内容)" }),
41
+ serverID: Type.String({ description: "回答这次查询的语言服务器 id" }),
42
+ },
43
+ { additionalProperties: false },
44
+ );