@trim21/personal-pi-extensions 0.1.559 → 0.1.561
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/bwrap/runtime.ts +17 -0
- package/src/claude-code/shell.ts +8 -3
- package/src/opencode/bash.ts +2 -1
- package/src/opencode/files.ts +67 -6
package/package.json
CHANGED
package/src/bwrap/runtime.ts
CHANGED
|
@@ -115,23 +115,35 @@ export class BashInterruptedError extends Error {
|
|
|
115
115
|
readonly kind: "timeout" | "aborted";
|
|
116
116
|
readonly partial: BashExecutionPartial;
|
|
117
117
|
readonly sandboxHint: string | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* 命令实际运行时长(毫秒):从审批结束、命令真正开始执行算到终止,
|
|
120
|
+
* 不含审批弹窗等用户 UI 交互耗时。
|
|
121
|
+
*/
|
|
122
|
+
readonly elapsedMs: number;
|
|
118
123
|
|
|
119
124
|
constructor(
|
|
120
125
|
kind: "timeout" | "aborted",
|
|
121
126
|
message: string,
|
|
122
127
|
partial: BashExecutionPartial,
|
|
123
128
|
sandboxHint: string | undefined,
|
|
129
|
+
elapsedMs: number,
|
|
124
130
|
cause: unknown,
|
|
125
131
|
) {
|
|
126
132
|
super(message, { cause });
|
|
127
133
|
this.kind = kind;
|
|
128
134
|
this.partial = partial;
|
|
129
135
|
this.sandboxHint = sandboxHint;
|
|
136
|
+
this.elapsedMs = elapsedMs;
|
|
130
137
|
// 对齐标准错误分类:中断=AbortError(用户取消),超时=TimeoutError
|
|
131
138
|
this.name = kind === "aborted" ? "AbortError" : "TimeoutError";
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
/** 命令运行时长的展示文案(秒,一位小数),超时/中断状态文本共用。 */
|
|
143
|
+
export function formatElapsedSeconds(elapsedMs: number): string {
|
|
144
|
+
return `${(elapsedMs / 1000).toFixed(1)} seconds`;
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
function escapeHtml(text: string): string {
|
|
136
148
|
return text
|
|
137
149
|
.replaceAll("&", "&")
|
|
@@ -425,6 +437,8 @@ export class BwrapRuntime {
|
|
|
425
437
|
}
|
|
426
438
|
// 命令失败时附带的沙箱状态(写边界 + 网络层级),由上层拼进错误文本
|
|
427
439
|
const sandboxHint = describeSandbox(runtime, local);
|
|
440
|
+
// 计时起点放在审批之后:审批弹窗的等待时长属于用户 UI 操作,不是命令运行时间
|
|
441
|
+
const startedAt = Date.now();
|
|
428
442
|
await using output = new BashOutput(request.ctx.sessionManager.getSessionId());
|
|
429
443
|
const { onUpdate } = request;
|
|
430
444
|
|
|
@@ -463,6 +477,7 @@ export class BwrapRuntime {
|
|
|
463
477
|
truncation: partial.truncation,
|
|
464
478
|
};
|
|
465
479
|
} catch (error) {
|
|
480
|
+
const elapsedMs = Date.now() - startedAt;
|
|
466
481
|
// 超时/中断:把命令终止前已捕获的输出附在错误上(文本 + 落盘路径),
|
|
467
482
|
// 展示时输出在前、状态在最后(对齐 pi 内置 bash),避免只报超时丢输出
|
|
468
483
|
// 超时识别:优先 name=TimeoutError(对齐标准错误分类),
|
|
@@ -477,6 +492,7 @@ export class BwrapRuntime {
|
|
|
477
492
|
`Command timed out after ${error.message.slice("timeout:".length)} seconds`,
|
|
478
493
|
partial,
|
|
479
494
|
sandboxHint,
|
|
495
|
+
elapsedMs,
|
|
480
496
|
error,
|
|
481
497
|
);
|
|
482
498
|
}
|
|
@@ -489,6 +505,7 @@ export class BwrapRuntime {
|
|
|
489
505
|
"Command aborted by user",
|
|
490
506
|
partial,
|
|
491
507
|
sandboxHint,
|
|
508
|
+
elapsedMs,
|
|
492
509
|
error,
|
|
493
510
|
);
|
|
494
511
|
}
|
package/src/claude-code/shell.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
BashInterruptedError,
|
|
15
15
|
type BwrapRuntime,
|
|
16
16
|
createBwrapRuntime,
|
|
17
|
+
formatElapsedSeconds,
|
|
17
18
|
sandboxHintBlock,
|
|
18
19
|
} from "../bwrap/runtime.js";
|
|
19
20
|
import { resolveWorkdir } from "../lib/path.js";
|
|
@@ -152,9 +153,13 @@ export function registerShellTools(
|
|
|
152
153
|
error.partial.fullOutputPath,
|
|
153
154
|
);
|
|
154
155
|
if (error.kind === "aborted") {
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
// 用户取消:直接返回已捕获的输出,不抛错;时长只算命令真正运行的时间,
|
|
157
|
+
// 不含审批弹窗等 UI 交互
|
|
158
|
+
const status = `Command aborted by user after ${formatElapsedSeconds(error.elapsedMs)}`;
|
|
159
|
+
return {
|
|
160
|
+
content: [{ type: "text", text: text ? `${text}\n\n${status}` : status }],
|
|
161
|
+
details: undefined,
|
|
162
|
+
};
|
|
158
163
|
}
|
|
159
164
|
const full = text
|
|
160
165
|
? `${text}\n\nCommand timed out after ${timeout} milliseconds`
|
package/src/opencode/bash.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
BashInterruptedError,
|
|
9
9
|
type BwrapRuntime,
|
|
10
10
|
createBwrapRuntime,
|
|
11
|
+
formatElapsedSeconds,
|
|
11
12
|
sandboxHintBlock,
|
|
12
13
|
} from "../bwrap/runtime.js";
|
|
13
14
|
import { resolveWorkdir } from "../lib/path.js";
|
|
@@ -114,7 +115,7 @@ export default function opencodeBash(
|
|
|
114
115
|
const status =
|
|
115
116
|
error.kind === "timeout"
|
|
116
117
|
? `Command exceeded timeout of ${timeout} ms. Retry with a larger timeout if the command is expected to take longer.`
|
|
117
|
-
:
|
|
118
|
+
: `Command aborted by user after ${formatElapsedSeconds(error.elapsedMs)}`;
|
|
118
119
|
const full = text ? `${text}\n\n${status}` : status;
|
|
119
120
|
// 对齐上游 opencode:超时与中断都不抛错,输出与状态文本一起返回;
|
|
120
121
|
// 超时可能是沙箱的网络限制导致的,附加沙箱状态(用户中断与沙箱无关)
|
package/src/opencode/files.ts
CHANGED
|
@@ -192,11 +192,17 @@ export interface LinePage {
|
|
|
192
192
|
* - per-line truncation to MAX_LINE_LENGTH
|
|
193
193
|
* - line cap via maxLines (more): keep scanning so count is the file total
|
|
194
194
|
* - byte cap via maxBytes (cut): stop immediately
|
|
195
|
+
*
|
|
196
|
+
* offset 为负数时从文件末尾倒数(`offset=-2` 等价 `tail -n 2`):流式单遍
|
|
197
|
+
* 扫描全程只保留末尾窗口,返回前把 offset 换算为正的绝对起始行号
|
|
198
|
+
* (`totalLines + offset + 1`,超出文件长度时钳到 1),行号/续读提示与
|
|
199
|
+
* 正向语义一致。
|
|
195
200
|
*/
|
|
196
201
|
export async function readLines(
|
|
197
202
|
filePath: string,
|
|
198
203
|
opts: { offset: number; limit: number; maxBytes?: number },
|
|
199
204
|
): Promise<LinePage> {
|
|
205
|
+
if (opts.offset < 0) return readTailLines(filePath, opts);
|
|
200
206
|
const start = opts.offset - 1;
|
|
201
207
|
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
202
208
|
const raw: string[] = [];
|
|
@@ -236,6 +242,59 @@ export async function readLines(
|
|
|
236
242
|
return { raw, count, cut, more, offset: opts.offset };
|
|
237
243
|
}
|
|
238
244
|
|
|
245
|
+
/**
|
|
246
|
+
* tail 语义的负 offset 实现:单遍流式扫描,环形缓冲只保留末尾 K 行
|
|
247
|
+
* (K = -offset),扫完后对窗口应用 limit 与字节上限,避免为取末尾几行
|
|
248
|
+
* 而把整个文件驻留内存。
|
|
249
|
+
*/
|
|
250
|
+
async function readTailLines(
|
|
251
|
+
filePath: string,
|
|
252
|
+
opts: { offset: number; limit: number; maxBytes?: number },
|
|
253
|
+
): Promise<LinePage> {
|
|
254
|
+
const windowSize = -opts.offset;
|
|
255
|
+
const window: string[] = [];
|
|
256
|
+
let head = 0;
|
|
257
|
+
let count = 0;
|
|
258
|
+
|
|
259
|
+
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
260
|
+
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
261
|
+
try {
|
|
262
|
+
for await (const text of rl) {
|
|
263
|
+
count += 1;
|
|
264
|
+
const line =
|
|
265
|
+
text.length > MAX_LINE_LENGTH ? text.slice(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX : text;
|
|
266
|
+
window.push(line);
|
|
267
|
+
if (window.length > windowSize) head += 1;
|
|
268
|
+
}
|
|
269
|
+
} finally {
|
|
270
|
+
rl.close();
|
|
271
|
+
stream.destroy();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const offset = Math.max(count + opts.offset + 1, 1);
|
|
275
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
276
|
+
const raw: string[] = [];
|
|
277
|
+
let bytes = 0;
|
|
278
|
+
let cut = false;
|
|
279
|
+
let more = false;
|
|
280
|
+
for (const line of window.slice(head)) {
|
|
281
|
+
if (raw.length >= opts.limit) {
|
|
282
|
+
more = true;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
const size = Buffer.byteLength(line, "utf8") + (raw.length > 0 ? 1 : 0);
|
|
286
|
+
if (bytes + size > maxBytes) {
|
|
287
|
+
cut = true;
|
|
288
|
+
more = true;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
raw.push(line);
|
|
292
|
+
bytes += size;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { raw, count, cut, more, offset };
|
|
296
|
+
}
|
|
297
|
+
|
|
239
298
|
function truncationFromPage(page: LinePage): TruncationResult {
|
|
240
299
|
const content = page.raw.join("\n");
|
|
241
300
|
return {
|
|
@@ -299,15 +358,17 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
299
358
|
pi.registerTool({
|
|
300
359
|
name: "read",
|
|
301
360
|
label: "read",
|
|
302
|
-
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. When you need the full file, continue with offset until complete.`,
|
|
361
|
+
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.`,
|
|
303
362
|
promptSnippet: "Read file contents",
|
|
304
|
-
promptGuidelines: [
|
|
363
|
+
promptGuidelines: [
|
|
364
|
+
"Use read to examine files instead of cat, sed, or tail. A negative offset reads from the end of the file (offset=-2 reads the last 2 lines).",
|
|
365
|
+
],
|
|
305
366
|
parameters: Type.Object({
|
|
306
367
|
filePath: Type.String({ description: "The absolute path to the file or directory to read" }),
|
|
307
368
|
offset: Type.Optional(
|
|
308
369
|
Type.Integer({
|
|
309
|
-
|
|
310
|
-
|
|
370
|
+
description:
|
|
371
|
+
"The line number to start reading from (1-indexed; 0 is treated as 1). A negative value counts from the end of the file (offset=-2 reads the last 2 lines).",
|
|
311
372
|
}),
|
|
312
373
|
),
|
|
313
374
|
limit: Type.Optional(
|
|
@@ -340,9 +401,9 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService): void
|
|
|
340
401
|
if (fileStat.isDirectory()) {
|
|
341
402
|
const entries = await formatDirectoryEntries(absolutePath);
|
|
342
403
|
const limitVal = limit ?? DEFAULT_MAX_LINES;
|
|
343
|
-
// opencode: params.offset || 1(0 视为 1
|
|
404
|
+
// opencode: params.offset || 1(0 视为 1);负数从末尾倒数
|
|
344
405
|
const offsetVal = offset || 1;
|
|
345
|
-
const start = offsetVal - 1;
|
|
406
|
+
const start = offsetVal < 0 ? Math.max(entries.length + offsetVal, 0) : offsetVal - 1;
|
|
346
407
|
const sliced = entries.slice(start, start + limitVal);
|
|
347
408
|
const totalEntries = entries.length;
|
|
348
409
|
const truncated = start + sliced.length < totalEntries;
|