@trim21/personal-pi-extensions 0.1.558 → 0.1.560
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/claude-code/files.ts +15 -10
- package/src/claude-code/read.md +1 -0
- package/src/opencode/files.ts +67 -6
package/package.json
CHANGED
package/src/claude-code/files.ts
CHANGED
|
@@ -118,6 +118,10 @@ function splitFileLines(content: string): string[] {
|
|
|
118
118
|
* 的 `N\t` 前缀——tab 分隔符在 Go 等 tab 缩进语言里会与内容缩进连排,模型
|
|
119
119
|
* 易误判多一层缩进。limit 未指定时读取全部。无 PARTIAL 提示、无单行截断
|
|
120
120
|
* (由 execute 层的字节/token 上限兜底)。
|
|
121
|
+
*
|
|
122
|
+
* offset 为负数时从文件末尾倒数(`offset=-5` 等价 `tail -n 5`),超出文件
|
|
123
|
+
* 长度时钳到文件开头(与 tail 一致,不告警)。输出行号始终为绝对行号;
|
|
124
|
+
* offset=0 的「行号从 0 起」是对齐 Claude Code lineOffset 的历史语义。
|
|
121
125
|
*/
|
|
122
126
|
export function formatReadOutput(
|
|
123
127
|
content: string,
|
|
@@ -138,11 +142,11 @@ export function formatReadOutput(
|
|
|
138
142
|
totalLines,
|
|
139
143
|
};
|
|
140
144
|
}
|
|
141
|
-
|
|
142
|
-
const
|
|
145
|
+
const startIndex = offset === 0 ? 0 : offset > 0 ? offset - 1 : Math.max(totalLines + offset, 0);
|
|
146
|
+
const firstLine = offset === 0 ? 0 : startIndex + 1;
|
|
143
147
|
const selected =
|
|
144
148
|
limit === undefined ? lines.slice(startIndex) : lines.slice(startIndex, startIndex + limit);
|
|
145
|
-
const text = selected.map((line, index) => `${
|
|
149
|
+
const text = selected.map((line, index) => `${firstLine + index}: ${line}`).join("\n");
|
|
146
150
|
return { text, totalLines };
|
|
147
151
|
}
|
|
148
152
|
|
|
@@ -234,6 +238,7 @@ export function registerFileTools(
|
|
|
234
238
|
"Reads a file from the local filesystem. You can access any file directly using this tool.",
|
|
235
239
|
"Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.",
|
|
236
240
|
"The file_path parameter must be an absolute path. By default, it reads the entire file; files over 256 KB or 25K tokens require offset and limit.",
|
|
241
|
+
"Read parts of files with offset and limit instead of running head/tail via Bash: a positive offset is the starting line number, a negative offset counts from the end of the file (offset=-5 reads the last 5 lines, like tail -n 5).",
|
|
237
242
|
"Results use cat -n style line numbers starting at 1. Images are returned visually.",
|
|
238
243
|
"This tool reads files, not directories.",
|
|
239
244
|
].join("\n"),
|
|
@@ -243,9 +248,12 @@ export function registerFileTools(
|
|
|
243
248
|
{
|
|
244
249
|
file_path: Type.String({ description: "The absolute path to the file to read" }),
|
|
245
250
|
offset: Type.Optional(
|
|
246
|
-
Type.
|
|
251
|
+
Type.Integer({
|
|
252
|
+
description:
|
|
253
|
+
"The line number to start reading from. A negative value counts from the end of the file (offset=-5 starts 5 lines before EOF).",
|
|
254
|
+
}),
|
|
247
255
|
),
|
|
248
|
-
limit: Type.Optional(Type.
|
|
256
|
+
limit: Type.Optional(Type.Integer({ description: "The number of lines to read" })),
|
|
249
257
|
pages: Type.Optional(
|
|
250
258
|
Type.String({ description: 'Page range for PDF files (for example "1-5" or "3")' }),
|
|
251
259
|
),
|
|
@@ -255,11 +263,8 @@ export function registerFileTools(
|
|
|
255
263
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
256
264
|
signal?.throwIfAborted();
|
|
257
265
|
const filePath = requireAbsolutePath(params.file_path);
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
(!Number.isSafeInteger(params.offset) || params.offset < 0)
|
|
261
|
-
) {
|
|
262
|
-
throw new Error("offset must be a non-negative integer");
|
|
266
|
+
if (params.offset !== undefined && !Number.isSafeInteger(params.offset)) {
|
|
267
|
+
throw new Error("offset must be an integer");
|
|
263
268
|
}
|
|
264
269
|
if (params.limit !== undefined && (!Number.isSafeInteger(params.limit) || params.limit < 1)) {
|
|
265
270
|
throw new Error("limit must be a positive integer");
|
package/src/claude-code/read.md
CHANGED
|
@@ -7,6 +7,7 @@ Usage:
|
|
|
7
7
|
- The file_path parameter must be an absolute path, not a relative path
|
|
8
8
|
- By default, it reads the entire file; files over 256 KB or 25K tokens require offset and limit to read specific portions
|
|
9
9
|
- When you already know which part of the file you need, only read that part. This can be important for larger files.
|
|
10
|
+
- Do not use head/tail (or sed -n) via the Bash tool to read parts of files — use offset and limit here instead. They are line-based: `offset=5, limit=10` reads 10 lines starting at line 5 (like `sed -n '5,14p'`), and a negative offset counts from the end of the file — `offset=-5` reads the last 5 lines (like `tail -n 5`). Line numbers in the output are always absolute.
|
|
10
11
|
- Results are returned as `<lineNumber>: <content>` lines, 1-indexed
|
|
11
12
|
- This tool allows you to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually.
|
|
12
13
|
- This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool.
|
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;
|