@vincemakes/kiso-tools-node 0.24.5 → 0.26.0
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 +23 -1
- package/dist/index.js +78 -3
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -91,14 +91,36 @@ export interface WorkspaceToolsOptions {
|
|
|
91
91
|
* STRIPPED — a shell command must not inherit the agent's API keys (a
|
|
92
92
|
* nested kiso would hit the REAL provider and blow up faux e2e runs;
|
|
93
93
|
* the keys are an exposure surface for any command).
|
|
94
|
+
*
|
|
95
|
+
* ADR-0031 Amendment 1: a RECORD is the third shape — the STRIPPED
|
|
96
|
+
* environment plus these explicit entries, the entries winning (the
|
|
97
|
+
* MCP surface's decision 2, applied here). An embedding host injects
|
|
98
|
+
* what its tools need without a temp file and without opening the
|
|
99
|
+
* whole environment; an entry may deliberately re-add a stripped
|
|
100
|
+
* name — explicit beats the heuristic, as it does for MCP servers.
|
|
94
101
|
*/
|
|
95
|
-
readonly shellEnv?: "inherit"
|
|
102
|
+
readonly shellEnv?: "inherit" | Readonly<Record<string, string>>;
|
|
96
103
|
/**
|
|
97
104
|
* DC-54 — the bounds that keep a tool call finite. Every field is
|
|
98
105
|
* optional and defaults to the constant beside it; a host embedding
|
|
99
106
|
* kiso over an unusually large or unusually small tree can move them,
|
|
100
107
|
* and the gate can set them low enough to observe the stop.
|
|
101
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[];
|
|
102
124
|
readonly limits?: {
|
|
103
125
|
/** search_text: skip a file larger than this (default 1 MiB). */
|
|
104
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)
|
|
@@ -803,6 +847,7 @@ export function writeFileTool(opts) {
|
|
|
803
847
|
promptSnippet: "write_file — create or replace a whole file",
|
|
804
848
|
promptGuidelines: ["write/edit: cite the file's latest revision as expectedRevision; each successful mutation returns the next one; use \"absent\" only to create"],
|
|
805
849
|
execute: async ({ path, content, expectedRevision: citedRevision }) => {
|
|
850
|
+
const maxReadBytes = opts.limits?.readMaxFileBytes ?? READ_MAX_FILE_BYTES;
|
|
806
851
|
// WR-1-F2: normalize every plausible copy of the token FIRST —
|
|
807
852
|
// tolerance in the reader, strictness in the comparison.
|
|
808
853
|
const expectedRevision = citedRevision === undefined ? undefined : normalizeRevision(citedRevision);
|
|
@@ -838,6 +883,19 @@ export function writeFileTool(opts) {
|
|
|
838
883
|
if (!exists) {
|
|
839
884
|
return precondition(`write_file: ${path} no longer exists — pass expectedRevision:"absent" to create it`);
|
|
840
885
|
}
|
|
886
|
+
// DC-54 owed (R14) — the ceiling, before the read.
|
|
887
|
+
//
|
|
888
|
+
// `read_file` got this bound in 0.24.5; these two did not,
|
|
889
|
+
// and they read the WHOLE file to compute a revision BEFORE
|
|
890
|
+
// the comparison that would reject the call — so the freeze
|
|
891
|
+
// happened on the way to the refusal rather than instead of
|
|
892
|
+
// it. Same ceiling, same precondition shape, same reason: a
|
|
893
|
+
// revision over a prefix could never match, so there is
|
|
894
|
+
// nothing to compute and nothing to compare.
|
|
895
|
+
const wSize = statSync(full).size;
|
|
896
|
+
if (wSize > maxReadBytes) {
|
|
897
|
+
return precondition(`write_file: ${path} is ${mib(wSize)} — too large to read (ceiling ${mib(maxReadBytes)}); use shell with sed/head to take a range`);
|
|
898
|
+
}
|
|
841
899
|
const current = contentRevision(readFileSync(full));
|
|
842
900
|
if (current !== expectedRevision) {
|
|
843
901
|
return precondition(`write_file: ${path} changed since ${expectedRevision} — read it again and cite its [rev:…] line, then re-apply the change`);
|
|
@@ -937,6 +995,7 @@ export function editFileTool(opts) {
|
|
|
937
995
|
promptSnippet: "edit_file — replace an exact old_string block (never rewrite whole files)",
|
|
938
996
|
promptGuidelines: ["write/edit: cite the file's latest revision as expectedRevision; each successful mutation returns the next one; use \"absent\" only to create"],
|
|
939
997
|
execute: async ({ path, search, replace, edits, expectedRevision: citedRevision }) => {
|
|
998
|
+
const maxReadBytes = opts.limits?.readMaxFileBytes ?? READ_MAX_FILE_BYTES;
|
|
940
999
|
// WR-1-F2: normalize the citation (see write_file).
|
|
941
1000
|
const expectedRevision = citedRevision === undefined ? undefined : normalizeRevision(citedRevision);
|
|
942
1001
|
// WR-1E2 — the form XOR (the draft-07 subset has no oneOf; the
|
|
@@ -983,6 +1042,19 @@ export function editFileTool(opts) {
|
|
|
983
1042
|
// what was edited (the external validation→replacement window
|
|
984
1043
|
// remains and is the narrowed claim). Staleness reports BEFORE
|
|
985
1044
|
// the pattern search: the truer cause first.
|
|
1045
|
+
// DC-54 owed (R14) — the ceiling, before the read.
|
|
1046
|
+
//
|
|
1047
|
+
// `read_file` got this bound in 0.24.5; these two did not,
|
|
1048
|
+
// and they read the WHOLE file to compute a revision BEFORE
|
|
1049
|
+
// the comparison that would reject the call — so the freeze
|
|
1050
|
+
// happened on the way to the refusal rather than instead of
|
|
1051
|
+
// it. Same ceiling, same precondition shape, same reason: a
|
|
1052
|
+
// revision over a prefix could never match, so there is
|
|
1053
|
+
// nothing to compute and nothing to compare.
|
|
1054
|
+
const eSize = statSync(full).size;
|
|
1055
|
+
if (eSize > maxReadBytes) {
|
|
1056
|
+
return precondition(`edit_file: ${path} is ${mib(eSize)} — too large to read (ceiling ${mib(maxReadBytes)}); use shell with sed/head to take a range`);
|
|
1057
|
+
}
|
|
986
1058
|
const bytes = readFileSync(full);
|
|
987
1059
|
const current = contentRevision(bytes);
|
|
988
1060
|
if (current !== expectedRevision) {
|
|
@@ -1105,13 +1177,16 @@ export function shellTool(opts) {
|
|
|
1105
1177
|
// not just the outer shell (Area 4). cwd is the workspace.
|
|
1106
1178
|
// bootstrap #3 (finding #7): the shell child NEVER inherits kiso's own
|
|
1107
1179
|
// provider credentials by default — only the explicit
|
|
1108
|
-
// shellEnv: "inherit" opt-in keeps them.
|
|
1180
|
+
// shellEnv: "inherit" opt-in keeps them. A record (ADR-0031
|
|
1181
|
+
// Amendment 1) rides on the STRIPPED base and wins per key.
|
|
1109
1182
|
const child = spawn(command, {
|
|
1110
1183
|
shell: true,
|
|
1111
1184
|
detached: true,
|
|
1112
1185
|
cwd: opts.workspaceRoot,
|
|
1113
1186
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1114
|
-
env: opts.shellEnv === "inherit"
|
|
1187
|
+
env: opts.shellEnv === "inherit"
|
|
1188
|
+
? process.env
|
|
1189
|
+
: { ...strippedShellEnv(process.env), ...(opts.shellEnv ?? {}) },
|
|
1115
1190
|
});
|
|
1116
1191
|
let stdout = "";
|
|
1117
1192
|
let stderr = "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tools-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
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.
|
|
24
|
+
"@vincemakes/kiso-core": "0.26.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|