@molecule/api-ai-tools 1.11.1 → 1.13.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/utilities.js CHANGED
@@ -5,9 +5,10 @@
5
5
  * @module
6
6
  */
7
7
  import { posix } from 'path';
8
- import { commandSegments, executedCommands, locatedCommandSegments, statementArgumentEnd, withoutTimeoutWrapper, } from './command-position.js';
8
+ import { commandSegments, executedCommands, locatedCommandSegments, statementArgumentEnd, statementBoundary, withoutTimeoutWrapper, } from './command-position.js';
9
9
  import { classifyRunFailure } from './failure-kind.js';
10
10
  import { commandRunsNoTests } from './no-test-run.js';
11
+ import { PLAYWRIGHT_REPORT_ENV } from './playwright-report.js';
11
12
  import { isDiagnosisRun, scopedRerunCommand, suiteCommandTarget, suiteRunInTheWay, withoutHeredocs, withSuiteConfig, } from './suite-overlap.js';
12
13
  /**
13
14
  * Shell-safe quoting using single quotes. Unlike JSON.stringify (double quotes),
@@ -163,8 +164,41 @@ export function redactSecrets(s) {
163
164
  export function redactSecretsInCode(s) {
164
165
  return s.replace(SECRET_ENV_ASSIGNMENT, '$1=[REDACTED]');
165
166
  }
167
+ /**
168
+ * The placeholder the output filter substitutes for a hidden value. Copying it into
169
+ * application configuration changes the value rather than recovering it. The write
170
+ * tools check for added occurrences ({@link addedRedactionMask}); intentional literals
171
+ * require the caller's explicit per-call declaration.
172
+ *
173
+ * Only the shapes a value can take count: quoted (`'[REDACTED]'`, `"[REDACTED]"`) or
174
+ * assigned (`=[REDACTED]`). Prose that mentions the token is ordinary text.
175
+ */
176
+ const REDACTION_MASK = /'\[REDACTED\]'|"\[REDACTED\]"|=[ \t]*\[REDACTED\]/g;
177
+ /**
178
+ * The redaction mask `next` adds over `previous`, or null when it adds none.
179
+ *
180
+ * `previous` is the file's previous content for `write_file`, and the replacement's own
181
+ * `old_string` for `edit_file` — an edit that carries an existing mask through (a file that
182
+ * already had one, read back and rewritten) adds no occurrence. A new occurrence may
183
+ * be copied output or intentional source; the caller declares the latter explicitly.
184
+ *
185
+ * @param next - The content about to be written (or the replacement's `new_string`).
186
+ * @param previous - The content it replaces, already on disk (or the `old_string`).
187
+ * @returns The added mask as written (`'[REDACTED]'`, `"[REDACTED]"`, `=[REDACTED]`), or null.
188
+ */
189
+ export function addedRedactionMask(next, previous) {
190
+ const before = previous.match(REDACTION_MASK)?.length ?? 0;
191
+ const after = next.match(REDACTION_MASK) ?? [];
192
+ if (after.length <= before)
193
+ return null;
194
+ return after[before] ?? after[0] ?? '[REDACTED]';
195
+ }
166
196
  /** Source-file extensions a printing command's output is read as code for. */
167
197
  const SOURCE_FILE_RE = /\.(?:[cm]?[jt]sx?|css|scss|html|vue|svelte|md|sql|hurl)$/;
198
+ /** Programs whose output is file NAMES or counts, never another file's content. */
199
+ const INSPECTION_PROGRAMS = new Set(['find', 'ls', 'wc']);
200
+ /** `find` options that run a program or write a file — not read-only inspection. */
201
+ const FIND_NOT_INSPECTION = /^-(?:exec|execdir|ok|okdir|delete|fls|fprint|fprint0|fprintf)/;
168
202
  /**
169
203
  * A literal argument, not a shell expansion. The shared parser owns statement boundaries;
170
204
  * this deliberately small word grammar only decides whether an argument can read other data.
@@ -253,9 +287,46 @@ function sourcePrintFiles(program, args) {
253
287
  * @returns True when the command prints source files, with only literal headings and filters.
254
288
  */
