@trim21/personal-pi-extensions 0.1.612 → 0.1.619
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/README.md +19 -10
- 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 +69 -24
- package/src/bwrap/runtime.ts +30 -10
- package/src/bwrap/sandbox.ts +1 -1
- 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
|
@@ -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",
|
|
@@ -54,7 +54,9 @@ export function releaseAssetDir(repo: string, tag: string): string {
|
|
|
54
54
|
|
|
55
55
|
/** Split the comma-separated `pattern` toolcall parameter into gh pattern values. */
|
|
56
56
|
export function releasePatterns(pattern: string | undefined): string[] {
|
|
57
|
-
if (pattern === undefined)
|
|
57
|
+
if (pattern === undefined) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
58
60
|
return pattern
|
|
59
61
|
.split(",")
|
|
60
62
|
.map((value) => value.trim())
|
|
@@ -75,8 +77,12 @@ export function releaseDownloadArgs(options: {
|
|
|
75
77
|
}): string[] {
|
|
76
78
|
const { tag, repo, dir, patterns, archive } = options;
|
|
77
79
|
const args = ["release", "download", tag, ...repoArgs(repo)];
|
|
78
|
-
if (archive !== undefined)
|
|
79
|
-
|
|
80
|
+
if (archive !== undefined) {
|
|
81
|
+
args.push("--archive", archive);
|
|
82
|
+
}
|
|
83
|
+
for (const pattern of patterns) {
|
|
84
|
+
args.push("--pattern", pattern);
|
|
85
|
+
}
|
|
80
86
|
args.push("--dir", dir, "--skip-existing");
|
|
81
87
|
return args;
|
|
82
88
|
}
|
|
@@ -86,7 +92,9 @@ async function listReleaseFiles(dir: string): Promise<ReleaseFile[]> {
|
|
|
86
92
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
87
93
|
const files: ReleaseFile[] = [];
|
|
88
94
|
for (const entry of entries) {
|
|
89
|
-
if (!entry.isFile())
|
|
95
|
+
if (!entry.isFile()) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
90
98
|
const path = join(dir, entry.name);
|
|
91
99
|
const info = await stat(path);
|
|
92
100
|
files.push({ name: entry.name, path, bytes: info.size });
|
|
@@ -16,7 +16,9 @@ export function addListReleasesTool(_gh: GhClient, pi: ExtensionAPI) {
|
|
|
16
16
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
17
17
|
const { repo, limit } = params;
|
|
18
18
|
const args = ["release", "list", ...repoArgs(repo)];
|
|
19
|
-
if (limit)
|
|
19
|
+
if (limit) {
|
|
20
|
+
args.push("--limit", String(limit));
|
|
21
|
+
}
|
|
20
22
|
const result = toToolResult(
|
|
21
23
|
await ghExec(args, { cwd: ctx.cwd, signal, input: params }),
|
|
22
24
|
params,
|
|
@@ -20,9 +20,15 @@ export function addListWorkflowRunsTool(_gh: GhClient, pi: ExtensionAPI) {
|
|
|
20
20
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
21
21
|
const { repo, limit, status, workflow } = params;
|
|
22
22
|
const args = ["run", "list", ...repoArgs(repo)];
|
|
23
|
-
if (limit)
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
if (limit) {
|
|
24
|
+
args.push("--limit", String(limit));
|
|
25
|
+
}
|
|
26
|
+
if (status) {
|
|
27
|
+
args.push("--status", status);
|
|
28
|
+
}
|
|
29
|
+
if (workflow) {
|
|
30
|
+
args.push("--workflow", workflow);
|
|
31
|
+
}
|
|
26
32
|
const result = toToolResult(
|
|
27
33
|
await ghExec(args, { cwd: ctx.cwd, signal, input: params }),
|
|
28
34
|
params,
|
|
@@ -138,7 +138,9 @@ const TIME_MATCH_SCORE = 2;
|
|
|
138
138
|
|
|
139
139
|
function headerTimestamp(line: string): number | null {
|
|
140
140
|
const match = LOG_TIMESTAMP_RE.exec(line);
|
|
141
|
-
if (match === null)
|
|
141
|
+
if (match === null) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
142
144
|
const ms = Date.parse(match[1]);
|
|
143
145
|
return Number.isNaN(ms) ? null : ms;
|
|
144
146
|
}
|
|
@@ -149,12 +151,18 @@ function stepHeaders(lines: string[]): StepHeader[] {
|
|
|
149
151
|
let depth = 0;
|
|
150
152
|
for (const [i, line] of lines.entries()) {
|
|
151
153
|
if (line.includes("##[endgroup]")) {
|
|
152
|
-
if (depth > 0)
|
|
154
|
+
if (depth > 0) {
|
|
155
|
+
depth--;
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!line.includes("##[group]")) {
|
|
153
160
|
continue;
|
|
154
161
|
}
|
|
155
|
-
if (!line.includes("##[group]")) continue;
|
|
156
162
|
depth++;
|
|
157
|
-
if (depth !== 1)
|
|
163
|
+
if (depth !== 1) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
158
166
|
const name = /##\[group\](.*)/.exec(line)?.[1].trim() ?? "";
|
|
159
167
|
if (HEADER_PREFIX_RE.test(name)) {
|
|
160
168
|
headers.push({
|
|
@@ -252,7 +260,9 @@ function alignStepsToHeaders(
|
|
|
252
260
|
|
|
253
261
|
/** Exclusive end index with trailing blank lines dropped, so a span slices to real text. */
|
|
254
262
|
function trimTrailingBlankLines(lines: string[], start: number, end: number): number {
|
|
255
|
-
while (end > start && (lines[end - 1] ?? "").trim() === "")
|
|
263
|
+
while (end > start && (lines[end - 1] ?? "").trim() === "") {
|
|
264
|
+
end--;
|
|
265
|
+
}
|
|
256
266
|
return end;
|
|
257
267
|
}
|
|
258
268
|
|
|
@@ -306,7 +316,9 @@ export function extractStepFromLog(
|
|
|
306
316
|
apiSteps: readonly { number: number; name: string }[],
|
|
307
317
|
): string | null {
|
|
308
318
|
const span = stepLineSpans(log, apiSteps).get(stepNumber);
|
|
309
|
-
if (span === undefined)
|
|
319
|
+
if (span === undefined) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
310
322
|
return log.split("\n").slice(span.start, span.end).join("\n").trimEnd();
|
|
311
323
|
}
|
|
312
324
|
|
|
@@ -382,7 +394,9 @@ async function ciLogs(gh: GhClient, call: ToolCall<JobIdParams>): Promise<ToolRe
|
|
|
382
394
|
target = await gh.checks.job(owner, name, jobId, signal);
|
|
383
395
|
} catch (error) {
|
|
384
396
|
const status = (error as { status?: number }).status;
|
|
385
|
-
if (status !== 404)
|
|
397
|
+
if (status !== 404) {
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
386
400
|
return failure(
|
|
387
401
|
`Job ${jobId} not found in ${effectiveRepo} — job IDs come from \`get-github-workflow-jobs\`.`,
|
|
388
402
|
);
|
|
@@ -15,7 +15,9 @@ export function addReadRepoTool(_gh: GhClient, pi: ExtensionAPI) {
|
|
|
15
15
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
16
16
|
const { repo } = params;
|
|
17
17
|
const args = ["repo", "view"];
|
|
18
|
-
if (repo)
|
|
18
|
+
if (repo) {
|
|
19
|
+
args.push(repo);
|
|
20
|
+
}
|
|
19
21
|
const result = toToolResult(
|
|
20
22
|
await ghExec(args, { cwd: ctx.cwd, signal, input: params }),
|
|
21
23
|
params,
|
package/src/lib/cli-args.ts
CHANGED
|
@@ -35,7 +35,9 @@ export function shlexSplit(raw: string): string[] {
|
|
|
35
35
|
continue;
|
|
36
36
|
}
|
|
37
37
|
if (c === "\\") {
|
|
38
|
-
if (i + 1 < raw.length)
|
|
38
|
+
if (i + 1 < raw.length) {
|
|
39
|
+
cur += raw[i + 1];
|
|
40
|
+
}
|
|
39
41
|
i += 2;
|
|
40
42
|
continue;
|
|
41
43
|
}
|
|
@@ -53,6 +55,8 @@ export function shlexSplit(raw: string): string[] {
|
|
|
53
55
|
if (quote !== undefined) {
|
|
54
56
|
throw new SyntaxError(`unterminated quote in command arguments: ${raw}`);
|
|
55
57
|
}
|
|
56
|
-
if (cur)
|
|
58
|
+
if (cur) {
|
|
59
|
+
tokens.push(cur);
|
|
60
|
+
}
|
|
57
61
|
return tokens;
|
|
58
62
|
}
|
package/src/lib/cli.ts
CHANGED
|
@@ -84,16 +84,28 @@ interface FlagInfo {
|
|
|
84
84
|
|
|
85
85
|
/** schema 的种类名,仅用于报错文案。 */
|
|
86
86
|
function schemaKindLabel(schema: TSchema): string {
|
|
87
|
-
if ("~kind" in schema && typeof schema["~kind"] === "string")
|
|
88
|
-
|
|
87
|
+
if ("~kind" in schema && typeof schema["~kind"] === "string") {
|
|
88
|
+
return schema["~kind"];
|
|
89
|
+
}
|
|
90
|
+
if ("type" in schema && typeof schema.type === "string") {
|
|
91
|
+
return schema.type;
|
|
92
|
+
}
|
|
89
93
|
return "unknown";
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
function kindOf(key: string, schema: TSchema): FlagKind {
|
|
93
|
-
if (IsKind(schema, "Boolean"))
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (IsKind(schema, "
|
|
97
|
+
if (IsKind(schema, "Boolean")) {
|
|
98
|
+
return "boolean";
|
|
99
|
+
}
|
|
100
|
+
if (IsKind(schema, "String")) {
|
|
101
|
+
return "string";
|
|
102
|
+
}
|
|
103
|
+
if (IsKind(schema, "Number") || IsKind(schema, "Integer")) {
|
|
104
|
+
return "number";
|
|
105
|
+
}
|
|
106
|
+
if (IsKind(schema, "Union")) {
|
|
107
|
+
return "enum";
|
|
108
|
+
}
|
|
97
109
|
throw new TypeError(
|
|
98
110
|
`Unsupported flag type for '${key}': ${schemaKindLabel(schema)} ` +
|
|
99
111
|
"(use Type.Boolean/String/Number/Integer or a string literal union)",
|
|
@@ -118,10 +130,14 @@ function flagDescription(schema: TSchema): string {
|
|
|
118
130
|
|
|
119
131
|
/** Allowed values for a string-literal union flag, or undefined for mixed unions. */
|
|
120
132
|
function enumValues(schema: TSchema): string[] | undefined {
|
|
121
|
-
if (!("anyOf" in schema) || !isUnknownArray(schema.anyOf))
|
|
133
|
+
if (!("anyOf" in schema) || !isUnknownArray(schema.anyOf)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
122
136
|
const values: string[] = [];
|
|
123
137
|
for (const variant of schema.anyOf) {
|
|
124
|
-
if (!isRecord(variant) || typeof variant.const !== "string")
|
|
138
|
+
if (!isRecord(variant) || typeof variant.const !== "string") {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
125
141
|
values.push(variant.const);
|
|
126
142
|
}
|
|
127
143
|
return values;
|
|
@@ -164,7 +180,9 @@ function helpResult<TFlags extends TObject>(
|
|
|
164
180
|
autoHelp: boolean,
|
|
165
181
|
): CommandResult<TFlags> {
|
|
166
182
|
const lines = [`Usage: /${spec.name} ${spec.usage}`];
|
|
167
|
-
if (spec.description)
|
|
183
|
+
if (spec.description) {
|
|
184
|
+
lines.push("", spec.description);
|
|
185
|
+
}
|
|
168
186
|
const rows = flags.map((f) => ({
|
|
169
187
|
rawName:
|
|
170
188
|
(f.short ? `-${f.short}, ` : "") +
|
|
@@ -172,15 +190,21 @@ function helpResult<TFlags extends TObject>(
|
|
|
172
190
|
(f.kind === "boolean" ? "" : ` ${f.placeholder}`),
|
|
173
191
|
description: f.description,
|
|
174
192
|
}));
|
|
175
|
-
if (autoHelp)
|
|
193
|
+
if (autoHelp) {
|
|
194
|
+
rows.push({ rawName: "-h, --help", description: "Display this message" });
|
|
195
|
+
}
|
|
176
196
|
if (rows.length > 0) {
|
|
177
197
|
lines.push("", "Options:");
|
|
178
198
|
const width = Math.max(...rows.map((r) => r.rawName.length));
|
|
179
|
-
for (const r of rows)
|
|
199
|
+
for (const r of rows) {
|
|
200
|
+
lines.push(` ${r.rawName.padEnd(width)} ${r.description}`);
|
|
201
|
+
}
|
|
180
202
|
}
|
|
181
203
|
if (spec.examples?.length) {
|
|
182
204
|
lines.push("", "Examples:");
|
|
183
|
-
for (const e of spec.examples)
|
|
205
|
+
for (const e of spec.examples) {
|
|
206
|
+
lines.push(` ${e}`);
|
|
207
|
+
}
|
|
184
208
|
}
|
|
185
209
|
return { kind: "help", text: lines.join("\n") };
|
|
186
210
|
}
|
|
@@ -233,9 +257,13 @@ export function parseCommand<TFlags extends TObject>(
|
|
|
233
257
|
const body = token.slice(2);
|
|
234
258
|
const eq = body.indexOf("=");
|
|
235
259
|
const name = eq === -1 ? body : body.slice(0, eq);
|
|
236
|
-
if (autoHelp && name === "help")
|
|
260
|
+
if (autoHelp && name === "help") {
|
|
261
|
+
return helpResult(spec, flags, autoHelp);
|
|
262
|
+
}
|
|
237
263
|
const info = byLong.get(name);
|
|
238
|
-
if (!info)
|
|
264
|
+
if (!info) {
|
|
265
|
+
return errorResult(spec, `Unknown option '--${name}'`);
|
|
266
|
+
}
|
|
239
267
|
const inline = eq === -1 ? undefined : body.slice(eq + 1);
|
|
240
268
|
if (inline !== undefined) {
|
|
241
269
|
rawFlags[info.key] = inline;
|
|
@@ -258,9 +286,13 @@ export function parseCommand<TFlags extends TObject>(
|
|
|
258
286
|
const rest = token.slice(1);
|
|
259
287
|
for (let j = 0; j < rest.length; j++) {
|
|
260
288
|
const c = rest[j];
|
|
261
|
-
if (autoHelp && c === "h")
|
|
289
|
+
if (autoHelp && c === "h") {
|
|
290
|
+
return helpResult(spec, flags, autoHelp);
|
|
291
|
+
}
|
|
262
292
|
const info = byShort.get(c);
|
|
263
|
-
if (!info)
|
|
293
|
+
if (!info) {
|
|
294
|
+
return errorResult(spec, `Unknown option '-${c}'`);
|
|
295
|
+
}
|
|
264
296
|
if (info.kind === "boolean") {
|
|
265
297
|
rawFlags[info.key] = true;
|
|
266
298
|
continue;
|
package/src/lib/file-reads.ts
CHANGED
|
@@ -64,7 +64,9 @@ export async function digestIfExists(filePath: string): Promise<string | undefin
|
|
|
64
64
|
try {
|
|
65
65
|
return await fileDigest(filePath);
|
|
66
66
|
} catch (error) {
|
|
67
|
-
if (isMissingPath(error))
|
|
67
|
+
if (isMissingPath(error)) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
68
70
|
throw error;
|
|
69
71
|
}
|
|
70
72
|
}
|
|
@@ -77,7 +79,9 @@ export async function readStateKey(filePath: string): Promise<string> {
|
|
|
77
79
|
try {
|
|
78
80
|
return await realpath(filePath);
|
|
79
81
|
} catch (error) {
|
|
80
|
-
if (isMissingPath(error))
|
|
82
|
+
if (isMissingPath(error)) {
|
|
83
|
+
return filePath;
|
|
84
|
+
}
|
|
81
85
|
throw error;
|
|
82
86
|
}
|
|
83
87
|
}
|
|
@@ -120,7 +124,9 @@ export function requireUnchangedRead(
|
|
|
120
124
|
currentDigest: string | undefined,
|
|
121
125
|
): void {
|
|
122
126
|
const readSnapshot = state.reads.get(key);
|
|
123
|
-
if (!readSnapshot)
|
|
127
|
+
if (!readSnapshot) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
124
130
|
if (currentDigest === undefined || readSnapshot.digest !== currentDigest) {
|
|
125
131
|
throw new Error(
|
|
126
132
|
"File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.",
|
|
@@ -135,9 +141,13 @@ export function requireUnchangedRead(
|
|
|
135
141
|
*/
|
|
136
142
|
export function deserializeReads(data: unknown): Map<string, FileSnapshot> {
|
|
137
143
|
const reads = new Map<string, FileSnapshot>();
|
|
138
|
-
if (typeof data !== "object" || data === null || Array.isArray(data))
|
|
144
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
145
|
+
return reads;
|
|
146
|
+
}
|
|
139
147
|
for (const [filePath, snapshot] of Object.entries(data)) {
|
|
140
|
-
if (Value.Check(fileSnapshotSchema, snapshot))
|
|
148
|
+
if (Value.Check(fileSnapshotSchema, snapshot)) {
|
|
149
|
+
reads.set(filePath, snapshot);
|
|
150
|
+
}
|
|
141
151
|
}
|
|
142
152
|
return reads;
|
|
143
153
|
}
|
|
@@ -153,10 +163,16 @@ export function restoreReads(
|
|
|
153
163
|
): void {
|
|
154
164
|
state.reads.clear();
|
|
155
165
|
for (const entry of sessionManager.getBranch()) {
|
|
156
|
-
if (entry.type !== "message" || entry.message.role !== "toolResult")
|
|
157
|
-
|
|
166
|
+
if (entry.type !== "message" || entry.message.role !== "toolResult") {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!toolNames.has(entry.message.toolName)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
158
172
|
const details = entry.message.details;
|
|
159
|
-
if (!isRecord(details) || !details.reads)
|
|
173
|
+
if (!isRecord(details) || !details.reads) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
160
176
|
for (const [filePath, snapshot] of deserializeReads(details.reads)) {
|
|
161
177
|
state.reads.set(filePath, snapshot);
|
|
162
178
|
}
|