@vincemakes/kiso-tools-node 0.51.1 → 0.53.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 CHANGED
@@ -174,6 +174,15 @@ export interface WorkspaceToolsOptions {
174
174
  readonly maxForegroundMs?: number;
175
175
  };
176
176
  }
177
+ /** WR-1: does this read_file call, with the result it got, show the file from its first
178
+ * line to its last? It starts at the top (no offset, or offset 1) and carries none of the
179
+ * three notices that say more remains: the continuation note, the output cap, the
180
+ * over-long-line cap. A `limit` that covers the file is fine, because a short one always
181
+ * leaves the continuation note. The hosts' observed-revision rule uses this for the
182
+ * whole-file overwrite; it lives beside the notices so the two cannot drift apart. */
183
+ export declare function readShowsWholeFile(input: {
184
+ readonly offset?: unknown;
185
+ }, body: string): boolean;
177
186
  export declare function readFileTool(opts: WorkspaceToolsOptions): Tool<{
178
187
  path: string;
179
188
  offset?: number;
@@ -264,6 +273,8 @@ export interface ShellTasks {
264
273
  ready(match: string): void;
265
274
  ended(exitCode: number | null, signal: string | null): void;
266
275
  unconfirmed(pids: readonly number[]): void;
276
+ /** the hand-over failed after the task was recorded; absent in a manager that predates it */
277
+ handoverFailed?(reason: string): void;
267
278
  };
268
279
  stop(id: string, by: "model"): boolean;
269
280
  get(id: string): {
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import { execFile } from "node:child_process";
21
21
  import { promisify } from "node:util";
22
- import { appendFileSync, chmodSync, closeSync, existsSync, fstatSync, linkSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
22
+ import { appendFileSync, chmodSync, closeSync, existsSync, fstatSync, linkSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
23
23
  import { Worker } from "node:worker_threads";
24
24
  import { fileURLToPath } from "node:url";
25
25
  import { killTree, NO_BASH, processStartTime, RotatingOutput, startCommand } from "./process.js";
@@ -89,6 +89,12 @@ const DEFAULT_MAX_FOREGROUND_MS = 300_000;
89
89
  const TASK_OUTPUT_CAP = 64 * 1024 * 1024;
90
90
  /** A task's stop: SIGTERM, this long, then the confirmed sweep. */
91
91
  const TASK_STOP_GRACE_MS = 5_000;
92
+ /** The shell exited but its pipes did not close: how long after the exit (or
93
+ * the last byte that followed it) before a descendant is taken to hold them. */
94
+ const EXIT_DRAIN_MS = 300;
95
+ /** ...and the most it may be renewed: a descendant that writes every few
96
+ * hundred ms must not keep a finished command in the foreground. */
97
+ const EXIT_DRAIN_MAX_MS = 2_000;
92
98
  /** ADR-0058 Amendment 7: how long task_stop waits for the end it reports —
93
99
  * the runner's journal poll, TERM, the grace, KILL and the sweep. */
94
100
  const TASK_STOP_WAIT_MS = 8_000;
@@ -372,6 +378,18 @@ function moreLinesNote(nextOffset, remaining, limit) {
372
378
  // read onward. Measured before the fix: 7.3% of real reads took that path.
373
379
  return `\n… ${remaining} more ${remaining === 1 ? "line" : "lines"} (call again with offset=${nextOffset} limit=${limit})`;
374
380
  }
381
+ /** WR-1: does this read_file call, with the result it got, show the file from its first
382
+ * line to its last? It starts at the top (no offset, or offset 1) and carries none of the
383
+ * three notices that say more remains: the continuation note, the output cap, the
384
+ * over-long-line cap. A `limit` that covers the file is fine, because a short one always
385
+ * leaves the continuation note. The hosts' observed-revision rule uses this for the
386
+ * whole-file overwrite; it lives beside the notices so the two cannot drift apart. */
387
+ export function readShowsWholeFile(input, body) {
388
+ const offset = input.offset;
389
+ if (offset !== undefined && !(typeof offset === "number" && offset <= 1))
390
+ return false;
391
+ return !body.includes("(call again with offset=") && !body.includes("[output capped at") && !/\[a line near .* exceeds the \d+-char cap/.test(body);
392
+ }
375
393
  export function readFileTool(opts) {
376
394
  return defineTool({
377
395
  name: "read_file",
@@ -907,7 +925,7 @@ export function writeFileTool(opts) {
907
925
  // the trajectory audit: after ranged reads the model retried this
908
926
  // in a loop — say that only a WHOLE read lets it replace the file
909
927
  if (expectedRevision === undefined)
910
- return precondition(`write_file: ${path} already exists — to replace it, first read the whole file (read_file with no offset or limit; a ranged read is not enough), or change part of it with edit_file`);
928
+ return precondition(`write_file: ${path} already exists — to replace it, first read the whole file from its first line (a read that starts later or stops early is not enough; for a long file pass a limit that covers it), or change part of it with edit_file`);
911
929
  }
912
930
  // WR-1 — the observed-revision stale-write guard. The decision
913
931
  // lattice runs BEFORE any bytes move; every refusal is a
@@ -953,8 +971,39 @@ export function writeFileTool(opts) {
953
971
  return precondition(opts.observedRevision !== undefined ? `write_file: ${path} changed since you last observed it — read it again, then re-apply the change` : `write_file: ${path} changed since ${expectedRevision} — read it again and cite its [rev:…] line, then re-apply the change`);
954
972
  }
955
973
  }
974
+ // A new file may sit under directories that do not exist yet. The
975
+ // containment check above already walked the missing tail up to its
976
+ // deepest existing ancestor, so creating the tail cannot leave the
977
+ // workspace; the directories this call creates are removed again
978
+ // if the file never lands.
979
+ let createdDirs;
980
+ if (!exists) {
981
+ try {
982
+ createdDirs = mkdirSync(dirname(full), { recursive: true });
983
+ }
984
+ catch (err) {
985
+ return precondition(`write_file: cannot create the directory for ${path} — ${describeFsError(err, path)}`);
986
+ }
987
+ }
988
+ const dropCreatedDirs = () => {
989
+ if (createdDirs === undefined)
990
+ return;
991
+ for (let dir = dirname(full); dir.length >= createdDirs.length; dir = dirname(dir)) {
992
+ try {
993
+ rmdirSync(dir);
994
+ }
995
+ catch {
996
+ return; // not empty or not ours — leave it
997
+ }
998
+ if (dir === createdDirs)
999
+ return;
1000
+ }
1001
+ };
956
1002
  const tmp = `${full}.kiso-tmp-${process.pid}-${crypto.randomUUID()}`;
957
1003
  let preservedMode;
1004
+ // Nothing reaches the target before the commit step below, so a
1005
+ // failure while staging the temp file leaves the world as it was.
1006
+ let committing = false;
958
1007
  try {
959
1008
  // E group: SAFE REPLACEMENT — write a temp file next to the
960
1009
  // target and rename it over the directory entry. A hard link
@@ -969,12 +1018,15 @@ export function writeFileTool(opts) {
969
1018
  writeFileSync(tmp, content, "utf8");
970
1019
  if (preservedMode !== undefined)
971
1020
  chmodSync(tmp, preservedMode);
1021
+ committing = true;
972
1022
  if (!exists) {
973
1023
  // WR-1A ②: link(2) or refusal — atomic no-clobber, fail
974
1024
  // closed. No rename fallback: "absent" is a contract.
975
1025
  const refused = publishNewFile(tmp, full, path);
976
- if (refused !== null)
1026
+ if (refused !== null) {
1027
+ dropCreatedDirs();
977
1028
  return refused;
1029
+ }
978
1030
  }
979
1031
  else {
980
1032
  // WR-1A ③: the world must STILL match the citation right
@@ -1019,11 +1071,21 @@ export function writeFileTool(opts) {
1019
1071
  }
1020
1072
  if (err instanceof PathEscapeError)
1021
1073
  return escapeResult(err.message);
1022
- return { content: `write_file failed: ${err.message}`, isError: true, errorKind: "fatal" };
1074
+ if (!committing) {
1075
+ dropCreatedDirs();
1076
+ return precondition(`write_file: could not write ${path} — ${describeFsError(err, path)}; nothing was changed`);
1077
+ }
1078
+ return { content: `write_file failed: ${describeFsError(err, path)}`, isError: true, errorKind: "fatal" };
1023
1079
  }
1024
1080
  },
1025
1081
  });
1026
1082
  }
1083
+ /** An fs error as the model should read it: the file it asked for, not the
1084
+ * `.kiso-tmp-*` staging name the failing syscall happened to see. */
1085
+ function describeFsError(err, path) {
1086
+ const message = err instanceof Error ? err.message : String(err);
1087
+ return message.replace(/'[^']*\.kiso-tmp-[^']*'/g, `'${path}'`);
1088
+ }
1027
1089
  /** ACI-2: every offset where `search` occurs, OVERLAPPING ones included —
1028
1090
  * "aa" occurs twice in "aaa", and the two resolutions differ, so the call
1029
1091
  * is ambiguous. A scan that steps past each match by its own length
@@ -1212,7 +1274,7 @@ export function editFileTool(opts) {
1212
1274
  if (expectedRevision === undefined && opts.observedRevision !== undefined) {
1213
1275
  expectedRevision = opts.observedRevision(ctx?.sessionId, full);
1214
1276
  if (expectedRevision === undefined)
1215
- return precondition(`edit_file: ${path} — read it first; there is no observation of it in this session`);
1277
+ return precondition(`edit_file: ${path} — read it first with read_file; there is no observation of it in this session (shell output does not count)`);
1216
1278
  }
1217
1279
  // WR-1: edits always target an existing file — the revision is
1218
1280
  // not optional here, and the refusal teaches the protocol.
@@ -1346,7 +1408,7 @@ export function editFileTool(opts) {
1346
1408
  catch {
1347
1409
  // already renamed or never created
1348
1410
  }
1349
- return { content: `edit_file failed: ${err.message}`, isError: true, errorKind: "fatal" };
1411
+ return { content: `edit_file failed: ${describeFsError(err, path)}`, isError: true, errorKind: "fatal" };
1350
1412
  }
1351
1413
  },
1352
1414
  });
@@ -1516,6 +1578,22 @@ export function shellTool(opts) {
1516
1578
  let promoted = null;
1517
1579
  let stopping = false;
1518
1580
  let readySeen = false;
1581
+ /** the top-level shell's own exit, once seen: `close` waits for every holder of its pipes */
1582
+ let exitSeen = null;
1583
+ let drainTimer;
1584
+ let exitAtMs = 0;
1585
+ /** the command had exited when the manager took its pipes: its root is gone, so a stop can reach only the descendants it can find */
1586
+ let rootGone = false;
1587
+ /** the stop of a hand-over was recorded unconfirmed: the sink stays open for whatever still holds the pipes, and closes with them */
1588
+ let stopLeftHolders = false;
1589
+ /** after the shell's exit: one more drain window from now (a byte that follows the exit renews it) */
1590
+ const armDrain = () => {
1591
+ if (exitSeen === null)
1592
+ return;
1593
+ clearTimeout(drainTimer);
1594
+ drainTimer = setTimeout(onDrainExpired, Math.max(0, Math.min(EXIT_DRAIN_MS, exitAtMs + EXIT_DRAIN_MAX_MS - Date.now())));
1595
+ drainTimer.unref?.();
1596
+ };
1519
1597
  /** 3e: the unregister of this command's detach — while it runs in the foreground */
1520
1598
  let undetachable = () => { };
1521
1599
  let recent = ""; // the tail readyWhen is matched against
@@ -1565,6 +1643,7 @@ export function shellTool(opts) {
1565
1643
  undetachable();
1566
1644
  dropProgress();
1567
1645
  clearTimeout(timer);
1646
+ clearTimeout(drainTimer);
1568
1647
  // E group: the abort listener is removed once settled — it
1569
1648
  // must not accumulate across runs.
1570
1649
  ctx.signal.removeEventListener("abort", onAbort);
@@ -1578,6 +1657,7 @@ export function shellTool(opts) {
1578
1657
  const r = capAccumulate(stdout, text);
1579
1658
  stdout = r.text;
1580
1659
  stdoutDropped += r.dropped;
1660
+ armDrain();
1581
1661
  if (tasks !== undefined && matchReady(text))
1582
1662
  promote("ready");
1583
1663
  });
@@ -1589,6 +1669,7 @@ export function shellTool(opts) {
1589
1669
  const r = capAccumulate(stderr, text);
1590
1670
  stderr = r.text;
1591
1671
  stderrDropped += r.dropped;
1672
+ armDrain();
1592
1673
  if (tasks !== undefined && matchReady(text))
1593
1674
  promote("ready");
1594
1675
  });
@@ -1602,6 +1683,11 @@ export function shellTool(opts) {
1602
1683
  ? ""
1603
1684
  : `\n… [${stream} capped at ${OUTPUT_CAP} chars — ${dropped} more chars dropped; capture to a file and read it with read_file, or narrow the command]`;
1604
1685
  const combinedOutput = () => (stdout + overflowNote(stdoutDropped, "stdout") + (stderr ? `\n[stderr] ${stderr}` : "") + overflowNote(stderrDropped, "stderr")).trim();
1686
+ /** the output captured so far, for a timeout or abort verdict: bounded like a normal result (the same caps and notes) */
1687
+ const partialOutput = () => {
1688
+ const o = combinedOutput();
1689
+ return o === "" ? "" : `\n${o}`;
1690
+ };
1605
1691
  let exitInfo = null;
1606
1692
  child.on("close", (code, signal) => {
1607
1693
  if (promoted !== null) {
@@ -1611,6 +1697,9 @@ export function shellTool(opts) {
1611
1697
  promoted.out.close();
1612
1698
  promoted.task.ended(code, signal);
1613
1699
  }
1700
+ else if (stopLeftHolders) {
1701
+ promoted.out.close();
1702
+ }
1614
1703
  return;
1615
1704
  }
1616
1705
  if (killing)
@@ -1620,6 +1709,44 @@ export function shellTool(opts) {
1620
1709
  ? { content: combined || "(no output)", isError: false }
1621
1710
  : { content: `exit ${code}: ${combined}`, isError: true, errorKind: "fatal" });
1622
1711
  });
1712
+ // The shell's own exit arrives before `close` whenever a descendant keeps
1713
+ // its stdout/stderr open (a daemon started by the command): `close` then
1714
+ // waits for the descendant, not for the command. The exit is recorded;
1715
+ // after a short drain window with no `close`, a manager takes the
1716
+ // descendants over (below). It is never read as "done" on its own.
1717
+ child.on("exit", (code, signal) => {
1718
+ // our own kill (timeout, abort) is not the command's exit
1719
+ if (killing)
1720
+ return;
1721
+ exitAtMs = Date.now();
1722
+ exitSeen = { code, signal, afterMs: exitAtMs - spawnedAt };
1723
+ if (settled || promoted !== null)
1724
+ return;
1725
+ armDrain();
1726
+ });
1727
+ const exitHow = () => (exitSeen === null ? "" : exitSeen.signal !== null ? `signal ${exitSeen.signal}` : `code ${exitSeen.code}`);
1728
+ /** a timeout/abort verdict on a command that had already exited says so */
1729
+ const exitNote = () => exitSeen === null ? "" : `; the command itself had already exited (${exitHow()}, after ${exitSeen.afterMs} ms) but processes it started still held its output`;
1730
+ const onDrainExpired = () => {
1731
+ if (settled || killing || promoted !== null || exitSeen === null)
1732
+ return;
1733
+ // No manager: nothing can own the descendants, so the call is NOT complete
1734
+ // and is not reported as such — it keeps waiting for `close` under the
1735
+ // same kill timeout and abort.
1736
+ if (tasks === undefined)
1737
+ return;
1738
+ // win32: `taskkill /T` walks from a live root, so a stop could not reach
1739
+ // the descendants of an exited one. Not handed over there until it can.
1740
+ if (process.platform === "win32")
1741
+ return;
1742
+ try {
1743
+ promote("exited");
1744
+ }
1745
+ catch {
1746
+ // the hand-over was refused: keep waiting. Success is never reported for
1747
+ // work no manager owns.
1748
+ }
1749
+ };
1623
1750
  // The kernel's abort reaches the command AND its whole tree.
1624
1751
  // The listener is removed by settle (E group).
1625
1752
  const uncertainVerdict = (unconfirmed) => unconfirmed.length > 0
@@ -1628,7 +1755,7 @@ export function shellTool(opts) {
1628
1755
  const onAbort = () => {
1629
1756
  killing = true;
1630
1757
  void killTree(child).then(({ unconfirmed }) => settle({
1631
- content: `shell aborted${uncertainVerdict(unconfirmed) ? ` — ${uncertainVerdict(unconfirmed)}` : ""}`,
1758
+ content: `shell aborted${uncertainVerdict(unconfirmed) ? ` — ${uncertainVerdict(unconfirmed)}` : ""}${exitNote()}${partialOutput()}`,
1632
1759
  isError: true,
1633
1760
  errorKind: "fatal",
1634
1761
  }));
@@ -1649,12 +1776,47 @@ export function shellTool(opts) {
1649
1776
  startedAt: spawnedAt,
1650
1777
  stop: () => stopPromoted(),
1651
1778
  });
1652
- const out = new RotatingOutput(task.outputPath, TASK_OUTPUT_CAP);
1779
+ // the task is in the journal from here. If its output cannot be prepared the
1780
+ // hand-over failed and NOTHING about the command ended: it still runs under
1781
+ // this call. The record says only that the task's outcome is not known (no
1782
+ // terminal is invented: the journal has no record for a failed hand-over, see
1783
+ // ADR-0058 Amendment 17); the failure is the caller's to see.
1784
+ let out;
1653
1785
  const sofar = combinedOutput();
1654
- if (sofar !== "")
1655
- out.write(Buffer.from(`${sofar}\n`));
1786
+ try {
1787
+ out = new RotatingOutput(task.outputPath, TASK_OUTPUT_CAP);
1788
+ if (sofar !== "")
1789
+ out.write(Buffer.from(`${sofar}\n`));
1790
+ }
1791
+ catch (err) {
1792
+ try {
1793
+ out?.close();
1794
+ }
1795
+ catch {
1796
+ // nothing more to release
1797
+ }
1798
+ if (task.handoverFailed !== undefined)
1799
+ task.handoverFailed(err instanceof Error ? err.message : String(err));
1800
+ else
1801
+ task.unconfirmed([]);
1802
+ throw err;
1803
+ }
1804
+ rootGone = reason === "exited";
1656
1805
  promoted = { task, out };
1657
1806
  const tail = sofar.length > 2048 ? `…${sofar.slice(-2048)}` : sofar;
1807
+ if (reason === "exited" && exitSeen !== null) {
1808
+ // the command is done; what the manager now owns is whatever it left running on its pipes
1809
+ const failed = exitSeen.signal !== null || exitSeen.code !== 0;
1810
+ settle({
1811
+ content: `the command exited (${exitHow()}, after ${exitSeen.afterMs} ms)${failed ? " and FAILED" : ""}, but processes it started are still running and hold its output` +
1812
+ `${failed ? " — its side effects may be incomplete" : ""}; ` +
1813
+ `they continue as background task ${task.id} (not killed — it keeps running). ${taskNote(task.outputPath)}` +
1814
+ (tail !== "" ? `\nOutput so far:\n${tail}` : ""),
1815
+ isError: failed,
1816
+ ...(failed ? { errorKind: "fatal" } : {}),
1817
+ });
1818
+ return;
1819
+ }
1658
1820
  settle({
1659
1821
  content: (reason === "ready"
1660
1822
  ? `ready — the output contains "${readyWhen}"; `
@@ -1688,6 +1850,18 @@ export function shellTool(opts) {
1688
1850
  promoted.task.unconfirmed(unconfirmed);
1689
1851
  return;
1690
1852
  }
1853
+ // A root that had already exited leaves nothing to enumerate a descendant
1854
+ // from if that descendant left its group and was reparented, and the pipes
1855
+ // closing proves nothing about it: it may have closed its stdout/stderr and
1856
+ // lived on. What the sweep found is gone; that nothing else is left cannot be
1857
+ // shown, so the stop is recorded as unconfirmed and never as stopped.
1858
+ if (rootGone) {
1859
+ promoted.task.unconfirmed([]);
1860
+ stopLeftHolders = true;
1861
+ if (exitInfo !== null)
1862
+ promoted.out.close();
1863
+ return;
1864
+ }
1691
1865
  const waitClose = Date.now() + 1_000;
1692
1866
  while (exitInfo === null && Date.now() < waitClose)
1693
1867
  await new Promise((r) => setTimeout(r, 25));
@@ -1708,7 +1882,7 @@ export function shellTool(opts) {
1708
1882
  }
1709
1883
  killing = true;
1710
1884
  void killTree(child).then(({ unconfirmed }) => settle({
1711
- content: `shell timed out after ${timeout}ms${uncertainVerdict(unconfirmed) ? ` — ${uncertainVerdict(unconfirmed)}` : ""}`,
1885
+ content: `shell timed out after ${timeout}ms${uncertainVerdict(unconfirmed) ? ` — ${uncertainVerdict(unconfirmed)}` : ""}${exitNote()}${partialOutput()}`,
1712
1886
  isError: true,
1713
1887
  errorKind: "fatal",
1714
1888
  }));
@@ -14,6 +14,11 @@ export interface ProcessTaskBackendOptions {
14
14
  readonly runnerPath?: string;
15
15
  /** How long the runner has to record itself. Default 10 s. */
16
16
  readonly startTimeoutMs?: number;
17
+ /** Environment for the RUNNER process only. The command the runner starts
18
+ * does not inherit it. A host whose `process.execPath` is not node (an
19
+ * Electron app: `ELECTRON_RUN_AS_NODE=1` makes the runner a plain node
20
+ * process and keeps it out of the Dock) says so here. */
21
+ readonly runnerEnv?: Readonly<Record<string, string>>;
17
22
  }
18
23
  export interface ProcessTaskBackend {
19
24
  spawn(spec: {
@@ -22,7 +22,12 @@ export function processTaskBackend(options = {}) {
22
22
  for (const [k, v] of Object.entries(env))
23
23
  if (v !== undefined)
24
24
  clean[k] = v;
25
- const child = spawn(process.execPath, [runnerPath, dir], { detached: true, windowsHide: true, stdio: "ignore", env: clean });
25
+ // the runner hands its own environment to the command; the names it
26
+ // was given for itself are listed so it can take them out again
27
+ const own = options.runnerEnv ?? {};
28
+ const ownNames = Object.keys(own);
29
+ const runnerEnv = ownNames.length === 0 ? clean : { ...clean, ...own, KISO_TASK_RUNNER_OWN_ENV: ownNames.join(",") };
30
+ const child = spawn(process.execPath, [runnerPath, dir], { detached: true, windowsHide: true, stdio: "ignore", env: runnerEnv });
26
31
  child.unref();
27
32
  const journal = join(dir, "journal.jsonl");
28
33
  const deadline = Date.now() + (options.startTimeoutMs ?? 10_000);
@@ -105,6 +105,11 @@ function main(dir) {
105
105
  delete env.KISO_TASK_RUNNER_PAUSE_AFTER;
106
106
  delete env.KISO_TASK_RUNNER_STOP_UNCONFIRMED;
107
107
  delete env.KISO_TASK_OUTPUT_CAP;
108
+ // names the backend set for the runner alone (processTaskBackend runnerEnv)
109
+ for (const name of (env.KISO_TASK_RUNNER_OWN_ENV ?? "").split(","))
110
+ if (name !== "")
111
+ delete env[name];
112
+ delete env.KISO_TASK_RUNNER_OWN_ENV;
108
113
  // "" when the start time cannot be read: the identity is then the pid
109
114
  // alone, and the backend treats it as unverifiable, never as dead
110
115
  const self = processStartTime(process.pid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tools-node",
3
- "version": "0.51.1",
3
+ "version": "0.53.0",
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.51.1",
28
+ "@vincemakes/kiso-core": "0.53.0",
29
29
  "ignore": "^7.0.9"
30
30
  },
31
31
  "devDependencies": {