@trim21/personal-pi-extensions 0.1.661 → 0.1.664

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
@@ -43,10 +43,12 @@
43
43
  - **只有脚本输出进上下文**:`text(value)` / `console.log(...)` 与 `return` 值进入工具结果,
44
44
  中间的工具调用与它们的返回内容不会(也不在会话记录里留下工具调用条目)。
45
45
  - **脚本接口**:`tools` / `ALL_TOOLS` / `text` / `image` / `exit` / `console.*` /
46
- `store(key, value)` / `load(key)`;首行可选 `// @options: {"max_output_tokens": 10000}`。
46
+ `store.set` / `store.get` / `store.list`(会话内持久的键值表);首行可选
47
+ `// @options: {"max_output_tokens": 10000}`。
47
48
  脚本没有超时:死循环由调用方中止(Esc)结束,等嵌套调用返回(含用户审批弹窗)多久都不算超时。
48
- - **store** 写在会话的自定义 entry 上(跟随分支),输出超过 `max_output_tokens` 时头尾
49
- 截断并把全文落到 `$TMPDIR/pi-codemode-*.txt`。
49
+ - **store** 记在每次成功调用工具结果的 `details.store` 上(与 `src/lib/file-reads.ts` 的
50
+ 已读记账同一套做法),下一次调用从当前分支的 toolResult 重放;输出超过 `max_output_tokens`
51
+ 时头尾截断并把全文落到 `$TMPDIR/pi-codemode-*.txt`。
50
52
  - **构建**:worker 入口是 esbuild 产物 `src/codemode/worker.js`(随仓库提交,
51
53
  `pnpm run build:codemode-worker` 重新生成,pre-commit 会自动跑);`quickjs-wasi` 的
52
54
  wasm 在注册工具时编译一次,之后每次执行复用。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.661",
3
+ "version": "0.1.664",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -97,7 +97,6 @@ export function renderDeclarations(tools: readonly ToolLike[]): string {
97
97
  "declare function text(value: unknown): void;",
98
98
  "declare function image(value: unknown): void;",
99
99
  "declare function exit(): void;",
100
- "declare function store(key: string, value: unknown): void;",
101
- "declare function load(key: string): unknown;",
100
+ "declare const store: { set(key: string, value: unknown): void; get(key: string): unknown; list(): string[] };",
102
101
  ].join("\n");
103
102
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * codemode 脚本侧的 prelude:在 QuickJS VM 里先于脚本求值,构建脚本能看到的全部
3
- * 能力(`tools` / `ALL_TOOLS` / `text` / `image` / `exit` / `console` / `store` /
4
- * `load`),并把宿主桥接封在闭包里——脚本拿不到 `bridge` 本身。
3
+ * 能力(`tools` / `ALL_TOOLS` / `text` / `image` / `exit` / `console` / `store`),
4
+ * 并把宿主桥接封在闭包里——脚本拿不到 `bridge` 本身。
5
5
  *
6
6
  * 值与参数过桥时都是 JSON 文本,本侧负责 parse/stringify;异步调用用一个 pending
7
7
  * 表把 id 映射到 promise,由宿主在结果到达时 settle。
@@ -13,7 +13,7 @@
13
13
  * - `"done"`(ok, valueJsonOrErrorJson, writesJson)
14
14
  */
15
15
 
16
- /** `store()` 单个值的 JSON 上限(字符数)。 */
16
+ /** `store.set()` 单个值的 JSON 上限(字符数)。 */
17
17
  export const MAX_STORE_VALUE_CHARS = 256 * 1024;
18
18
  /** 全部 store 值的 JSON 上限(字符数)。 */
19
19
  export const MAX_STORE_TOTAL_CHARS = 1024 * 1024;
@@ -118,41 +118,47 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
118
118
  if (typeof key !== "string") throw new TypeErrorCtor(name + "() key must be a string");
119
119
  }
120
120
 
