@vincemakes/kiso-tools-node 0.1.26 → 0.1.27
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/dist/index.d.ts +7 -0
- package/dist/index.js +105 -12
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* only medium). No shell flags the kernel doesn't need: `shell` carries an
|
|
11
11
|
* explicit timeout and an output cap so a runaway command cannot flood the
|
|
12
12
|
* context.
|
|
13
|
+
*
|
|
14
|
+
* Token 轮: reads are RANGEABLE (read_file offset/limit, default head 200
|
|
15
|
+
* lines) and search/list are capped (50 / 200) — every truncation carries
|
|
16
|
+
* an actionable continuation note (deterministic per file state), so the
|
|
17
|
+
* model always has a path to the full content.
|
|
13
18
|
*/
|
|
14
19
|
import { type Tool, type ToolResult } from "@vincemakes/kiso-core";
|
|
15
20
|
/**
|
|
@@ -54,6 +59,8 @@ export interface WorkspaceToolsOptions {
|
|
|
54
59
|
}
|
|
55
60
|
export declare function readFileTool(opts: WorkspaceToolsOptions): Tool<{
|
|
56
61
|
path: string;
|
|
62
|
+
offset?: number;
|
|
63
|
+
limit?: number;
|
|
57
64
|
}>;
|
|
58
65
|
export declare function listDirTool(opts: WorkspaceToolsOptions): Tool<{
|
|
59
66
|
path?: string;
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* only medium). No shell flags the kernel doesn't need: `shell` carries an
|
|
11
11
|
* explicit timeout and an output cap so a runaway command cannot flood the
|
|
12
12
|
* context.
|
|
13
|
+
*
|
|
14
|
+
* Token 轮: reads are RANGEABLE (read_file offset/limit, default head 200
|
|
15
|
+
* lines) and search/list are capped (50 / 200) — every truncation carries
|
|
16
|
+
* an actionable continuation note (deterministic per file state), so the
|
|
17
|
+
* model always has a path to the full content.
|
|
13
18
|
*/
|
|
14
19
|
import { execFileSync, spawn } from "node:child_process";
|
|
15
20
|
import { chmodSync, existsSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
@@ -17,6 +22,14 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:pat
|
|
|
17
22
|
import { defineTool } from "@vincemakes/kiso-core";
|
|
18
23
|
const OUTPUT_CAP = 100_000; // chars of output a tool result may carry
|
|
19
24
|
const DEFAULT_SHELL_TIMEOUT_MS = 30_000;
|
|
25
|
+
// Token 轮: the scoped-read defaults — read_file shows the head 200 lines
|
|
26
|
+
// of a large file (with an actionable continuation note, never a silent
|
|
27
|
+
// drop), search_text caps at 50 excerpts, list_dir at 200 entries. The
|
|
28
|
+
// red line: every truncation names its continuation — the model always
|
|
29
|
+
// has a path to the full content.
|
|
30
|
+
const DEFAULT_READ_LINES = 200;
|
|
31
|
+
const MAX_SEARCH_MATCHES = 50;
|
|
32
|
+
const MAX_DIR_ENTRIES = 200;
|
|
20
33
|
function cap(text) {
|
|
21
34
|
return text.length > OUTPUT_CAP ? `${text.slice(0, OUTPUT_CAP)}\n…[truncated]` : text;
|
|
22
35
|
}
|
|
@@ -156,24 +169,92 @@ function inodeReadPolicy(root, full) {
|
|
|
156
169
|
}
|
|
157
170
|
return null;
|
|
158
171
|
}
|
|
172
|
+
/** The "… N more lines" note — the actionable continuation: the exact
|
|
173
|
+
* line the next read must start at, so the model can always reach the
|
|
174
|
+
* full content in ranges (the red line). */
|
|
175
|
+
function moreLinesNote(nextOffset, remaining) {
|
|
176
|
+
return `\n… ${remaining} more ${remaining === 1 ? "line" : "lines"} (call again with offset=${nextOffset})`;
|
|
177
|
+
}
|
|
159
178
|
export function readFileTool(opts) {
|
|
160
179
|
return defineTool({
|
|
161
180
|
name: "read_file",
|
|
162
|
-
description: "Read a file's content from disk. Relative to the workspace root.",
|
|
181
|
+
description: "Read a file's content from disk. Relative to the workspace root. Returns the first 200 lines by default; a file with more lines appends a note with the exact count and the offset to continue from. Pass offset (1-based first line) and/or limit (line count) to read a range.",
|
|
163
182
|
parameters: {
|
|
164
183
|
type: "object",
|
|
165
|
-
properties: {
|
|
184
|
+
properties: {
|
|
185
|
+
path: { type: "string", description: "Workspace-relative path of the file to read" },
|
|
186
|
+
offset: { type: "number", description: "1-based first line to read (default: 1)" },
|
|
187
|
+
limit: { type: "number", description: "Maximum number of lines to read (default: to the end of the file)" },
|
|
188
|
+
},
|
|
166
189
|
required: ["path"],
|
|
167
190
|
},
|
|
168
191
|
idempotent: true,
|
|
169
|
-
execute: async ({ path }) => {
|
|
192
|
+
execute: async ({ path, offset, limit }) => {
|
|
170
193
|
try {
|
|
171
194
|
const full = resolveWithinRoot(opts.workspaceRoot, path);
|
|
172
195
|
const denied = inodeReadPolicy(opts.workspaceRoot, full);
|
|
173
196
|
if (denied !== null)
|
|
174
197
|
return escapeResult(denied);
|
|
175
198
|
const content = readFileSync(full, "utf8");
|
|
176
|
-
|
|
199
|
+
// The lines the file DISPLAYS: a trailing newline's empty split
|
|
200
|
+
// element is not a line. Line k = split[k-1], 1-based.
|
|
201
|
+
const parts = content.split("\n");
|
|
202
|
+
const total = content.endsWith("\n") ? parts.length - 1 : parts.length;
|
|
203
|
+
const badCount = (v, name) => typeof v !== "number" || !Number.isInteger(v) || v < 1
|
|
204
|
+
? `read_file: ${name} must be a positive integer (got ${JSON.stringify(v)})`
|
|
205
|
+
: undefined;
|
|
206
|
+
if (offset !== undefined) {
|
|
207
|
+
const bad = badCount(offset, "offset");
|
|
208
|
+
if (bad !== undefined)
|
|
209
|
+
return { content: bad, isError: true, errorKind: "invalid_input" };
|
|
210
|
+
}
|
|
211
|
+
if (limit !== undefined) {
|
|
212
|
+
const bad = badCount(limit, "limit");
|
|
213
|
+
if (bad !== undefined)
|
|
214
|
+
return { content: bad, isError: true, errorKind: "invalid_input" };
|
|
215
|
+
}
|
|
216
|
+
const start = offset ?? 1;
|
|
217
|
+
if (start > total) {
|
|
218
|
+
return {
|
|
219
|
+
content: `read_file: offset=${start} is past the end of ${path} (${total} lines)`,
|
|
220
|
+
isError: true,
|
|
221
|
+
errorKind: "invalid_input",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const end = limit === undefined ? total : Math.min(start + limit - 1, total);
|
|
225
|
+
// DEFAULT: the head 200 lines; a larger file ends with the
|
|
226
|
+
// honest continuation note (small files ≤ 200 lines are
|
|
227
|
+
// byte-identical to the pre-token-round behavior).
|
|
228
|
+
let text;
|
|
229
|
+
let note = "";
|
|
230
|
+
if (offset === undefined && limit === undefined) {
|
|
231
|
+
text = total <= DEFAULT_READ_LINES ? content : parts.slice(0, DEFAULT_READ_LINES).join("\n");
|
|
232
|
+
if (total > DEFAULT_READ_LINES)
|
|
233
|
+
note = moreLinesNote(DEFAULT_READ_LINES + 1, total - DEFAULT_READ_LINES);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
text = parts.slice(start - 1, end).join("\n");
|
|
237
|
+
if (end < total)
|
|
238
|
+
note = moreLinesNote(end + 1, total - end);
|
|
239
|
+
}
|
|
240
|
+
// The output cap's cut must STAY actionable: cut at a line
|
|
241
|
+
// boundary and name the exact next offset (the generic cap()
|
|
242
|
+
// would leave the model blind mid-file).
|
|
243
|
+
if (text.length > OUTPUT_CAP) {
|
|
244
|
+
const cut = text.lastIndexOf("\n", OUTPUT_CAP);
|
|
245
|
+
if (cut > 0) {
|
|
246
|
+
text = text.slice(0, cut);
|
|
247
|
+
note = `\n… [output capped at ${OUTPUT_CAP} chars — continue with offset=${start + text.split("\n").length}]` + note;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// The first 100000 chars are one line (or a blank line
|
|
251
|
+
// then one): offset ranges cannot split it — shell is
|
|
252
|
+
// the only honest path.
|
|
253
|
+
text = text.slice(0, OUTPUT_CAP);
|
|
254
|
+
note = `\n… [a line near ${path}:${start} exceeds the ${OUTPUT_CAP}-char cap — slice it with shell]` + note;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { content: `${text}${note}`, isError: false };
|
|
177
258
|
}
|
|
178
259
|
catch (err) {
|
|
179
260
|
if (err instanceof PathEscapeError)
|
|
@@ -186,7 +267,7 @@ export function readFileTool(opts) {
|
|
|
186
267
|
export function listDirTool(opts) {
|
|
187
268
|
return defineTool({
|
|
188
269
|
name: "list_dir",
|
|
189
|
-
description: "List the entries of a directory. Omit path to list the workspace root.",
|
|
270
|
+
description: "List the entries of a directory. Omit path to list the workspace root. Capped at 200 entries with an overflow note (narrow to a subdirectory for more).",
|
|
190
271
|
parameters: {
|
|
191
272
|
type: "object",
|
|
192
273
|
properties: { path: { type: "string", description: "Workspace-relative directory to list" } },
|
|
@@ -199,7 +280,11 @@ export function listDirTool(opts) {
|
|
|
199
280
|
const isDir = e.isDirectory();
|
|
200
281
|
return `${isDir ? "dir " : "file"} ${e.name}${isDir ? "/" : ""}`;
|
|
201
282
|
});
|
|
202
|
-
|
|
283
|
+
let content = entries.length ? cap(entries.slice(0, MAX_DIR_ENTRIES).join("\n")) : "(empty directory)";
|
|
284
|
+
if (entries.length > MAX_DIR_ENTRIES) {
|
|
285
|
+
content += `\n… +${entries.length - MAX_DIR_ENTRIES} more entries (narrow to a subdirectory)`;
|
|
286
|
+
}
|
|
287
|
+
return { content, isError: false };
|
|
203
288
|
}
|
|
204
289
|
catch (err) {
|
|
205
290
|
if (err instanceof PathEscapeError)
|
|
@@ -212,7 +297,7 @@ export function listDirTool(opts) {
|
|
|
212
297
|
export function searchTextTool(opts) {
|
|
213
298
|
return defineTool({
|
|
214
299
|
name: "search_text",
|
|
215
|
-
description: "Search files under a workspace directory (recursive) for a regular expression. Returns matching file:line excerpts, capped.",
|
|
300
|
+
description: "Search files under a workspace directory (recursive) for a regular expression. Returns matching file:line excerpts, capped at 50 — an overflow note states the count of further matches (narrow the pattern to see them).",
|
|
216
301
|
parameters: {
|
|
217
302
|
type: "object",
|
|
218
303
|
properties: {
|
|
@@ -233,9 +318,13 @@ export function searchTextTool(opts) {
|
|
|
233
318
|
throw err;
|
|
234
319
|
}
|
|
235
320
|
const regex = new RegExp(pattern, "i");
|
|
321
|
+
// The walk NEVER early-aborts on the cap: the overflow note's count
|
|
322
|
+
// must be the file-true total, not a bound (the red line). The
|
|
323
|
+
// depth cap and the node_modules/dotfile skip stay.
|
|
236
324
|
const matches = [];
|
|
325
|
+
let totalMatches = 0;
|
|
237
326
|
const walk = (dir, depth) => {
|
|
238
|
-
if (depth > 8
|
|
327
|
+
if (depth > 8)
|
|
239
328
|
return;
|
|
240
329
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
241
330
|
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
@@ -257,9 +346,10 @@ export function searchTextTool(opts) {
|
|
|
257
346
|
const text = readFileSync(full, "utf8");
|
|
258
347
|
for (const [i, line] of text.split("\n").entries()) {
|
|
259
348
|
if (regex.test(line)) {
|
|
260
|
-
|
|
261
|
-
if (matches.length
|
|
262
|
-
|
|
349
|
+
totalMatches += 1;
|
|
350
|
+
if (matches.length < MAX_SEARCH_MATCHES) {
|
|
351
|
+
matches.push(`${full}:${i + 1}: ${line.trim().slice(0, 160)}`);
|
|
352
|
+
}
|
|
263
353
|
}
|
|
264
354
|
}
|
|
265
355
|
}
|
|
@@ -275,7 +365,10 @@ export function searchTextTool(opts) {
|
|
|
275
365
|
catch (err) {
|
|
276
366
|
return { content: `search_text failed: ${err.message}`, isError: true, errorKind: "fatal" };
|
|
277
367
|
}
|
|
278
|
-
|
|
368
|
+
let content = matches.length ? cap(matches.join("\n")) : "(no matches)";
|
|
369
|
+
if (totalMatches > matches.length)
|
|
370
|
+
content += `\n… +${totalMatches - matches.length} more matches (narrow the pattern)`;
|
|
371
|
+
return { content, isError: false };
|
|
279
372
|
},
|
|
280
373
|
});
|
|
281
374
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tools-node",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "kiso coding tools for Node hosts
|
|
3
|
+
"version": "0.1.27",
|
|
4
|
+
"description": "kiso coding tools for Node hosts — read file, list directory, search text, write/edit file, shell command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"exports": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@vincemakes/kiso-core": "0.1.
|
|
24
|
+
"@vincemakes/kiso-core": "0.1.27"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|