@vincemakes/kiso-tools-node 0.1.30 → 0.1.32

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 CHANGED
@@ -12,9 +12,10 @@
12
12
  * context.
13
13
  *
14
14
  * the token round: 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.
15
+ * lines), search/list are capped (50 / 200), shell output is capped — every
16
+ * truncation carries an actionable continuation note in the N of M form and
17
+ * states what was dropped (deterministic per file state), so the model
18
+ * always has a path to the full content.
18
19
  */
19
20
  import { type Tool, type ToolResult } from "@vincemakes/kiso-core";
20
21
  /**
package/dist/index.js CHANGED
@@ -12,9 +12,10 @@
12
12
  * context.
13
13
  *
14
14
  * the token round: 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.
15
+ * lines), search/list are capped (50 / 200), shell output is capped — every
16
+ * truncation carries an actionable continuation note in the N of M form and
17
+ * states what was dropped (deterministic per file state), so the model
18
+ * always has a path to the full content.
18
19
  */
19
20
  import { execFileSync, spawn } from "node:child_process";
20
21
  import { chmodSync, existsSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
@@ -33,6 +34,17 @@ const MAX_DIR_ENTRIES = 200;
33
34
  function cap(text) {
34
35
  return text.length > OUTPUT_CAP ? `${text.slice(0, OUTPUT_CAP)}\n…[truncated]` : text;
35
36
  }
37
+ /**
38
+ * R-C item 2: chunked accumulation under the cap that COUNTS what it drops —
39
+ * the shell overflow note names the dropped bytes and the recovery path
40
+ * (the W10 continuation, made model-facing: what was dropped, how many).
41
+ */
42
+ function capAccumulate(current, chunk) {
43
+ const room = OUTPUT_CAP - current.length;
44
+ if (room >= chunk.length)
45
+ return { text: current + chunk, dropped: 0 };
46
+ return { text: room <= 0 ? current : current + chunk.slice(0, room), dropped: chunk.length - Math.max(room, 0) };
47
+ }
36
48
  /**
37
49
  * A path that the workspace boundary refuses — never attempted, reported as
38
50
  * a precondition (the tool COULD run it, the gate refused it).
@@ -178,7 +190,7 @@ function moreLinesNote(nextOffset, remaining) {
178
190
  export function readFileTool(opts) {
179
191
  return defineTool({
180
192
  name: "read_file",
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.",
193
+ description: "Read a file's content from disk (the workspace reader — prefer it over shell cat/head/tail). 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.",
182
194
  parameters: {
183
195
  type: "object",
184
196
  properties: {
@@ -189,6 +201,8 @@ export function readFileTool(opts) {
189
201
  required: ["path"],
190
202
  },
191
203
  idempotent: true,
204
+ promptSnippet: "read_file — whole files or offset/limit ranges, workspace-relative paths",
205
+ promptGuidelines: ["read only the range you need — offset/limit beat whole-file reads"],
192
206
  execute: async ({ path, offset, limit }) => {
193
207
  try {
194
208
  const full = resolveWithinRoot(opts.workspaceRoot, path);
@@ -273,6 +287,8 @@ export function listDirTool(opts) {
273
287
  properties: { path: { type: "string", description: "Workspace-relative directory to list" } },
274
288
  },
275
289
  idempotent: true,
290
+ promptSnippet: "list_dir — directory entries (the workspace ls)",
291
+ promptGuidelines: ["narrow to a subdirectory when the listing caps at 200 entries"],
276
292
  execute: async ({ path }) => {
277
293
  try {
278
294
  const dir = resolveWithinRoot(opts.workspaceRoot, path ?? ".");
@@ -282,7 +298,9 @@ export function listDirTool(opts) {
282
298
  });
283
299
  let content = entries.length ? cap(entries.slice(0, MAX_DIR_ENTRIES).join("\n")) : "(empty directory)";
284
300
  if (entries.length > MAX_DIR_ENTRIES) {
285
- content += `\n… +${entries.length - MAX_DIR_ENTRIES} more entries (narrow to a subdirectory)`;
301
+ // R-C item 2: the N of M form the cap names its
302
+ // continuation (narrow to a subdirectory for more).
303
+ content += `\n… ${MAX_DIR_ENTRIES} of ${entries.length} entries shown (narrow to a subdirectory for more)`;
286
304
  }
287
305
  return { content, isError: false };
288
306
  }
@@ -297,7 +315,7 @@ export function listDirTool(opts) {
297
315
  export function searchTextTool(opts) {
298
316
  return defineTool({
299
317
  name: "search_text",
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).",
318
+ description: "Search files under a workspace directory (recursive) for a regular expression (the workspace grep — prefer it over shell grep/rg). Returns matching file:line excerpts, capped at 50 — an overflow note states the count of further matches (narrow the pattern to see them).",
301
319
  parameters: {
302
320
  type: "object",
303
321
  properties: {
@@ -307,6 +325,8 @@ export function searchTextTool(opts) {
307
325
  required: ["pattern"],
308
326
  },
309
327
  idempotent: true,
328
+ promptSnippet: "search_text — regex search over workspace files",
329
+ promptGuidelines: ["narrow the pattern when the result caps — never re-run a broad search"],
310
330
  execute: async ({ pattern, path }) => {
311
331
  let root;
312
332
  try {
@@ -366,8 +386,11 @@ export function searchTextTool(opts) {
366
386
  return { content: `search_text failed: ${err.message}`, isError: true, errorKind: "fatal" };
367
387
  }
368
388
  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)`;
389
+ if (totalMatches > matches.length) {
390
+ // R-C item 2: the N of M form the cap names its
391
+ // continuation (narrow the pattern for more).
392
+ content += `\n… ${matches.length} of ${totalMatches} matches shown (narrow the pattern for more)`;
393
+ }
371
394
  return { content, isError: false };
372
395
  },
373
396
  });
@@ -384,6 +407,7 @@ export function writeFileTool(opts) {
384
407
  },
385
408
  required: ["path", "content"],
386
409
  },
410
+ promptSnippet: "write_file — write a whole file (creates parent directories)",
387
411
  execute: async ({ path, content }) => {
388
412
  let full;
389
413
  try {
@@ -450,6 +474,8 @@ export function editFileTool(opts) {
450
474
  },
451
475
  required: ["path", "search", "replace"],
452
476
  },
477
+ promptSnippet: "edit_file — replace an exact old_string block (never rewrite whole files)",
478
+ promptGuidelines: ["the search text must match the file EXACTLY — read the target first"],
453
479
  execute: async ({ path, search, replace }) => {
454
480
  let full;
455
481
  try {
@@ -532,6 +558,8 @@ export function shellTool(opts) {
532
558
  },
533
559
  required: ["command"],
534
560
  },
561
+ promptSnippet: "shell — real system commands only (builds, tests, git)",
562
+ promptGuidelines: ["commands run in the workspace root; on failure read the error and adjust — never repeat blindly"],
535
563
  execute: async ({ command, timeoutMs }, ctx) => {
536
564
  const timeout = timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
537
565
  // E group: a PRE-aborted signal never spawns the command.
@@ -554,6 +582,8 @@ export function shellTool(opts) {
554
582
  });
555
583
  let stdout = "";
556
584
  let stderr = "";
585
+ let stdoutDropped = 0;
586
+ let stderrDropped = 0;
557
587
  let exited = false;
558
588
  let settled = false;
559
589
  let killing = false;
@@ -665,10 +695,14 @@ export function shellTool(opts) {
665
695
  });
666
696
  });
667
697
  child.stdout?.on("data", (d) => {
668
- stdout = cap(stdout + d.toString());
698
+ const r = capAccumulate(stdout, d.toString());
699
+ stdout = r.text;
700
+ stdoutDropped += r.dropped;
669
701
  });
670
702
  child.stderr?.on("data", (d) => {
671
- stderr = cap(stderr + d.toString());
703
+ const r = capAccumulate(stderr, d.toString());
704
+ stderr = r.text;
705
+ stderrDropped += r.dropped;
672
706
  });
673
707
  child.on("error", (err) => {
674
708
  settle({ content: `shell failed: ${err.message}`, isError: true, errorKind: "fatal" });
@@ -677,7 +711,16 @@ export function shellTool(opts) {
677
711
  exited = true;
678
712
  if (killing)
679
713
  return; // the timeout/abort verdict owns the result
680
- const combined = (stdout + (stderr ? `\n[stderr] ${stderr}` : "")).trim();
714
+ // R-C item 2: the overflow note names WHAT was dropped and
715
+ // the recovery path — a silent tail-cut would be the exact
716
+ // destructive class this round kills (W10 made model-facing).
717
+ const overflowNote = (dropped, stream) => dropped === 0
718
+ ? ""
719
+ : `\n… [${stream} capped at ${OUTPUT_CAP} chars — ${dropped} more chars dropped; capture to a file and read it with read_file, or narrow the command]`;
720
+ const combined = (stdout +
721
+ overflowNote(stdoutDropped, "stdout") +
722
+ (stderr ? `\n[stderr] ${stderr}` : "") +
723
+ overflowNote(stderrDropped, "stderr")).trim();
681
724
  settle(code === 0
682
725
  ? { content: combined || "(no output)", isError: false }
683
726
  : { content: `exit ${code}: ${combined}`, isError: true, errorKind: "fatal" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tools-node",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
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",
@@ -21,7 +21,7 @@
21
21
  "test": "vitest run"
22
22
  },
23
23
  "dependencies": {
24
- "@vincemakes/kiso-core": "0.1.29"
24
+ "@vincemakes/kiso-core": "0.1.31"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.2",