121
- function store(key, value) {
122
- checkKey("store", key);
123
- const previous = stored.has(key) ? key.length + stored.get(key).length : 0;
124
- if (value === undefined) {
125
- stored.delete(key);
126
- storedChars -= previous;
127
- writes.set(key, undefined);
128
- return;
129
- }
130
- let json;
131
- try {
132
- json = stringify(value);
133
- } catch (error) {
134
- throw new TypeErrorCtor("store(" + stringify(key) + ") value is not JSON-serializable: " + format(error));
135
- }
136
- if (json === undefined) {
137
- throw new TypeErrorCtor("store(" + stringify(key) + ") value is not JSON-serializable");
138
- }
139
- if (json.length > ${MAX_STORE_VALUE_CHARS}) {
140
- throw new RangeError("store(" + stringify(key) + ") value exceeds ${MAX_STORE_VALUE_CHARS} characters of JSON");
141
- }
142
- const next = storedChars - previous + key.length + json.length;
143
- if (next > ${MAX_STORE_TOTAL_CHARS}) {
144
- throw new RangeError("store is full: stored values would exceed ${MAX_STORE_TOTAL_CHARS} characters of JSON");
145
- }
146
- stored.set(key, json);
147
- storedChars = next;
148
- writes.set(key, json);
149
- }
150
-
151
- function load(key) {
152
- checkKey("load", key);
153
- const json = stored.get(key);
154
- return json === undefined ? undefined : parse(json);
155
- }
121
+ // 脚本侧是一个键值表:store.set / store.get / store.list,数据本身留在闭包里
122
+ const store = Object.freeze({
123
+ set(key, value) {
124
+ checkKey("store.set", key);
125
+ const previous = stored.has(key) ? key.length + stored.get(key).length : 0;
126
+ if (value === undefined) {
127
+ stored.delete(key);
128
+ storedChars -= previous;
129
+ writes.set(key, undefined);
130
+ return;
131
+ }
132
+ let json;
133
+ try {
134
+ json = stringify(value);
135
+ } catch (error) {
136
+ throw new TypeErrorCtor("store.set(" + stringify(key) + ") value is not JSON-serializable: " + format(error));
137
+ }
138
+ if (json === undefined) {
139
+ throw new TypeErrorCtor("store.set(" + stringify(key) + ") value is not JSON-serializable");
140
+ }
141
+ if (json.length > ${MAX_STORE_VALUE_CHARS}) {
142
+ throw new RangeError("store.set(" + stringify(key) + ") value exceeds ${MAX_STORE_VALUE_CHARS} characters of JSON");
143
+ }
144
+ const next = storedChars - previous + key.length + json.length;
145
+ if (next > ${MAX_STORE_TOTAL_CHARS}) {
146
+ throw new RangeError("store is full: stored values would exceed ${MAX_STORE_TOTAL_CHARS} characters of JSON");
147
+ }
148
+ stored.set(key, json);
149
+ storedChars = next;
150
+ writes.set(key, json);
151
+ },
152
+ get(key) {
153
+ checkKey("store.get", key);
154
+ const json = stored.get(key);
155
+ return json === undefined ? undefined : parse(json);
156
+ },
157
+ // 升序返回当前键:上下文压缩后模型可以靠它找回自己写过的名字
158
+ list() {
159
+ return Array.from(stored.keys()).sort();
160
+ },
161
+ });
156
162
 
157
163
  function serializeWrites() {
158
164
  const entries = [];
@@ -161,7 +167,6 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
161
167
  }
162
168
 
163
169
  Object.defineProperty(globalThis, "store", { value: store, enumerable: true });
164
- Object.defineProperty(globalThis, "load", { value: load, enumerable: true });
165
170
 
166
171
  // 原始值转字符串,其余 JSON 化
