@trim21/personal-pi-extensions 0.1.662 → 0.1.668

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,7 +43,8 @@
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
49
  - **store** 记在每次成功调用工具结果的 `details.store` 上(与 `src/lib/file-reads.ts` 的
49
50
  已读记账同一套做法),下一次调用从当前分支的 toolResult 重放;输出超过 `max_output_tokens`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.662",
3
+ "version": "0.1.668",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
package/src/aft/index.ts CHANGED
@@ -8,6 +8,11 @@
8
8
  * openai_compatible / ollama 且有 base_url)时注册;aft 默认的本地 ONNX
9
9
  * fastembed 后端不使用。
10
10
  *
11
+ * 项目根是**会话工作目录**(`session_start` 的 `ctx.cwd`),不是 pi 进程的启动
12
+ * 目录:引擎按项目根建索引与调用图存储,用错会把用户家目录当项目根并自动关闭这
13
+ * 些能力。扩展自己只读用户级 aft.jsonc(路径与会话目录无关),项目级 aft.jsonc
14
+ * 由 bridge 按项目根自行读取。
15
+ *
11
16
  * bridge 状态(日志 + 常驻 aft 子进程)的生命周期跟 session 走:session_start
12
17
  * 时先解析 aft 二进制(含 GitHub release auto-download 兜底)——找不到就
13
18
  * notify warning 且不注册任何 aft 工具,避免模型看到只会抛 "not initialized"
@@ -19,7 +24,7 @@
19
24
  * pi -e ./aft/index.ts
20
25
  */
21
26
 
22
- import { resolveCortexKitConfigPaths } from "@cortexkit/aft-bridge";
27
+ import { resolveCortexKitUserConfigPath } from "@cortexkit/aft-bridge";
23
28
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
24
29
 
25
30
  import type { ToolBus } from "../lib/tool-bus.js";
@@ -27,6 +32,7 @@ import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
27
32
  import { createAftState, findBinary, resolveSessionId, shutdownAftPool } from "./bridge.js";
28
33
  import { loadAftConfig } from "./config.js";
