@trim21/personal-pi-extensions 0.1.651 → 0.1.653

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.
Files changed (52) hide show
  1. package/README.md +67 -9
  2. package/package.json +3 -11
  3. package/src/aft/index.ts +40 -23
  4. package/src/aft/tools.ts +10 -9
  5. package/src/claude-code/files.ts +99 -54
  6. package/src/claude-code/glob.ts +8 -5
  7. package/src/claude-code/grep.ts +8 -5
  8. package/src/claude-code/index.ts +56 -10
  9. package/src/claude-code/search.ts +4 -3
  10. package/src/claude-code/session-tools.ts +4 -3
  11. package/src/claude-code/shell.ts +3 -8
  12. package/src/gh/index.ts +48 -25
  13. package/src/gh/tools/download-release-assets.ts +3 -3
  14. package/src/gh/tools/get-workflow-jobs.ts +3 -3
  15. package/src/gh/tools/list-issues.ts +3 -3
  16. package/src/gh/tools/list-prs.ts +3 -3
  17. package/src/gh/tools/list-releases.ts +3 -3
  18. package/src/gh/tools/list-workflow-runs.ts +3 -3
  19. package/src/gh/tools/read-ci-logs.ts +3 -4
  20. package/src/gh/tools/read-issue-comments.ts +3 -3
  21. package/src/gh/tools/read-issue.ts +3 -3
  22. package/src/gh/tools/read-pr-comments.ts +3 -3
  23. package/src/gh/tools/read-pr-diff.ts +3 -3
  24. package/src/gh/tools/read-pr-status.ts +3 -3
  25. package/src/gh/tools/read-pr.ts +3 -3
  26. package/src/gh/tools/read-release.ts +3 -3
  27. package/src/gh/tools/read-repo.ts +3 -3
  28. package/src/gh/tools/wait-commit-checks.ts +3 -3
  29. package/src/gh/tools/wait-pr-checks.ts +3 -3
  30. package/src/gh/tools/watch-run.ts +3 -3
  31. package/src/index.ts +120 -0
  32. package/src/lib/lsp/inspect-tool.ts +5 -5
  33. package/src/lib/lsp/rename-tool.ts +4 -7
  34. package/src/lib/tool-bus.ts +126 -0
  35. package/src/lib/tool-registration.ts +83 -0
  36. package/src/lib/tool-services.ts +81 -0
  37. package/src/lib/tool-units.ts +102 -0
  38. package/src/lib/tools-config.ts +277 -0
  39. package/src/opencode/bash.ts +7 -2
  40. package/src/opencode/files.ts +90 -43
  41. package/src/opencode/glob.ts +7 -1
  42. package/src/opencode/grep.ts +7 -1
  43. package/src/opencode/index.ts +66 -21
  44. package/src/opencode/question.ts +8 -2
  45. package/src/opencode/todo.ts +8 -2
  46. package/src/orphan-worker-cleaner.ts +38 -0
  47. package/src/spawn-agent.ts +160 -175
  48. package/src/talk/index.ts +92 -66
  49. package/src/vision-agent.ts +31 -7
  50. package/src/web/fetch.ts +89 -72
  51. package/src/web/search.ts +14 -2
  52. package/src/gh-readonly.ts +0 -10
@@ -28,6 +28,8 @@ import { Value } from "typebox/value";
28
28
 
29
29
  import { completeText, type CompleteTextResult, type ModelRegistryLike } from "./lib/model-call.js";
30
30
  import { type ToolPendant } from "./lib/pendant.js";
31
+ import type { ToolBus } from "./lib/tool-bus.js";
32
+ import { createToolRegistration } from "./lib/tool-registration.js";
31
33
 
32
34
  // ── constants ────────────────────────────────────────────────────────────────
33
35
 
