@vincemakes/kiso-tools-node 0.39.2 → 0.40.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 +14 -0
- package/dist/index.js +30 -5
- package/dist/protected.d.ts +50 -0
- package/dist/protected.js +106 -0
- package/dist/search-worker.d.ts +5 -0
- package/dist/search-worker.js +15 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -127,6 +127,15 @@ export interface WorkspaceToolsOptions {
|
|
|
127
127
|
* read.
|
|
128
128
|
*/
|
|
129
129
|
readonly excludeRoots?: readonly string[];
|
|
130
|
+
/**
|
|
131
|
+
* ACCESS, where excludeRoots is discovery: files kiso NEVER serves to a
|
|
132
|
+
* model, however they are named (protected.ts) — read_file, edit_file
|
|
133
|
+
* and write_file refuse them, search_text never returns a line from
|
|
134
|
+
* them. The CLI passes its own credential store (and a user's own
|
|
135
|
+
* `protectedPaths`). A protected file's name-suffixed siblings (the
|
|
136
|
+
* writer's temp file and lock) share the protection.
|
|
137
|
+
*/
|
|
138
|
+
readonly protectedFiles?: readonly string[];
|
|
130
139
|
readonly limits?: {
|
|
131
140
|
/** search_text: skip a file larger than this (default 1 MiB). */
|
|
132
141
|
readonly searchMaxFileBytes?: number;
|
|
@@ -174,6 +183,11 @@ export declare function editFileTool(opts: WorkspaceToolsOptions): Tool<{
|
|
|
174
183
|
expectedRevision?: string;
|
|
175
184
|
}>;
|
|
176
185
|
export { SHELL_STRIP_EXACT, strippedShellEnv } from "./secret-env.js";
|
|
186
|
+
export { PROTECTED_REFUSAL, diskPath, isProtectedPath, protectedIdentity, protectedRefusalText, readUnlessProtected, type ProtectedIdentity } from "./protected.js";
|
|
187
|
+
/** The search corpus's credential rule, by name — exported so the CLI's
|
|
188
|
+
* read-only shell allow holds a shell read to the same definition rather
|
|
189
|
+
* than a copy of it. */
|
|
190
|
+
export { isCredentialName } from "./corpus.js";
|
|
177
191
|
export declare function shellTool(opts: WorkspaceToolsOptions): Tool<{
|
|
178
192
|
command: string;
|
|
179
193
|
timeoutMs?: number;
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
21
21
|
import { promisify } from "node:util";
|
|
22
|
-
import { appendFileSync, chmodSync, existsSync, linkSync, mkdirSync,
|
|
22
|
+
import { appendFileSync, chmodSync, existsSync, linkSync, mkdirSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
23
23
|
import { Worker } from "node:worker_threads";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { createHash } from "node:crypto";
|
|
@@ -29,6 +29,7 @@ import { defineTool } from "@vincemakes/kiso-core";
|
|
|
29
29
|
// WR-1/WR-1A — the revision-guard primitives (unit-tested in wr1a-coda):
|
|
30
30
|
import { strippedShellEnv } from "./secret-env.js";
|
|
31
31
|
import { contentRevision, normalizeRevision, postEffectEscape, precondition, publishNewFile, revalidateBeforeRename } from "./wr1.js";
|
|
32
|
+
import { isProtectedPath, protectedIdentity, protectedRefusalText, readUnlessProtected } from "./protected.js";
|
|
32
33
|
import { CORPUS_MAX_DEPTH, globToRegExp, walkCorpus } from "./corpus.js";
|
|
33
34
|
import { describeSearchMiss } from "./search-miss.js";
|
|
34
35
|
/**
|
|
@@ -353,6 +354,11 @@ export function readFileTool(opts) {
|
|
|
353
354
|
const maxReadBytes = opts.limits?.readMaxFileBytes ?? READ_MAX_FILE_BYTES;
|
|
354
355
|
try {
|
|
355
356
|
const full = resolveWithinRoot(opts.workspaceRoot, path);
|
|
357
|
+
// the credential store is never served (protected.ts) — checked
|
|
358
|
+
// by the disk's own resolution and by inode, before any read
|
|
359
|
+
const guard = protectedIdentity(opts.protectedFiles);
|
|
360
|
+
if (isProtectedPath(full, guard))
|
|
361
|
+
return precondition(protectedRefusalText("read_file", path));
|
|
356
362
|
const denied = await inodeReadPolicy(opts.workspaceRoot, full);
|
|
357
363
|
if (denied !== null)
|
|
358
364
|
return escapeResult(denied);
|
|
@@ -388,7 +394,10 @@ export function readFileTool(opts) {
|
|
|
388
394
|
// 64 MiB read — about 100 ms, measured — against the 180+
|
|
389
395
|
// seconds this finding is named for. Determinism is worth
|
|
390
396
|
// more than that hitch.
|
|
391
|
-
|
|
397
|
+
// one descriptor, compared by inode before a byte is read
|
|
398
|
+
const bytes = readUnlessProtected(full, guard);
|
|
399
|
+
if (bytes === null)
|
|
400
|
+
return precondition(protectedRefusalText("read_file", path));
|
|
392
401
|
const content = bytes.toString("utf8");
|
|
393
402
|
// The lines the file DISPLAYS: a trailing newline's empty split
|
|
394
403
|
// element is not a line. Line k = split[k-1], 1-based.
|
|
@@ -737,7 +746,7 @@ export function searchTextTool(opts) {
|
|
|
737
746
|
// root uses: `full` is walked from a realpath'd root, and making
|
|
738
747
|
// a path relative between a resolved and an unresolved base
|
|
739
748
|
// yields `../..` the moment a symlink sits between them.
|
|
740
|
-
{ token: 0, root: searchRootReal, workspaceRoot: realOrSelf(opts.workspaceRoot), single, pattern, flags, excluded, maxFileBytes, maxFiles, deadline, maxMatches: MAX_SEARCH_MATCHES, sniffBytes: BINARY_SNIFF_BYTES }, deadline, ctx.signal);
|
|
749
|
+
{ token: 0, root: searchRootReal, workspaceRoot: realOrSelf(opts.workspaceRoot), single, pattern, flags, excluded, protectedIdentity: protectedIdentity(opts.protectedFiles), maxFileBytes, maxFiles, deadline, maxMatches: MAX_SEARCH_MATCHES, sniffBytes: BINARY_SNIFF_BYTES }, deadline, ctx.signal);
|
|
741
750
|
if (outcome.kind === "aborted")
|
|
742
751
|
return { content: "search_text aborted", isError: true, errorKind: "fatal" };
|
|
743
752
|
if (outcome.kind === "error")
|
|
@@ -807,6 +816,9 @@ export function writeFileTool(opts) {
|
|
|
807
816
|
return escapeResult(err.message);
|
|
808
817
|
throw err;
|
|
809
818
|
}
|
|
819
|
+
const guard = protectedIdentity(opts.protectedFiles);
|
|
820
|
+
if (isProtectedPath(full, guard))
|
|
821
|
+
return precondition(protectedRefusalText("write_file", path));
|
|
810
822
|
// WR-1 — the observed-revision stale-write guard. The decision
|
|
811
823
|
// lattice runs BEFORE any bytes move; every refusal is a
|
|
812
824
|
// precondition with one actionable sentence. What it proves:
|
|
@@ -843,7 +855,10 @@ export function writeFileTool(opts) {
|
|
|
843
855
|
if (wSize > maxReadBytes) {
|
|
844
856
|
return precondition(`write_file: ${path} is ${mib(wSize)} — too large to read (ceiling ${mib(maxReadBytes)}); use shell with sed/head to take a range`);
|
|
845
857
|
}
|
|
846
|
-
const
|
|
858
|
+
const held = readUnlessProtected(full, guard);
|
|
859
|
+
if (held === null)
|
|
860
|
+
return precondition(protectedRefusalText("write_file", path));
|
|
861
|
+
const current = contentRevision(held);
|
|
847
862
|
if (current !== expectedRevision) {
|
|
848
863
|
return precondition(`write_file: ${path} changed since ${expectedRevision} — read it again and cite its [rev:…] line, then re-apply the change`);
|
|
849
864
|
}
|
|
@@ -1022,6 +1037,9 @@ export function editFileTool(opts) {
|
|
|
1022
1037
|
return escapeResult(err.message);
|
|
1023
1038
|
throw err;
|
|
1024
1039
|
}
|
|
1040
|
+
const guard = protectedIdentity(opts.protectedFiles);
|
|
1041
|
+
if (isProtectedPath(full, guard))
|
|
1042
|
+
return precondition(protectedRefusalText("edit_file", path));
|
|
1025
1043
|
// WR-1: edits always target an existing file — the revision is
|
|
1026
1044
|
// not optional here, and the refusal teaches the protocol.
|
|
1027
1045
|
if (expectedRevision === undefined) {
|
|
@@ -1054,7 +1072,9 @@ export function editFileTool(opts) {
|
|
|
1054
1072
|
if (eSize > maxReadBytes) {
|
|
1055
1073
|
return precondition(`edit_file: ${path} is ${mib(eSize)} — too large to read (ceiling ${mib(maxReadBytes)}); use shell with sed/head to take a range`);
|
|
1056
1074
|
}
|
|
1057
|
-
const bytes =
|
|
1075
|
+
const bytes = readUnlessProtected(full, guard);
|
|
1076
|
+
if (bytes === null)
|
|
1077
|
+
return precondition(protectedRefusalText("edit_file", path));
|
|
1058
1078
|
const current = contentRevision(bytes);
|
|
1059
1079
|
if (current !== expectedRevision) {
|
|
1060
1080
|
return precondition(`edit_file: ${path} changed since ${expectedRevision} — read it again and cite its [rev:…] line, then re-apply the change`);
|
|
@@ -1145,6 +1165,11 @@ export function editFileTool(opts) {
|
|
|
1145
1165
|
// with a hand-kept copy over there; the copy never learned the
|
|
1146
1166
|
// declared names, which is how MCP children kept leaking.
|
|
1147
1167
|
export { SHELL_STRIP_EXACT, strippedShellEnv } from "./secret-env.js";
|
|
1168
|
+
export { PROTECTED_REFUSAL, diskPath, isProtectedPath, protectedIdentity, protectedRefusalText, readUnlessProtected } from "./protected.js";
|
|
1169
|
+
/** The search corpus's credential rule, by name — exported so the CLI's
|
|
1170
|
+
* read-only shell allow holds a shell read to the same definition rather
|
|
1171
|
+
* than a copy of it. */
|
|
1172
|
+
export { isCredentialName } from "./corpus.js";
|
|
1148
1173
|
export function shellTool(opts) {
|
|
1149
1174
|
return defineTool({
|
|
1150
1175
|
name: "shell",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kiso never serves its own credential store to a model.
|
|
3
|
+
*
|
|
4
|
+
* The 2026-09-14 incident: a session with its cwd at home called `read_file
|
|
5
|
+
* .kiso/auth.json` in default mode. The read was auto-allowed, and the
|
|
6
|
+
* store's API key and OAuth tokens went to the provider as conversation
|
|
7
|
+
* context. `excludeRoots` could not have stopped it — it governs what a walk
|
|
8
|
+
* DISCOVERS, not what a named path may ACCESS (its own header says so).
|
|
9
|
+
*
|
|
10
|
+
* This is the access rule, for the files the host names (`protectedFiles`):
|
|
11
|
+
* the credential store and its name-suffixed siblings (the writer's temp
|
|
12
|
+
* file and lock), plus whatever a user lists in their own config. A path is
|
|
13
|
+
* protected when the DISK resolves it to one of them — `realpath.native` of
|
|
14
|
+
* the longest existing prefix, so a symlink, a `..` through a symlink and a
|
|
15
|
+
* case variant all reach the same file — or when it is the same INODE (a
|
|
16
|
+
* hard link under another name).
|
|
17
|
+
*
|
|
18
|
+
* This module imports nothing beyond node itself, as secret-env.ts does:
|
|
19
|
+
* the same rule is read by the search worker and by the CLI's shell check.
|
|
20
|
+
*/
|
|
21
|
+
export declare const PROTECTED_REFUSAL = "kiso never serves its own credential store to a model";
|
|
22
|
+
/** The path as the disk resolves it: realpath.native of the longest
|
|
23
|
+
* existing prefix, then the tail that does not exist yet. */
|
|
24
|
+
export declare function diskPath(p: string): string;
|
|
25
|
+
export interface ProtectedIdentity {
|
|
26
|
+
/** the protected files as the disk spells them */
|
|
27
|
+
readonly paths: readonly string[];
|
|
28
|
+
/** `dev:ino` of each protected file that exists — a hard link is the same file */
|
|
29
|
+
readonly inodes: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
/** Resolve the protected list once; the per-path checks compare against it. */
|
|
32
|
+
export declare function protectedIdentity(files?: readonly string[]): ProtectedIdentity;
|
|
33
|
+
/** Whether a disk-resolved path is a protected file or one of its
|
|
34
|
+
* name-suffixed siblings (`auth.json.tmp-<pid>`, `auth.json.lock/…`). */
|
|
35
|
+
export declare function isProtectedDiskPath(d: string, id: ProtectedIdentity): boolean;
|
|
36
|
+
/** The whole question for one path: by where the disk resolves it, and by
|
|
37
|
+
* inode when it exists. */
|
|
38
|
+
export declare function isProtectedPath(p: string, id: ProtectedIdentity): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Read a file through ONE descriptor, and refuse it when that descriptor
|
|
41
|
+
* is a protected file. The path check before it judges a NAME, and a name
|
|
42
|
+
* can be swapped for a symlink between the check and the read; the
|
|
43
|
+
* descriptor is the file actually opened. Returns null when it is
|
|
44
|
+
* protected. (The sandbox is the guarantee; this closes the file tools'
|
|
45
|
+
* own check-then-use window.)
|
|
46
|
+
*/
|
|
47
|
+
export declare function readUnlessProtected(full: string, id: ProtectedIdentity): Buffer | null;
|
|
48
|
+
/** The refusal a tool returns — a precondition (refused, never attempted),
|
|
49
|
+
* stated as permanent so a model does not retry it under another name. */
|
|
50
|
+
export declare function protectedRefusalText(tool: string, path: string): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kiso never serves its own credential store to a model.
|
|
3
|
+
*
|
|
4
|
+
* The 2026-09-14 incident: a session with its cwd at home called `read_file
|
|
5
|
+
* .kiso/auth.json` in default mode. The read was auto-allowed, and the
|
|
6
|
+
* store's API key and OAuth tokens went to the provider as conversation
|
|
7
|
+
* context. `excludeRoots` could not have stopped it — it governs what a walk
|
|
8
|
+
* DISCOVERS, not what a named path may ACCESS (its own header says so).
|
|
9
|
+
*
|
|
10
|
+
* This is the access rule, for the files the host names (`protectedFiles`):
|
|
11
|
+
* the credential store and its name-suffixed siblings (the writer's temp
|
|
12
|
+
* file and lock), plus whatever a user lists in their own config. A path is
|
|
13
|
+
* protected when the DISK resolves it to one of them — `realpath.native` of
|
|
14
|
+
* the longest existing prefix, so a symlink, a `..` through a symlink and a
|
|
15
|
+
* case variant all reach the same file — or when it is the same INODE (a
|
|
16
|
+
* hard link under another name).
|
|
17
|
+
*
|
|
18
|
+
* This module imports nothing beyond node itself, as secret-env.ts does:
|
|
19
|
+
* the same rule is read by the search worker and by the CLI's shell check.
|
|
20
|
+
*/
|
|
21
|
+
import { basename, dirname, join } from "node:path";
|
|
22
|
+
import { closeSync, fstatSync, openSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
23
|
+
export const PROTECTED_REFUSAL = "kiso never serves its own credential store to a model";
|
|
24
|
+
/** The path as the disk resolves it: realpath.native of the longest
|
|
25
|
+
* existing prefix, then the tail that does not exist yet. */
|
|
26
|
+
export function diskPath(p) {
|
|
27
|
+
let head = p;
|
|
28
|
+
const tail = [];
|
|
29
|
+
for (;;) {
|
|
30
|
+
try {
|
|
31
|
+
const real = realpathSync.native(head);
|
|
32
|
+
return tail.length > 0 ? join(real, ...tail) : real;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
const parent = dirname(head);
|
|
36
|
+
if (parent === head)
|
|
37
|
+
return p;
|
|
38
|
+
tail.unshift(basename(head));
|
|
39
|
+
head = parent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Resolve the protected list once; the per-path checks compare against it. */
|
|
44
|
+
export function protectedIdentity(files = []) {
|
|
45
|
+
const paths = files.map(diskPath);
|
|
46
|
+
const inodes = [];
|
|
47
|
+
for (const p of paths) {
|
|
48
|
+
try {
|
|
49
|
+
const st = statSync(p);
|
|
50
|
+
if (st.isFile())
|
|
51
|
+
inodes.push(`${st.dev}:${st.ino}`);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// not there yet — the path rule still holds
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { paths, inodes };
|
|
58
|
+
}
|
|
59
|
+
/** Whether a disk-resolved path is a protected file or one of its
|
|
60
|
+
* name-suffixed siblings (`auth.json.tmp-<pid>`, `auth.json.lock/…`). */
|
|
61
|
+
export function isProtectedDiskPath(d, id) {
|
|
62
|
+
return id.paths.some((pf) => d === pf || d.startsWith(`${pf}.`));
|
|
63
|
+
}
|
|
64
|
+
/** The whole question for one path: by where the disk resolves it, and by
|
|
65
|
+
* inode when it exists. */
|
|
66
|
+
export function isProtectedPath(p, id) {
|
|
67
|
+
if (id.paths.length === 0)
|
|
68
|
+
return false;
|
|
69
|
+
const d = diskPath(p);
|
|
70
|
+
if (isProtectedDiskPath(d, id))
|
|
71
|
+
return true;
|
|
72
|
+
if (id.inodes.length === 0)
|
|
73
|
+
return false;
|
|
74
|
+
try {
|
|
75
|
+
const st = statSync(d);
|
|
76
|
+
return st.isFile() && id.inodes.includes(`${st.dev}:${st.ino}`);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Read a file through ONE descriptor, and refuse it when that descriptor
|
|
84
|
+
* is a protected file. The path check before it judges a NAME, and a name
|
|
85
|
+
* can be swapped for a symlink between the check and the read; the
|
|
86
|
+
* descriptor is the file actually opened. Returns null when it is
|
|
87
|
+
* protected. (The sandbox is the guarantee; this closes the file tools'
|
|
88
|
+
* own check-then-use window.)
|
|
89
|
+
*/
|
|
90
|
+
export function readUnlessProtected(full, id) {
|
|
91
|
+
const fd = openSync(full, "r");
|
|
92
|
+
try {
|
|
93
|
+
const st = fstatSync(fd);
|
|
94
|
+
if (id.inodes.includes(`${st.dev}:${st.ino}`))
|
|
95
|
+
return null;
|
|
96
|
+
return readFileSync(fd);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
closeSync(fd);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** The refusal a tool returns — a precondition (refused, never attempted),
|
|
103
|
+
* stated as permanent so a model does not retry it under another name. */
|
|
104
|
+
export function protectedRefusalText(tool, path) {
|
|
105
|
+
return `${tool}: ${path} — ${PROTECTED_REFUSAL}. This is permanent: do not retry it, under this path or another.`;
|
|
106
|
+
}
|
package/dist/search-worker.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* one `SearchReply` and exits. A call token rides both ways so a late
|
|
16
16
|
* message from a superseded worker is ignored.
|
|
17
17
|
*/
|
|
18
|
+
import { type ProtectedIdentity } from "./protected.js";
|
|
18
19
|
export interface SearchRequest {
|
|
19
20
|
readonly token: number;
|
|
20
21
|
readonly root: string;
|
|
@@ -29,6 +30,10 @@ export interface SearchRequest {
|
|
|
29
30
|
readonly pattern: string;
|
|
30
31
|
readonly flags: string;
|
|
31
32
|
readonly excluded: readonly string[];
|
|
33
|
+
/** files never searched: kiso's credential store (protected.ts) — skipped
|
|
34
|
+
* without a hit, matched by inode and, for a protected name, by the
|
|
35
|
+
* disk's own resolution */
|
|
36
|
+
readonly protectedIdentity?: ProtectedIdentity;
|
|
32
37
|
readonly maxFileBytes: number;
|
|
33
38
|
readonly maxFiles: number;
|
|
34
39
|
/** the call's wall-clock deadline (epoch ms): the walk stops COOPERATIVELY
|
package/dist/search-worker.js
CHANGED
|
@@ -19,6 +19,19 @@ import { open, readdir } from "node:fs/promises";
|
|
|
19
19
|
import { basename, join, relative } from "node:path";
|
|
20
20
|
import { corpusSkips, layersEntering, readLayer } from "./corpus.js";
|
|
21
21
|
import { isMainThread, parentPort } from "node:worker_threads";
|
|
22
|
+
import { diskPath, isProtectedDiskPath } from "./protected.js";
|
|
23
|
+
/** A scanned file is protected by inode (the store itself, under any name
|
|
24
|
+
* or case), or — for a name that starts like a protected one, the writer's
|
|
25
|
+
* temp file and lock — by the disk's own resolution. The resolution runs
|
|
26
|
+
* only for such a name, so an ordinary search pays nothing for it. */
|
|
27
|
+
function isProtectedScan(full, st, id) {
|
|
28
|
+
if (id.inodes.includes(`${st.dev}:${st.ino}`))
|
|
29
|
+
return true;
|
|
30
|
+
const name = basename(full).toLowerCase();
|
|
31
|
+
if (!id.paths.some((p) => name.startsWith(basename(p).toLowerCase())))
|
|
32
|
+
return false;
|
|
33
|
+
return isProtectedDiskPath(diskPath(full), id);
|
|
34
|
+
}
|
|
22
35
|
/** ACI-5 — the excerpt WINDOWS THE MATCH instead of taking the line's head.
|
|
23
36
|
*
|
|
24
37
|
* `line.trim().slice(0, 160)` answers "what does this line start with",
|
|
@@ -77,6 +90,8 @@ export async function runSearch(req) {
|
|
|
77
90
|
let text;
|
|
78
91
|
try {
|
|
79
92
|
const st = await fh.stat();
|
|
93
|
+
if (req.protectedIdentity !== undefined && isProtectedScan(full, st, req.protectedIdentity))
|
|
94
|
+
return;
|
|
80
95
|
if (st.nlink > 1) {
|
|
81
96
|
multiLink += 1;
|
|
82
97
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tools-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.1",
|
|
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.
|
|
28
|
+
"@vincemakes/kiso-core": "0.40.1",
|
|
29
29
|
"ignore": "^7.0.9"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|