29
34
  import {
35
+ type AftToolContext,
30
36
  registerCallgraphTool,
31
37
  registerOutlineTool,
32
38
  registerSearchTool,
@@ -35,14 +41,15 @@ import {
35
41
 
36
42
  /**
37
43
  * aft 工具集:入口用它。`registerForSession` 必须在会话启动时调用——二进制
38
- * 探测、semantic_search 的提示、bridge 状态的创建都在里面;找不到二进制就不
39
- * 注册任何工具(避免模型看到只会抛 "not initialized" 的死工具)。
44
+ * 探测、semantic_search 的提示、bridge 状态(项目根 = 本会话的 `ctx.cwd`)的
45
+ * 创建都在里面;找不到二进制就不注册任何工具(避免模型看到只会抛
46
+ * "not initialized" 的死工具)。
40
47
  */
41
48
  export function createAftTools(pi: ExtensionAPI): {
42
49
  registerForSession(bus: ToolBus, ctx: ExtensionContext): Promise<void>;
43
50
  } {
44
- const cwd = process.cwd();
45
- const cfg = loadAftConfig(resolveCortexKitConfigPaths(cwd).userConfigPath);
51
+ // 扩展自己的门控只读用户级配置:项目级 aft.jsonc 由 bridge 按会话工作目录读。
52
+ const cfg = loadAftConfig(resolveCortexKitUserConfigPath());
46
53
  // bridge 状态跟 session 生命周期走,作用域就是本工厂闭包,不落到模块级。
47
54
  let state: Awaited<ReturnType<typeof createAftState>> | null = null;
48
55
 
@@ -55,7 +62,7 @@ export function createAftTools(pi: ExtensionAPI): {
55
62
  return state;
56
63
  };
57
64
 
58
- const toolCtx = { cwd, getState };
65
+ const toolCtx: AftToolContext = { getState };
59
66
 
60
67
  async function registerForSession(bus: ToolBus, ctx: ExtensionContext): Promise<void> {
61
68
  if (!cfg.enabled) {
@@ -91,7 +98,7 @@ export function createAftTools(pi: ExtensionAPI): {
91
98
  registerSearchTool(bus, toolCtx);
92
99
  }
93
100
 
94
- state = await createAftState(cwd, resolveSessionId(ctx), binaryPath, cfg.semanticRemote);
101
+ state = await createAftState(ctx.cwd, resolveSessionId(ctx), binaryPath, cfg.semanticRemote);
95
102
  }
96
103
 
97
104
  // 释放当前 session 的 bridge 状态。session_shutdown 是 pi 的正常生命周期;
package/src/aft/tools.ts CHANGED
@@ -57,13 +57,13 @@ const AFT_GUIDELINES = readFileSync(
57
57
  ).trim();
58
58
 
59
59
  export interface AftToolContext {
60
- cwd: string;
61
60
  /** 当前 session 的 bridge 状态;session 未初始化时抛错。 */
62
61
  getState(): AftState;
63
62
  }
64
63
 
65
64
  function bridgeFor(ctx: AftToolContext): AftProjectTransport {
66
- return ctx.getState().pool.pool.getBridge(ctx.cwd);
65
+ const { pool } = ctx.getState();
66
+ return pool.pool.getBridge(pool.projectRoot);
67
67
  }
68
68
 
69
69
  /**
@@ -152,7 +152,7 @@ export function registerOutlineTool(bus: ToolBus, ctx: AftToolContext): void {
152
152
  filesMode = stats?.isDirectory() ?? false;
153
153
  }
154
154
  const rawArgs = compactArgs({
155
- target: filesMode ? target : resolved,
155
+ target: resolved,
156
156
  files: filesMode || undefined,
157
157
  includeTests: params.includeTests,
158
158
  });
@@ -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) {
@@ -3,8 +3,9 @@
3
3
  * 的能力是调用 `tools.*`——每个嵌套调用都由主线程经本仓库的工具总线执行,因此工具实现
4
4
  * 内部的审批(工作区外写入、Bash 沙箱提权等)照常生效;codemode 不再加自己的确认层。
5
5
  *
6
- * 可调用集合:总线上实际注册的工具减去 codemode 自身,执行时再与 active 列表求交——
7
- * pi 自己的 `defaultTools` / `--tools` / 子代理白名单的排除因此同样生效。
6
+ * 可调用集合:总线上实际注册的工具减去 codemode 自身与 spawn-agent,执行时再与 active
7
+ * 列表求交——pi 自己的 `defaultTools` / `--tools` / 子代理白名单的排除因此同样生效。
8
+ * spawn-agent 被排除是因为它启动一个新的隔离会话、成本与运行时长都不适合放进脚本编排。
8
9
  *
9
10
  * wasm 在注册这个工具时编译一次(`createCodemodeSandbox`),worker 复用编译结果。
10
11
  */
@@ -26,6 +27,9 @@ import { CODEMODE_SOURCE_GRAMMAR, DEFAULT_OUTPUT_TOKENS, parseCodemodeSource } f
26
27
 
27
28
  export const CODEMODE_TOOL_NAME = "codemode";
28
29
 
30
+ /** 不暴露给脚本的工具:codemode 自身(防递归)与 spawn-agent(见文件头注释)。 */
31
+ const EXCLUDED_TOOL_NAMES: ReadonlySet<string> = new Set([CODEMODE_TOOL_NAME, "spawn-agent"]);
32
+
29
33
  /** 估计 token 用的字符数(与 pi 一致)。 */
30
34
  const CHARS_PER_TOKEN = 4;
31
35
 
@@ -56,7 +60,7 @@ function allowedToolNames(pi: ExtensionAPI): Set<string> | undefined {
56
60
  function collectTools(bus: ToolBus, allowed: Set<string> | undefined): CallableTool[] {
57
61
  return bus
58
62
  .list()
59
- .filter((definition) => definition.name !== CODEMODE_TOOL_NAME)
63
+ .filter((definition) => !EXCLUDED_TOOL_NAMES.has(definition.name))
60
64
  .filter((definition) => allowed === undefined || allowed.has(definition.name))
61
65
  .map((definition) => ({
62
66
  name: definition.name,
@@ -73,7 +77,8 @@ function buildDescription(tools: readonly CallableTool[]): string {
73
77
  " and a failing tool rejects with an `Error` you can catch.",
74
78
  "- Only what the script passes to `text(value)` / `console.log(...)` and its `return` value enter",
75
79
  " this conversation; nested calls and their results stay out of it.",
76
- "- `store(key, value)` and `load(key)` keep values across codemode calls in this session.",
80
+ "- `store.set(key, value)`, `store.get(key)` and `store.list()` are a small key/value store that",
81
+ " persists across codemode calls in this session; `store.set(key, undefined)` removes a key.",
77
82
  "- The sandbox has no file system, network, timers or Node APIs: calling tools is its only",
78
83
  " capability. Tool calls still go through each tool's own approvals, so a call that needs the",
79
84
  " user's consent will ask for it.",
@@ -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) {
@@ -131,7 +131,10 @@ const spawnAgentSchema = Type.Object({
131
131
  description:
132
132
  "Name of the subagent type to invoke. Choose one of the available subagent types listed in your system prompt.",
133
133
  }),
134
- task: Type.String({ description: "Task to delegate to the subagent" }),
134
+ task: Type.String({
135
+ description:
136
+ "Self-contained task for the subagent, which sees none of this conversation: state the repository path, the exact question, and what the answer must contain (file paths with line numbers).",
137
+ }),
135
138
  });
136
139
 
137
140
  // ── result types ─────────────────────────────────────────────────────────────
@@ -499,9 +502,19 @@ export async function runAgent(
499
502
  export function formatAgentListSection(agents: AgentConfig[]): string {
500
503
  const lines = agents.map((a) => `- \`${a.name}\`: ${a.description}`);
501
504
  return [
505
+ "### Delegating to subagents",
506
+ "",
507
+ "Delegate a focused, read-only research task when the answer is a summary rather than source you must quote or edit:",
508
+ "",
509
+ "- locating code whose position is unknown, or answering a question that spans several files;",
510
+ "- several independent lookups at once — issue one `spawn-agent` call per lookup in the same message and they run concurrently;",
511
+ "- keeping the searching and reading out of your own context window.",
512
+ "",
513
+ "Do not delegate work you are about to do yourself — a single file you will edit, a change whose evidence must be the exact source text, or anything that writes. A subagent sees none of this conversation, so its task must be self-contained. Treat its answer as a pointer and read the location it names before acting on it.",
514
+ "",
502
515
  "### Available subagents",
503
516
  "",
504
- "You can delegate tasks to the following subagent types by calling the `spawn-agent` tool with their name in the `agent` parameter:",
517
+ "Pass one of these names as the `agent` parameter:",
505
518
  "",
506
519
  ...lines,
507
520
  ].join("\n");
@@ -526,9 +539,11 @@ export function createSpawnAgentTool(): { register(bus: ToolBus): void } {
526
539
  bus.register<typeof spawnAgentSchema, SubagentDetails>({
527
540
  name: "spawn-agent",
528
541
  label: "spawn-agent",
542
+ promptSnippet: "Delegate focused research to a subagent with its own context window",
529
543
  description: [
530
- "Delegate a task to a subagent that runs in an isolated session with its own context window, inside this pi process rather than a separate one.",
531
- "The call blocks until the subagent finishes its turn; its final output comes back as the tool result.",
544
+ "Delegate a self-contained research task to a subagent that runs in an isolated session with its own context window, inside this pi process rather than a separate one.",
545
+ "The call blocks until the subagent finishes its turn and returns only its final answer: its intermediate tool calls are not visible to you.",
546
+ "Several `spawn-agent` calls issued in the same message run concurrently.",
532
547
  "The `agent` parameter must be one of the available subagent types listed in the system prompt.",
533
548
  `Subagents run read-only (${DEFAULT_TOOLS.join(", ")}) unless the agent declares an explicit toolset.`,
534
549
  ].join(" "),