@trim21/personal-pi-extensions 0.1.20261012003405 → 0.1.20261012023047

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 CHANGED
@@ -31,7 +31,8 @@
31
31
 
32
32
  `codemode` 让模型写一段 JavaScript(作为 async 函数体,`await` 与 `return` 都可用),在
33
33
  进程内的 worker 线程里用 QuickJS wasm VM 执行。VM 里没有 node、文件系统、网络、timer
34
- 或模块加载,脚本的出口只有 `call(name, args)` 与 `fs.read` / `fs.write` 两个文件原语。
34
+ 或模块加载,脚本的出口只有 `call(name, args)`、`fs.read` / `fs.write` 两个文件原语与内建的
35
+ `sleep`。
35
36
 
36
37
  - **嵌套调用经工具总线**:`call("Bash", { command })` 最终执行的是 `Bash` 工具自己的
37
38
  `execute`,所以工具的审批照常生效(Bash 沙箱外执行会弹自己的提权确认)。codemode
@@ -53,16 +54,18 @@
53
54
  直接改,不进脚本)。
54
55
  - **只有脚本输出进上下文**:`text(value)` / `console.log(...)` 与 `return` 值进入工具结果,
55
56
  中间的工具调用与它们的返回内容不会(也不在会话记录里留下工具调用条目)。
56
- - **返回值**:声明了 `structuredSchema` 的工具把结果放在 `structuredResult` 里,`call()`
57
- 解包成对象给脚本——gh-readonly 的读类工具给解析后的 JSON,`Bash` / `bash` 给
58
- `{ exitCode, output }`(`exitCode` 为 `null` 表示命令被超时/中止杀掉;命令非零退出是正常
59
- 结果,直接读 `exitCode` 分支即可)。`{ ok: false, error }` 会 reject 成 `CallFailedError`
57
+ - **返回值**:工具的结果就是结构化载荷,`call()` 直接把它解包成对象给脚本——gh-readonly
58
+ 的工具给 REST 原物或从响应解析出的结构,`Bash` / `bash` 给 `{ exitCode, output }`
59
+ (`exitCode` 为 `null` 表示命令被超时/中止杀掉;命令非零退出是正常结果,直接读
60
+ `exitCode` 分支即可)。`{ ok: false, error }` 会 reject 成 `CallFailedError`
60
61
  (脚本可按 `instanceof CallFailedError` 区分调用失败与自身运行期错误)。没有声明输出
61
- 结构的工具回退成工具输出的文本。
62
+ 结构的工具不进集合,`call()` 会报「not available in codemode」。
62
63
  - **脚本接口**:`call` / `CallFailedError` / `ALL_TOOLS` / `fs.read` / `fs.write` /
63
- `text` / `image` / `exit` / `console.*` / `store.set` / `store.get` / `store.list`
64
+ `text` / `image` / `exit` / `sleep` / `console.*` / `store.set` / `store.get` / `store.list`
64
65
  (会话内持久的键值表);首行可选 `// @options: {"max_output_tokens": 10000}`。
65
66
  脚本没有超时:死循环由调用方中止(Esc)结束,等嵌套调用返回(含用户审批弹窗)多久都不算超时。
67
+ `await sleep(seconds)`(秒,可为小数)是脚本唯一的等待手段,由 worker 线程自己的定时器驱动
68
+ (不经主线程、不产生进度帧),也没有上限——要等多久由脚本决定,不想等就中止。
66
69
  - **工具描述**里给出每个可调用工具的 `declare function call(name, args): Promise<T>` 重载,
67
70
  参数与返回类型都取自工具自己的 schema,所以模型在写脚本前就知道返回值形状。
68
71
  - **store** 记在每次成功调用工具结果的 `details.store` 上(与 `src/lib/file-reads.ts` 的
@@ -94,7 +97,7 @@
94
97
  "disabledTools": ["talk-*", { "tools": ["web_*"], "models": ["gpt-*"] }],
95
98
  "enabledTools": [{ "tools": ["web_search"], "models": ["glm-*"] }],
96
99
  // 只进 codemode、不交给模型的工具(须声明结构化输出才会生效)
97
- "codemodeOnlyTools": [{ "tools": ["read-github-*"], "models": ["deepseek-*"] }],
100
+ "codemodeOnlyTools": [{ "tools": ["aft_*"], "models": ["deepseek-*"] }],
98
101
  },
99
102
  }