167
172
  function outputText(value) {
@@ -26,9 +26,6 @@ import { CODEMODE_SOURCE_GRAMMAR, DEFAULT_OUTPUT_TOKENS, parseCodemodeSource } f
26
26
 
27
27
  export const CODEMODE_TOOL_NAME = "codemode";
28
28
 
29
- /** store 落在 session 的自定义 entry 上:随分支保存与恢复。 */
30
- const STORE_ENTRY_TYPE = "codemode-store";
31
-
32
29
  /** 估计 token 用的字符数(与 pi 一致)。 */
33
30
  const CHARS_PER_TOKEN = 4;
34
31
 
@@ -76,7 +73,8 @@ function buildDescription(tools: readonly CallableTool[]): string {
76
73
  " and a failing tool rejects with an `Error` you can catch.",
77
74
  "- Only what the script passes to `text(value)` / `console.log(...)` and its `return` value enter",
78
75
  " this conversation; nested calls and their results stay out of it.",
79
- "- `store(key, value)` and `load(key)` keep values across codemode calls in this session.",
76
+ "- `store.set(key, value)`, `store.get(key)` and `store.list()` are a small key/value store that",
77
+ " persists across codemode calls in this session; `store.set(key, undefined)` removes a key.",
80
78
  "- The sandbox has no file system, network, timers or Node APIs: calling tools is its only",
81
79
  " capability. Tool calls still go through each tool's own approvals, so a call that needs the",
82
80
  " user's consent will ask for it.",
@@ -89,17 +87,29 @@ function buildDescription(tools: readonly CallableTool[]): string {
89
87
  ].join("\n");
90
88
  }
91
89
 
90
+ /**
91
+ * store 的恢复:把当前分支上每次 codemode 调用记在工具结果 `details.store` 里的写入
92
+ * 按顺序重放(与 `src/lib/file-reads.ts` 从 toolResult 的 details 重放已读同一套做法)。
93
+ * 只认分支上的结果,所以 rewind / fork / resume 后 store 跟着分支走。
94
+ */
92
95
  function readStore(ctx: ExtensionContext): Record<string, unknown> {
93
96
  const store = new Map<string, unknown>();
94
97
  for (const entry of ctx.sessionManager.getBranch()) {
95
- if (entry.type !== "custom" || entry.customType !== STORE_ENTRY_TYPE) {
98
+ if (entry.type !== "message" || entry.message.role !== "toolResult") {
99
+ continue;
100
+ }
101
+ if (entry.message.toolName !== CODEMODE_TOOL_NAME) {
102
+ continue;
103
+ }
104
+ const details = entry.message.details;
105
+ if (typeof details !== "object" || details === null) {
96
106
  continue;
97
107
  }
98
- const data = (entry as { data?: unknown }).data;
99
- if (typeof data !== "object" || data === null) {
108
+ const record = (details as { store?: unknown }).store;
109
+ if (typeof record !== "object" || record === null || Array.isArray(record)) {
100
110
  continue;
101
111
  }
102
- const writes = data as Partial<StoreWrites>;
112
+ const writes = record as Partial<StoreWrites>;
103
113
  for (const key of writes.delete ?? []) {
104
114
  store.delete(key);
105
115
  }
@@ -294,9 +304,7 @@ export function createCodemodeTools(pi: ExtensionAPI): CodemodeTools {
294
304
  const images = imagesOf(outcome.output);
295
305
  if (outcome.ok) {
296
306
  const writes = outcome.writes;
297
- if (Object.keys(writes.set).length > 0 || writes.delete.length > 0) {
298
- pi.appendEntry(STORE_ENTRY_TYPE, writes);
299
- }
307
+ const hasWrites = Object.keys(writes.set).length > 0 || writes.delete.length > 0;
300
308
  const value =
301
309
  outcome.value === undefined ? "" : `\n\n${JSON.stringify(outcome.value, null, 2)}`;
302
310
  const truncated = await truncateOutput(
@@ -315,6 +323,8 @@ export function createCodemodeTools(pi: ExtensionAPI): CodemodeTools {
315
323
  ],
316
324
  details: {
317
325
  calls: outcome.calls,
326
+ // store 的写入随工具结果持久化,下一次调用从这里重放恢复
327
+ ...(hasWrites && { store: writes }),
318
328
  pendant: scriptPendant(code, `${outcome.calls.length} tool call(s)`),
319
329
  ...(truncated.fullOutputPath && { fullOutputPath: truncated.fullOutputPath }),
320
330
  },
@@ -103,41 +103,47 @@ var PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson) {
103
103
  if (typeof key !== "string") throw new TypeErrorCtor(name + "() key must be a string");
104
104
  }
105
105
 
106
- function store(key, value) {
107
- checkKey("store", key);
108
- const previous = stored.has(key) ? key.length + stored.get(key).length : 0;
109
- if (value === undefined) {
110
- stored.delete(key);
111
- storedChars -= previous;
112
- writes.set(key, undefined);
113
- return;
114
- }
115
- let json;
116
- try {
117
- json = stringify(value);
118
- } catch (error) {
119
- throw new TypeErrorCtor("store(" + stringify(key) + ") value is not JSON-serializable: " + format(error));
120
- }
121
- if (json === undefined) {
122
- throw new TypeErrorCtor("store(" + stringify(key) + ") value is not JSON-serializable");
123
- }
124
- if (json.length > ${MAX_STORE_VALUE_CHARS}) {
125
- throw new RangeError("store(" + stringify(key) + ") value exceeds ${MAX_STORE_VALUE_CHARS} characters of JSON");
126
- }
127
- const next = storedChars - previous + key.length + json.length;
128
- if (next > ${MAX_STORE_TOTAL_CHARS}) {
129
- throw new RangeError("store is full: stored values would exceed ${MAX_STORE_TOTAL_CHARS} characters of JSON");
130
- }
131
- stored.set(key, json);
132
- storedChars = next;
133
- writes.set(key, json);
134
- }
135
-
136
- function load(key) {
137
- checkKey("load", key);
138
- const json = stored.get(key);
139
- return json === undefined ? undefined : parse(json);
140
- }
106
+ // 脚本侧是一个键值表:store.set / store.get / store.list,数据本身留在闭包里
107
+ const store = Object.freeze({
108
+ set(key, value) {
109
+ checkKey("store.set", key);
110
+ const previous = stored.has(key) ? key.length + stored.get(key).length : 0;
111
+ if (value === undefined) {
112
+ stored.delete(key);
113
+ storedChars -= previous;
114
+ writes.set(key, undefined);
115
+ return;
116
+ }
117
+ let json;
118
+ try {
119
+ json = stringify(value);
120
+ } catch (error) {
121
+ throw new TypeErrorCtor("store.set(" + stringify(key) + ") value is not JSON-serializable: " + format(error));
122
+ }
123
+ if (json === undefined) {
124
+ throw new TypeErrorCtor("store.set(" + stringify(key) + ") value is not JSON-serializable");
125
+ }
126
+ if (json.length > ${MAX_STORE_VALUE_CHARS}) {
127
+ throw new RangeError("store.set(" + stringify(key) + ") value exceeds ${MAX_STORE_VALUE_CHARS} characters of JSON");
128
+ }
129
+ const next = storedChars - previous + key.length + json.length;
130
+ if (next > ${MAX_STORE_TOTAL_CHARS}) {
131
+ throw new RangeError("store is full: stored values would exceed ${MAX_STORE_TOTAL_CHARS} characters of JSON");
132
+ }
133
+ stored.set(key, json);
134
+ storedChars = next;
135
+ writes.set(key, json);
136
+ },
137
+ get(key) {
138
+ checkKey("store.get", key);
139
+ const json = stored.get(key);
140
+ return json === undefined ? undefined : parse(json);
141
+ },
142
+ // 升序返回当前键:上下文压缩后模型可以靠它找回自己写过的名字
143
+ list() {
144
+ return Array.from(stored.keys()).sort();
145
+ },
146
+ });
141
147
 
142
148
  function serializeWrites() {
143
149
  const entries = [];
@@ -146,7 +152,6 @@ var PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson) {
146
152
  }
147
153
 
148
154
  Object.defineProperty(globalThis, "store", { value: store, enumerable: true });
149
- Object.defineProperty(globalThis, "load", { value: load, enumerable: true });
150
155
 
151
156
  // 原始值转字符串,其余 JSON 化
152
157
  function outputText(value) {