@@ -296,7 +298,23 @@ export async function callVision(
296
298
 
297
299
  // ── extension ────────────────────────────────────────────────────────────────
298
300
 
299
- export default function visionAgent(pi: ExtensionAPI) {
301
+ /** 视觉代理工具集(未配置视觉模型时返回 undefined):入口从它取注册函数。 */
302
+ export function createVisionTools(): { register(bus: ToolBus): void } | undefined {
303
+ // 未配置视觉模型就不注册工具:agent 看不到也调不到。provider 的
304
+ // baseUrl/apiKey 由 pi 的模型注册表在调用时解析(execute 里 find),
305
+ // 配置好 settings.json / models.json 后重新加载(/reload)即可生效。
306
+ if (!loadVisionConfig()?.model) {
307
+ return undefined;
308
+ }
309
+ return {
310
+ register(bus) {
311
+ registerVisionTool(bus);
312
+ },
313
+ };
314
+ }
315
+
316
+ export default function visionAgent(pi: ExtensionAPI): void {
317
+ const registration = createToolRegistration(pi);
300
318
  // 只依赖 input 字段,不绑定 pi 内部类型
301
319
  interface AnyModel {
302
320
  id?: string;
@@ -334,15 +352,21 @@ export default function visionAgent(pi: ExtensionAPI) {
334
352
  syncVisionMode(ctx.model);
335
353
  });
336
354
 
337
- // 未配置视觉模型就不注册工具:agent 看不到也调不到。provider 的
338
- // baseUrl/apiKey 由 pi 的模型注册表在调用时解析(execute 里 find),
339
- // 配置好 settings.json / models.json 后重新加载(/reload)即可生效。
340
- const visionConfig = loadVisionConfig();
341
- if (!visionConfig?.model) {
355
+ // 未配置视觉模型就不注册工具(见 createVisionTools)。
356
+ const tools = createVisionTools();
357
+ if (!tools) {
342
358
  return;
343
359
  }
344
360
 
345
- pi.registerTool({
361
+ // 工具在 session_start 里注册:禁用规则可以带 models,只有那时才知道本会话
362
+ // 的模型。
363
+ registration.onSessionStart(() => {
364
+ tools.register(registration.bus);
365
+ });
366
+ }
367
+
368
+ function registerVisionTool(bus: ToolBus): void {
369
+ bus.register({
346
370
  name: TOOL_NAME,
347
371
  label: "Describe Image",
348
372
  description:
package/src/web/fetch.ts CHANGED
@@ -24,6 +24,8 @@ import { Type } from "typebox";
24
24
  import { egress } from "../lib/egress.js";
25
25
  import { resolvePathArg } from "../lib/path.js";
26
26
  import { createRequestPolicy } from "../lib/request-policy.js";
27
+ import type { ToolBus } from "../lib/tool-bus.js";
28
+ import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
27
29
  import { guardWriteAccess } from "../lib/write-guard.js";
28
30
 
29
31
  const MAX_REDIRECTS = 5;
@@ -358,83 +360,98 @@ function truncateMarkdown(text: string): { text: string; truncated: boolean } {
358
360
  return { text: (cut > 0 ? sliced.slice(0, cut) : sliced) + "\n…(已截断)", truncated: true };
359
361
  }
360
362
 
361
- export default function webFetchTool(pi: ExtensionAPI): void {
362
- // 本工具是独立扩展入口(pi 给每个入口单独建 jiti 实例),自建一份非沙盒请求
363
- // 策略并订阅 pi.events:/bwrap-deny-request 在 bash 入口切换时会同步过来。
363
+ export function createWebFetchTool(pi: ExtensionAPI): (bus: ToolBus) => void {
364
+ // 自己建一份非沙盒请求策略并订阅 pi.events:/bwrap-deny-request 在 bash 入口
365
+ // 切换时会同步过来。
364
366
  const policy = createRequestPolicy(pi.events);
365
- pi.registerTool({
366
- name: "web_fetch",
367
- label: "Web Fetch",
368
- description:
369
- "Fetch a URL and return its content as markdown (HTML pages) or raw text " +
370
- "(JSON/XML/plain-text API responses). With output_path the body is saved to that " +
371
- "file verbatim instead — any content type, no extraction, no truncation — and the " +
372
- "result is the JSON summary {url, file_path, content_type, bytes} rather than the " +
373
- "content; use it for images, logs and other attachments (GitHub user-attachments " +
374
- "links from issue bodies, release assets, raw files). Give the file the extension " +
375
- "matching the response's content_type: the Read tool decides image support by " +
376
- "extension. Requests honour the proxy in ~/.pi/agent/proxy.json, so they reach hosts " +
377
- "the shell sandbox blocks. SSRF-protected: refuses private/internal addresses.",
378
- promptSnippet: "Fetch a web page, API response, or download a file",
379
- parameters: Type.Object({
380
- url: Type.String({ description: "The URL to fetch" }),
381
- output_path: Type.Optional(
382
- Type.String({
383
- description:
384
- "Save the response body to this path verbatim instead of returning it (absolute, or relative to the session cwd; ~ is expanded). Parent directories are created and an existing file is overwritten.",
385
- }),
386
- ),
387
- }),
388
- async execute(_id, params, signal, _onUpdate, ctx) {
389
- const destination =
390
- params.output_path === undefined ? undefined : resolvePathArg(ctx.cwd, params.output_path);
391
- // 落盘位置的审批与写文件工具同一套:工作区与 /tmp 自动放行,其余问用户。
392
- // 放在 try 外面,拒绝的原因(user deny)不该被改写成「抓取失败」。
393
- if (destination !== undefined) {
394
- await guardWriteAccess(ctx, {
395
- toolName: "web_fetch",
396
- absolutePath: destination,
397
- policy,
398
- signal,
399
- });
400
- }
401
-
402
- try {
367
+ return (bus) => {
368
+ bus.register({
369
+ name: "web_fetch",
370
+ label: "Web Fetch",
371
+ description:
372
+ "Fetch a URL and return its content as markdown (HTML pages) or raw text " +
373
+ "(JSON/XML/plain-text API responses). With output_path the body is saved to that " +
374
+ "file verbatim instead — any content type, no extraction, no truncation — and the " +
375
+ "result is the JSON summary {url, file_path, content_type, bytes} rather than the " +
376
+ "content; use it for images, logs and other attachments (GitHub user-attachments " +
377
+ "links from issue bodies, release assets, raw files). Give the file the extension " +
378
+ "matching the response's content_type: the Read tool decides image support by " +
379
+ "extension. Requests honour the proxy in ~/.pi/agent/proxy.json, so they reach hosts " +
380
+ "the shell sandbox blocks. SSRF-protected: refuses private/internal addresses.",
381
+ promptSnippet: "Fetch a web page, API response, or download a file",
382
+ parameters: Type.Object({
383
+ url: Type.String({ description: "The URL to fetch" }),
384
+ output_path: Type.Optional(
385
+ Type.String({
386
+ description:
387
+ "Save the response body to this path verbatim instead of returning it (absolute, or relative to the session cwd; ~ is expanded). Parent directories are created and an existing file is overwritten.",
388
+ }),
389
+ ),
390
+ }),
391
+ async execute(_id, params, signal, _onUpdate, ctx) {
392
+ const destination =
393
+ params.output_path === undefined
394
+ ? undefined
395
+ : resolvePathArg(ctx.cwd, params.output_path);
396
+ // 落盘位置的审批与写文件工具同一套:工作区与 /tmp 自动放行,其余问用户。
397
+ // 放在 try 外面,拒绝的原因(user deny)不该被改写成「抓取失败」。
403
398
  if (destination !== undefined) {
404
- const file = await saveUrlToFile(params.url, destination, signal);
405
- const payload = {
406
- url: file.url,
407
- file_path: file.filePath,
408
- content_type: file.contentType,
409
- bytes: file.bytes,
399
+ await guardWriteAccess(ctx, {
400
+ toolName: "web_fetch",
401
+ absolutePath: destination,
402
+ policy,
403
+ signal,
404
+ });
405
+ }
406
+
407
+ try {
408
+ if (destination !== undefined) {
409
+ const file = await saveUrlToFile(params.url, destination, signal);
410
+ const payload = {
411
+ url: file.url,
412
+ file_path: file.filePath,
413
+ content_type: file.contentType,
414
+ bytes: file.bytes,
415
+ };
416
+ return {
417
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
418
+ details: payload,
419
+ };
420
+ }
421
+
422
+ const page = await fetchPage(params.url, signal);
423
+ const { text, truncated } = truncateMarkdown(page.markdown);
424
+ const details: Record<string, unknown> = {
425
+ url: page.url,
426
+ title: page.title,
427
+ bytes: Buffer.byteLength(page.markdown, "utf8"),
428
+ truncated,
429
+ };
430
+ return {
431
+ content: [{ type: "text", text }],
432
+ details,
410
433
  };
434
+ } catch (error) {
435
+ const message = error instanceof Error ? error.message : String(error);
436
+ const details: Record<string, unknown> = { error: message, url: params.url };
411
437
  return {
412
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
413
- details: payload,
438
+ isError: true,
439
+ content: [{ type: "text", text: `抓取失败: ${message}` }],
440
+ details,
414
441
  };
415
442
  }
443
+ },
444
+ });
445
+ };
446
+ }
416
447
 
417
- const page = await fetchPage(params.url, signal);
418
- const { text, truncated } = truncateMarkdown(page.markdown);
419
- const details: Record<string, unknown> = {
420
- url: page.url,
421
- title: page.title,
422
- bytes: Buffer.byteLength(page.markdown, "utf8"),
423
- truncated,
424
- };
425
- return {
426
- content: [{ type: "text", text }],
427
- details,
428
- };
429
- } catch (error) {
430
- const message = error instanceof Error ? error.message : String(error);
431
- const details: Record<string, unknown> = { error: message, url: params.url };
432
- return {
433
- isError: true,
434
- content: [{ type: "text", text: `抓取失败: ${message}` }],
435
- details,
436
- };
437
- }
438
- },
439
- });
448
+ /** 工具集入口形态:入口用它取注册函数。 */
449
+ export function createWebFetch(pi: ExtensionAPI): { register(bus: ToolBus): void } {
450
+ const registerTool = createWebFetchTool(pi);
451
+ return { register: registerTool };
452
+ }
453
+
454
+ /** 独立扩展入口:在 session_start 里按本会话模型注册。 */
455
+ export default function webFetchTool(pi: ExtensionAPI): void {
456
+ registerToolsOnSessionStart(pi, createWebFetchTool(pi));
440
457
  }
package/src/web/search.ts CHANGED
@@ -12,6 +12,8 @@ import { Type } from "typebox";
12
12
  import { Value } from "typebox/value";
13
13
 
14
14
  import { egress } from "../lib/egress.js";
15
+ import type { ToolBus } from "../lib/tool-bus.js";
16
+ import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
15
17
  import { loadSearch1ApiKey } from "./config.js";
16
18
 
17
19
  const SEARCH_URL = "https://api.search1api.com/search";
@@ -169,8 +171,8 @@ export async function searchWeb(
169
171
  return { query, hits: mapHits(parsed.results) };
170
172
  }
171
173
 
172
- export default function webSearchTool(pi: ExtensionAPI): void {
173
- pi.registerTool({
174
+ export function registerWebSearchTool(bus: ToolBus): void {
175
+ bus.register({
174
176
  name: "web_search",
175
177
  label: "Web Search",
176
178
  description:
@@ -285,3 +287,13 @@ export default function webSearchTool(pi: ExtensionAPI): void {
285
287
  },
286
288
  });
287
289
  }
290
+
291
+ /** 工具集入口形态:入口用它取注册函数。 */
292
+ export function createWebSearchTool(): { register(bus: ToolBus): void } {
293
+ return { register: registerWebSearchTool };
294
+ }
295
+
296
+ /** 独立扩展入口:在 session_start 里按本会话模型注册。 */
297
+ export default function webSearchTool(pi: ExtensionAPI): void {
298
+ registerToolsOnSessionStart(pi, registerWebSearchTool);
299
+ }
@@ -1,10 +0,0 @@
1
- /**
2
- * GitHub Read-Only Tools Extension — extension entry point.
3
- *
4
- * The implementation lives in `src/gh/` (shared helpers in `base.ts`, one tool
5
- * per file in `tools/`, registration in `index.ts`). This file only forwards
6
- * the registration function and the public API consumed by tests.
7
- */
8
-
9
- export { default } from "./gh/index.js";
10
- export * from "./gh/index.js";