@vincemakes/kiso-tools-node 0.47.0 → 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 +14 -0
- package/dist/corpus.js +23 -3
- package/dist/index.js +49 -15
- package/dist/search-worker.d.ts +34 -0
- package/dist/search-worker.js +27 -2
- package/package.json +2 -2
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
|
|
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
|
|
600
|
+
if (walk?.cutByCap === true)
|
|
573
601
|
notes.push(`${MAX_DIR_ENTRIES} shown (narrow the pattern for more)`);
|
|
574
|
-
if (walk
|
|
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" };
|
package/dist/search-worker.d.ts
CHANGED
|
@@ -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>;
|
package/dist/search-worker.js
CHANGED
|
@@ -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.
|
|
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.
|
|
28
|
+
"@vincemakes/kiso-core": "0.47.2",
|
|
29
29
|
"ignore": "^7.0.9"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|