@trim21/personal-pi-extensions 0.1.611 → 0.1.613
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/package.json +2 -2
- package/src/aft/config.ts +9 -3
- package/src/aft/index.ts +6 -2
- package/src/aft/logger.ts +9 -3
- package/src/aft/tools.ts +5 -3
- package/src/bwrap/approval-rules.ts +51 -18
- package/src/bwrap/approval-suggest.ts +12 -4
- package/src/bwrap/core.ts +39 -13
- package/src/bwrap/dcg-scan.ts +16 -6
- package/src/bwrap/holder.ts +3 -1
- package/src/bwrap/mihomo-config.ts +18 -6
- package/src/bwrap/network-stack.ts +30 -10
- package/src/bwrap/runtime.ts +36 -14
- package/src/claude-code/common.ts +6 -2
- package/src/claude-code/edit-utils.ts +3 -1
- package/src/claude-code/files.ts +40 -14
- package/src/claude-code/glob.ts +12 -4
- package/src/claude-code/grep.ts +41 -14
- package/src/claude-code/session-tools.ts +18 -6
- package/src/claude-code/shell.ts +9 -3
- package/src/gh/base.ts +72 -24
- package/src/gh/tools/download-release-assets.ts +12 -4
- package/src/gh/tools/list-releases.ts +3 -1
- package/src/gh/tools/list-workflow-runs.ts +9 -3
- package/src/gh/tools/read-ci-logs.ts +21 -7
- package/src/gh/tools/read-repo.ts +3 -1
- package/src/lib/cli-args.ts +6 -2
- package/src/lib/cli.ts +55 -22
- package/src/lib/file-reads.ts +24 -8
- package/src/lib/github.ts +35 -12
- package/src/lib/lsp/adapter.ts +6 -2
- package/src/lib/lsp/bin.ts +12 -4
- package/src/lib/lsp/client.ts +205 -69
- package/src/lib/lsp/diagnostic.ts +6 -2
- package/src/lib/lsp/inspect-tool.ts +25 -9
- package/src/lib/lsp/lsp.ts +154 -58
- package/src/lib/lsp/rename-tool.ts +11 -4
- package/src/lib/lsp/rename.ts +25 -8
- package/src/lib/lsp/server-config.ts +41 -14
- package/src/lib/lsp/watcher.ts +51 -17
- package/src/lib/path.ts +18 -6
- package/src/lib/proc.ts +3 -1
- package/src/lib/proxy.ts +9 -3
- package/src/lib/request-policy.ts +3 -1
- package/src/lib/seq-state.ts +3 -1
- package/src/lib/ui.ts +23 -8
- package/src/lib/write-guard.ts +21 -7
- package/src/openai-cost/config.ts +3 -1
- package/src/openai-cost/cost.ts +54 -18
- package/src/openai-cost/index.ts +3 -1
- package/src/openai-cost/provider.ts +6 -2
- package/src/opencode/bash.ts +9 -3
- package/src/opencode/edit-engine.ts +63 -41
- package/src/opencode/files.ts +30 -10
- package/src/opencode/glob.ts +3 -1
- package/src/opencode/grep.ts +12 -4
- package/src/opencode/question.ts +3 -1
- package/src/opencode/ripgrep.ts +23 -9
- package/src/opencode/todo.ts +3 -1
- package/src/session-name.ts +40 -16
- package/src/spawn-agent-agents.ts +18 -6
- package/src/spawn-agent.ts +58 -21
- package/src/system-prompt/index.ts +9 -3
- package/src/talk/core.ts +143 -48
- package/src/talk/format.ts +12 -4
- package/src/talk/group.ts +9 -3
- package/src/talk/index.ts +30 -10
- package/src/talk/mailbox.ts +30 -10
- package/src/talk/policy.ts +10 -5
- package/src/talk/registry.ts +35 -12
- package/src/talk/storage.ts +3 -1
- package/src/vision-agent.ts +41 -14
- package/src/web/config.ts +3 -1
- package/src/web/fetch.ts +75 -25
- package/src/web/search.ts +30 -10
|
@@ -88,7 +88,9 @@ export function mergeServerRecords(
|
|
|
88
88
|
): Record<string, ServerConfig> | undefined {
|
|
89
89
|
const merged: Record<string, ServerConfig> = {};
|
|
90
90
|
for (const record of records) {
|
|
91
|
-
if (!record)
|
|
91
|
+
if (!record) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
92
94
|
Object.assign(merged, record);
|
|
93
95
|
}
|
|
94
96
|
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
@@ -118,8 +120,12 @@ function interpolateEnvVars(value: string, env: NodeJS.ProcessEnv): string {
|
|
|
118
120
|
|
|
119
121
|
/** 深遍历配置值,对所有字符串做环境变量插值。 */
|
|
120
122
|
function interpolateEnvDeep(value: unknown, env: NodeJS.ProcessEnv): unknown {
|
|
121
|
-
if (typeof value === "string")
|
|
122
|
-
|
|
123
|
+
if (typeof value === "string") {
|
|
124
|
+
return interpolateEnvVars(value, env);
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
return value.map((item) => interpolateEnvDeep(item, env));
|
|
128
|
+
}
|
|
123
129
|
if (value !== null && typeof value === "object") {
|
|
124
130
|
return Object.fromEntries(
|
|
125
131
|
Object.entries(value).map(([key, item]) => [key, interpolateEnvDeep(item, env)]),
|
|
@@ -135,7 +141,9 @@ async function resolveEnv(
|
|
|
135
141
|
cwd: string,
|
|
136
142
|
): Promise<NodeJS.ProcessEnv | undefined> {
|
|
137
143
|
const entries = Object.entries(config ?? {});
|
|
138
|
-
if (entries.length === 0)
|
|
144
|
+
if (entries.length === 0) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
139
147
|
const resolved = await Promise.all(
|
|
140
148
|
entries.map(async ([key, value]): Promise<[string, string]> => {
|
|
141
149
|
if (typeof value === "string") {
|
|
@@ -226,8 +234,12 @@ function parseInitializationOptionsOutput(
|
|
|
226
234
|
|
|
227
235
|
/** 非法输出的类型描述(typebox 的 ParseError 只有 "Parse",自己给出可读文案)。 */
|
|
228
236
|
function describeValue(value: unknown): string {
|
|
229
|
-
if (value === null)
|
|
230
|
-
|
|
237
|
+
if (value === null) {
|
|
238
|
+
return "null";
|
|
239
|
+
}
|
|
240
|
+
if (Array.isArray(value)) {
|
|
241
|
+
return "array";
|
|
242
|
+
}
|
|
231
243
|
return typeof value;
|
|
232
244
|
}
|
|
233
245
|
|
|
@@ -246,7 +258,9 @@ async function resolveInitializationOptions(
|
|
|
246
258
|
const staticOptions = interpolateEnvDeep(config.initializationOptions, variables) as
|
|
247
259
|
Record<string, unknown> | undefined;
|
|
248
260
|
const command = config.initializationOptionsCommand;
|
|
249
|
-
if (!command)
|
|
261
|
+
if (!command) {
|
|
262
|
+
return staticOptions;
|
|
263
|
+
}
|
|
250
264
|
const argv = command.map((arg) => interpolateEnvVars(resolveTemplate(arg, root, cwd), variables));
|
|
251
265
|
const output = await runConfigCommand(argv, {
|
|
252
266
|
cwd: root,
|
|
@@ -258,7 +272,9 @@ async function resolveInitializationOptions(
|
|
|
258
272
|
|
|
259
273
|
/** 解析可执行文件:绝对/相对路径直接用;名字走项目工作区(node_modules/.bin 等)→ PATH。 */
|
|
260
274
|
async function resolveBinary(bin: string, root: string, cwd: string): Promise<string | undefined> {
|
|
261
|
-
if (isAbsolute(bin))
|
|
275
|
+
if (isAbsolute(bin)) {
|
|
276
|
+
return existsSync(bin) ? bin : undefined;
|
|
277
|
+
}
|
|
262
278
|
if (bin.includes("/") || bin.includes("\\")) {
|
|
263
279
|
const relativePath = join(cwd, bin);
|
|
264
280
|
return exists(relativePath) ? relativePath : undefined;
|
|
@@ -277,14 +293,21 @@ export function matchesInclude(
|
|
|
277
293
|
root: string,
|
|
278
294
|
cwd: string,
|
|
279
295
|
): boolean {
|
|
280
|
-
if (patterns.length === 0)
|
|
296
|
+
if (patterns.length === 0) {
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
281
299
|
const positives: string[] = [];
|
|
282
300
|
const negatives: string[] = [];
|
|
283
301
|
for (const pattern of patterns) {
|
|
284
|
-
if (pattern.startsWith("!"))
|
|
285
|
-
|
|
302
|
+
if (pattern.startsWith("!")) {
|
|
303
|
+
negatives.push(pattern.slice(1));
|
|
304
|
+
} else {
|
|
305
|
+
positives.push(pattern);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (positives.length === 0) {
|
|
309
|
+
positives.push("**");
|
|
286
310
|
}
|
|
287
|
-
if (positives.length === 0) positives.push("**");
|
|
288
311
|
const candidates = [relative(root, file), relative(cwd, file)]
|
|
289
312
|
.map((p) => p.split(sep).join("/"))
|
|
290
313
|
.filter((p) => !p.startsWith(".."));
|
|
@@ -319,9 +342,13 @@ export class ConfigAdapter implements LspServerAdapter {
|
|
|
319
342
|
|
|
320
343
|
async spawn(root: string, cwd: string): Promise<LspServerHandle | undefined> {
|
|
321
344
|
const bin = this.config.bin;
|
|
322
|
-
if (!bin)
|
|
345
|
+
if (!bin) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
323
348
|
const resolved = await resolveBinary(bin, root, cwd);
|
|
324
|
-
if (!resolved)
|
|
349
|
+
if (!resolved) {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
325
352
|
const env = await resolveEnv(this.config.env, root, cwd);
|
|
326
353
|
const initialization = await resolveInitializationOptions(this.config, root, cwd, env);
|
|
327
354
|
return {
|
package/src/lib/lsp/watcher.ts
CHANGED
|
@@ -68,7 +68,9 @@ export interface WorkspaceWatcher {
|
|
|
68
68
|
|
|
69
69
|
function isIgnored(path: string, dir: string, patterns: string[]): boolean {
|
|
70
70
|
const candidate = relative(dir, path).split(sep).join("/");
|
|
71
|
-
if (candidate.startsWith(".."))
|
|
71
|
+
if (candidate.startsWith("..")) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
72
74
|
return patterns.some((pattern) => minimatch(candidate, pattern));
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -98,15 +100,21 @@ export function watchWorkspace(
|
|
|
98
100
|
let maxTimer: ReturnType<typeof setTimeout> | undefined;
|
|
99
101
|
|
|
100
102
|
const clearTimers = (): void => {
|
|
101
|
-
if (flushTimer)
|
|
102
|
-
|
|
103
|
+
if (flushTimer) {
|
|
104
|
+
clearTimeout(flushTimer);
|
|
105
|
+
}
|
|
106
|
+
if (maxTimer) {
|
|
107
|
+
clearTimeout(maxTimer);
|
|
108
|
+
}
|
|
103
109
|
flushTimer = undefined;
|
|
104
110
|
maxTimer = undefined;
|
|
105
111
|
};
|
|
106
112
|
|
|
107
113
|
const flush = (): void => {
|
|
108
114
|
clearTimers();
|
|
109
|
-
if (pending.length === 0)
|
|
115
|
+
if (pending.length === 0) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
110
118
|
let batch = pending;
|
|
111
119
|
pending = [];
|
|
112
120
|
if (batch.length > maxBatch) {
|
|
@@ -117,34 +125,54 @@ export function watchWorkspace(
|
|
|
117
125
|
};
|
|
118
126
|
|
|
119
127
|
const push = (change: FileChange): void => {
|
|
120
|
-
if (stopped)
|
|
121
|
-
|
|
128
|
+
if (stopped) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (pending.length === 0) {
|
|
132
|
+
maxTimer = setTimeout(flush, flushMs);
|
|
133
|
+
}
|
|
122
134
|
pending.push(change);
|
|
123
|
-
if (flushTimer)
|
|
135
|
+
if (flushTimer) {
|
|
136
|
+
clearTimeout(flushTimer);
|
|
137
|
+
}
|
|
124
138
|
flushTimer = setTimeout(flush, debounceMs);
|
|
125
139
|
};
|
|
126
140
|
|
|
127
141
|
const classify = async (path: string, type: FileChangeType): Promise<FileChange | undefined> => {
|
|
128
|
-
if (path === dir)
|
|
142
|
+
if (path === dir) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
129
145
|
let isDirectory = false;
|
|
130
146
|
let exists = true;
|
|
131
147
|
try {
|
|
132
148
|
const info = await lstat(path);
|
|
133
149
|
isDirectory = info.isDirectory();
|
|
134
|
-
if (isDirectory)
|
|
150
|
+
if (isDirectory) {
|
|
151
|
+
seenDirectories.add(path);
|
|
152
|
+
}
|
|
135
153
|
} catch {
|
|
136
154
|
exists = false;
|
|
137
|
-
if (seenDirectories.delete(path))
|
|
155
|
+
if (seenDirectories.delete(path)) {
|
|
156
|
+
isDirectory = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (type === "deleted") {
|
|
160
|
+
return { path, type, isDirectory };
|
|
138
161
|
}
|
|
139
|
-
if (type === "deleted") return { path, type, isDirectory };
|
|
140
162
|
// 事件与 lstat 之间的竞态:目标已消失按删除处理
|
|
141
|
-
if (!exists)
|
|
142
|
-
|
|
163
|
+
if (!exists) {
|
|
164
|
+
return { path, type: "deleted", isDirectory };
|
|
165
|
+
}
|
|
166
|
+
if (isDirectory) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
143
169
|
return { path, type, isDirectory };
|
|
144
170
|
};
|
|
145
171
|
|
|
146
172
|
const handleError = (error: unknown): void => {
|
|
147
|
-
if (stopped)
|
|
173
|
+
if (stopped) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
148
176
|
options?.onError?.(`workspace watcher failed for ${dir}: ${String(error)}`);
|
|
149
177
|
};
|
|
150
178
|
|
|
@@ -155,13 +183,19 @@ export function watchWorkspace(
|
|
|
155
183
|
}
|
|
156
184
|
void (async () => {
|
|
157
185
|
for (const event of events) {
|
|
158
|
-
if (stopped)
|
|
186
|
+
if (stopped) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
159
189
|
const path = normalize(event.path);
|
|
160
190
|
const type: FileChangeType =
|
|
161
191
|
event.type === "create" ? "created" : event.type === "update" ? "changed" : "deleted";
|
|
162
192
|
const change = await classify(path, type);
|
|
163
|
-
if (!change)
|
|
164
|
-
|
|
193
|
+
if (!change) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (isIgnored(change.path, dir, ignorePatterns)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
165
199
|
push(change);
|
|
166
200
|
}
|
|
167
201
|
})();
|
package/src/lib/path.ts
CHANGED
|
@@ -13,8 +13,12 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:pat
|
|
|
13
13
|
* - anything else (including `~user`) is left untouched.
|
|
14
14
|
*/
|
|
15
15
|
export function expandHome(p: string): string {
|
|
16
|
-
if (p === "~")
|
|
17
|
-
|
|
16
|
+
if (p === "~") {
|
|
17
|
+
return homedir();
|
|
18
|
+
}
|
|
19
|
+
if (p.startsWith("~/")) {
|
|
20
|
+
return join(homedir(), p.slice(2));
|
|
21
|
+
}
|
|
18
22
|
return p;
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -72,18 +76,26 @@ export function formatDisplayPath(cwd: string, filePath: string): string {
|
|
|
72
76
|
export const MAX_SUBTITLE_PATH_LENGTH = 30;
|
|
73
77
|
|
|
74
78
|
function shortenSubtitlePath(filePath: string, display: string): string {
|
|
75
|
-
if (display.length <= MAX_SUBTITLE_PATH_LENGTH)
|
|
79
|
+
if (display.length <= MAX_SUBTITLE_PATH_LENGTH) {
|
|
80
|
+
return display;
|
|
81
|
+
}
|
|
76
82
|
const name = basename(filePath);
|
|
77
83
|
const parentDir = dirname(filePath);
|
|
78
84
|
const parentName = basename(parentDir);
|
|
79
|
-
if (parentName === "" || parentName === "." || parentDir === parentName)
|
|
85
|
+
if (parentName === "" || parentName === "." || parentDir === parentName) {
|
|
86
|
+
return name;
|
|
87
|
+
}
|
|
80
88
|
return join(parentName, name);
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
function formatSubtitleCounts(errorCount?: number, warningCount?: number): string {
|
|
84
92
|
const parts: string[] = [];
|
|
85
|
-
if (errorCount)
|
|
86
|
-
|
|
93
|
+
if (errorCount) {
|
|
94
|
+
parts.push(`ⓧ ${errorCount}`);
|
|
95
|
+
}
|
|
96
|
+
if (warningCount) {
|
|
97
|
+
parts.push(`⚠ ${warningCount}`);
|
|
98
|
+
}
|
|
87
99
|
return parts.join(" ");
|
|
88
100
|
}
|
|
89
101
|
|
package/src/lib/proc.ts
CHANGED
|
@@ -14,7 +14,9 @@ export async function forEachLine(
|
|
|
14
14
|
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
15
15
|
const line = buffer.slice(0, newline);
|
|
16
16
|
buffer = buffer.slice(newline + 1);
|
|
17
|
-
if (callback(line) === false)
|
|
17
|
+
if (callback(line) === false) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
}
|
package/src/lib/proxy.ts
CHANGED
|
@@ -63,7 +63,9 @@ const NO_PROXY_ENV_NAMES = ["NO_PROXY", "no_proxy"];
|
|
|
63
63
|
function firstEnv(names: readonly string[], env: NodeJS.ProcessEnv): string | undefined {
|
|
64
64
|
for (const name of names) {
|
|
65
65
|
const value = env[name]?.trim();
|
|
66
|
-
if (value)
|
|
66
|
+
if (value) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
67
69
|
}
|
|
68
70
|
return undefined;
|
|
69
71
|
}
|
|
@@ -124,7 +126,9 @@ export function readProxySettings(
|
|
|
124
126
|
/** 要注入 gh 子进程的代理环境变量;未配置代理时为空对象(子进程继承父进程环境)。 */
|
|
125
127
|
export function proxyEnvVars(settings: HttpProxySettings): NodeJS.ProcessEnv {
|
|
126
128
|
const { proxy, noProxy } = settings;
|
|
127
|
-
if (!proxy)
|
|
129
|
+
if (!proxy) {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
128
132
|
return {
|
|
129
133
|
// gh 是 Go 程序,https 目标只认 HTTPS_PROXY;统一填全部变量,避免用户只设了
|
|
130
134
|
// HTTP_PROXY 时 https 请求直连。小写变体给 curl 系的子进程用。
|
|
@@ -179,7 +183,9 @@ export function createHttpProxy(
|
|
|
179
183
|
let dispatcher: EnvHttpProxyAgent | undefined;
|
|
180
184
|
|
|
181
185
|
const fetch: typeof undiciFetch = (input, init) => {
|
|
182
|
-
if (!proxy)
|
|
186
|
+
if (!proxy) {
|
|
187
|
+
return undiciFetch(input, init);
|
|
188
|
+
}
|
|
183
189
|
dispatcher ??= createProxyDispatcher(proxy, noProxy);
|
|
184
190
|
return undiciFetch(input, { ...init, dispatcher });
|
|
185
191
|
};
|
|
@@ -33,7 +33,9 @@ export function createRequestPolicy(events?: EventBus): RequestPolicy {
|
|
|
33
33
|
let denyRequests = false;
|
|
34
34
|
|
|
35
35
|
events?.on(REQUEST_POLICY_CHANNEL, (data) => {
|
|
36
|
-
if (Value.Check(requestPolicyMessageSchema, data))
|
|
36
|
+
if (Value.Check(requestPolicyMessageSchema, data)) {
|
|
37
|
+
denyRequests = data.denyRequests;
|
|
38
|
+
}
|
|
37
39
|
});
|
|
38
40
|
|
|
39
41
|
return {
|
package/src/lib/seq-state.ts
CHANGED
package/src/lib/ui.ts
CHANGED
|
@@ -42,9 +42,13 @@ export async function selectWithOptionalInput(
|
|
|
42
42
|
actions.map((action) => action.label),
|
|
43
43
|
{ signal },
|
|
44
44
|
);
|
|
45
|
-
if (choice === undefined)
|
|
45
|
+
if (choice === undefined) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
46
48
|
const action = actions.find((candidate) => candidate.label === choice);
|
|
47
|
-
if (action?.inputPrompt === undefined)
|
|
49
|
+
if (action?.inputPrompt === undefined) {
|
|
50
|
+
return { label: choice, prompted: false };
|
|
51
|
+
}
|
|
48
52
|
const answer = await ui.input(title, action.inputPrompt, { signal });
|
|
49
53
|
return {
|
|
50
54
|
label: choice,
|
|
@@ -87,7 +91,9 @@ export async function selectMultiple(
|
|
|
87
91
|
}
|
|
88
92
|
// 经 inputPrompt 输入的自定义答案不是固定条目:单独列出,同样可反选
|
|
89
93
|
for (const label of selected) {
|
|
90
|
-
if (entries.some((entry) => entry.label === label))
|
|
94
|
+
if (entries.some((entry) => entry.label === label)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
91
97
|
const display = `${CHECKED_PREFIX}${label}`;
|
|
92
98
|
displayToLabel.set(display, label);
|
|
93
99
|
round.push({ label: display });
|
|
@@ -98,16 +104,25 @@ export async function selectMultiple(
|
|
|
98
104
|
ui,
|
|
99
105
|
opts,
|
|
100
106
|
);
|
|
101
|
-
if (result === undefined || result.label === opts.doneLabel)
|
|
107
|
+
if (result === undefined || result.label === opts.doneLabel) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
102
110
|
if (result.prompted) {
|
|
103
111
|
// 自定义答案进入已选并继续循环:可反选或继续勾选,最终手动提交
|
|
104
|
-
if (result.input && !selectedSet.has(result.input))
|
|
112
|
+
if (result.input && !selectedSet.has(result.input)) {
|
|
113
|
+
selected.push(result.input);
|
|
114
|
+
}
|
|
105
115
|
continue;
|
|
106
116
|
}
|
|
107
117
|
const label = displayToLabel.get(result.label);
|
|
108
|
-
if (label === undefined)
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
if (label === undefined) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (selectedSet.has(label)) {
|
|
122
|
+
selected.splice(selected.indexOf(label), 1);
|
|
123
|
+
} else {
|
|
124
|
+
selected.push(label);
|
|
125
|
+
}
|
|
111
126
|
}
|
|
112
127
|
return selected;
|
|
113
128
|
}
|
package/src/lib/write-guard.ts
CHANGED
|
@@ -33,10 +33,14 @@ function isInside(dir: string, filePath: string): boolean {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function isPathAllowed(absolutePath: string, cwd: string): boolean {
|
|
36
|
-
if (isInside(cwd, absolutePath))
|
|
36
|
+
if (isInside(cwd, absolutePath)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
37
39
|
|
|
38
40
|
for (const allowed of ALWAYS_ALLOW) {
|
|
39
|
-
if (isInside(allowed, absolutePath))
|
|
41
|
+
if (isInside(allowed, absolutePath)) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
return false;
|
|
@@ -144,9 +148,13 @@ export async function guardWriteAccess(
|
|
|
144
148
|
ctx: WriteGuardContext | undefined,
|
|
145
149
|
opts: WriteGuardOptions,
|
|
146
150
|
): Promise<void> {
|
|
147
|
-
if (!ctx)
|
|
151
|
+
if (!ctx) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
148
154
|
const { absolutePath } = opts;
|
|
149
|
-
if (isPathAllowed(absolutePath, ctx.cwd))
|
|
155
|
+
if (isPathAllowed(absolutePath, ctx.cwd)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
150
158
|
|
|
151
159
|
// 非沙盒请求策略生效时不弹审批框:工作区外写入按用户点 "Block"(无理由)处理。
|
|
152
160
|
// 放在 win32 / 无 UI 分支之前,策略优先级高于各平台的降级路径。
|
|
@@ -182,12 +190,18 @@ export async function guardWriteAccess(
|
|
|
182
190
|
ctx.abort?.();
|
|
183
191
|
throw new Error(`user deny ${opts.toolName}: cancelled`);
|
|
184
192
|
}
|
|
185
|
-
if (choice === "Approve once")
|
|
186
|
-
|
|
193
|
+
if (choice === "Approve once") {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (choice === "Block") {
|
|
197
|
+
throw new Error(`user deny ${opts.toolName}: blocked`);
|
|
198
|
+
}
|
|
187
199
|
const feedback = await ctx.ui.input("Why was this write denied?", undefined, {
|
|
188
200
|
signal: opts.signal,
|
|
189
201
|
});
|
|
190
|
-
if (feedback === undefined)
|
|
202
|
+
if (feedback === undefined) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
191
205
|
throw new Error(
|
|
192
206
|
feedback ? `user deny ${opts.toolName}: ${feedback}` : `user deny ${opts.toolName}: blocked`,
|
|
193
207
|
);
|
|
@@ -65,7 +65,9 @@ function nonEmpty(value: string | undefined): string | undefined {
|
|
|
65
65
|
|
|
66
66
|
function normalizeModel(model: Static<typeof modelSchema>): OpenaiCostModelConfig | undefined {
|
|
67
67
|
const id = nonEmpty(model.id);
|
|
68
|
-
if (!id)
|
|
68
|
+
if (!id) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
69
71
|
const input = model.input ?? [];
|
|
70
72
|
return {
|
|
71
73
|
id,
|
package/src/openai-cost/cost.ts
CHANGED
|
@@ -48,10 +48,16 @@ const chunkSchema = Type.Object(
|
|
|
48
48
|
function costFromUsage(
|
|
49
49
|
usage: { cost?: number | { total?: number } } | undefined,
|
|
50
50
|
): number | undefined {
|
|
51
|
-
if (!usage)
|
|
51
|
+
if (!usage) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
52
54
|
const asNumber = finiteNumber(usage.cost);
|
|
53
|
-
if (asNumber !== undefined)
|
|
54
|
-
|
|
55
|
+
if (asNumber !== undefined) {
|
|
56
|
+
return asNumber;
|
|
57
|
+
}
|
|
58
|
+
if (usage.cost && typeof usage.cost === "object") {
|
|
59
|
+
return finiteNumber(usage.cost.total);
|
|
60
|
+
}
|
|
55
61
|
return undefined;
|
|
56
62
|
}
|
|
57
63
|
|
|
@@ -64,19 +70,27 @@ export function extractReportedCost(value: unknown): number | undefined {
|
|
|
64
70
|
return undefined;
|
|
65
71
|
}
|
|
66
72
|
const fromUsage = costFromUsage(chunk.usage);
|
|
67
|
-
if (fromUsage !== undefined)
|
|
73
|
+
if (fromUsage !== undefined) {
|
|
74
|
+
return fromUsage;
|
|
75
|
+
}
|
|
68
76
|
for (const choice of chunk.choices ?? []) {
|
|
69
77
|
const fromChoice = costFromUsage(choice.usage);
|
|
70
|
-
if (fromChoice !== undefined)
|
|
78
|
+
if (fromChoice !== undefined) {
|
|
79
|
+
return fromChoice;
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
82
|
return undefined;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
export function costFromSseLine(line: string): number | undefined {
|
|
76
86
|
const trimmed = line.trim();
|
|
77
|
-
if (!trimmed.startsWith("data:"))
|
|
87
|
+
if (!trimmed.startsWith("data:")) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
78
90
|
const data = trimmed.slice("data:".length).trim();
|
|
79
|
-
if (!data || data === "[DONE]")
|
|
91
|
+
if (!data || data === "[DONE]") {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
80
94
|
try {
|
|
81
95
|
return extractReportedCost(JSON.parse(data) as unknown);
|
|
82
96
|
} catch {
|
|
@@ -92,18 +106,24 @@ export async function scanSseCost(body: ReadableStream<Uint8Array>): Promise<num
|
|
|
92
106
|
try {
|
|
93
107
|
for (;;) {
|
|
94
108
|
const { done, value } = await reader.read();
|
|
95
|
-
if (done)
|
|
109
|
+
if (done) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
96
112
|
buffer += decoder.decode(value, { stream: true });
|
|
97
113
|
const lines = buffer.split("\n");
|
|
98
114
|
buffer = lines.pop() ?? "";
|
|
99
115
|
for (const line of lines) {
|
|
100
116
|
const cost = costFromSseLine(line);
|
|
101
|
-
if (cost !== undefined)
|
|
117
|
+
if (cost !== undefined) {
|
|
118
|
+
last = cost;
|
|
119
|
+
}
|
|
102
120
|
}
|
|
103
121
|
}
|
|
104
122
|
buffer += decoder.decode();
|
|
105
123
|
const cost = costFromSseLine(buffer);
|
|
106
|
-
if (cost !== undefined)
|
|
124
|
+
if (cost !== undefined) {
|
|
125
|
+
last = cost;
|
|
126
|
+
}
|
|
107
127
|
return last;
|
|
108
128
|
} catch {
|
|
109
129
|
return last;
|
|
@@ -115,14 +135,22 @@ export async function scanSseCost(body: ReadableStream<Uint8Array>): Promise<num
|
|
|
115
135
|
type FetchInput = Parameters<typeof globalThis.fetch>[0];
|
|
116
136
|
|
|
117
137
|
function requestUrl(input: FetchInput): string {
|
|
118
|
-
if (typeof input === "string")
|
|
119
|
-
|
|
138
|
+
if (typeof input === "string") {
|
|
139
|
+
return input;
|
|
140
|
+
}
|
|
141
|
+
if (input instanceof URL) {
|
|
142
|
+
return input.href;
|
|
143
|
+
}
|
|
120
144
|
return input.url;
|
|
121
145
|
}
|
|
122
146
|
|
|
123
147
|
function shouldScan(input: FetchInput, response: Response): boolean {
|
|
124
|
-
if (!response.ok || !response.body)
|
|
125
|
-
|
|
148
|
+
if (!response.ok || !response.body) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (requestUrl(input).includes("/chat/completions")) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
126
154
|
const contentType = response.headers.get("content-type") ?? "";
|
|
127
155
|
return contentType.includes("text/event-stream");
|
|
128
156
|
}
|
|
@@ -133,7 +161,9 @@ export function createReportedCostCapture(
|
|
|
133
161
|
let scan: Promise<number | undefined> | undefined;
|
|
134
162
|
const fetch: typeof globalThis.fetch = async (input, init) => {
|
|
135
163
|
const response = await innerFetch(input, init);
|
|
136
|
-
if (!shouldScan(input, response) || !response.body)
|
|
164
|
+
if (!shouldScan(input, response) || !response.body) {
|
|
165
|
+
return response;
|
|
166
|
+
}
|
|
137
167
|
const [forSdk, forScan] = response.body.tee();
|
|
138
168
|
scan = scanSseCost(forScan);
|
|
139
169
|
return new Response(forSdk, {
|
|
@@ -157,8 +187,12 @@ export function applyReportedCost(usage: Usage, reported: number): void {
|
|
|
157
187
|
}
|
|
158
188
|
|
|
159
189
|
function messageOf(event: AssistantMessageEvent): AssistantMessage {
|
|
160
|
-
if (event.type === "done")
|
|
161
|
-
|
|
190
|
+
if (event.type === "done") {
|
|
191
|
+
return event.message;
|
|
192
|
+
}
|
|
193
|
+
if (event.type === "error") {
|
|
194
|
+
return event.error;
|
|
195
|
+
}
|
|
162
196
|
return event.partial;
|
|
163
197
|
}
|
|
164
198
|
|
|
@@ -166,7 +200,9 @@ export function applyCostToEvent(
|
|
|
166
200
|
event: AssistantMessageEvent,
|
|
167
201
|
reported: number | undefined,
|
|
168
202
|
): AssistantMessageEvent {
|
|
169
|
-
if (reported === undefined)
|
|
203
|
+
if (reported === undefined) {
|
|
204
|
+
return event;
|
|
205
|
+
}
|
|
170
206
|
applyReportedCost(messageOf(event).usage, reported);
|
|
171
207
|
return event;
|
|
172
208
|
}
|
package/src/openai-cost/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { createOpenaiCostProvider } from "./provider.js";
|
|
|
9
9
|
|
|
10
10
|
export default async function openaiCostExtension(pi: ExtensionAPI) {
|
|
11
11
|
const config = await loadOpenaiCostConfig();
|
|
12
|
-
if (!config)
|
|
12
|
+
if (!config) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
13
15
|
pi.registerProvider(createOpenaiCostProvider(config));
|
|
14
16
|
}
|
|
@@ -69,7 +69,9 @@ export function mapRemoteModels(
|
|
|
69
69
|
const models: Model<"openai-completions">[] = [];
|
|
70
70
|
for (const item of payload.data) {
|
|
71
71
|
const id = item.id.trim();
|
|
72
|
-
if (!id)
|
|
72
|
+
if (!id) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
73
75
|
models.push(
|
|
74
76
|
toPiModel(config, {
|
|
75
77
|
id,
|
|
@@ -93,7 +95,9 @@ export async function fetchRemoteModels(
|
|
|
93
95
|
): Promise<Model<"openai-completions">[]> {
|
|
94
96
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
95
97
|
const headers: Record<string, string> = { Accept: "application/json" };
|
|
96
|
-
if (options.apiKey)
|
|
98
|
+
if (options.apiKey) {
|
|
99
|
+
headers.Authorization = `Bearer ${options.apiKey}`;
|
|
100
|
+
}
|
|
97
101
|
const response = await fetchImpl(modelsUrl(config.baseUrl), { headers, signal: options.signal });
|
|
98
102
|
const raw = await response.text();
|
|
99
103
|
if (!response.ok) {
|
package/src/opencode/bash.ts
CHANGED
|
@@ -29,9 +29,13 @@ function appendTruncationNotice(
|
|
|
29
29
|
truncation: TruncationResult,
|
|
30
30
|
fullOutputPath: string | undefined,
|
|
31
31
|
): string {
|
|
32
|
-
if (!truncation.truncated)
|
|
32
|
+
if (!truncation.truncated) {
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
33
35
|
let out = `${text}\n\n${CAPTURE_TRUNCATED_NOTICE}`;
|
|
34
|
-
if (fullOutputPath)
|
|
36
|
+
if (fullOutputPath) {
|
|
37
|
+
out += `\nFull output: ${fullOutputPath}`;
|
|
38
|
+
}
|
|
35
39
|
return out;
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -106,7 +110,9 @@ export default function opencodeBash(
|
|
|
106
110
|
onUpdate,
|
|
107
111
|
});
|
|
108
112
|
} catch (error) {
|
|
109
|
-
if (!(error instanceof Error))
|
|
113
|
+
if (!(error instanceof Error)) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
110
116
|
if (error instanceof BashInterruptedError) {
|
|
111
117
|
const text = appendTruncationNotice(
|
|
112
118
|
error.partial.output || "",
|