@trim21/personal-pi-extensions 0.1.691 → 0.1.694
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 +8 -0
- package/package.json +1 -1
- package/src/codemode/tool.ts +18 -6
- package/src/index.ts +3 -0
- package/src/lib/tool-bus.ts +30 -2
- package/src/lib/tool-registration.ts +6 -0
- package/src/lib/tools-config.ts +22 -3
package/README.md
CHANGED
|
@@ -93,6 +93,8 @@
|
|
|
93
93
|
// 工具可用性规则:条目是工具名模式,或 { tools, models }
|
|
94
94
|
"disabledTools": ["talk-*", { "tools": ["web_*"], "models": ["gpt-*"] }],
|
|
95
95
|
"enabledTools": [{ "tools": ["web_search"], "models": ["glm-*"] }],
|
|
96
|
+
// 只进 codemode、不交给模型的工具(须声明结构化输出才会生效)
|
|
97
|
+
"codemodeOnlyTools": [{ "tools": ["read-github-*"], "models": ["deepseek-*"] }],
|
|
96
98
|
},
|
|
97
99
|
}
|
|
98
100
|
```
|
|
@@ -101,6 +103,12 @@
|
|
|
101
103
|
条目只在当前模型命中时参与判定。未配置该 section 时用 `fileIo: "claude-code"` 且不
|
|
102
104
|
禁用任何工具。
|
|
103
105
|
|
|
106
|
+
`codemodeOnlyTools` 把工具从模型可见列表移进 `codemode`:命中它、未被禁用、且声明了
|
|
107
|
+
`structuredSchema` 的工具只登记在工具总线上、不交给 pi,模型只能在 codemode 脚本里用
|
|
108
|
+
`call(...)` 调用它(`codemode` 的工具描述里仍有它的参数与返回类型声明)。没有结构化输出
|
|
109
|
+
的工具进不了 codemode,因此会被忽略、保持直接可用,并在会话启动时给出一条 warning——
|
|
110
|
+
想真正移除工具用 `disabledTools`。
|
|
111
|
+
|
|
104
112
|
**判定时机是每个会话启动**:pi 在启动 / `/new` / `resume` / `/fork` / `/reload` 时重建
|
|
105
113
|
扩展,注册就发生在 `session_start` 里(模型在扩展加载期读不到),因此新会话按自己的
|
|
106
114
|
模型重新判定;**同一会话内 `/model` 切换不会改变工具集合**,换模型请开新会话。
|
package/package.json
CHANGED
package/src/codemode/tool.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 可调用集合:总线上实际注册的、**声明了 `structuredSchema`** 的工具,执行时再与 active
|
|
9
9
|
* 列表求交——pi 自己的 `defaultTools` / `--tools` / 子代理白名单的排除因此同样生效。
|
|
10
|
+
* codemode-only 工具(`codemodeOnlyTools` 命中且声明了 schema,因而没有交给 pi 注册)不在
|
|
11
|
+
* active 列表里是常态,直接进集合、不求交。
|
|
10
12
|
*
|
|
11
13
|
* wasm 在注册这个工具时编译一次(`createCodemodeSandbox`),worker 复用编译结果。
|
|
12
14
|
*/
|
|
@@ -36,11 +38,15 @@ export const CODEMODE_TOOL_NAME = "codemode";
|
|
|
36
38
|
*
|
|
37
39
|
* 脚本拿到的返回值必须有确定的形状——只给文本的工具在脚本里既没法当数据用(得解析文本),
|
|
38
40
|
* 也没有返回类型能写进声明。用「有没有结构化输出」当门槛,这条规则自己会维持一致:不给工具
|
|
39
|
-
* 加 schema
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* 加 schema 就不进集合,不需要维护一份会随工具增减而漂移的黑名单。
|
|
42
|
+
*
|
|
43
|
+
* active 求交只作用于直接工具;命中了 `codemodeOnlyTools` 的工具(声明了 schema、因而没有
|
|
44
|
+
* 交给 pi 注册)不在 active 列表里是常态,直接进集合。
|
|
45
|
+
*
|
|
46
|
+
* 下列工具天然不在集合里,理由各自成立:`codemode` 自身与 `spawn-agent`(没有 schema)、
|
|
47
|
+
* 文件读写工具(脚本用 `fs.read` / `fs.write`,不重复一套为 LLM 上下文设计的行号/锚点
|
|
48
|
+
* 语义)、搜索工具(脚本用 `call("Bash", { command })` 跑 `rg`,退出码可用、能拼管道)、
|
|
49
|
+
* `lsp-rename`(写工具)以及 talk / 会话工具(会把执行时间交给外部输入)。
|
|
44
50
|
*/
|
|
45
51
|
/** 估计 token 用的字符数(与 pi 一致)。 */
|
|
46
52
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -82,7 +88,10 @@ function collectTools(bus: ToolBus, allowed: Set<string> | undefined): CallableT
|
|
|
82
88
|
return bus
|
|
83
89
|
.list()
|
|
84
90
|
.filter((definition) => definition.structuredSchema !== undefined)
|
|
85
|
-
.filter(
|
|
91
|
+
.filter(
|
|
92
|
+
(definition) =>
|
|
93
|
+
definition.codemodeOnly === true || allowed === undefined || allowed.has(definition.name),
|
|
94
|
+
)
|
|
86
95
|
.map((definition) => ({
|
|
87
96
|
name: definition.name,
|
|
88
97
|
description: definition.description,
|
|
@@ -99,6 +108,8 @@ function buildDescription(tools: readonly CallableTool[]): string {
|
|
|
99
108
|
" and a failing tool rejects with a `CallFailedError` you can catch.",
|
|
100
109
|
"- Only what the script passes to `text(value)` / `console.log(...)` and its `return` value enter",
|
|
101
110
|
" this conversation; nested calls and their results stay out of it.",
|
|
111
|
+
"- Pass only necessary content to `text(value)`: print the content the next step needs, not whole",
|
|
112
|
+
" tool results or data you will not use.",
|
|
102
113
|
"- `store.set(key, value)`, `store.get(key)` and `store.list()` are a small key/value store that",
|
|
103
114
|
" persists across codemode calls in this session; `store.set(key, undefined)` removes a key.",
|
|
104
115
|
"- The sandbox has no file system, network, timers or Node APIs: calling tools is its only",
|
|
@@ -263,6 +274,7 @@ export function createCodemodeTools(pi: ExtensionAPI): CodemodeTools {
|
|
|
263
274
|
promptSnippet: "codemode: run JavaScript that calls tools in parallel",
|
|
264
275
|
promptGuidelines: [
|
|
265
276
|
"Use codemode to batch or chain several tool calls, or to filter large tool output down to what matters, instead of issuing many individual tool calls.",
|
|
277
|
+
"Keep codemode output minimal: pass only the content the next step needs to `text(value)`, not whole tool results or data you will not use.",
|
|
266
278
|
],
|
|
267
279
|
parameters: Type.Object({
|
|
268
280
|
code: Type.String({
|
package/src/index.ts
CHANGED
|
@@ -126,6 +126,9 @@ export default function personalExtensions(pi: ExtensionAPI): void {
|
|
|
126
126
|
codemode.register(bus, { policy: services.policy, reads: fileToolset.reads }),
|
|
127
127
|
);
|
|
128
128
|
|
|
129
|
+
// 注册期的可诊断问题(例如 codemode-only 命中但没有结构化输出的工具)与其它警告一起上报。
|
|
130
|
+
warnings.push(...registration.diagnostics());
|
|
131
|
+
|
|
129
132
|
for (const warning of warnings) {
|
|
130
133
|
ctx.ui.notify(warning, "warning");
|
|
131
134
|
}
|
package/src/lib/tool-bus.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* (按名执行,不必绕回模型)。
|
|
4
4
|
*
|
|
5
5
|
* - `register`:按配置过滤(被禁用时跳过并返回 false)后交给 `pi.registerTool`,
|
|
6
|
-
* 并保留通过的定义供 `list` / `get`
|
|
6
|
+
* 并保留通过的定义供 `list` / `get` 查询。命中 codemode-only 且声明了
|
|
7
|
+
* `structuredSchema` 的工具只保留在总线上、不交给 pi(标 `codemodeOnly: true`);
|
|
8
|
+
* 命中但没有 schema 的工具照常注册,并在 `diagnostics` 里留一条说明。
|
|
7
9
|
* - `executeTool`:先用工具自身的参数 schema 校验参数,再调用定义的 `execute`;
|
|
8
10
|
* 参数校验失败与工具抛出的异常都归一化成错误结果返回,不向调用方抛异常。
|
|
9
11
|
*
|
|
@@ -85,9 +87,10 @@ export interface ExecuteToolOptions {
|
|
|
85
87
|
toolCallId?: string;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
/** 注册后的定义:多出的 `structuredSchema` 供 codemode
|
|
90
|
+
/** 注册后的定义:多出的 `structuredSchema` 供 codemode 渲染脚本侧返回类型,`codemodeOnly` 标出未交给 pi 的只读脚本工具。 */
|
|
89
91
|
export type RegisteredToolDefinition = ToolDefinition<TSchema, unknown, unknown> & {
|
|
90
92
|
structuredSchema?: TSchema;
|
|
93
|
+
codemodeOnly?: boolean;
|
|
91
94
|
};
|
|
92
95
|
|
|
93
96
|
export interface ToolBus {
|
|
@@ -99,6 +102,8 @@ export interface ToolBus {
|
|
|
99
102
|
list(): readonly RegisteredToolDefinition[];
|
|
100
103
|
/** 所有尝试注册过的工具名(含被禁用而跳过的),用于校验配置里的模式是否有效。 */
|
|
101
104
|
declaredNames(): readonly string[];
|
|
105
|
+
/** 注册期发现的可诊断问题(去重),由调用方在能提示用户时上报。 */
|
|
106
|
+
diagnostics(): readonly string[];
|
|
102
107
|
get(name: string): RegisteredToolDefinition | undefined;
|
|
103
108
|
/** 按名执行一个已注册工具;任何失败都以 `isError` 结果返回,不抛异常。 */
|
|
104
109
|
executeTool(
|
|
@@ -111,6 +116,8 @@ export interface ToolBus {
|
|
|
111
116
|
export interface ToolBusOptions {
|
|
112
117
|
/** 返回 true 表示该工具被禁用、不注册。 */
|
|
113
118
|
isDisabled?: (name: string) => boolean;
|
|
119
|
+
/** 返回 true 表示该工具应只进 codemode(是否真的如此还要看它有没有声明 `structuredSchema`)。 */
|
|
120
|
+
isCodemodeOnly?: (name: string) => boolean;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
function errorResult(text: string): ToolExecutionResult {
|
|
@@ -180,6 +187,7 @@ function checkStructuredResult(
|
|
|
180
187
|
export function createToolBus(pi: ExtensionAPI, options: ToolBusOptions = {}): ToolBus {
|
|
181
188
|
const registered = new Map<string, RegisteredToolDefinition>();
|
|
182
189
|
const declared = new Set<string>();
|
|
190
|
+
const diagnostics = new Set<string>();
|
|
183
191
|
let callCounter = 0;
|
|
184
192
|
|
|
185
193
|
return {
|
|
@@ -188,6 +196,22 @@ export function createToolBus(pi: ExtensionAPI, options: ToolBusOptions = {}): T
|
|
|
188
196
|
if (options.isDisabled?.(definition.name) === true) {
|
|
189
197
|
return false;
|
|
190
198
|
}
|
|
199
|
+
if (options.isCodemodeOnly?.(definition.name) === true) {
|
|
200
|
+
// schema 门控:进不了 codemode 的工具照常直接注册,只留一条可诊断的说明,
|
|
201
|
+
// 避免配置把工具从模型面前静默移除(见 openspec design.md)。
|
|
202
|
+
if ((definition as { structuredSchema?: unknown }).structuredSchema === undefined) {
|
|
203
|
+
diagnostics.add(
|
|
204
|
+
`personalExtensions.codemodeOnlyTools: tool "${definition.name}" declares no structuredSchema; it stays directly available.`,
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
const codemodeStored = {
|
|
208
|
+
...(definition as unknown as RegisteredToolDefinition),
|
|
209
|
+
codemodeOnly: true,
|
|
210
|
+
};
|
|
211
|
+
registered.set(codemodeStored.name, codemodeStored);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
191
215
|
const stored = definition as unknown as RegisteredToolDefinition;
|
|
192
216
|
pi.registerTool(stored);
|
|
193
217
|
registered.set(stored.name, stored);
|
|
@@ -202,6 +226,10 @@ export function createToolBus(pi: ExtensionAPI, options: ToolBusOptions = {}): T
|
|
|
202
226
|
return [...declared];
|
|
203
227
|
},
|
|
204
228
|
|
|
229
|
+
diagnostics() {
|
|
230
|
+
return [...diagnostics];
|
|
231
|
+
},
|
|
232
|
+
|
|
205
233
|
get(name) {
|
|
206
234
|
return registered.get(name);
|
|
207
235
|
},
|
|
@@ -41,6 +41,8 @@ export interface ToolRegistration {
|
|
|
41
41
|
onSessionStart(register: (bus: ToolBus, ctx: ExtensionContext) => void | Promise<void>): void;
|
|
42
42
|
/** 哪些配置模式没命中过任何工具(在注册完成后调用)。 */
|
|
43
43
|
unmatchedPatterns(): UnmatchedPattern[];
|
|
44
|
+
/** 注册期发现的可诊断问题(去重),与 unmatchedPatterns 一样在注册完成后上报。 */
|
|
45
|
+
diagnostics(): readonly string[];
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -62,6 +64,7 @@ export function createToolRegistration(pi: ExtensionAPI, settingsPath?: string):
|
|
|
62
64
|
// 每次注册都按当前模型重新判定:工具在 session_start 里注册,模型由下面
|
|
63
65
|
// 的 handler 更新,二者在同一个会话内保持一致。
|
|
64
66
|
isDisabled: (name) => resolveToolAvailability(config, model).isDisabled(name),
|
|
67
|
+
isCodemodeOnly: (name) => resolveToolAvailability(config, model).isCodemodeOnly(name),
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -79,5 +82,8 @@ export function createToolRegistration(pi: ExtensionAPI, settingsPath?: string):
|
|
|
79
82
|
unmatchedPatterns() {
|
|
80
83
|
return resolveToolAvailability(config, model).unmatchedPatterns(bus.declaredNames());
|
|
81
84
|
},
|
|
85
|
+
diagnostics() {
|
|
86
|
+
return bus.diagnostics();
|
|
87
|
+
},
|
|
82
88
|
};
|
|
83
89
|
}
|
package/src/lib/tools-config.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* "fileIo": "claude-code",
|
|
8
8
|
* "fileIoByModel": [{ "models": ["glm-*"], "fileIo": "opencode" }],
|
|
9
9
|
* "disabledTools": ["talk-*", { "tools": ["web_*"], "models": ["gpt-*"] }],
|
|
10
|
-
* "enabledTools": [{ "tools": ["web_search"], "models": ["glm-*"] }]
|
|
10
|
+
* "enabledTools": [{ "tools": ["web_search"], "models": ["glm-*"] }],
|
|
11
|
+
* "codemodeOnlyTools": [{ "tools": ["read-github-*"], "models": ["glm-*"] }]
|
|
11
12
|
* }
|
|
12
13
|
* }
|
|
13
14
|
* ```
|
|
@@ -15,6 +16,9 @@
|
|
|
15
16
|
* 工具名与模型名都用 minimatch 通配模式匹配;模型名同时试 `model.id` 与
|
|
16
17
|
* `provider/model` 两种写法。带 `models` 的条目只在当前模型命中时才参与判定。
|
|
17
18
|
* 非法字段/条目被忽略并产出警告,不影响其余配置生效。
|
|
19
|
+
*
|
|
20
|
+
* `codemodeOnlyTools` 只表示曝光方式:命中它的工具由总线决定是否按 codemode-only
|
|
21
|
+
* 注册(还要看工具有没有声明 `structuredSchema`,见 `tool-bus.ts`)。
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
24
|
import { readFileSync } from "node:fs";
|
|
@@ -45,7 +49,7 @@ const toolRuleSchema = Type.Object({
|
|
|
45
49
|
|
|
46
50
|
export type FileIoToolset = "claude-code" | "opencode";
|
|
47
51
|
|
|
48
|
-
export type ToolRuleField = "disabledTools" | "enabledTools";
|
|
52
|
+
export type ToolRuleField = "disabledTools" | "enabledTools" | "codemodeOnlyTools";
|
|
49
53
|
|
|
50
54
|
export interface FileIoByModelEntry {
|
|
51
55
|
models: readonly string[];
|
|
@@ -65,6 +69,8 @@ export interface ToolsConfig {
|
|
|
65
69
|
fileIoByModel: readonly FileIoByModelEntry[];
|
|
66
70
|
disabledTools: readonly ToolPatternRule[];
|
|
67
71
|
enabledTools: readonly ToolPatternRule[];
|
|
72
|
+
/** 命中且声明了 `structuredSchema` 的工具只进 codemode、不交给 pi。 */
|
|
73
|
+
codemodeOnlyTools: readonly ToolPatternRule[];
|
|
68
74
|
/** 解析阶段发现的问题,由调用方在能提示用户时上报。 */
|
|
69
75
|
warnings: readonly string[];
|
|
70
76
|
}
|
|
@@ -82,6 +88,8 @@ export interface UnmatchedPattern {
|
|
|
82
88
|
export interface ToolAvailability {
|
|
83
89
|
/** 该工具名是否被禁用(命中 disabledTools 且未命中 enabledTools)。 */
|
|
84
90
|
isDisabled(name: string): boolean;
|
|
91
|
+
/** 该工具名是否命中 codemodeOnlyTools(是否真的按 codemode-only 注册还要看它有没有结构化输出)。 */
|
|
92
|
+
isCodemodeOnly(name: string): boolean;
|
|
85
93
|
/** 哪些模式没匹配到任何工具(含被规则禁用的工具)。 */
|
|
86
94
|
unmatchedPatterns(declaredNames: readonly string[]): UnmatchedPattern[];
|
|
87
95
|
}
|
|
@@ -165,6 +173,7 @@ export function parseToolsConfig(section: unknown, source = "personalExtensions"
|
|
|
165
173
|
fileIoByModel: [],
|
|
166
174
|
disabledTools: [],
|
|
167
175
|
enabledTools: [],
|
|
176
|
+
codemodeOnlyTools: [],
|
|
168
177
|
warnings,
|
|
169
178
|
};
|
|
170
179
|
}
|
|
@@ -175,6 +184,7 @@ export function parseToolsConfig(section: unknown, source = "personalExtensions"
|
|
|
175
184
|
fileIoByModel: [],
|
|
176
185
|
disabledTools: [],
|
|
177
186
|
enabledTools: [],
|
|
187
|
+
codemodeOnlyTools: [],
|
|
178
188
|
warnings,
|
|
179
189
|
};
|
|
180
190
|
}
|
|
@@ -184,6 +194,11 @@ export function parseToolsConfig(section: unknown, source = "personalExtensions"
|
|
|
184
194
|
fileIoByModel: parseFileIoByModel(section.fileIoByModel, `${source}.fileIoByModel`, warnings),
|
|
185
195
|
disabledTools: parseToolRules(section.disabledTools, `${source}.disabledTools`, warnings),
|
|
186
196
|
enabledTools: parseToolRules(section.enabledTools, `${source}.enabledTools`, warnings),
|
|
197
|
+
codemodeOnlyTools: parseToolRules(
|
|
198
|
+
section.codemodeOnlyTools,
|
|
199
|
+
`${source}.codemodeOnlyTools`,
|
|
200
|
+
warnings,
|
|
201
|
+
),
|
|
187
202
|
warnings,
|
|
188
203
|
};
|
|
189
204
|
}
|
|
@@ -258,9 +273,13 @@ export function resolveToolAvailability(
|
|
|
258
273
|
return config.enabledTools.every((rule) => !ruleMatches(rule, name, model));
|
|
259
274
|
},
|
|
260
275
|
|
|
276
|
+
isCodemodeOnly(name) {
|
|
277
|
+
return config.codemodeOnlyTools.some((rule) => ruleMatches(rule, name, model));
|
|
278
|
+
},
|
|
279
|
+
|
|
261
280
|
unmatchedPatterns(declaredNames) {
|
|
262
281
|
const unmatched: UnmatchedPattern[] = [];
|
|
263
|
-
for (const field of ["disabledTools", "enabledTools"] as const) {
|
|
282
|
+
for (const field of ["disabledTools", "enabledTools", "codemodeOnlyTools"] as const) {
|
|
264
283
|
for (const rule of config[field]) {
|
|
265
284
|
for (const pattern of rule.tools) {
|
|
266
285
|
if (declaredNames.every((name) => !matchesPattern(pattern, name))) {
|