@trim21/personal-pi-extensions 0.1.612 → 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 +1 -1
- 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 +3 -1
- package/src/bwrap/approval-rules.ts +42 -14
- 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 +30 -10
- 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 +48 -16
- 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 +138 -46
- 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 +24 -8
- 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 +18 -6
- package/src/opencode/todo.ts +3 -1
- package/src/session-name.ts +39 -13
- package/src/spawn-agent-agents.ts +18 -6
- package/src/spawn-agent.ts +53 -18
- 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 +6 -2
- package/src/talk/registry.ts +33 -11
- 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
package/src/bwrap/runtime.ts
CHANGED
|
@@ -166,7 +166,9 @@ const BASH_TAIL_LIMIT_BYTES = 1024 * 1024;
|
|
|
166
166
|
function countNewlines(data: Buffer): number {
|
|
167
167
|
let count = 0;
|
|
168
168
|
for (const byte of data) {
|
|
169
|
-
if (byte === 0x0a)
|
|
169
|
+
if (byte === 0x0a) {
|
|
170
|
+
count++;
|
|
171
|
+
}
|
|
170
172
|
}
|
|
171
173
|
return count;
|
|
172
174
|
}
|
|
@@ -219,7 +221,9 @@ class BashOutput {
|
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
close(): Promise<void> {
|
|
222
|
-
if (!this.stream)
|
|
224
|
+
if (!this.stream) {
|
|
225
|
+
return Promise.resolve();
|
|
226
|
+
}
|
|
223
227
|
const stream = this.stream;
|
|
224
228
|
this.stream = undefined;
|
|
225
229
|
return new Promise((resolve) => {
|
|
@@ -286,8 +290,12 @@ function notifyChange(
|
|
|
286
290
|
ctx: { ui: { notify: (message: string, type?: "info" | "warning" | "error") => void } },
|
|
287
291
|
change: SandboxChange,
|
|
288
292
|
): void {
|
|
289
|
-
if (change.fs !== undefined)
|
|
290
|
-
|
|
293
|
+
if (change.fs !== undefined) {
|
|
294
|
+
ctx.ui.notify(FS_LABELS[change.fs], "info");
|
|
295
|
+
}
|
|
296
|
+
if (change.network !== undefined) {
|
|
297
|
+
ctx.ui.notify(NETWORK_LABELS[change.network], "info");
|
|
298
|
+
}
|
|
291
299
|
}
|
|
292
300
|
|
|
293
301
|
/** 沙箱默认写边界:根只读、工作区与 /tmp 可写、.git 只读。不展开用户配置的额外可写路径。 */
|
|
@@ -342,7 +350,9 @@ export function describeSandbox(
|
|
|
342
350
|
unsandboxed: boolean,
|
|
343
351
|
fixed = false,
|
|
344
352
|
): string | undefined {
|
|
345
|
-
if (unsandboxed)
|
|
353
|
+
if (unsandboxed) {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
346
356
|
return [
|
|
347
357
|
"<system-reminder>",
|
|
348
358
|
`This command ran in a sandbox: ${describeLimits(resolved)}.`,
|
|
@@ -464,7 +474,9 @@ export class BwrapRuntime {
|
|
|
464
474
|
|
|
465
475
|
// 固定沙箱(创建时显式传入配置)不注册 /bwrap-* 命令:切模式与恢复提权审批
|
|
466
476
|
// 都可能放宽声明的沙箱
|
|
467
|
-
if (this.config === undefined)
|
|
477
|
+
if (this.config === undefined) {
|
|
478
|
+
this.registerCommands(pi);
|
|
479
|
+
}
|
|
468
480
|
}
|
|
469
481
|
|
|
470
482
|
setMode(cwd: string, change: SandboxChange): ResolvedBwrap {
|
|
@@ -663,7 +675,9 @@ export class BwrapRuntime {
|
|
|
663
675
|
}
|
|
664
676
|
|
|
665
677
|
private resolve(ctx: Pick<ExtensionContext, "cwd">): ResolvedBwrap {
|
|
666
|
-
if (!this.resolved)
|
|
678
|
+
if (!this.resolved) {
|
|
679
|
+
this.resolved = resolveBwrap(this.config ?? loadBwrapConfig(ctx.cwd));
|
|
680
|
+
}
|
|
667
681
|
return this.resolved;
|
|
668
682
|
}
|
|
669
683
|
|
|
@@ -674,7 +688,9 @@ export class BwrapRuntime {
|
|
|
674
688
|
execCwd: string,
|
|
675
689
|
): Promise<FullAccessGrant> {
|
|
676
690
|
// hasUI 判定推迟到审批时刻:无 UI 会话弹不了审批框,按用户点 Deny 的标准文案拒绝
|
|
677
|
-
if (!ctx.hasUI)
|
|
691
|
+
if (!ctx.hasUI) {
|
|
692
|
+
throw new Error(UNSANDBOXED_DENIED);
|
|
693
|
+
}
|
|
678
694
|
const decision = await this.approveFullAccessUI(ctx, command, reason, execCwd);
|
|
679
695
|
// 关闭对话框 = 中断并拒绝,不循环重问
|
|
680
696
|
if (decision === undefined) {
|
|
@@ -802,7 +818,9 @@ export class BwrapRuntime {
|
|
|
802
818
|
const verdict = await selectWithOptionalInput(description + pending, actions, ctx.ui, {
|
|
803
819
|
signal: ctx.signal,
|
|
804
820
|
});
|
|
805
|
-
if (verdict === undefined)
|
|
821
|
+
if (verdict === undefined) {
|
|
822
|
+
return undefined;
|
|
823
|
+
}
|
|
806
824
|
if (verdict.label !== EDIT_RULES) {
|
|
807
825
|
return {
|
|
808
826
|
result: verdict.label,
|
|
@@ -826,7 +844,9 @@ export class BwrapRuntime {
|
|
|
826
844
|
patterns?: string[],
|
|
827
845
|
): Promise<void> {
|
|
828
846
|
const rulePatterns = patterns ?? (await commandPatternsFor(command));
|
|
829
|
-
if (rulePatterns.length === 0)
|
|
847
|
+
if (rulePatterns.length === 0) {
|
|
848
|
+
return; // 解析失败:本次处理,不写规则
|
|
849
|
+
}
|
|
830
850
|
const newRules: ApprovalRule[] = rulePatterns.map((pattern) => ({ action: "allow", pattern }));
|
|
831
851
|
const { project } = getBwrapConfigPaths(ctx.cwd);
|
|
832
852
|
let config: Record<string, unknown> = {};
|
|
@@ -14,7 +14,9 @@ export function resolveToolFilePath(filePath: string, cwd: string): string {
|
|
|
14
14
|
|
|
15
15
|
/** Resolve a search root path against the working directory. */
|
|
16
16
|
export function searchRoot(path: string | undefined, cwd: string): string {
|
|
17
|
-
if (!path)
|
|
17
|
+
if (!path) {
|
|
18
|
+
return cwd;
|
|
19
|
+
}
|
|
18
20
|
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -82,6 +84,8 @@ export async function suggestPathUnderCwd(
|
|
|
82
84
|
/** "Did you mean" 建议:优先 cwd 重定位,其次同名不同扩展(对齐 Claude Code)。 */
|
|
83
85
|
export async function didYouMean(filePath: string, cwd: string): Promise<string | undefined> {
|
|
84
86
|
const cwdSuggestion = await suggestPathUnderCwd(filePath, cwd);
|
|
85
|
-
if (cwdSuggestion)
|
|
87
|
+
if (cwdSuggestion) {
|
|
88
|
+
return cwdSuggestion;
|
|
89
|
+
}
|
|
86
90
|
return findSimilarFile(filePath);
|
|
87
91
|
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* convertLeadingTabsToSpaces)。仅用于 details 里展示的 diff,不影响写盘内容。
|
|
13
13
|
*/
|
|
14
14
|
export function convertLeadingTabsToSpaces(content: string): string {
|
|
15
|
-
if (!content.includes("\t"))
|
|
15
|
+
if (!content.includes("\t")) {
|
|
16
|
+
return content;
|
|
17
|
+
}
|
|
16
18
|
return content.replaceAll(/^\t+/gm, (tabs) => " ".repeat(tabs.length));
|
|
17
19
|
}
|
package/src/claude-code/files.ts
CHANGED
|
@@ -44,9 +44,15 @@ const MAX_READ_TOKENS = 25_000;
|
|
|
44
44
|
const MAX_EDIT_FILE_SIZE = 1024 * 1024 * 1024; // 1 GiB
|
|
45
45
|
|
|
46
46
|
function formatFileSize(bytes: number): string {
|
|
47
|
-
if (bytes < 1024)
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
if (bytes < 1024) {
|
|
48
|
+
return `${bytes} B`;
|
|
49
|
+
}
|
|
50
|
+
if (bytes < 1024 * 1024) {
|
|
51
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
52
|
+
}
|
|
53
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
54
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
55
|
+
}
|
|
50
56
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -81,7 +87,9 @@ async function assertReadableFile(filePath: string): Promise<Stats> {
|
|
|
81
87
|
if (value.isDirectory()) {
|
|
82
88
|
throw new Error(`EISDIR: illegal operation on a directory, read '${filePath}'`);
|
|
83
89
|
}
|
|
84
|
-
if (!value.isFile())
|
|
90
|
+
if (!value.isFile()) {
|
|
91
|
+
throw new Error(`File not found: ${filePath}`);
|
|
92
|
+
}
|
|
85
93
|
await access(filePath, constants.R_OK);
|
|
86
94
|
return value;
|
|
87
95
|
}
|
|
@@ -89,8 +97,12 @@ async function assertReadableFile(filePath: string): Promise<Stats> {
|
|
|
89
97
|
function isBinary(sample: Uint8Array): boolean {
|
|
90
98
|
let suspicious = 0;
|
|
91
99
|
for (const byte of sample) {
|
|
92
|
-
if (byte === 0)
|
|
93
|
-
|
|
100
|
+
if (byte === 0) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (byte < 9 || (byte > 13 && byte < 32)) {
|
|
104
|
+
suspicious++;
|
|
105
|
+
}
|
|
94
106
|
}
|
|
95
107
|
return sample.length > 0 && suspicious / sample.length > 0.3;
|
|
96
108
|
}
|
|
@@ -104,7 +116,9 @@ function isBinary(sample: Uint8Array): boolean {
|
|
|
104
116
|
*/
|
|
105
117
|
function splitFileLines(content: string): string[] {
|
|
106
118
|
const text = content.replace(/^\uFEFF/, "");
|
|
107
|
-
if (text === "")
|
|
119
|
+
if (text === "") {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
108
122
|
return (text.endsWith("\n") ? text : `${text}\n`)
|
|
109
123
|
.split("\n")
|
|
110
124
|
.map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line));
|
|
@@ -157,7 +171,9 @@ export function formatReadOutput(
|
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
function countMatches(content: string, needle: string): number {
|
|
160
|
-
if (needle === "")
|
|
174
|
+
if (needle === "") {
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
161
177
|
let count = 0;
|
|
162
178
|
let index = 0;
|
|
163
179
|
while ((index = content.indexOf(needle, index)) !== -1) {
|
|
@@ -173,17 +189,24 @@ export function exactReplace(
|
|
|
173
189
|
newString: string,
|
|
174
190
|
replaceAll = false,
|
|
175
191
|
): string {
|
|
176
|
-
if (oldString === newString)
|
|
192
|
+
if (oldString === newString) {
|
|
177
193
|
throw new Error("No changes to apply: old_string and new_string are identical.");
|
|
178
|
-
|
|
194
|
+
}
|
|
195
|
+
if (oldString === "") {
|
|
196
|
+
throw new Error("old_string must not be empty.");
|
|
197
|
+
}
|
|
179
198
|
const matches = countMatches(content, oldString);
|
|
180
|
-
if (matches === 0)
|
|
199
|
+
if (matches === 0) {
|
|
200
|
+
throw new Error("String to replace not found in file.");
|
|
201
|
+
}
|
|
181
202
|
if (!replaceAll && matches > 1) {
|
|
182
203
|
throw new Error(
|
|
183
204
|
`Found ${matches} matches of the string to replace, but replace_all is false. To replace all occurrences, set replace_all to true. To replace only one occurrence, provide more context to make old_string unique.`,
|
|
184
205
|
);
|
|
185
206
|
}
|
|
186
|
-
if (replaceAll)
|
|
207
|
+
if (replaceAll) {
|
|
208
|
+
return content.split(oldString).join(newString);
|
|
209
|
+
}
|
|
187
210
|
const index = content.indexOf(oldString);
|
|
188
211
|
return content.slice(0, index) + newString + content.slice(index + oldString.length);
|
|
189
212
|
}
|
|
@@ -284,8 +307,9 @@ export function registerFileTools(
|
|
|
284
307
|
const key = await readStateKey(filePath);
|
|
285
308
|
const buffer = await readFile(filePath);
|
|
286
309
|
|
|
287
|
-
if (isBinary(buffer.subarray(0, SAMPLE_BYTES)))
|
|
310
|
+
if (isBinary(buffer.subarray(0, SAMPLE_BYTES))) {
|
|
288
311
|
throw new Error(`Cannot read binary file: ${filePath}`);
|
|
312
|
+
}
|
|
289
313
|
// 全读(limit 未传)时受字节上限约束(对齐 Claude Code)
|
|
290
314
|
if (params.limit === undefined && buffer.length > MAX_READ_SIZE_BYTES) {
|
|
291
315
|
throw new Error(
|
|
@@ -398,7 +422,9 @@ export function registerFileTools(
|
|
|
398
422
|
throw error;
|
|
399
423
|
}
|
|
400
424
|
}
|
|
401
|
-
if (!exists)
|
|
425
|
+
if (!exists) {
|
|
426
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
427
|
+
}
|
|
402
428
|
await writeFile(filePath, newString, "utf8");
|
|
403
429
|
const snapshot = snapshotOf(newString);
|
|
404
430
|
const key = await readStateKey(filePath);
|
package/src/claude-code/glob.ts
CHANGED
|
@@ -40,11 +40,15 @@ function extractGlobBaseDirectory(pattern: string): {
|
|
|
40
40
|
}
|
|
41
41
|
const staticPrefix = pattern.slice(0, match.index);
|
|
42
42
|
const lastSepIndex = Math.max(staticPrefix.lastIndexOf("/"), staticPrefix.lastIndexOf(sep));
|
|
43
|
-
if (lastSepIndex === -1)
|
|
43
|
+
if (lastSepIndex === -1) {
|
|
44
|
+
return { baseDir: "", relativePattern: pattern };
|
|
45
|
+
}
|
|
44
46
|
let baseDir = staticPrefix.slice(0, lastSepIndex);
|
|
45
47
|
const relativePattern = pattern.slice(lastSepIndex + 1);
|
|
46
48
|
// 根目录 pattern(如 /*.txt):baseDir 为空但应使用 "/"
|
|
47
|
-
if (baseDir === "" && lastSepIndex === 0)
|
|
49
|
+
if (baseDir === "" && lastSepIndex === 0) {
|
|
50
|
+
baseDir = "/";
|
|
51
|
+
}
|
|
48
52
|
return { baseDir, relativePattern };
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -89,7 +93,9 @@ export async function globFiles(
|
|
|
89
93
|
} catch (error) {
|
|
90
94
|
// rg exit code 1 = 搜索完成但无匹配,对齐 Claude Code 的 ripGrep(正常空结果)
|
|
91
95
|
const code = (error as { code?: unknown }).code;
|
|
92
|
-
if (code === 1)
|
|
96
|
+
if (code === 1) {
|
|
97
|
+
return { files: [], truncated: false };
|
|
98
|
+
}
|
|
93
99
|
const detail =
|
|
94
100
|
(error as { stderr?: string }).stderr?.trim() ||
|
|
95
101
|
(error instanceof Error ? error.message : String(error));
|
|
@@ -143,7 +149,9 @@ export function registerGlobTool(pi: ExtensionAPI): void {
|
|
|
143
149
|
}
|
|
144
150
|
throw error;
|
|
145
151
|
}
|
|
146
|
-
if (!stats.isDirectory())
|
|
152
|
+
if (!stats.isDirectory()) {
|
|
153
|
+
throw new Error(`Path is not a directory: ${params.path}`);
|
|
154
|
+
}
|
|
147
155
|
}
|
|
148
156
|
const { files, truncated } = await globFiles(params.pattern, root, signal);
|
|
149
157
|
const filenames = files.map((filePath) => toRelativePath(filePath, ctx.cwd));
|
package/src/claude-code/grep.ts
CHANGED
|
@@ -35,7 +35,9 @@ const VCS_DIRECTORIES_TO_EXCLUDE = [".git", ".svn", ".hg", ".bzr", ".jj", ".sl"]
|
|
|
35
35
|
const DEFAULT_HEAD_LIMIT = 250;
|
|
36
36
|
|
|
37
37
|
function truncateOutput(output: string, maxCharacters = 30_000): string {
|
|
38
|
-
if (output.length <= maxCharacters)
|
|
38
|
+
if (output.length <= maxCharacters) {
|
|
39
|
+
return output;
|
|
40
|
+
}
|
|
39
41
|
return `${output.slice(0, maxCharacters)}\n\n[Output truncated at ${maxCharacters} characters]`;
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -102,22 +104,36 @@ export function buildGrepArguments(params: GrepParameters, cwd: string): string[
|
|
|
102
104
|
case "content": {
|
|
103
105
|
args.push("--no-heading", "--with-filename");
|
|
104
106
|
const showLineNumbers = params["-n"] === true || params["-n"] === undefined;
|
|
105
|
-
if (showLineNumbers)
|
|
107
|
+
if (showLineNumbers) {
|
|
108
|
+
args.push("--line-number");
|
|
109
|
+
}
|
|
106
110
|
const before = params["-B"];
|
|
107
111
|
const after = params["-A"];
|
|
108
112
|
const around = params.context ?? params["-C"];
|
|
109
113
|
if (around === undefined) {
|
|
110
|
-
if (before !== undefined)
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
if (before !== undefined) {
|
|
115
|
+
args.push("--before-context", String(before));
|
|
116
|
+
}
|
|
117
|
+
if (after !== undefined) {
|
|
118
|
+
args.push("--after-context", String(after));
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
args.push("--context", String(around));
|
|
122
|
+
}
|
|
113
123
|
|
|
114
124
|
break;
|
|
115
125
|
}
|
|
116
126
|
// No default
|
|
117
127
|
}
|
|
118
|
-
if (params["-i"] === true)
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
if (params["-i"] === true) {
|
|
129
|
+
args.push("--ignore-case");
|
|
130
|
+
}
|
|
131
|
+
if (params.type) {
|
|
132
|
+
args.push("--type", params.type);
|
|
133
|
+
}
|
|
134
|
+
if (params.multiline === true) {
|
|
135
|
+
args.push("--multiline", "--multiline-dotall");
|
|
136
|
+
}
|
|
121
137
|
// glob 按逗号/空格拆分(花括号模式不拆),对齐 Claude Code
|
|
122
138
|
if (params.glob) {
|
|
123
139
|
const globPatterns: string[] = [];
|
|
@@ -129,12 +145,17 @@ export function buildGrepArguments(params: GrepParameters, cwd: string): string[
|
|
|
129
145
|
}
|
|
130
146
|
}
|
|
131
147
|
for (const globPattern of globPatterns) {
|
|
132
|
-
if (globPattern)
|
|
148
|
+
if (globPattern) {
|
|
149
|
+
args.push("--glob", globPattern);
|
|
150
|
+
}
|
|
133
151
|
}
|
|
134
152
|
}
|
|
135
153
|
// 以 - 开头的 pattern 用 -e 显式声明,防止被 rg 当作选项
|
|
136
|
-
if (params.pattern.startsWith("-"))
|
|
137
|
-
|
|
154
|
+
if (params.pattern.startsWith("-")) {
|
|
155
|
+
args.push("-e", params.pattern);
|
|
156
|
+
} else {
|
|
157
|
+
args.push(params.pattern);
|
|
158
|
+
}
|
|
138
159
|
args.push(searchRoot(params.path, cwd));
|
|
139
160
|
return args;
|
|
140
161
|
}
|
|
@@ -147,7 +168,9 @@ export function buildGrepArguments(params: GrepParameters, cwd: string): string[
|
|
|
147
168
|
*/
|
|
148
169
|
export async function sortFilesByMtime(output: string): Promise<string> {
|
|
149
170
|
const paths = output.replace(/\n$/, "").split("\n").filter(Boolean);
|
|
150
|
-
if (paths.length <= 1)
|
|
171
|
+
if (paths.length <= 1) {
|
|
172
|
+
return output;
|
|
173
|
+
}
|
|
151
174
|
const stats = await Promise.allSettled(paths.map((path) => stat(path)));
|
|
152
175
|
const sorted = paths
|
|
153
176
|
.map((path, index) => ({
|
|
@@ -208,8 +231,12 @@ function formatLimitInfo(
|
|
|
208
231
|
appliedOffset: number | undefined,
|
|
209
232
|
): string {
|
|
210
233
|
const parts: string[] = [];
|
|
211
|
-
if (appliedLimit !== undefined)
|
|
212
|
-
|
|
234
|
+
if (appliedLimit !== undefined) {
|
|
235
|
+
parts.push(`limit: ${appliedLimit}`);
|
|
236
|
+
}
|
|
237
|
+
if (appliedOffset) {
|
|
238
|
+
parts.push(`offset: ${appliedOffset}`);
|
|
239
|
+
}
|
|
213
240
|
return parts.join(", ");
|
|
214
241
|
}
|
|
215
242
|
|
|
@@ -63,7 +63,9 @@ const questionSchema = Type.Object(
|
|
|
63
63
|
type QuestionInput = Static<typeof questionSchema>;
|
|
64
64
|
|
|
65
65
|
function formatTodos(todos: readonly ClaudeCodeTodo[]): string[] | undefined {
|
|
66
|
-
if (todos.length === 0)
|
|
66
|
+
if (todos.length === 0) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
67
69
|
const markers: Record<TodoStatus, string> = {
|
|
68
70
|
pending: " ",
|
|
69
71
|
in_progress: ">",
|
|
@@ -95,7 +97,9 @@ async function askSingle(
|
|
|
95
97
|
ctx.ui,
|
|
96
98
|
{ signal },
|
|
97
99
|
);
|
|
98
|
-
if (result === undefined)
|
|
100
|
+
if (result === undefined) {
|
|
101
|
+
return "Unanswered";
|
|
102
|
+
}
|
|
99
103
|
return result.prompted ? result.input || "Unanswered" : result.label;
|
|
100
104
|
}
|
|
101
105
|
|
|
@@ -123,8 +127,12 @@ export function registerSessionTools(pi: ExtensionAPI): void {
|
|
|
123
127
|
// 的列表重新渲染(完整列表替换语义,后出现的覆盖前面的)。
|
|
124
128
|
pi.on("session_start", (_event, ctx) => {
|
|
125
129
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
126
|
-
if (entry.type !== "message" || entry.message.role !== "toolResult")
|
|
127
|
-
|
|
130
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult") {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (entry.message.toolName !== "TodoWrite") {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
128
136
|
if (Value.Check(todoSchema, entry.message.details)) {
|
|
129
137
|
ctx.ui.setWidget("claude-code-todos", formatTodos(entry.message.details.todos));
|
|
130
138
|
}
|
|
@@ -152,7 +160,9 @@ export function registerSessionTools(pi: ExtensionAPI): void {
|
|
|
152
160
|
throw new Error("Todo content and activeForm must not be blank.");
|
|
153
161
|
}
|
|
154
162
|
const inProgress = todos.filter((todo) => todo.status === "in_progress");
|
|
155
|
-
if (inProgress.length > 1)
|
|
163
|
+
if (inProgress.length > 1) {
|
|
164
|
+
throw new Error("Only one todo may be in_progress at a time.");
|
|
165
|
+
}
|
|
156
166
|
ctx.ui.setWidget("claude-code-todos", formatTodos(todos));
|
|
157
167
|
return Promise.resolve({
|
|
158
168
|
content: [
|
|
@@ -190,7 +200,9 @@ export function registerSessionTools(pi: ExtensionAPI): void {
|
|
|
190
200
|
),
|
|
191
201
|
executionMode: "sequential",
|
|
192
202
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
193
|
-
if (!ctx.hasUI)
|
|
203
|
+
if (!ctx.hasUI) {
|
|
204
|
+
throw new Error("Cannot ask questions: interactive UI is not available");
|
|
205
|
+
}
|
|
194
206
|
const answers: Record<string, string> = {};
|
|
195
207
|
for (const question of params.questions) {
|
|
196
208
|
signal?.throwIfAborted();
|
package/src/claude-code/shell.ts
CHANGED
|
@@ -35,7 +35,9 @@ const BASH_PROMPT = readFileSync(fileURLToPath(new URL("bash.md", import.meta.ur
|
|
|
35
35
|
*/
|
|
36
36
|
function formatBashError(exitCode: number | null, output: string): string {
|
|
37
37
|
const full = [`Exit code ${exitCode ?? 1}`, output].filter(Boolean).join("\n");
|
|
38
|
-
if (full.length <= MAX_ERROR_CHARS)
|
|
38
|
+
if (full.length <= MAX_ERROR_CHARS) {
|
|
39
|
+
return full;
|
|
40
|
+
}
|
|
39
41
|
const half = MAX_ERROR_CHARS / 2;
|
|
40
42
|
return (
|
|
41
43
|
full.slice(0, half) +
|
|
@@ -50,7 +52,9 @@ function appendTruncationNotice(
|
|
|
50
52
|
truncation: TruncationResult,
|
|
51
53
|
fullOutputPath: string | undefined,
|
|
52
54
|
): string {
|
|
53
|
-
if (!fullOutputPath || !truncation.truncated)
|
|
55
|
+
if (!fullOutputPath || !truncation.truncated) {
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
54
58
|
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
55
59
|
const endLine = truncation.totalLines;
|
|
56
60
|
if (truncation.lastLinePartial) {
|
|
@@ -145,7 +149,9 @@ export function registerShellTools(
|
|
|
145
149
|
onUpdate,
|
|
146
150
|
});
|
|
147
151
|
} catch (error) {
|
|
148
|
-
if (!(error instanceof Error))
|
|
152
|
+
if (!(error instanceof Error)) {
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
149
155
|
if (error instanceof BashInterruptedError) {
|
|
150
156
|
// 输出在前(必要时带截断提示),状态文本在最后
|
|
151
157
|
const text = appendTruncationNotice(
|
package/src/gh/base.ts
CHANGED
|
@@ -66,10 +66,14 @@ export interface GhResult {
|
|
|
66
66
|
export function isGhAvailable(): boolean {
|
|
67
67
|
const pathEnv = process.env.PATH ?? "";
|
|
68
68
|
for (const directory of pathEnv.split(delimiter)) {
|
|
69
|
-
if (existsSync(join(directory, "gh")))
|
|
69
|
+
if (existsSync(join(directory, "gh"))) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
70
72
|
}
|
|
71
73
|
for (const candidate of ["/usr/bin/gh", "/usr/local/bin/gh", "/run/current-system/sw/bin/gh"]) {
|
|
72
|
-
if (existsSync(candidate))
|
|
74
|
+
if (existsSync(candidate)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
73
77
|
}
|
|
74
78
|
return false;
|
|
75
79
|
}
|
|
@@ -110,7 +114,9 @@ export function runGh(
|
|
|
110
114
|
killReason = reason;
|
|
111
115
|
proc.kill("SIGTERM");
|
|
112
116
|
setTimeout(() => {
|
|
113
|
-
if (!proc.killed)
|
|
117
|
+
if (!proc.killed) {
|
|
118
|
+
proc.kill("SIGKILL");
|
|
119
|
+
}
|
|
114
120
|
}, 5000);
|
|
115
121
|
};
|
|
116
122
|
|
|
@@ -144,7 +150,9 @@ export function runGh(
|
|
|
144
150
|
});
|
|
145
151
|
|
|
146
152
|
proc.on("close", (code) => {
|
|
147
|
-
if (timeoutId)
|
|
153
|
+
if (timeoutId) {
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
}
|
|
148
156
|
if (onAbort && ctx.signal) {
|
|
149
157
|
ctx.signal.removeEventListener("abort", onAbort);
|
|
150
158
|
}
|
|
@@ -163,7 +171,9 @@ export function runGh(
|
|
|
163
171
|
});
|
|
164
172
|
|
|
165
173
|
proc.on("error", (err: Error) => {
|
|
166
|
-
if (timeoutId)
|
|
174
|
+
if (timeoutId) {
|
|
175
|
+
clearTimeout(timeoutId);
|
|
176
|
+
}
|
|
167
177
|
if (onAbort && ctx.signal) {
|
|
168
178
|
ctx.signal.removeEventListener("abort", onAbort);
|
|
169
179
|
}
|
|
@@ -284,7 +294,9 @@ export async function resolveRepo(
|
|
|
284
294
|
cwd: string | undefined,
|
|
285
295
|
input?: unknown,
|
|
286
296
|
): Promise<string> {
|
|
287
|
-
if (repo)
|
|
297
|
+
if (repo) {
|
|
298
|
+
return repo;
|
|
299
|
+
}
|
|
288
300
|
const stdout = await ghExec(["repo", "view", "--json", "nameWithOwner"], { cwd, signal, input });
|
|
289
301
|
const { nameWithOwner } = Value.Parse(repoViewSchema, JSON.parse(stdout));
|
|
290
302
|
return nameWithOwner;
|
|
@@ -303,13 +315,19 @@ export function truncate(
|
|
|
303
315
|
const out: string[] = [];
|
|
304
316
|
let bytes = 0;
|
|
305
317
|
for (const line of lines) {
|
|
306
|
-
if (out.length >= maxLines)
|
|
318
|
+
if (out.length >= maxLines) {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
307
321
|
const lineBytes = Buffer.byteLength(line + "\n", "utf8");
|
|
308
|
-
if (bytes + lineBytes > maxBytes)
|
|
322
|
+
if (bytes + lineBytes > maxBytes) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
309
325
|
out.push(line);
|
|
310
326
|
bytes += lineBytes;
|
|
311
327
|
}
|
|
312
|
-
if (out.length > 0)
|
|
328
|
+
if (out.length > 0) {
|
|
329
|
+
return { text: out.join("\n"), truncated: true };
|
|
330
|
+
}
|
|
313
331
|
|
|
314
332
|
// 首行自己就超过 maxBytes(如单行超长文本):保底保留字节前缀,
|
|
315
333
|
// 不能把非空输出截成空字符串。
|
|
@@ -361,10 +379,16 @@ export function subtitlePendant<IdKey extends string = never>(
|
|
|
361
379
|
idKey?: IdKey,
|
|
362
380
|
): ToolPendant | undefined {
|
|
363
381
|
const parts: string[] = [];
|
|
364
|
-
if (params.repo)
|
|
382
|
+
if (params.repo) {
|
|
383
|
+
parts.push(`repo=${params.repo}`);
|
|
384
|
+
}
|
|
365
385
|
const id = idKey === undefined ? undefined : params[idKey];
|
|
366
|
-
if (typeof id === "string" || typeof id === "number")
|
|
367
|
-
|
|
386
|
+
if (typeof id === "string" || typeof id === "number") {
|
|
387
|
+
parts.push(`${idKey}=${id}`);
|
|
388
|
+
}
|
|
389
|
+
if (parts.length === 0) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
368
392
|
return { subtitle: parts.join(" ") };
|
|
369
393
|
}
|
|
370
394
|
|
|
@@ -394,12 +418,24 @@ export function listGithubArgs(kind: "issue" | "pr", params: ListFilters): strin
|
|
|
394
418
|
const { repo, state, label, author, assignee, milestone, limit } = params;
|
|
395
419
|
|
|
396
420
|
const args = [kind, "list", ...repoArgs(repo)];
|
|
397
|
-
if (state)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
421
|
+
if (state) {
|
|
422
|
+
args.push("--state", state);
|
|
423
|
+
}
|
|
424
|
+
if (label) {
|
|
425
|
+
args.push("--label", label);
|
|
426
|
+
}
|
|
427
|
+
if (author) {
|
|
428
|
+
args.push("--author", author);
|
|
429
|
+
}
|
|
430
|
+
if (assignee) {
|
|
431
|
+
args.push("--assignee", assignee);
|
|
432
|
+
}
|
|
433
|
+
if (milestone) {
|
|
434
|
+
args.push("--milestone", milestone);
|
|
435
|
+
}
|
|
436
|
+
if (limit) {
|
|
437
|
+
args.push("--limit", String(limit));
|
|
438
|
+
}
|
|
403
439
|
return args;
|
|
404
440
|
}
|
|
405
441
|
|
|
@@ -472,14 +508,20 @@ export interface MergedCheck {
|
|
|
472
508
|
}
|
|
473
509
|
|
|
474
510
|
function statusBucket(state: string): CheckBucket {
|
|
475
|
-
if (state === "success")
|
|
476
|
-
|
|
511
|
+
if (state === "success") {
|
|
512
|
+
return "pass";
|
|
513
|
+
}
|
|
514
|
+
if (state === "failure" || state === "error") {
|
|
515
|
+
return "fail";
|
|
516
|
+
}
|
|
477
517
|
// pending, expected, and anything unknown must not end the wait
|
|
478
518
|
return "pending";
|
|
479
519
|
}
|
|
480
520
|
|
|
481
521
|
function checkRunBucket(run: CheckRun): CheckBucket {
|
|
482
|
-
if (run.status !== "completed" || run.conclusion === null)
|
|
522
|
+
if (run.status !== "completed" || run.conclusion === null) {
|
|
523
|
+
return "pending";
|
|
524
|
+
}
|
|
483
525
|
switch (run.conclusion) {
|
|
484
526
|
case "success": {
|
|
485
527
|
return "pass";
|
|
@@ -640,7 +682,9 @@ export async function pollPrChecks(options: PollPrChecksOptions): Promise<Checks
|
|
|
640
682
|
let everSucceeded = false;
|
|
641
683
|
|
|
642
684
|
for (let round = 1; ; round++) {
|
|
643
|
-
if (signal.aborted)
|
|
685
|
+
if (signal.aborted) {
|
|
686
|
+
throw new Error("PR checks polling was aborted");
|
|
687
|
+
}
|
|
644
688
|
try {
|
|
645
689
|
const [statuses, runs] = await Promise.all([
|
|
646
690
|
checks.statuses(owner, repo, headSha, signal),
|
|
@@ -782,8 +826,12 @@ export function renderChecksVerdict(options: {
|
|
|
782
826
|
` - workflow: [${j.runName} (#${j.runId})](${j.runUrl})`,
|
|
783
827
|
);
|
|
784
828
|
}
|
|
785
|
-
if (enrichmentError)
|
|
786
|
-
|
|
829
|
+
if (enrichmentError) {
|
|
830
|
+
lines.push(` - _Actions job details unavailable: ${enrichmentError}_`);
|
|
831
|
+
}
|
|
832
|
+
if (pending.length > 0) {
|
|
833
|
+
lines.push(`\n_${pending.length} other check(s) still in flight._`);
|
|
834
|
+
}
|
|
787
835
|
|
|
788
836
|
return {
|
|
789
837
|
status: "failure",
|