255
289
  export function printsOnlySource(command) {
290
+ return printsOnly(command, false);
291
+ }
292
+ /**
293
+ * Whether a shell command only INSPECTS the workspace WITHOUT printing a file's contents:
294
+ * it prints source files (a pure literal read, {@link printsOnlySource}), lists names
295
+ * (`ls`, `find`), or counts lines (`wc`) — joined by `;`, `&&` or `|`, with literal
296
+ * `echo`/`printf` headings and `cd`.
297
+ *
298
+ * Its output is code or names, not an env dump, so it gets the code-safe redaction
299
+ * ({@link redactSecretsInCode}) instead of the name-keyed env-dump passes. Without this,
300
+ * ordinary code values such as `tokenKey: 'jwtToken'` in a mixed source read and
301
+ * `ls | head` command would be masked by the environment-dump rules.
302
+ *
303
+ * A RECURSIVE SEARCH IS NOT INSPECTION (`grep -rn x .`, `rg x src/`): its output is the
304
+ * content of whichever files matched, including `.env` files and JSON secret dumps, so it
305
+ * keeps the full grade. A path with no extension is not evidence that it is a directory —
306
+ * `/dev/stdin` and an ordinary `secrets` file read the same way.
307
+ *
308
+ * Env dumps keep the full grade: `.env` anywhere in the command, `env`/`printenv`, a
309
+ * non-literal or redirecting form, and an interpreter one-liner (`node -e`, `python -c`)
310
+ * all return false.
311
+ *
312
+ * @param command - The shell command.
313
+ * @returns True when the command only reads or lists workspace content.
314
+ */
315
+ export function isReadOnlyInspection(command) {
316
+ return printsOnly(command, true);
317
+ }
318
+ /**
319
+ * The one segment grammar both grades share: literal words only, no wrapper or group, no
320
+ * backgrounding, and every byte between segments a separator.
321
+ *
322
+ * @param command - The shell command.
323
+ * @param inspection - Also accept name/count listing (`ls`, `find`, `wc`).
324
+ * @returns True when every segment is allowed and at least one prints something.
325
+ */
326
+ function printsOnly(command, inspection) {
256
327
  if (/\.env\b/.test(command))
257
328
  return false;
258
- let readsSource = false;
329
+ let prints = false;
259
330
  let previousEnd = 0;
260
331
  for (const located of locatedCommandSegments(command)) {
261
332
  const { segment, start, end, programOffset, separator, terminator } = located;
@@ -302,6 +373,13 @@ export function printsOnlySource(command) {
302
373
  return false;
303
374
  }
304
375
  }
376
+ else if (inspection && INSPECTION_PROGRAMS.has(program) && separator !== '|') {
377
+ // Names and counts only — nothing here can be another file's content. `find` is the
378
+ // exception that can run a program or write a file.
379
+ if (program === 'find' && args.some((arg) => FIND_NOT_INSPECTION.test(arg)))
380
+ return false;
381
+ prints = true;
382
+ }
305
383
  else {
306
384
  const filter = separator === '|';
307
385
  if (!(filter ? /^(?:head|tail|sort|uniq|wc|cut|nl|grep)$/ : /^(?:cat|head|tail|nl|sed|grep|rg)$/).test(program)) {
@@ -319,11 +397,11 @@ export function printsOnlySource(command) {
319
397
  !files.every((file) => !file.startsWith('-') && SOURCE_FILE_RE.test(file))) {
320
398
  return false;
321
399
  }
322
- readsSource = true;
400
+ prints = true;
323
401
  }
324
402
  }
325
403
  }
326
- return readsSource && /^[\s;]*$/.test(command.slice(previousEnd));
404
+ return prints && /^[\s;]*$/.test(command.slice(previousEnd));
327
405
  }
