@trim21/personal-pi-extensions 0.1.650 → 0.1.652
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 +67 -9
- package/package.json +5 -13
- package/src/aft/index.ts +40 -23
- package/src/aft/tools.ts +10 -9
- package/src/claude-code/files.ts +99 -54
- package/src/claude-code/glob.ts +8 -5
- package/src/claude-code/grep.ts +8 -5
- package/src/claude-code/index.ts +56 -10
- package/src/claude-code/search.ts +4 -3
- package/src/claude-code/session-tools.ts +4 -3
- package/src/claude-code/shell.ts +3 -8
- package/src/gh/index.ts +48 -25
- package/src/gh/tools/download-release-assets.ts +3 -3
- package/src/gh/tools/get-workflow-jobs.ts +3 -3
- package/src/gh/tools/list-issues.ts +3 -3
- package/src/gh/tools/list-prs.ts +3 -3
- package/src/gh/tools/list-releases.ts +3 -3
- package/src/gh/tools/list-workflow-runs.ts +3 -3
- package/src/gh/tools/read-ci-logs.ts +3 -4
- package/src/gh/tools/read-issue-comments.ts +3 -3
- package/src/gh/tools/read-issue.ts +3 -3
- package/src/gh/tools/read-pr-comments.ts +3 -3
- package/src/gh/tools/read-pr-diff.ts +3 -3
- package/src/gh/tools/read-pr-status.ts +3 -3
- package/src/gh/tools/read-pr.ts +3 -3
- package/src/gh/tools/read-release.ts +3 -3
- package/src/gh/tools/read-repo.ts +3 -3
- package/src/gh/tools/wait-commit-checks.ts +3 -3
- package/src/gh/tools/wait-pr-checks.ts +3 -3
- package/src/gh/tools/watch-run.ts +3 -3
- package/src/index.ts +120 -0
- package/src/lib/lsp/inspect-tool.ts +5 -5
- package/src/lib/lsp/rename-tool.ts +4 -7
- package/src/lib/tool-bus.ts +126 -0
- package/src/lib/tool-registration.ts +83 -0
- package/src/lib/tool-services.ts +65 -0
- package/src/lib/tool-units.ts +102 -0
- package/src/lib/tools-config.ts +277 -0
- package/src/opencode/bash.ts +7 -2
- package/src/opencode/files.ts +90 -43
- package/src/opencode/glob.ts +7 -1
- package/src/opencode/grep.ts +7 -1
- package/src/opencode/index.ts +66 -21
- package/src/opencode/question.ts +8 -2
- package/src/opencode/todo.ts +8 -2
- package/src/orphan-worker-cleaner.ts +38 -0
- package/src/spawn-agent.ts +160 -175
- package/src/talk/index.ts +92 -66
- package/src/vision-agent.ts +31 -7
- package/src/web/fetch.ts +89 -72
- package/src/web/search.ts +14 -2
- package/src/gh-readonly.ts +0 -10
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本仓库工具注册的配置:`~/.pi/agent/settings.json` 的 `personalExtensions` section。
|
|
3
|
+
*
|
|
4
|
+
* ```jsonc
|
|
5
|
+
* {
|
|
6
|
+
* "personalExtensions": {
|
|
7
|
+
* "fileIo": "claude-code",
|
|
8
|
+
* "fileIoByModel": [{ "models": ["glm-*"], "fileIo": "opencode" }],
|
|
9
|
+
* "disabledTools": ["talk-*", { "tools": ["web_*"], "models": ["gpt-*"] }],
|
|
10
|
+
* "enabledTools": [{ "tools": ["web_search"], "models": ["glm-*"] }]
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* 工具名与模型名都用 minimatch 通配模式匹配;模型名同时试 `model.id` 与
|
|
16
|
+
* `provider/model` 两种写法。带 `models` 的条目只在当前模型命中时才参与判定。
|
|
17
|
+
* 非法字段/条目被忽略并产出警告,不影响其余配置生效。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { minimatch } from "minimatch";
|
|
25
|
+
import { Type } from "typebox";
|
|
26
|
+
import { Value } from "typebox/value";
|
|
27
|
+
|
|
28
|
+
// ── schema ───────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const fileIoSchema = Type.Union([Type.Literal("claude-code"), Type.Literal("opencode")]);
|
|
31
|
+
|
|
32
|
+
const fileIoByModelEntrySchema = Type.Object({
|
|
33
|
+
models: Type.Array(Type.String()),
|
|
34
|
+
fileIo: fileIoSchema,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const toolRuleSchema = Type.Object({
|
|
38
|
+
tools: Type.Array(Type.String()),
|
|
39
|
+
models: Type.Optional(Type.Array(Type.String())),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ── 类型 ─────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export type FileIoToolset = "claude-code" | "opencode";
|
|
45
|
+
|
|
46
|
+
export type ToolRuleField = "disabledTools" | "enabledTools";
|
|
47
|
+
|
|
48
|
+
export interface FileIoByModelEntry {
|
|
49
|
+
models: readonly string[];
|
|
50
|
+
fileIo: FileIoToolset;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ToolPatternRule {
|
|
54
|
+
tools: readonly string[];
|
|
55
|
+
/** 空表示对所有模型生效。 */
|
|
56
|
+
models: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ToolsConfig {
|
|
60
|
+
/** 兜底的文件 IO 工具集。 */
|
|
61
|
+
fileIo: FileIoToolset;
|
|
62
|
+
/** 按模型覆盖文件 IO 工具集,按顺序取首条命中。 */
|
|
63
|
+
fileIoByModel: readonly FileIoByModelEntry[];
|
|
64
|
+
disabledTools: readonly ToolPatternRule[];
|
|
65
|
+
enabledTools: readonly ToolPatternRule[];
|
|
66
|
+
/** 解析阶段发现的问题,由调用方在能提示用户时上报。 */
|
|
67
|
+
warnings: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ModelIdentity {
|
|
71
|
+
id: string;
|
|
72
|
+
provider?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface UnmatchedPattern {
|
|
76
|
+
field: ToolRuleField;
|
|
77
|
+
pattern: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ToolAvailability {
|
|
81
|
+
/** 该工具名是否被禁用(命中 disabledTools 且未命中 enabledTools)。 */
|
|
82
|
+
isDisabled(name: string): boolean;
|
|
83
|
+
/** 哪些模式没匹配到任何工具(含被规则禁用的工具)。 */
|
|
84
|
+
unmatchedPatterns(declaredNames: readonly string[]): UnmatchedPattern[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const DEFAULT_FILE_IO: FileIoToolset = "claude-code";
|
|
88
|
+
|
|
89
|
+
// ── 读取与解析 ───────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
92
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseFileIo(value: unknown, field: string, warnings: string[]): FileIoToolset | undefined {
|
|
96
|
+
if (value === undefined) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
if (Value.Check(fileIoSchema, value)) {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
warnings.push(
|
|
103
|
+
`${field}: invalid value ${JSON.stringify(value)} ignored (expected "claude-code" or "opencode")`,
|
|
104
|
+
);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseFileIoByModel(
|
|
109
|
+
value: unknown,
|
|
110
|
+
field: string,
|
|
111
|
+
warnings: string[],
|
|
112
|
+
): FileIoByModelEntry[] {
|
|
113
|
+
if (value === undefined) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
if (!Array.isArray(value)) {
|
|
117
|
+
warnings.push(`${field}: expected an array, ignored`);
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const entries: FileIoByModelEntry[] = [];
|
|
122
|
+
for (const [index, entry] of value.entries()) {
|
|
123
|
+
if (Value.Check(fileIoByModelEntrySchema, entry)) {
|
|
124
|
+
entries.push({ models: [...entry.models], fileIo: entry.fileIo });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
warnings.push(
|
|
128
|
+
`${field}[${index}]: invalid entry ignored (expected { models: string[], fileIo: "claude-code" | "opencode" })`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return entries;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseToolRules(value: unknown, fieldPath: string, warnings: string[]): ToolPatternRule[] {
|
|
135
|
+
if (value === undefined) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(value)) {
|
|
139
|
+
warnings.push(`${fieldPath}: expected an array, ignored`);
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const rules: ToolPatternRule[] = [];
|
|
144
|
+
for (const [index, entry] of value.entries()) {
|
|
145
|
+
if (typeof entry === "string") {
|
|
146
|
+
rules.push({ tools: [entry], models: [] });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (Value.Check(toolRuleSchema, entry)) {
|
|
150
|
+
rules.push({ tools: [...entry.tools], models: entry.models ? [...entry.models] : [] });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
warnings.push(
|
|
154
|
+
`${fieldPath}[${index}]: invalid entry ignored (expected a tool name pattern or { tools, models })`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return rules;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 解析 `personalExtensions` section(纯函数,便于测试)。 */
|
|
161
|
+
export function parseToolsConfig(section: unknown, source = "personalExtensions"): ToolsConfig {
|
|
162
|
+
const warnings: string[] = [];
|
|
163
|
+
|
|
164
|
+
if (section === undefined) {
|
|
165
|
+
return {
|
|
166
|
+
fileIo: DEFAULT_FILE_IO,
|
|
167
|
+
fileIoByModel: [],
|
|
168
|
+
disabledTools: [],
|
|
169
|
+
enabledTools: [],
|
|
170
|
+
warnings,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (!isRecord(section)) {
|
|
174
|
+
warnings.push(`${source}: expected an object, ignored`);
|
|
175
|
+
return {
|
|
176
|
+
fileIo: DEFAULT_FILE_IO,
|
|
177
|
+
fileIoByModel: [],
|
|
178
|
+
disabledTools: [],
|
|
179
|
+
enabledTools: [],
|
|
180
|
+
warnings,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
fileIo: parseFileIo(section.fileIo, `${source}.fileIo`, warnings) ?? DEFAULT_FILE_IO,
|
|
186
|
+
fileIoByModel: parseFileIoByModel(section.fileIoByModel, `${source}.fileIoByModel`, warnings),
|
|
187
|
+
disabledTools: parseToolRules(section.disabledTools, `${source}.disabledTools`, warnings),
|
|
188
|
+
enabledTools: parseToolRules(section.enabledTools, `${source}.enabledTools`, warnings),
|
|
189
|
+
warnings,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 读取全局 settings.json;文件缺失 / 损坏 / 无该 section 时用默认值。 */
|
|
194
|
+
export function readToolsConfig(settingsPath = join(getAgentDir(), "settings.json")): ToolsConfig {
|
|
195
|
+
let section: unknown;
|
|
196
|
+
try {
|
|
197
|
+
const parsed: unknown = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
198
|
+
section = isRecord(parsed) ? parsed.personalExtensions : undefined;
|
|
199
|
+
} catch {
|
|
200
|
+
return parseToolsConfig(undefined);
|
|
201
|
+
}
|
|
202
|
+
return parseToolsConfig(section);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── 判定 ─────────────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
function matchesPattern(pattern: string, value: string): boolean {
|
|
208
|
+
return minimatch(value, pattern);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function modelCandidates(model: ModelIdentity | undefined): string[] {
|
|
212
|
+
if (!model) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
return model.provider ? [model.id, `${model.provider}/${model.id}`] : [model.id];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function matchesModel(patterns: readonly string[], model: ModelIdentity | undefined): boolean {
|
|
219
|
+
if (patterns.length === 0) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
const candidates = modelCandidates(model);
|
|
223
|
+
return patterns.some((pattern) =>
|
|
224
|
+
candidates.some((candidate) => matchesPattern(pattern, candidate)),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function ruleMatches(
|
|
229
|
+
rule: ToolPatternRule,
|
|
230
|
+
name: string,
|
|
231
|
+
model: ModelIdentity | undefined,
|
|
232
|
+
): boolean {
|
|
233
|
+
return (
|
|
234
|
+
rule.tools.some((pattern) => matchesPattern(pattern, name)) && matchesModel(rule.models, model)
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function resolveFileIoToolset(
|
|
239
|
+
config: ToolsConfig,
|
|
240
|
+
model: ModelIdentity | undefined,
|
|
241
|
+
): FileIoToolset {
|
|
242
|
+
for (const entry of config.fileIoByModel) {
|
|
243
|
+
if (matchesModel(entry.models, model)) {
|
|
244
|
+
return entry.fileIo;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return config.fileIo;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function resolveToolAvailability(
|
|
251
|
+
config: ToolsConfig,
|
|
252
|
+
model: ModelIdentity | undefined,
|
|
253
|
+
): ToolAvailability {
|
|
254
|
+
return {
|
|
255
|
+
isDisabled(name) {
|
|
256
|
+
const disabled = config.disabledTools.some((rule) => ruleMatches(rule, name, model));
|
|
257
|
+
if (!disabled) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return config.enabledTools.every((rule) => !ruleMatches(rule, name, model));
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
unmatchedPatterns(declaredNames) {
|
|
264
|
+
const unmatched: UnmatchedPattern[] = [];
|
|
265
|
+
for (const field of ["disabledTools", "enabledTools"] as const) {
|
|
266
|
+
for (const rule of config[field]) {
|
|
267
|
+
for (const pattern of rule.tools) {
|
|
268
|
+
if (declaredNames.every((name) => !matchesPattern(pattern, name))) {
|
|
269
|
+
unmatched.push({ field, pattern });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return unmatched;
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
package/src/opencode/bash.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
} from "../bwrap/runtime.js";
|
|
14
14
|
import { resolveWorkdir } from "../lib/path.js";
|
|
15
15
|
import { createRequestPolicy } from "../lib/request-policy.js";
|
|
16
|
+
import type { ToolBus } from "../lib/tool-bus.js";
|
|
17
|
+
import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
|
|
16
18
|
|
|
17
19
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
18
20
|
const MAX_TIMEOUT_MS = 7_200_000;
|
|
@@ -48,10 +50,13 @@ export default function opencodeBash(
|
|
|
48
50
|
pi: ExtensionAPI,
|
|
49
51
|
runtime: BwrapRuntime = createBwrapRuntime(createRequestPolicy(pi.events)),
|
|
50
52
|
): void {
|
|
53
|
+
registerToolsOnSessionStart(pi, (bus) => registerBashTool(bus, pi, runtime));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registerBashTool(bus: ToolBus, pi: ExtensionAPI, runtime: BwrapRuntime): void {
|
|
51
57
|
// 每个扩展实例持有自己的 runtime:不依赖模块级全局状态,状态随扩展
|
|
52
58
|
// 实例生命周期(进程启动 / /reload / session 切换时工厂重建即重置)。
|
|
53
|
-
|
|
54
|
-
pi.registerTool({
|
|
59
|
+
bus.register({
|
|
55
60
|
name: "bash",
|
|
56
61
|
label: "bash",
|
|
57
62
|
description: [
|
package/src/opencode/files.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { createInterface } from "node:readline";
|
|
|
30
30
|
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
|
31
31
|
import {
|
|
32
32
|
type ExtensionAPI,
|
|
33
|
+
type ExtensionContext,
|
|
33
34
|
generateDiffString,
|
|
34
35
|
generateUnifiedPatch,
|
|
35
36
|
withFileMutationQueue,
|
|
@@ -50,10 +51,17 @@ import {
|
|
|
50
51
|
} from "../lib/file-reads.js";
|
|
51
52
|
import { appendLspDiagnosticText } from "../lib/lsp/diagnostic.js";
|
|
52
53
|
import { registerLspInspectTools } from "../lib/lsp/inspect-tool.js";
|
|
53
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
createLspManager,
|
|
56
|
+
type LspManager,
|
|
57
|
+
type LspService,
|
|
58
|
+
type LspServiceOptions,
|
|
59
|
+
} from "../lib/lsp/lsp.js";
|
|
54
60
|
import { registerLspRenameTool } from "../lib/lsp/rename-tool.js";
|
|
55
61
|
import { formatSubtitlePath } from "../lib/path.js";
|
|
56
62
|
import { createRequestPolicy, type RequestPolicy } from "../lib/request-policy.js";
|
|
63
|
+
import type { ToolBus } from "../lib/tool-bus.js";
|
|
64
|
+
import { createToolRegistration } from "../lib/tool-registration.js";
|
|
57
65
|
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
58
66
|
import { applyEdit, normalizeToLF, stripBom } from "./edit-engine.js";
|
|
59
67
|
|
|
@@ -402,8 +410,8 @@ async function formatDirectoryEntries(dirPath: string): Promise<string[]> {
|
|
|
402
410
|
/** 会更新 reads 记账(src/lib/file-reads.ts)并随 details 持久化快照的工具名。 */
|
|
403
411
|
const READS_TOOL_NAMES = new Set(["read", "edit", "write", "lsp-rename"]);
|
|
404
412
|
|
|
405
|
-
function registerReadTool(
|
|
406
|
-
|
|
413
|
+
function registerReadTool(bus: ToolBus, getService: () => LspService, state: ReadsState): void {
|
|
414
|
+
bus.register({
|
|
407
415
|
name: "read",
|
|
408
416
|
label: "read",
|
|
409
417
|
description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. A negative offset counts from the end of the file (offset=-2 reads the last 2 lines). When you need the full file, continue with offset until complete.`,
|
|
@@ -598,12 +606,12 @@ const editSchema = Type.Object({
|
|
|
598
606
|
});
|
|
599
607
|
|
|
600
608
|
function registerEditTool(
|
|
601
|
-
|
|
609
|
+
bus: ToolBus,
|
|
602
610
|
getService: () => LspService,
|
|
603
611
|
policy: RequestPolicy,
|
|
604
612
|
state: ReadsState,
|
|
605
613
|
): void {
|
|
606
|
-
|
|
614
|
+
bus.register({
|
|
607
615
|
name: "edit",
|
|
608
616
|
label: "edit",
|
|
609
617
|
description:
|
|
@@ -775,12 +783,12 @@ export function resolveBom(
|
|
|
775
783
|
}
|
|
776
784
|
|
|
777
785
|
function registerWriteTool(
|
|
778
|
-
|
|
786
|
+
bus: ToolBus,
|
|
779
787
|
getService: () => LspService,
|
|
780
788
|
policy: RequestPolicy,
|
|
781
789
|
state: ReadsState,
|
|
782
790
|
): void {
|
|
783
|
-
|
|
791
|
+
bus.register({
|
|
784
792
|
name: "write",
|
|
785
793
|
label: "write",
|
|
786
794
|
description:
|
|
@@ -864,64 +872,103 @@ function registerWriteTool(
|
|
|
864
872
|
// ── 入口 ─────────────────────────────────────────────────────────────────────
|
|
865
873
|
|
|
866
874
|
function registerFileTools(
|
|
867
|
-
|
|
875
|
+
bus: ToolBus,
|
|
868
876
|
getService: () => LspService,
|
|
869
877
|
policy: RequestPolicy,
|
|
870
878
|
state: ReadsState,
|
|
871
879
|
): void {
|
|
872
|
-
registerReadTool(
|
|
873
|
-
registerEditTool(
|
|
874
|
-
registerWriteTool(
|
|
880
|
+
registerReadTool(bus, getService, state);
|
|
881
|
+
registerEditTool(bus, getService, policy, state);
|
|
882
|
+
registerWriteTool(bus, getService, policy, state);
|
|
875
883
|
// lsp-rename / inspect 工具由 manager 的 onEnabled 回调注册,不在这里注册。
|
|
876
884
|
}
|
|
877
885
|
|
|
878
886
|
export interface OpencodeFileToolOptions extends LspServiceOptions {
|
|
879
887
|
/** 与 bash runtime 共享的非沙盒请求策略;独立入口不传,自建一份。 */
|
|
880
888
|
policy?: RequestPolicy;
|
|
889
|
+
/** 聚合入口注入的总线(与同入口其他模块共享);独立入口不传,自建一份。 */
|
|
890
|
+
bus?: ToolBus;
|
|
891
|
+
/** 聚合入口注入的共享 LSP manager(与另一套文件工具集共享同一份)。 */
|
|
892
|
+
manager?: LspManager;
|
|
881
893
|
}
|
|
882
894
|
|
|
883
|
-
/**
|
|
884
|
-
export
|
|
895
|
+
/** 一套文件工具集:reads state 归实例所有,LSP 与请求策略可以由入口注入共享。 */
|
|
896
|
+
export interface OpencodeFileToolset {
|
|
897
|
+
register(bus: ToolBus): void;
|
|
898
|
+
onLspEnabled(bus: ToolBus, service: LspService): void;
|
|
899
|
+
restoreReads(ctx: ExtensionContext): void;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* 创建文件工具集。入口在会话启动、知道本会话模型之后再调用 `register`。
|
|
904
|
+
*/
|
|
905
|
+
export function createOpencodeFileTools(
|
|
885
906
|
pi: ExtensionAPI,
|
|
886
907
|
options?: OpencodeFileToolOptions,
|
|
887
|
-
):
|
|
888
|
-
//
|
|
889
|
-
//
|
|
908
|
+
): OpencodeFileToolset {
|
|
909
|
+
// 聚合入口注入与 bash runtime 共享的那一份;独立入口自建并靠 pi.events 跟随
|
|
910
|
+
// 同一开关。
|
|
890
911
|
const policy = options?.policy ?? createRequestPolicy(pi.events);
|
|
891
912
|
const state = createReadsState();
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
913
|
+
// 聚合入口注入自己的总线(与同入口其他模块共享);独立入口自建一份。
|
|
914
|
+
const bus = options?.bus ?? createToolRegistration(pi).bus;
|
|
915
|
+
|
|
916
|
+
const toolset: OpencodeFileToolset = {
|
|
917
|
+
register(currentBus) {
|
|
918
|
+
registerFileTools(currentBus, () => manager.mustLazyGetService(), policy, state);
|
|
919
|
+
},
|
|
920
|
+
|
|
921
|
+
onLspEnabled(currentBus, service) {
|
|
922
|
+
registerLspRenameTool(currentBus, service, {
|
|
923
|
+
policy,
|
|
924
|
+
recordReads: (applied) =>
|
|
925
|
+
recordReads(
|
|
926
|
+
state,
|
|
927
|
+
applied.map((fileEdit) => ({
|
|
928
|
+
path: fileEdit.path,
|
|
929
|
+
snapshot: snapshotOf(fileEdit.newText),
|
|
930
|
+
})),
|
|
931
|
+
),
|
|
932
|
+
});
|
|
933
|
+
registerLspInspectTools(currentBus, service);
|
|
934
|
+
},
|
|
935
|
+
|
|
936
|
+
// 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的已读
|
|
937
|
+
// 记账随之丢失,这里从当前分支的历史工具结果恢复;digest 仍是当时的值,文件
|
|
938
|
+
// 若在此期间被外部修改,edit 时的指纹对比照样要求重新 read。
|
|
939
|
+
restoreReads(ctx) {
|
|
940
|
+
restoreReads(state, ctx.sessionManager, READS_TOOL_NAMES);
|
|
909
941
|
},
|
|
910
|
-
|
|
911
|
-
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
const manager =
|
|
945
|
+
options?.manager ??
|
|
946
|
+
createLspManager(
|
|
947
|
+
pi,
|
|
948
|
+
{ onEnabled: (_pi, service) => toolset.onLspEnabled(bus, service) },
|
|
949
|
+
options,
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
return toolset;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** 独立入口:创建文件工具集并在每次会话启动时注册。 */
|
|
956
|
+
export default function opencodeFileTools(
|
|
957
|
+
pi: ExtensionAPI,
|
|
958
|
+
options?: OpencodeFileToolOptions,
|
|
959
|
+
): void {
|
|
960
|
+
const registration = createToolRegistration(pi);
|
|
961
|
+
const toolset = createOpencodeFileTools(pi, { ...options, bus: registration.bus });
|
|
912
962
|
|
|
913
|
-
// 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的已读
|
|
914
|
-
// 记账随之丢失,这里从当前分支的历史工具结果恢复;digest 仍是当时的值,文件
|
|
915
|
-
// 若在此期间被外部修改,edit 时的指纹对比照样要求重新 read。
|
|
916
963
|
pi.on("session_start", (_event, ctx) => {
|
|
917
|
-
restoreReads(
|
|
964
|
+
toolset.restoreReads(ctx);
|
|
918
965
|
});
|
|
919
966
|
// rewind / 树内跳转走 branch(),只发 session_tree:重放当前分支,丢弃被抛弃
|
|
920
967
|
// 分支的记账,避免 state 与当前分支脱节。
|
|
921
968
|
pi.on("session_tree", (_event, ctx) => {
|
|
922
|
-
restoreReads(
|
|
969
|
+
toolset.restoreReads(ctx);
|
|
970
|
+
});
|
|
971
|
+
registration.onSessionStart(() => {
|
|
972
|
+
toolset.register(registration.bus);
|
|
923
973
|
});
|
|
924
|
-
|
|
925
|
-
// 文件工具无条件注册;service 惰性获取,disabled 时为 no-op。
|
|
926
|
-
registerFileTools(pi, () => manager.mustLazyGetService(), policy, state);
|
|
927
974
|
}
|
package/src/opencode/glob.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { Type } from "typebox";
|
|
20
20
|
|
|
21
|
+
import type { ToolBus } from "../lib/tool-bus.js";
|
|
22
|
+
import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
|
|
21
23
|
import { didYouMean } from "./files.js";
|
|
22
24
|
import { RIPGREP_RESULT_LIMIT, runRipgrep } from "./ripgrep.js";
|
|
23
25
|
|
|
@@ -82,7 +84,11 @@ async function glob(
|
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
export default function opencodeGlob(pi: ExtensionAPI): void {
|
|
85
|
-
pi
|
|
87
|
+
registerToolsOnSessionStart(pi, registerGlobTool);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function registerGlobTool(bus: ToolBus): void {
|
|
91
|
+
bus.register({
|
|
86
92
|
name: "glob",
|
|
87
93
|
label: "glob",
|
|
88
94
|
description: [
|
package/src/opencode/grep.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { Type } from "typebox";
|
|
|
21
21
|
|
|
22
22
|
import { isRecord } from "../lib/narrow.js";
|
|
23
23
|
import { parseWithSchema } from "../lib/parse-with-schema.js";
|
|
24
|
+
import type { ToolBus } from "../lib/tool-bus.js";
|
|
25
|
+
import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
|
|
24
26
|
import { didYouMean } from "./files.js";
|
|
25
27
|
import { RIPGREP_RESULT_LIMIT, runRipgrep } from "./ripgrep.js";
|
|
26
28
|
|
|
@@ -178,7 +180,11 @@ async function grep(
|
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
export default function opencodeGrep(pi: ExtensionAPI): void {
|
|
181
|
-
pi
|
|
183
|
+
registerToolsOnSessionStart(pi, registerGrepTool);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function registerGrepTool(bus: ToolBus): void {
|
|
187
|
+
bus.register({
|
|
182
188
|
name: "grep",
|
|
183
189
|
label: "grep",
|
|
184
190
|
description: [
|
package/src/opencode/index.ts
CHANGED
|
@@ -11,25 +11,70 @@
|
|
|
11
11
|
* allowlist 只暴露声明的子集(与 claude-code 三件套映射同一文件一致)。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
|
|
16
|
-
import { createBwrapRuntime } from "../bwrap/runtime.js";
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
|
|
16
|
+
import { type BwrapRuntime, createBwrapRuntime } from "../bwrap/runtime.js";
|
|
17
|
+
import type { LspManager } from "../lib/lsp/lsp.js";
|
|
18
|
+
import { createRequestPolicy, type RequestPolicy } from "../lib/request-policy.js";
|
|
19
|
+
import type { ToolBus } from "../lib/tool-bus.js";
|
|
20
|
+
import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
|
|
21
|
+
import { registerBashTool } from "./bash.js";
|
|
22
|
+
import { createOpencodeFileTools } from "./files.js";
|
|
23
|
+
import { registerGlobTool } from "./glob.js";
|
|
24
|
+
import { registerGrepTool } from "./grep.js";
|
|
25
|
+
import { registerQuestionTool } from "./question.js";
|
|
26
|
+
import { registerTodoTool } from "./todo.js";
|
|
27
|
+
|
|
28
|
+
export interface OpencodeToolsetOptions {
|
|
29
|
+
bus?: ToolBus;
|
|
30
|
+
policy?: RequestPolicy;
|
|
31
|
+
runtime?: BwrapRuntime;
|
|
32
|
+
manager?: LspManager;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** opencode 风格工具集的共享部件:入口与独立默认导出都从这里取。 */
|
|
36
|
+
export function createOpencodeTools(pi: ExtensionAPI, options?: OpencodeToolsetOptions) {
|
|
37
|
+
// 非沙盒请求策略由本工具集创建,注入文件工具与 bash runtime 共享同一份。
|
|
38
|
+
const policy = options?.policy ?? createRequestPolicy(pi.events);
|
|
39
|
+
const runtime = options?.runtime ?? createBwrapRuntime(policy);
|
|
40
|
+
// 独立入口(不经 src/index.ts 的共享服务)要自己接上 bwrap 的会话钩子与 /bwrap* 命令。
|
|
41
|
+
if (options?.runtime === undefined) {
|
|
42
|
+
runtime.setup(pi);
|
|
43
|
+
}
|
|
44
|
+
const fileTools = createOpencodeFileTools(pi, {
|
|
45
|
+
policy,
|
|
46
|
+
bus: options?.bus,
|
|
47
|
+
manager: options?.manager,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
fileTools,
|
|
52
|
+
/** 会话启动 / 分支切换时恢复已读记账。 */
|
|
53
|
+
restoreReads(ctx: ExtensionContext): void {
|
|
54
|
+
fileTools.restoreReads(ctx);
|
|
55
|
+
},
|
|
56
|
+
/** 把本工具集的工具注册到总线。 */
|
|
57
|
+
register(bus: ToolBus): void {
|
|
58
|
+
fileTools.register(bus);
|
|
59
|
+
registerGrepTool(bus);
|
|
60
|
+
registerGlobTool(bus);
|
|
61
|
+
registerTodoTool(bus);
|
|
62
|
+
registerQuestionTool(bus);
|
|
63
|
+
registerBashTool(bus, pi, runtime);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default function opencode(pi: ExtensionAPI, options?: { bus?: ToolBus }): void {
|
|
69
|
+
const tools = createOpencodeTools(pi, { bus: options?.bus });
|
|
70
|
+
|
|
71
|
+
pi.on("session_start", (_event, ctx) => tools.restoreReads(ctx));
|
|
72
|
+
// rewind / 树内跳转只发 session_tree 不发 session_start,同样要重放当前分支。
|
|
73
|
+
pi.on("session_tree", (_event, ctx) => tools.restoreReads(ctx));
|
|
74
|
+
|
|
75
|
+
// 全部工具在同一次 session_start 里注册:禁用规则可以带 models,只有那时
|
|
76
|
+
// 才知道本会话的模型。
|
|
77
|
+
registerToolsOnSessionStart(pi, (bus) => {
|
|
78
|
+
tools.register(bus);
|
|
79
|
+
});
|
|
35
80
|
}
|
package/src/opencode/question.ts
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
25
25
|
import { type Static, Type } from "typebox";
|
|
26
26
|
|
|
27
|
+
import type { ToolBus } from "../lib/tool-bus.js";
|
|
28
|
+
import { registerToolsOnSessionStart } from "../lib/tool-registration.js";
|
|
27
29
|
import { selectMultiple, selectWithOptionalInput } from "../lib/ui.js";
|
|
28
30
|
|
|
29
31
|
// ── constants ────────────────────────────────────────────────────────────────
|
|
@@ -145,8 +147,12 @@ async function askMultiple(
|
|
|
145
147
|
|
|
146
148
|
// ── extension ────────────────────────────────────────────────────────────────
|
|
147
149
|
|
|
148
|
-
export default function question(pi: ExtensionAPI) {
|
|
149
|
-
pi
|
|
150
|
+
export default function question(pi: ExtensionAPI): void {
|
|
151
|
+
registerToolsOnSessionStart(pi, registerQuestionTool);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function registerQuestionTool(bus: ToolBus): void {
|
|
155
|
+
bus.register<typeof questionParamsSchema, QuestionDetails>({
|
|
150
156
|
name: TOOL_NAME,
|
|
151
157
|
label: "Question",
|
|
152
158
|
description: QUESTION_DESCRIPTION,
|