@vincemakes/kiso-tools-node 0.25.0 → 0.26.1

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
@@ -106,6 +106,21 @@ export interface WorkspaceToolsOptions {
106
106
  * kiso over an unusually large or unusually small tree can move them,
107
107
  * and the gate can set them low enough to observe the stop.
108
108
  */
109
+ /**
110
+ * DC-49 — roots a WALK may not descend into, as realpaths.
111
+ *
112
+ * The CLI passes `$KISO_HOME`. With the workspace at `~` the walks
113
+ * otherwise report the user's own session logs as though they were
114
+ * their work — and `~/.kiso` escapes the dot-name skip only by
115
+ * accident, so a KISO_HOME anywhere else is walked in full.
116
+ *
117
+ * DISCOVERY, NOT ACCESS. This removes a directory from what a walk
118
+ * FINDS when it descends from above. A path the user or the model
119
+ * NAMES is still served, at the root or inside it: the exclusion is
120
+ * not a permission, and anything the model can name it can still
121
+ * read.
122
+ */
123
+ readonly excludeRoots?: readonly string[];
109
124
  readonly limits?: {
110
125
  /** search_text: skip a file larger than this (default 1 MiB). */
111
126
  readonly searchMaxFileBytes?: number;
package/dist/index.js CHANGED
@@ -562,7 +562,35 @@ export function searchTextTool(opts) {
562
562
  // the call budget: a silent skip is a result the model cannot
563
563
  // tell is incomplete.
564
564
  let skippedFiles = 0;
565
+ let excludedDirs = 0;
565
566
  let filesSeen = 0;
567
+ // DC-49 — realpath on BOTH sides, so a symlinked HOME still
568
+ // matches (the reviewer's constraint (d): `/tmp` is a symlink to
569
+ // `/private/tmp` on darwin, and a raw string compare there is a
570
+ // gate that passes on one machine and not another).
571
+ //
572
+ // A root that CONTAINS the search root is not an exclusion: the
573
+ // caller pointed at it, and refusing would make an explicit path
574
+ // unservable. That is the reviewer's constraint (c), expressed
575
+ // where it belongs — in the predicate, not in four call sites.
576
+ const realOrSelf = (p) => {
577
+ try {
578
+ return realpathSync(p);
579
+ }
580
+ catch {
581
+ return p;
582
+ }
583
+ };
584
+ const searchRootReal = realOrSelf(root);
585
+ const excluded = (opts.excludeRoots ?? [])
586
+ .map(realOrSelf)
587
+ .filter((ex) => !(searchRootReal === ex || searchRootReal.startsWith(`${ex}/`)));
588
+ const isExcluded = (dir) => {
589
+ if (excluded.length === 0)
590
+ return false;
591
+ const r = realOrSelf(dir);
592
+ return excluded.some((ex) => r === ex || r.startsWith(`${ex}/`));
593
+ };
566
594
  // An explicit flag, NOT `stoppedAt > 0`: a wall-clock budget can
567
595
  // expire before the first file is scanned, and a zero-valued
568
596
  // sentinel would then read as "never stopped" — the walk would
@@ -742,8 +770,18 @@ export function searchTextTool(opts) {
742
770
  if (entry.name.startsWith(".") || entry.name === "node_modules")
743
771
  continue;
744
772
  const full = join(dir, entry.name);
745
- if (entry.isDirectory())
773
+ if (entry.isDirectory()) {
774
+ // DC-49 — a walk does not DESCEND into an excluded root.
775
+ // Reaching it from above is what is refused; being
776
+ // pointed at it is not (see `excluded` below, which is
777
+ // seeded from the SEARCH ROOT and therefore empty when
778
+ // the root is itself excluded).
779
+ if (isExcluded(full)) {
780
+ excludedDirs += 1;
781
+ continue;
782
+ }
746
783
  await walk(full, depth + 1);
784
+ }
747
785
  else if (entry.isFile())
748
786
  await scanFile(full);
749
787
  }
@@ -766,6 +804,12 @@ export function searchTextTool(opts) {
766
804
  content += `\n… ${unreadableDirs} unreadable ${unreadableDirs === 1 ? "directory" : "directories"} skipped`;
767
805
  // DC-54 — one merged sentence, and only when it happened. A note
768
806
  // that always fires says nothing.
807
+ // DC-49 — what a WALK did not enter, in the same discipline the
808
+ // file skips already follow: a scoped result that reads as total
809
+ // is one the model cannot tell is scoped.
810
+ if (excludedDirs > 0) {
811
+ content += `\n… ${excludedDirs} ${excludedDirs === 1 ? "directory" : "directories"} excluded`;
812
+ }
769
813
  if (skippedFiles > 0 || stopped) {
770
814
  const parts = [];
771
815
  if (skippedFiles > 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tools-node",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
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.25.0"
24
+ "@vincemakes/kiso-core": "0.26.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.2",