328
406
  /**
329
407
  * Whether a path is an env file, for which {@link redactSecrets}' full env-dump
@@ -535,10 +613,17 @@ function isStatusOnlyStep(step) {
535
613
  * excludes by name ({@link DiagnosisRunContext}). A `--grep-invert` of exactly those is the
536
614
  * request's own scope, not a diagnosis, so it keeps the suite's retries on a deployed proof
537
615
  * (xr-molecule-flash-m170 +33:16).
538
- * @returns The command to run and one sentence per change made (empty when none), or `error` when
539
- * the command starts the suite more than once.
540
- */
541
- export function streamingBackgroundCommand(command, context) {
616
+ * @param reportPath - Where this run should write Playwright's standard `json` report, when the
617
+ * caller keeps one ({@link ./playwright-report.js}). The reporter is added to the run's own
618
+ * reporter set, never in place of one, and its output goes to that file through
619
+ * `PLAYWRIGHT_JSON_OUTPUT_FILE` — so the log, the executors' own reporters and every verdict
620
+ * stay exactly as they are. Omitted, or a run whose reporter set is not this function's to place
621
+ * (already names `json`, sets the variable itself, or whose runner cannot be located), leaves
622
+ * the command untouched and the result's `reportPath` unset.
623
+ * @returns The command to run, one sentence per change made (empty when none), and the report path
624
+ * when one was actually wired — or `error` when the command starts the suite more than once.
625
+ */
626
+ export function streamingBackgroundCommand(command, context, reportPath) {
542
627
  // A command that runs no test is not a run to keep a growing log for: nothing about it is
543
628
  // rewritten (flash-m79's `--list` was read as a whole-suite run end to end, and got a ~4 KB
544
629
  // refusal to send itself again with `--max-failures=15`).
@@ -666,14 +751,26 @@ export function streamingBackgroundCommand(command, context) {
666
751
  // The last step's reporters: one printer, and it is `list`. Every branch below only ever moves
667
752
  // the command TO that shape — a reporter set is never left with two printers in it.
668
753
  const reporters = finalPlaywrightRun ? lastReporterFlag(lastStep) : null;
754
+ // The standard machine report this run may carry (`playwright-report.ts`). It is never taken from
755
+ // the executor: a reporter set already naming `json`, or a command that sets
756
+ // `PLAYWRIGHT_JSON_OUTPUT_FILE` itself, is the executor's own artifact and is left untouched.
757
+ const reportWanted = reportPath !== undefined &&
758
+ finalPlaywrightRun &&
759
+ !/(?:^|[\s;])PLAYWRIGHT_JSON_OUTPUT_FILE=/.test(out) &&
760
+ !(reporters?.values.includes('json') ?? false);
761
+ let report = null;
669
762
  if (reporters) {
670
763
  const printing = reporters.values.filter((v) => PRINTING_REPORTERS.has(v));
671
764
  // Already exactly `list`: the shape, and nothing printing beside it. Left verbatim — the
672
765
  // executor's own spelling of the command is not rewritten for nothing.
673
- if (printing.length > 0 && !(printing.length === 1 && printing[0] === 'list')) {
766
+ const printerRewrite = printing.length > 0 && !(printing.length === 1 && printing[0] === 'list');
767
+ const addReport = reportWanted && !reporters.values.includes('json');
768
+ if (printerRewrite || addReport) {
674
769
  // The printing reporters become ONE `list`, in the place of the first of them; every other
675
770
  // reporter (a `json`/`html`/`junit` file the executor asked for, or one this build does not
676
- // know) is left where it was.
771
+ // know) is left where it was. The machine report, when this run carries one, is appended to
772
+ // the same set: it writes the runner's own per-test identities and attachment paths to a
773
+ // file and prints nothing into the log.
677
774
  let placed = false;
678
775
  const kept = reporters.values
679
776
  .map((v) => {
@@ -685,6 +782,11 @@ export function streamingBackgroundCommand(command, context) {
685
782
  return 'list';
686
783
  })
687
784
  .filter((v) => v !== null);
785
+ // The sentence below is about the reporTERS this run prints with, so it reads the set as it
786
+ // was rewritten — the machine report is written to a file and is no part of that sentence.
787
+ const printers = kept.join(',');
788
+ if (addReport)
789
+ kept.push('json');
688
790
  // Only the last step is rewritten, and its offsets are the parse's own. The punctuation
689
791
  // after the step (`; }`, or the `)` attached to it) is kept verbatim.
690
792
  out =
@@ -694,26 +796,39 @@ export function streamingBackgroundCommand(command, context) {
694
796
  lastStep.slice(reporters.end) +
695
797
  rawLastStep.slice(lastStep.length) +
696
798
  out.slice(stepStart + rawLastStep.length);
697
- changes.push(
698
- // `list` was already there and a second printer sat beside it: that one is dropped. When
699
- // no `list` was named at all, the printer is SWITCHED to it (flash-m62's `line`).
700
- printing.includes('list')
701
- ? `Dropped \`${printing
702
- .filter((v) => v !== 'list')
703
- .join(',')}\` from your \`--reporter=${reporters.values.join(',')}\`: ` +
704
- 'two reporters that both print each failure write every test result into the log ' +
705
- 'twice — the whole log doubled for nothing (X1 flash-m83) — so this run keeps ' +
706
- '`list` alone.'
707
- : `Switched the test reporters to \`--reporter${reporters.separator}${kept.join(',')}\`: ` +
708
- '`list` writes each test result as it finishes and is the shape the platform reads ' +
709
- 'back from a background run, and it is the only printing reporter — two of them ' +
710
- 'write every result into the log twice (X1 flash-m83).');
799
+ if (printerRewrite) {
800
+ changes.push(
801
+ // `list` was already there and a second printer sat beside it: that one is dropped. When
802
+ // no `list` was named at all, the printer is SWITCHED to it (flash-m62's `line`).
803
+ printing.includes('list')
804
+ ? `Dropped \`${printing
805
+ .filter((v) => v !== 'list')
806
+ .join(',')}\` from your \`--reporter=${reporters.values.join(',')}\`: ` +
807
+ 'two reporters that both print each failure write every test result into the log ' +
808
+ 'twice — the whole log doubled for nothing (X1 flash-m83) — so this run keeps ' +
809
+ '`list` alone.'
810
+ : `Switched the test reporters to \`--reporter${reporters.separator}${printers}\`: ` +
811
+ '`list` writes each test result as it finishes and is the shape the platform reads ' +
812
+ 'back from a background run, and it is the only printing reporter — two of them ' +
813
+ 'write every result into the log twice (X1 flash-m83).');
814
+ }
815
+ if (addReport)
816
+ report = reportPath;
711
817
  }
712
818
  }
713
- else if (finalPlaywrightRun && addToRun('--reporter=list')) {
819
+ else if (finalPlaywrightRun &&
820
+ addToRun(reportWanted ? '--reporter=list,json' : '--reporter=list')) {
821
+ if (reportWanted)
822
+ report = reportPath;
714
823
  changes.push('Added `--reporter=list` so each test result is written to the log as it finishes ' +
715
824
  '(the config reporter may print nothing until the end).');
716
825
  }
826
+ // The report's own file option, set once at the head of the whole command — the json reporter
827
+ // reads it from the environment, resolved against the runner's directory, so an absolute path is
828
+ // used and the file lands beside the task's own log whatever `cd` the run opens with.
829
+ if (report !== null) {
830
+ out = `export ${PLAYWRIGHT_REPORT_ENV}=${shellQuote(report)}; ${out}`;
831
+ }
717
832
  // The run's failures, in the log as they fail (see PLAYWRIGHT_FAILURES_INLINE_ENV), on EVERY
718
833
  // background command that is not a no-test invocation — not only the ones whose text this
719
834
  // reader can place the runner in. The executor starts the suite through its own wrapper often
@@ -748,7 +863,58 @@ export function streamingBackgroundCommand(command, context) {
748
863
  if (addToRun('--retries=0'))
749
864
  changes.push(`Added \`--retries=0\`: ${why}.`);
750
865
  }
751
- return { command: out, changes };
866
+ return { command: out, changes, ...(report === null ? {} : { reportPath: report }) };
867
+ }
868
+ /**
869
+ * Put Playwright's standard `json` report on a FOREGROUND run that already names its own reporters.
870
+ *
871
+ * A background run is rewritten onto the platform's reporter anyway ({@link
872
+ * streamingBackgroundCommand}), so adding the machine report there costs nothing. A foreground run
873
+ * is not: its reporters are either the command's own flags or the suite's config, and replacing a
874
+ * config reporter would stop writing the artifact the executor configured. So this only ever
875
+ * APPENDS `json` to a `--reporter` set the command already wrote — the log keeps every printing
876
+ * reporter it had, the executor's file reporters keep their place, and the standard report goes to
877
+ * {@link PLAYWRIGHT_REPORT_ENV}'s file.
878
+ *
879
+ * A command that names no reporter at all is left exactly as written: its reporters are the suite
880
+ * config's, and this tool does not rewrite those. A command that already names `json`, or sets
881
+ * `PLAYWRIGHT_JSON_OUTPUT_FILE` itself, already has a report of its own — a path the executor chose
882
+ * is never overwritten.
883
+ *
884
+ * @param command - The command the executor sent, after every other rewrite.
885
+ * @param reportPath - The file this run's machine report should be written to.
886
+ * @returns The command to run and the report path when one was wired; `reportPath` is null when the
887
+ * run was left untouched and no report may be expected.
888
+ */
889
+ export function withPlaywrightRunReport(command, reportPath) {
890
+ const unchanged = { command, reportPath: null };
891
+ if (reportPath === '')
892
+ return unchanged;
893
+ if (!isPlaywrightRunCommand(command))
894
+ return unchanged;
895
+ if (/(?:^|[\s;])PLAYWRIGHT_JSON_OUTPUT_FILE=/.test(command))
896
+ return unchanged;
897
+ const run = lastSuiteRun(command);
898
+ const runner = run ? run.located[run.at] : undefined;
899
+ if (!runner || runner.start === null || runner.end === null)
900
+ return unchanged;
901
+ const at = statementArgumentEnd(command, runner.start, runner.end);
902
+ if (at === null)
903
+ return unchanged;
904
+ // The reporter flag the RUN's own statement names — not one an argument of another program
905
+ // carries (`grep --reporter x file`): the slice starts at the runner's own program word.
906
+ const flag = lastReporterFlag(command.slice(runner.start, at));
907
+ if (flag === null || flag.values.includes('json'))
908
+ return unchanged;
909
+ const start = runner.start + flag.start;
910
+ const end = runner.start + flag.end;
911
+ const rewritten = command.slice(0, start) +
912
+ `--reporter${flag.separator}${[...flag.values, 'json'].join(',')}` +
913
+ command.slice(end);
914
+ return {
915
+ command: `export ${PLAYWRIGHT_REPORT_ENV}=${shellQuote(reportPath)}; ${rewritten}`,
916
+ reportPath,
917
+ };
752
918
  }
753
919
  /**
754
920
  * How a shell command starts each runner whose whole-suite run saturates the
@@ -1171,6 +1337,10 @@ function lastSuiteRun(command) {
1171
1337
  * tail, and a 95-second walk's log held only the tally while the platform's own hold had handed
1172
1338
  * that command back).
1173
1339
  *
1340
+ * The command around the dropped stages is left as written: a group they sit in keeps its closer
1341
+ * (`( … | tail -25)` → `( … )`, never `( …` — xr-molecule-flash-m418 +23:31), and a stage the cut
1342
+ * would leave a closer dangling inside is not dropped at all.
1343
+ *
1174
1344
  * @param command - The shell command.
1175
1345
  * @returns The rewritten command and the stages dropped.
1176
1346
  */
@@ -1204,15 +1374,40 @@ export function stripTrailingSuiteFilters(command) {
1204
1374
  // A stage inside a nested `sh -c` string has no offsets in this command: nothing can be cut.
1205
1375
  if (previous.end === null || stage.start === null || lastDropped.end === null)
1206
1376
  return { command, dropped: '' };
1377
+ // Every stage dropped must be provably whole inside the command's own structure. A group
1378
+ // closer attached to one of them closes a group the pipeline below still runs in
1379
+ // (`( … | tail -5) | cat`), and a stage that OPENS a group it does not close (`| { cat; }`) has
1380
+ // its closer in a later statement — either cut would leave the punctuation dangling. Only the
1381
+ // last stage may end on an enclosing closer, and it is kept ({@link statementBoundary}).
1382
+ const boundaries = [];
1383
+ for (let i = first; i <= end; i += 1) {
1384
+ const droppedStage = located[i];
1385
+ if (!droppedStage || droppedStage.end === null)
1386
+ return { command, dropped: '' };
1387
+ const boundary = statementBoundary(command, droppedStage.start, droppedStage.end);
1388
+ if (boundary === null || boundary.leavesGroupOpen)
1389
+ return { command, dropped: '' };
1390
+ if (i < end && boundary.textEnd !== droppedStage.end)
1391
+ return { command, dropped: '' };
1392
+ boundaries.push(boundary);
1393
+ }
1394
+ const lastBoundary = boundaries[boundaries.length - 1];
1395
+ if (lastBoundary === undefined)
1396
+ return { command, dropped: '' };
1397
+ // Where the dropped stages' own text ends. The tokenizer attaches the closer of an enclosing
1398
+ // group to the stage's last word (`tail -25)`), so cutting at {@link LocatedSegment.end} deleted
1399
+ // the `)` and left `( … 2>&1` — a command no shell parses, which the background runner rejected
1400
+ // with `end of file unexpected` (xr-molecule-flash-m418 +23:31).
1401
+ const dropEnd = lastBoundary.textEnd;
1207
1402
  const pipe = stage.between.indexOf('|');
1208
1403
  if (pipe < 0)
1209
1404
  return { command, dropped: '' };
1210
1405
  const cut = previous.end + pipe;
1211
1406
  // The stages dropped are a middle slice when a statement follows, so the text after them is
1212
- // kept — `; echo done` still runs.
1407
+ // kept — `; echo done` still runs, and so does the punctuation closing the group they sat in.
1213
1408
  return {
1214
- command: `${command.slice(0, cut).trimEnd()}${command.slice(lastDropped.end)}`,
1215
- dropped: command.slice(cut, lastDropped.end).trim(),
1409
+ command: `${command.slice(0, cut).trimEnd()}${command.slice(dropEnd)}`,
1410
+ dropped: command.slice(cut, dropEnd).trim(),
1216
1411
  };
1217
1412
  }
1218
1413
  /**
@@ -3980,11 +4175,8 @@ export function repeatingFailureNote(found, o) {
3980
4175
  `repeats this one cause. Stop it and fix that cause first: \`exec_command: ${o.stop}\`, ` +
3981
4176
  `then run the whole suite again. It is still going (${where}).${reproduce}`);
3982
4177
  }
3983
- // Everything else is a note on a run the wait is still waiting for, and both its shapes end the
3984
- // same way: the run finishes, because it is the only thing that names the causes this finding is
3985
- // not. What it does NOT carry is the "Stop it if every test fails this way" clause — that one
3986
- // belongs to a PROVEN cause the run is still walking (the sentence below), never to a lead:
3987
- // xr-molecule-flash-m99's executor read it on a shared duration and voided the run.
4178
+ // The same predicate controls both an early return and stop advice. Without
4179
+ // that verdict, neither a repeated error nor a shared duration asks for a stop.
3988
4180
  const keepGoing = `Let the rest of the run finish — it is the only thing that will name the causes that are NOT ` +
3989
4181
  `this one, and a run stopped at its first repeating cause costs a second whole run to find ` +
3990
4182
  `them. It is still going (${where}).`;
@@ -3992,10 +4184,9 @@ export function repeatingFailureNote(found, o) {
3992
4184
  return (`${found.count} different tests in this run have already failed the same way — ${signature} — ` +
3993
4185
  `so one cause is repeating:\n${rows}${more}\n` +
3994
4186
  (o.editsInvalidateRun
3995
- ? `Work out the fix now: apply it when the run ends, not while it walks. Stop it only if every test it walks `
3996
- : `Start fixing it now, while the run keeps walking. Stop it only if every test it walks `) +
3997
- `fails this same way (the app unreachable, a 5xx on every request, the same selector timing ` +
3998
- `out on every test of every file so far).${reproduce} ${keepGoing}${thenRun}`);
4187
+ ? `Work out the fix now: apply it when the run ends, not while it walks.`
4188
+ : `Start fixing it now, while the run keeps walking.`) +
4189
+ `${reproduce} ${keepGoing}${thenRun}`);
3999
4190
  return (`${found.count} different tests in this run each failed after ~${found.seconds}s — the same ` +
4000
4191
  `duration to within half a second. A shared duration is a lead, not a cause: a suite whose ` +
4001
4192
  `config sets a timeout spends that timeout on every failure that waits for anything, so ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-ai-tools",
3
- "version": "1.11.1",
3
+ "version": "1.13.0",
4
4
  "description": "Shared AI agent tools with backend abstraction for sandbox and local execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,7 @@
35
35
  "@types/node": "26.1.2",
36
36
  "esbuild": "0.28.2",
37
37
  "typescript": "6.0.3",
38
- "vitest": "4.1.11"
38
+ "vitest": "4.1.12"
39
39
  },
40
40
  "repository": {
41
41
  "type": "git",