100
103
  ```
@@ -109,6 +112,9 @@
109
112
  的工具进不了 codemode,因此会被忽略、保持直接可用,并在会话启动时给出一条 warning——
110
113
  想真正移除工具用 `disabledTools`。
111
114
 
115
+ 有些工具由代码固定为 codemode-only(例如 gh-readonly 那一组,见
116
+ [GitHub 只读工具](#github-只读工具)),不需要这条配置;`disabledTools` 对它们同样生效。
117
+
112
118
  **判定时机是每个会话启动**:pi 在启动 / `/new` / `resume` / `/fork` / `/reload` 时重建
113
119
  扩展,注册就发生在 `session_start` 里(模型在扩展加载期读不到),因此新会话按自己的
114
120
  模型重新判定;**同一会话内 `/model` 切换不会改变工具集合**,换模型请开新会话。
@@ -471,11 +477,13 @@ pi -e ./src/aft/index.ts
471
477
 
472
478
  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。
473
479
 
480
+ **这一组工具只经 `codemode` 曝光、模型不能直接调用**:它们注册在工具总线上但不交给 pi,只能在脚本里 `call("read-github-issue", …)`。工具的返回值就是结构化载荷(REST 原物,或从响应解析出的统计 / 判定),没有面向模型的文本输出。想在某些环境或模型下彻底关掉它们,用 `disabledTools`(可带 `models`)。
481
+
474
482
  - **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`
475
483
  - **CI**:`read-github-ci-logs`、`list-github-workflow-runs`、`get-github-workflow-jobs`、`wait-github-pr-checks`、`wait-github-commit-checks`、`watch-github-run`
476
484
  - **仓库 / 发布**:`read-github-repo`、`list-github-releases`、`read-github-release`、`download-github-release-assets`
