@vincemakes/kiso-tools-node 0.47.1 → 0.47.2

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/corpus.d.ts CHANGED
@@ -56,6 +56,14 @@ export interface CorpusOptions {
56
56
  /** Start the walk here instead of at the root. The DECLARATION is still
57
57
  * read at `workspaceRoot` and paths are still relative to it. */
58
58
  walkFrom?: string;
59
+ /** 0.47.2: stop after VISITING this many files, accepted or not — the
60
+ * walk's own budget, beside `maxEntries`, which bounds what it returns.
61
+ * A glob that matches little walked the whole tree under it: 11 s from
62
+ * a home directory. Default: no cap. */
63
+ maxVisited?: number;
64
+ /** 0.47.2: the call's wall-clock deadline (epoch ms); the walk stops
65
+ * between entries once it has passed. Default: none. */
66
+ deadline?: number;
59
67
  }
60
68
  export interface CorpusWalk {
61
69
  /** Workspace-relative, POSIX-separated. */
@@ -68,6 +76,12 @@ export interface CorpusWalk {
68
76
  cutByDepth: boolean;
69
77
  /** The entry cap was reached. */
70
78
  cutByCap: boolean;
79
+ /** 0.47.2: the walk stopped at `maxVisited` files. */
80
+ cutByVisits: boolean;
81
+ /** 0.47.2: the walk stopped at its deadline. */
82
+ cutByDeadline: boolean;
83
+ /** The files visited, accepted or not. */
84
+ visited: number;
71
85
  }
72
86
  /** One directory's `.gitignore`, and the directory it is relative to. */
73
87
  export interface Layer {
package/dist/corpus.js CHANGED
@@ -135,15 +135,25 @@ export function walkCorpus(opts) {
135
135
  const root = opts.workspaceRoot;
136
136
  const maxDepth = opts.maxDepth ?? CORPUS_MAX_DEPTH;
137
137
  const maxEntries = opts.maxEntries ?? Number.POSITIVE_INFINITY;
138
+ const maxVisited = opts.maxVisited ?? Number.POSITIVE_INFINITY;
139
+ const deadline = opts.deadline ?? Number.POSITIVE_INFINITY;
138
140
  const rootLayer = readLayer(root);
139
141
  /** No declaration, nothing to trust: the pre-corpus rule, unchanged. */
140
142
  const declared = rootLayer !== null;
141
143
  const files = [];
142
144
  let cutByDepth = false;
143
145
  let cutByCap = false;
146
+ let cutByVisits = false;
147
+ let cutByDeadline = false;
148
+ let visited = 0;
149
+ const stopped = () => cutByCap || cutByVisits || cutByDeadline;
144
150
  const walk = (dir, depth, layers) => {
145
- if (cutByCap)
151
+ if (stopped())
146
152
  return;
153
+ if (Date.now() > deadline) {
154
+ cutByDeadline = true;
155
+ return;
156
+ }
147
157
  let entries;
148
158
  try {
149
159
  entries = readdirSync(dir, { withFileTypes: true });
@@ -153,7 +163,7 @@ export function walkCorpus(opts) {
153
163
  }
154
164
  const here = depth === 0 ? layers : layersEntering(dir, layers);
155
165
  for (const entry of entries) {
156
- if (cutByCap)
166
+ if (stopped())
157
167
  return;
158
168
  const name = entry.name;
159
169
  const full = join(dir, name);
@@ -170,6 +180,16 @@ export function walkCorpus(opts) {
170
180
  walk(full, depth + 1, here);
171
181
  continue;
172
182
  }
183
+ if (visited >= maxVisited) {
184
+ cutByVisits = true;
185
+ return;
186
+ }
187
+ visited += 1;
188
+ // the clock is read every 256 files, not per file
189
+ if ((visited & 255) === 0 && Date.now() > deadline) {
190
+ cutByDeadline = true;
191
+ return;
192
+ }
173
193
  const rel = relative(root, full).split(sep).join("/");
174
194
  if (opts.accept !== undefined && !opts.accept(rel))
175
195
  continue;
@@ -191,7 +211,7 @@ export function walkCorpus(opts) {
191
211
  }
192
212
  }
193
213
  walk(start, 0, startLayers);
194
- return { files, cutByDepth, cutByCap };
214
+ return { files, cutByDepth, cutByCap, cutByVisits, cutByDeadline, visited };
195
215
  }
196
216
  /** A minimal glob → RegExp, anchored, over workspace-relative POSIX paths.
197
217
  *
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ import { defineTool } from "@vincemakes/kiso-core";
31
31
  import { strippedShellEnv } from "./secret-env.js";
32
32
  import { contentRevision, normalizeRevision, postEffectEscape, precondition, publishNewFile, revalidateBeforeRename } from "./wr1.js";
33
33
  import { isProtectedPath, protectedIdentity, protectedRefusalText, readUnlessProtected } from "./protected.js";
34
- import { CORPUS_MAX_DEPTH, globToRegExp, walkCorpus } from "./corpus.js";
34
+ import { CORPUS_MAX_DEPTH, globToRegExp } from "./corpus.js";
35
35
  import { describeSearchMiss, regionForSearch } from "./search-miss.js";
36
36
  /**
37
37
  * TUI2-R1 (C) — THE SHELL PROGRESS SIDECAR.
@@ -546,7 +546,7 @@ export function listDirTool(opts) {
546
546
  effects: { precommitSafe: true, concurrency: "shared" },
547
547
  promptSnippet: "list_dir — directory entries (the workspace ls)",
548
548
  promptGuidelines: ["narrow to a subdirectory when the listing caps at 200 entries"],
549
- execute: async ({ path, glob }) => {
549
+ execute: async ({ path, glob }, ctx) => {
550
550
  try {
551
551
  const dir = resolveWithinRoot(opts.workspaceRoot, path ?? ".");
552
552
  // ACI-8: with a pattern this is a recursive search over the
@@ -554,25 +554,57 @@ export function listDirTool(opts) {
554
554
  // cannot drift the way they had.
555
555
  if (glob !== undefined) {
556
556
  const re = globToRegExp(glob);
557
- const walk = walkCorpus({
558
- workspaceRoot: opts.workspaceRoot,
559
- walkFrom: dir,
557
+ // 0.47.2 (the owner's trace, 2026-10-08): the walk ran HERE,
558
+ // synchronously — a project's name sought from a home directory held the
559
+ // main thread for 11.02 s, and no frame, timer or esc reached
560
+ // kiso until it returned. It runs on the search worker now
561
+ // (CX-1 F4's thread), under `search_text`'s budget: a cap on the
562
+ // files it visits, a deadline, and the abort.
563
+ const realOrSelf = (p) => {
564
+ try {
565
+ return realpathSync(p);
566
+ }
567
+ catch {
568
+ return p;
569
+ }
570
+ };
571
+ const deadline = Date.now() + (opts.limits?.searchMaxMs ?? SEARCH_MAX_MS);
572
+ const outcome = await runSearchWorker({
573
+ kind: "glob",
574
+ token: 0,
575
+ // both realpath'd: a root and a start resolved differently
576
+ // named every file `../../…` under a symlinked workspace
577
+ workspaceRoot: realOrSelf(opts.workspaceRoot),
578
+ walkFrom: realOrSelf(dir),
579
+ source: re.source,
580
+ flags: re.flags,
581
+ excluded: opts.excludeRoots ?? [],
560
582
  maxEntries: MAX_DIR_ENTRIES,
561
- accept: (rel) => re.test(rel),
562
- isExcluded: (full) => {
563
- const r = relative(opts.workspaceRoot, full).split(sep).join("/");
564
- return (opts.excludeRoots ?? []).some((ex) => r === ex || r.startsWith(`${ex}/`));
565
- },
566
- });
567
- const body = walk.files.length ? cap(walk.files.join("\n")) : `(no match for ${glob})`;
583
+ maxVisited: opts.limits?.searchMaxFiles ?? SEARCH_MAX_FILES,
584
+ deadline,
585
+ }, deadline, ctx.signal);
586
+ if (outcome.kind === "aborted")
587
+ return { content: "list_dir aborted", isError: true, errorKind: "fatal" };
588
+ if (outcome.kind === "error")
589
+ return { content: `list_dir failed: ${outcome.message}`, isError: true, errorKind: "fatal" };
590
+ const walk = outcome.reply;
591
+ if (walk?.error !== undefined)
592
+ return { content: `list_dir failed: ${walk.error}`, isError: true, errorKind: "fatal" };
593
+ const files = walk?.files ?? [];
594
+ const body = files.length ? cap(files.join("\n")) : `(no match for ${glob})`;
568
595
  // TWO truncations, different remedies, and neither claimed
569
596
  // when it did not happen: a walk that silently returns less
570
- // is the defect a note exists to prevent.
597
+ // is the defect a note exists to prevent. 0.47.2 adds the
598
+ // walk's own two stops, each said as itself.
571
599
  const notes = [];
572
- if (walk.cutByCap)
600
+ if (walk?.cutByCap === true)
573
601
  notes.push(`${MAX_DIR_ENTRIES} shown (narrow the pattern for more)`);
574
- if (walk.cutByDepth)
602
+ if (walk?.cutByDepth === true)
575
603
  notes.push(`the walk stopped at depth ${CORPUS_MAX_DEPTH} — anything deeper is not listed`);
604
+ if (walk?.cutByVisits === true)
605
+ notes.push(`the walk stopped after ${walk.visited} files — narrow the path`);
606
+ if (outcome.kind === "timeout" || walk?.cutByDeadline === true)
607
+ notes.push("the walk stopped — its budget elapsed before it finished (narrow the path or the pattern)");
576
608
  return { content: notes.length ? `${body}\n… ${notes.join("; ")}` : body, isError: false };
577
609
  }
578
610
  // DC-54 — TRUNCATED BEFORE IT BUILDS. It was a `.map` over
@@ -637,6 +669,8 @@ function searchWorkerUrl() {
637
669
  return beside;
638
670
  return new URL("../dist/search-worker.js", import.meta.url);
639
671
  }
672
+ /** One call on the search worker: `search_text`'s walk-and-match, or (0.47.2)
673
+ * `list_dir`'s glob walk — the same thread, slots, deadline and abort. */
640
674
  async function runSearchWorker(req, deadline, signal) {
641
675
  if (signal.aborted)
642
676
  return { kind: "aborted" };
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { type ProtectedIdentity } from "./protected.js";
19
19
  export interface SearchRequest {
20
+ readonly kind?: "search";
20
21
  readonly token: number;
21
22
  readonly root: string;
22
23
  /** The WORKSPACE root, which is not always the search root: a search under
@@ -57,4 +58,37 @@ export interface SearchReply {
57
58
  readonly stoppedAt: number;
58
59
  readonly error?: string;
59
60
  }
61
+ /** 0.47.2 — `list_dir`'s glob walk, on this thread. It walked on the main
62
+ * thread, synchronously: a project's name sought from a home directory held kiso
63
+ * for 11 s — no frame, no timer, no esc. The walk is the SAME
64
+ * `walkCorpus` it always was; it runs here, under the call's budget. */
65
+ export interface GlobRequest {
66
+ readonly kind: "glob";
67
+ readonly token: number;
68
+ /** Both realpath'd by the caller, for the reason `SearchRequest`'s
69
+ * workspace root is: a path made relative between a resolved and an
70
+ * unresolved base is `../..` the moment a symlink sits between them. */
71
+ readonly workspaceRoot: string;
72
+ readonly walkFrom: string;
73
+ /** the compiled glob (`globToRegExp`), as source and flags */
74
+ readonly source: string;
75
+ readonly flags: string;
76
+ /** DC-49 exclude roots, workspace-relative */
77
+ readonly excluded: readonly string[];
78
+ readonly maxEntries: number;
79
+ readonly maxVisited: number;
80
+ readonly deadline: number;
81
+ }
82
+ export interface GlobReply {
83
+ readonly kind: "glob";
84
+ readonly token: number;
85
+ readonly files: string[];
86
+ readonly cutByDepth: boolean;
87
+ readonly cutByCap: boolean;
88
+ readonly cutByVisits: boolean;
89
+ readonly cutByDeadline: boolean;
90
+ readonly visited: number;
91
+ readonly error?: string;
92
+ }
93
+ export declare function runGlob(req: GlobRequest): GlobReply;
60
94
  export declare function runSearch(req: SearchRequest): Promise<SearchReply>;
@@ -16,8 +16,8 @@
16
16
  * message from a superseded worker is ignored.
17
17
  */
18
18
  import { open, readdir } from "node:fs/promises";
19
- import { basename, join, relative } from "node:path";
20
- import { corpusSkips, layersEntering, readLayer } from "./corpus.js";
19
+ import { basename, join, relative, sep } from "node:path";
20
+ import { corpusSkips, layersEntering, readLayer, walkCorpus } from "./corpus.js";
21
21
  import { isMainThread, parentPort } from "node:worker_threads";
22
22
  import { diskPath, isProtectedDiskPath } from "./protected.js";
23
23
  /** A scanned file is protected by inode (the store itself, under any name
@@ -56,6 +56,22 @@ function excerptAround(line, regex) {
56
56
  const end = Math.min(trimmed.length, at + hit + EXCERPT_RADIUS);
57
57
  return `${start > 0 ? "…" : ""}${trimmed.slice(start, end)}${end < trimmed.length ? "…" : ""}`;
58
58
  }
59
+ export function runGlob(req) {
60
+ const re = new RegExp(req.source, req.flags);
61
+ const walk = walkCorpus({
62
+ workspaceRoot: req.workspaceRoot,
63
+ walkFrom: req.walkFrom,
64
+ maxEntries: req.maxEntries,
65
+ maxVisited: req.maxVisited,
66
+ deadline: req.deadline,
67
+ accept: (rel) => re.test(rel),
68
+ isExcluded: (full) => {
69
+ const r = relative(req.workspaceRoot, full).split(sep).join("/");
70
+ return req.excluded.some((ex) => r === ex || r.startsWith(`${ex}/`));
71
+ },
72
+ });
73
+ return { kind: "glob", token: req.token, ...walk };
74
+ }
59
75
  export async function runSearch(req) {
60
76
  const regex = new RegExp(req.pattern, req.flags);
61
77
  const matches = [];
@@ -180,6 +196,15 @@ export async function runSearch(req) {
180
196
  }
181
197
  if (!isMainThread && parentPort !== null) {
182
198
  parentPort.once("message", (req) => {
199
+ if (req.kind === "glob") {
200
+ try {
201
+ parentPort.postMessage(runGlob(req));
202
+ }
203
+ catch (err) {
204
+ parentPort.postMessage({ kind: "glob", token: req.token, files: [], cutByDepth: false, cutByCap: false, cutByVisits: false, cutByDeadline: false, visited: 0, error: err.message });
205
+ }
206
+ return;
207
+ }
183
208
  void runSearch(req).then((reply) => parentPort.postMessage(reply), (err) => parentPort.postMessage({ token: req.token, matches: [], totalMatches: 0, filesSeen: 0, skippedFiles: 0, multiLink: 0, unreadableDirs: 0, excludedDirs: 0, stopped: false, stoppedAt: 0, error: err.message }));
184
209
  });
185
210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tools-node",
3
- "version": "0.47.1",
3
+ "version": "0.47.2",
4
4
  "description": "kiso coding tools for Node hosts \u2014 read file, list directory, search text, write/edit file, shell command.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,7 +25,7 @@
25
25
  "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
- "@vincemakes/kiso-core": "0.47.1",
28
+ "@vincemakes/kiso-core": "0.47.2",
29
29
  "ignore": "^7.0.9"
30
30
  },
31
31
  "devDependencies": {