477
485
 
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`(未给 `repo` 时最前面多一列 `repo`),默认列之外的字段从结构化载荷 `items` 里取。`wait-github-pr-checks` / `wait-github-commit-checks` 30 秒轮询、600 秒截止:任一 fail 即返回,全部 pass / skipped 才算通过,超时返回快照不抛错;检查快照还包含「已结束但没有 check run」的 check suite——fork PR 上等待维护者批准的 workflow 只存在于这一层,遇到它立即返回 `awaiting_approval`(不会自行开始,轮询无意义),条目名字取自对应的 workflow run。
486
+ `list-github-issues` / `list-github-prs` 带 `keywords` 时走 GitHub search API,不带 `keywords` 时走 REST 列表端点(state 缺省只搜 open,`state: "all"` 不加 state qualifier 覆盖 open + closed),载荷给全部命中。`wait-github-pr-checks` / `wait-github-commit-checks` 30 秒轮询、600 秒截止:任一 fail 即返回,全部 pass / skipped 才算通过,超时返回快照不抛错;检查快照还包含「已结束但没有 check run」的 check suite——fork PR 上等待维护者批准的 workflow 只存在于这一层,遇到它立即返回 `awaiting_approval`(不会自行开始,轮询无意义),条目名字取自对应的 workflow run。`read-github-ci-logs` 把日志落盘并只返回 step 索引,日志正文用脚本的 `fs.read(log_file)` 读。
479
487
 
480
488
  出网代理:`~/.pi/agent/proxy.json`(`{ "proxy": "http://127.0.0.1:7890", "noProxy": "localhost" }`),缺省字段回退 `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` / `NO_PROXY` 环境变量。
481
489
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.20261012003405",
3
+ "version": "0.1.20261012023047",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -152,6 +152,7 @@ export function renderDeclarations(tools: readonly ToolLike[]): string {
152
152
  "declare function text(value: unknown): void;",
153
153
  "declare function image(value: unknown): void;",
154
154
  "declare function exit(): void;",
155
+ "declare function sleep(seconds: number): Promise<void>;",
155
156
  "declare const store: { set(key: string, value: unknown): void; get(key: string): unknown; list(): string[] };",
156
157
  ].join("\n");
157
158
  }
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * codemode 脚本侧的 prelude:在 QuickJS VM 里先于脚本求值,构建脚本能看到的全部
3
3
  * 能力(`call` / `CallFailedError` / `ALL_TOOLS` / `fs` / `text` / `image` / `exit` /
4
- * `console` / `store`),并把宿主桥接封在闭包里——脚本拿不到 `bridge` 本身。
4
+ * `sleep` / `console` / `store`),并把宿主桥接封在闭包里——脚本拿不到 `bridge` 本身。
5
5
  *
6
6
  * 值与参数过桥时都是 JSON 文本,本侧负责 parse/stringify;异步调用用一个 pending
7
7
  * 表把 id 映射到 promise,由宿主在结果到达时 settle。每次调用失败(工具或 fs 原语)
8
8
  * 都由宿主以 `ok: false` 回报,本侧统一 reject 成 `CallFailedError`(脚本可以按
9
9
  * instanceof 区分「调用失败」与自己的运行期错误)。
10
10
  *
11
- * 求值结果是一个函数 `(bridge, toolsJson, storeJson) => { settle, run, stalled }`。
11
+ * 求值结果是一个函数 `(bridge, toolsJson, storeJson) => { settle, timerDone, run, stalled }`。
12
12
  * `bridge(kind, a, b, c)`:
13
13
  * - `"call"`(id, name, argsJson):工具,以及名字为 `fs.read` / `fs.write` 的 fs 原语
14
+ * - `"sleep"`(id, ms):等待,由 worker 线程自己的定时器在 ms 后调 `timerDone(id)`
14
15
  * - `"output"`("text", text)/("image", data, mimeType)
15
16
  * - `"done"`(ok, valueJsonOrErrorJson, writesJson)
16
17
  */
@@ -259,6 +260,22 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
259
260
  throw EXIT;
260
261
  }
261
262
 
263
+ // 脚本唯一的等待手段:worker 线程在 ms 后调 timerDone(id) 解开它。沙箱里没有别的
264
+ // timer,也没有上限——睡多久由脚本决定,结束只能靠调用方中止。
265
+ function sleep(seconds) {
266
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) {
267
+ throw new TypeErrorCtor(
268
+ "sleep(seconds) expects a non-negative number of seconds, got " + format(seconds),
269
+ );
270
+ }
271
+ return new Promise((resolve) => {
272
+ const id = nextId++;
273
+ // sleep 只会被 timerDone 成功解开,reject 用不到
274
+ pending.set(id, { resolve, reject: () => {} });
275
+ bridge("sleep", id, Math.round(seconds * 1000));
276
+ });
277
+ }
278
+
262
279
  const console = {};
263
280
  for (const level of ["log", "info", "warn", "error", "debug"]) {
264
281
  console[level] = (...args) => {
@@ -275,6 +292,7 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
275
292
  Object.defineProperty(globalThis, "text", { value: text, enumerable: true });
276
293
  Object.defineProperty(globalThis, "image", { value: image, enumerable: true });
277
294
  Object.defineProperty(globalThis, "exit", { value: exit, enumerable: true });
295
+ Object.defineProperty(globalThis, "sleep", { value: sleep, enumerable: true });
278
296
 
279
297
  return {
280
298
  settle(id, ok, payload) {
@@ -294,6 +312,13 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
294
312
  }
295
313
  entry.resolve(value);
296
314
  },
315
+ // worker 线程的定时器到点后解开一次 sleep(成功路径,不带值)
316
+ timerDone(id) {
317
+ const entry = pending.get(id);
318
+ if (!entry) return;
319
+ pending.delete(id);
320
+ entry.resolve();
321
+ },
297
322
  run(fn) {
298
323
  let promise;
299
324
  try {
@@ -6,8 +6,9 @@
6
6
  * 主线程执行工具后把结果发回去。脚本没有超时,只有调用方的中止:中止时主线程直接
7
7
  * `worker.terminate()`,因此不需要取消消息。
8
8
  *
9
- * worker 边界两侧都做 typebox 校验:worker 里跑的是模型写的代码,回传的消息同样
10
- * 当外部输入对待。
9
+ * 校验只做一个方向:worker 里跑的是模型写的代码,它回传的消息由主线程用 typebox
10
+ * 当外部输入复核;反向的消息是主线程自己构造的,形状由 `HostMessage` 类型约束,
11
+ * worker 只按 `t` 分派。
11
12
  */
12
13
 
13
14
  import { type TSchema, Type } from "typebox";
@@ -32,35 +33,6 @@ const outputItemSchema = Type.Union([
32
33
  Type.Object({ type: Type.Literal("image"), data: Type.String(), mimeType: Type.String() }),
33
34
  ]);
34
35
 
35
- const toolDeclSchema = Type.Object({
36
- name: Type.String(),
37
- description: Type.Optional(Type.String()),
38
- /** 工具的 structuredSchema(TypeBox schema):脚本侧据此渲染 call() 的返回类型。 */
39
- structuredSchema: Type.Optional(Type.Unknown()),
40
- });
41
-
42
- const startSchema = Type.Object({
43
- t: Type.Literal("start"),
44
- code: Type.String(),
45
- tools: Type.Array(toolDeclSchema),
46
- store: Type.Record(Type.String(), Type.Unknown()),
47
- });
48
-
49
- const resultSchema = Type.Union([
50
- Type.Object({
51
- t: Type.Literal("result"),
52
- id: Type.Number(),
53
- ok: Type.Literal(true),
54
- value: Type.Unknown(),
55
- }),
56
- Type.Object({
57
- t: Type.Literal("result"),
58
- id: Type.Number(),
59
- ok: Type.Literal(false),
60
- error: Type.String(),
61
- }),
62
- ]);
63
-
64
36
  const callSchema = Type.Object({
65
37
  t: Type.Literal("call"),
66
38
  id: Type.Number(),
@@ -100,7 +72,6 @@ const doneSchema = Type.Union([
100
72
  }),
101
73
  ]);
102
74
 
103
- const hostMessageSchema = Type.Union([startSchema, resultSchema]);
104
75
  const workerMessageSchema = Type.Union([callSchema, outputFrameSchema, doneSchema]);
105
76
 
106
77
  /** worker 启动数据:注册时编译好的 wasm 模块(结构化克隆可以带它跨线程)。 */
@@ -155,11 +126,6 @@ function decode<T>(schema: TSchema, value: unknown): DecodeResult<T> {
155
126
  return { ok: true, frame: value as T };
156
127
  }
157
128
 
158
- /** 校验主线程发给 worker 的消息。 */
159
- export function decodeHostMessage(value: unknown): DecodeResult<HostMessage> {
160
- return decode<HostMessage>(hostMessageSchema, value);
161
- }
162
-
163
129
  /** 校验 worker 发回主线程的消息。 */
164
130
  export function decodeWorkerMessage(value: unknown): DecodeResult<WorkerMessage> {
165
131
  return decode<WorkerMessage>(workerMessageSchema, value);
@@ -24,7 +24,7 @@ import { Type } from "typebox";
24
24
  import type { FileSnapshot, ReadsState } from "../lib/file-reads.js";
25
25
  import type { ToolPendant } from "../lib/pendant.js";
26
26
  import type { RequestPolicy } from "../lib/request-policy.js";
27
- import { type ToolBus, toolResultText } from "../lib/tool-bus.js";
27
+ import { type ToolBus } from "../lib/tool-bus.js";
28
28
  import { renderDeclarations, toScriptTools } from "./declarations.js";
29
29
  import { createCodemodeFs } from "./fs.js";
30
30
  import type { CodemodeOutputItem, ScriptError, StoreWrites } from "./protocol.js";
@@ -113,6 +113,8 @@ function buildDescription(tools: readonly CallableTool[]): string {
113
113
  " fields you need into `text(value)` / `return` — e.g. `text(result.items.map((item) => item.title))`.",
114
114
  "- Do not stringify whole results (`JSON.stringify(result)`) or print every field the tool happened to",
115
115
  " return: urls, ids and metadata you will not use cost context and buy nothing.",
116
+ "- `await sleep(seconds)` waits that many seconds (fractions allowed). It is the only way to",
117
+ ' wait: the sandbox has no timers, and it beats `call("Bash", { command: "sleep 1" })`.',
116
118
  "- `store.set(key, value)`, `store.get(key)` and `store.list()` are a small key/value store that",
117
119
  " persists across codemode calls in this session; `store.set(key, undefined)` removes a key.",
118
120
  "- The sandbox has no file system, network, timers or Node APIs: calling tools is its only",
@@ -380,17 +382,7 @@ export function createCodemodeTools(pi: ExtensionAPI): CodemodeTools {
380
382
  if (!callable.has(name)) {
381
383
  return { ok: false, error: `Tool "${name}" is not available in codemode.` };
382
384
  }
383
- const result = await bus.executeTool(name, args, { ctx, signal });
384
- if (result.structuredResult) {
385
- return result.structuredResult.ok
386
- ? { ok: true, value: result.structuredResult.value }
387
- : { ok: false, error: result.structuredResult.error };
388
- }
389
- const text = toolResultText(result);
390
- if (result.isError) {
391
- return { ok: false, error: text };
392
- }
393
- return { ok: true, value: text };
385
+ return bus.executeCodemodeTool(name, args, { ctx, signal });
394
386
  },
395
387
  });
396
388