tickmarkr 1.76.0 → 1.77.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.
@@ -137,16 +137,37 @@ export function confirmKimiSeedBanner(banner, assignedModel) {
137
137
  }
138
138
  return { ok: true, sessionId: kimiBannerSessionId(banner) };
139
139
  }
140
- // v1.75 T1 / OBS-136: this readiness line is rendered inside Kimi Code's bordered steady-state
141
- // input box. The adapter owns the fingerprint; the driver only consults declarations generically.
140
+ const kimiTuiLaunch = (model) => `kimi -y -m ${shq(model)}`;
141
+ const KIMI_ANSI_SGR_RE = /\u001B\[[0-9;]*m/g;
142
+ const KIMI_EDITOR_TOP_RE = /^╭─+╮$/;
143
+ const KIMI_EDITOR_INPUT_RE = /^│ > .*│$/;
144
+ const KIMI_EDITOR_EMPTY_RE = /^│ > +│$/;
145
+ const KIMI_EDITOR_BOTTOM_RE = /^╰─+╯$/;
146
+ function matchesKimiEditorBox(paneText, empty) {
147
+ const lines = paneText.replace(KIMI_ANSI_SGR_RE, "").split("\n");
148
+ const input = empty ? KIMI_EDITOR_EMPTY_RE : KIMI_EDITOR_INPUT_RE;
149
+ return lines.some((line, index) => input.test(line)
150
+ && index > 0
151
+ && index + 1 < lines.length
152
+ && KIMI_EDITOR_TOP_RE.test(lines[index - 1])
153
+ && KIMI_EDITOR_BOTTOM_RE.test(lines[index + 1]));
154
+ }
155
+ // v1.75 T1 / OBS-136: Kimi's actual editor is a three-line bordered box whose content row starts
156
+ // `│ > `. The welcome panel separately contains "Send /help..." and is only launch-liveness
157
+ // evidence (OBS-142), so it must never satisfy this declaration. The adapter owns the full matcher,
158
+ // empty-state matcher, launch distinction, and bounded cold-start window; the driver stays generic.
142
159
  export const KIMI_INPUT_BOX = declareInputBox("kimi", {
143
- fingerprint: "Send /help for help information.",
160
+ fingerprint: "│ > ",
161
+ match: (paneText) => matchesKimiEditorBox(paneText, false),
162
+ emptyMatch: (paneText) => matchesKimiEditorBox(paneText, true),
163
+ launchCommand: (command) => command.startsWith("kimi -y -m "),
164
+ readinessTimeoutMs: 15_000,
144
165
  });
145
166
  // Shared launch-then-seed surface (T6) + banner confirm (T7/T2). One definition so the adapter
146
167
  // property and the daemon's generic runInteractiveSeed path cannot drift.
147
168
  const KIMI_SEED = {
148
169
  // v1.76 T2 / OBS-141: 0.29.0 resolves only the full config.toml model key on the first turn.
149
- launch: (model) => `kimi -y -m ${shq(model)}`,
170
+ launch: kimiTuiLaunch,
150
171
  readinessMatch: "Send /help for help information.",
151
172
  seedLine: (promptFile) => `Read ${promptFile} and do exactly what it says.`,
152
173
  confirmBanner: confirmKimiSeedBanner,
@@ -84,10 +84,15 @@ export interface TrustDialog {
84
84
  export declare function matchesTrustDialog(paneText: string, dialog: TrustDialog): boolean;
85
85
  export interface InputBox {
86
86
  fingerprint: string;
87
+ match?(paneText: string): boolean;
88
+ emptyMatch?(paneText: string): boolean;
89
+ launchCommand?(command: string): boolean;
90
+ readinessTimeoutMs?: number;
87
91
  }
88
92
  export declare function declareInputBox(adapterId: string, inputBox: InputBox): InputBox;
89
93
  export declare function declaredInputBoxForWorkerName(workerName: string): InputBox | undefined;
90
94
  export declare function matchesInputBox(paneText: string, inputBox: InputBox): boolean;
95
+ export declare function matchesEmptyInputBox(paneText: string, inputBox: InputBox): boolean;
91
96
  export interface WorkerAdapter {
92
97
  id: string;
93
98
  vendor: string;
@@ -40,7 +40,10 @@ export function declaredInputBoxForWorkerName(workerName) {
40
40
  return adapterId === undefined ? undefined : inputBoxes.get(adapterId);
41
41
  }
42
42
  export function matchesInputBox(paneText, inputBox) {
43
- return paneText.includes(inputBox.fingerprint);
43
+ return inputBox.match?.(paneText) ?? paneText.includes(inputBox.fingerprint);
44
+ }
45
+ export function matchesEmptyInputBox(paneText, inputBox) {
46
+ return inputBox.emptyMatch?.(paneText) === true;
44
47
  }
45
48
  export function channelsFromConfig(adapterId, cfg) {
46
49
  const e = cfg.tiers[adapterId];
@@ -2,6 +2,12 @@ import { type ExecutorDriver, type NotifyOpts, type Slot, type SlotOpts } from "
2
2
  export declare const TRAILER_SAFE_FLOOR_COLS = 108;
3
3
  export declare const TRAILER_WIDTH_MARGIN = 2;
4
4
  export declare const DELIVERY_ATTEMPTS = 3;
5
+ export declare class DeliveryReadinessError extends Error {
6
+ readonly waitedMs: number;
7
+ readonly transcript: string;
8
+ readonly phase = "READINESS";
9
+ constructor(waitedMs: number, transcript: string);
10
+ }
5
11
  /** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
6
12
  export declare function workerSplitDirection(paneCols: number | null, safeFloor?: number, margin?: number): "right" | "down";
7
13
  export declare class HerdrDriver implements ExecutorDriver {
@@ -41,8 +47,10 @@ export declare class HerdrDriver implements ExecutorDriver {
41
47
  private joinGroup;
42
48
  run(slot: Slot, cmd: string): Promise<void>;
43
49
  private deliver;
50
+ private deliverPersistentShellCommand;
44
51
  private submitVerifiedDelivery;
45
52
  private settleDeliveryLine;
53
+ private awaitDeliveryReadiness;
46
54
  private deliveryReadMatches;
47
55
  private waitOk;
48
56
  waitOutput(slot: Slot, pattern: string, timeoutMs: number, opts?: {
@@ -1,4 +1,4 @@
1
- import { declaredInputBoxForWorkerName, matchesInputBox, shq } from "../adapters/types.js";
1
+ import { declaredInputBoxForWorkerName, matchesEmptyInputBox, matchesInputBox, shq } from "../adapters/types.js";
2
2
  import { PANE_IDENTITY_ENV, paneIdentityLine } from "../brand.js";
3
3
  import { createWorktree, sh } from "../run/git.js";
4
4
  import { herdrSealShellPrefix } from "./subprocess.js";
@@ -13,6 +13,21 @@ const DELIVERY_VERIFY_TIMEOUT_MS = 2000; // per attempt — a paste that hasn't
13
13
  const DELIVERY_READ_LINES = 80;
14
14
  const DELIVERY_SETTLE_READ_ATTEMPTS = 6;
15
15
  const DELIVERY_SETTLE_POLL_MS = 100;
16
+ const DELIVERY_READINESS_TIMEOUT_MS = 1_000;
17
+ // OBS-142: a typed identity lets the daemon's next task distinguish cold-start variance from
18
+ // structural driver faults without parsing prose. The message also carries the bounded wait and
19
+ // final pane evidence so a terminal failure is diagnosable on its own.
20
+ export class DeliveryReadinessError extends Error {
21
+ waitedMs;
22
+ transcript;
23
+ phase = "READINESS";
24
+ constructor(waitedMs, transcript) {
25
+ super(`herdr delivery READINESS failed after ${waitedMs}ms — interface never became interactive (OBS-142); pane transcript:\n${transcript}`);
26
+ this.waitedMs = waitedMs;
27
+ this.transcript = transcript;
28
+ this.name = "DeliveryReadinessError";
29
+ }
30
+ }
16
31
  /** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
17
32
  export function workerSplitDirection(paneCols, safeFloor = TRAILER_SAFE_FLOOR_COLS, margin = TRAILER_WIDTH_MARGIN) {
18
33
  if (paneCols == null || paneCols <= 0)
@@ -96,17 +111,18 @@ export class HerdrDriver {
96
111
  const needle = norm(cmd);
97
112
  return needle.length > 0 && hay.includes(needle);
98
113
  }
99
- // Submission succeeds when the typed prompt disappears, or when it has moved above a fresh
100
- // adapter-declared input box (the prompt is now transcript, not input). Shell-line delivery uses
101
- // the same normalized seam: a prompt still at the bottom ends the pane text; output/a fresh prompt
102
- // after it proves Enter registered. No adapter-specific fingerprint lives in the driver.
114
+ // Submission requires positive post-Enter evidence. A prompt echoed above a fresh input target
115
+ // proves registration; a declared box that is visibly empty after the verified paste also proves
116
+ // it consumed the prompt. Prompt absence alone is never success (OBS-142).
103
117
  submissionRegistered(transcript, cmd, inputBox) {
104
118
  const norm = (s) => s.replace(/\s+/g, "");
105
119
  const hay = norm(transcript);
106
120
  const needle = norm(cmd);
107
121
  const promptAt = hay.lastIndexOf(needle);
108
- if (needle.length === 0 || promptAt < 0)
109
- return true;
122
+ if (needle.length === 0)
123
+ return false;
124
+ if (promptAt < 0)
125
+ return inputBox !== undefined && matchesEmptyInputBox(transcript, inputBox);
110
126
  if (inputBox && matchesInputBox(transcript, inputBox)) {
111
127
  return hay.lastIndexOf(norm(inputBox.fingerprint)) > promptAt;
112
128
  }
@@ -409,7 +425,8 @@ export class HerdrDriver {
409
425
  this.deliveredPanes.set(slot, paneId);
410
426
  });
411
427
  }
412
- async deliver(slot, cmd, pane) {
428
+ async deliver(slot, cmd, pane, verifySubmission = true) {
429
+ const readiness = await this.awaitDeliveryReadiness(slot, cmd, pane);
413
430
  let transcript = "";
414
431
  for (let attempt = 0; attempt < DELIVERY_ATTEMPTS; attempt++) {
415
432
  if (attempt > 0) {
@@ -436,7 +453,14 @@ export class HerdrDriver {
436
453
  throw new Error(`herdr pane send-text failed: ${typed.stderr || typed.stdout}`);
437
454
  const back = await this.herdr(`pane wait-output ${shq(pane)} --match ${shq(cmd)} --timeout ${DELIVERY_VERIFY_TIMEOUT_MS}`, slot.cwd, DELIVERY_VERIFY_TIMEOUT_MS + 15_000);
438
455
  if (this.waitOk(back.code, back.stdout) || await this.deliveryReadMatches(pane, cmd, slot.cwd)) {
439
- await this.submitVerifiedDelivery(slot, cmd, pane);
456
+ if (verifySubmission) {
457
+ await this.submitVerifiedDelivery(slot, cmd, pane, readiness);
458
+ }
459
+ else {
460
+ const enter = await this.herdr(`pane send-keys ${shq(pane)} Enter`, slot.cwd);
461
+ if (enter.code !== 0)
462
+ throw new Error(`herdr pane send-keys Enter failed: ${enter.stderr || enter.stdout}`);
463
+ }
440
464
  return;
441
465
  }
442
466
  // capture the corrupted delivery BEFORE clearing it — the OBS-85 byte-level evidence
@@ -444,9 +468,23 @@ export class HerdrDriver {
444
468
  }
445
469
  throw new Error(`herdr delivery corrupted after ${DELIVERY_ATTEMPTS} attempts — enter never pressed (OBS-85); pane transcript:\n${transcript}`);
446
470
  }
447
- async submitVerifiedDelivery(slot, cmd, pane) {
471
+ // The narrator launches a perpetual shell watch, not an adapter-backed input interface. Keep
472
+ // OBS-85's readiness + paste read-back and the historical single Enter, while worker/gate runs
473
+ // continue through positive post-Enter evidence in submitVerifiedDelivery.
474
+ async deliverPersistentShellCommand(slot, cmd) {
475
+ return this.deliveryQueue(async () => {
476
+ const pane = await this.paneId(slot);
477
+ await this.deliver(slot, cmd, pane, false);
478
+ this.deliveredPanes.set(slot, pane);
479
+ });
480
+ }
481
+ async submitVerifiedDelivery(slot, cmd, pane, readiness) {
448
482
  let transcript = "";
449
483
  const inputBox = this.inputBoxes.get(slot);
484
+ // The base window preserves OBS-140's bounded behavior. A slow readiness observation grants
485
+ // the same measured time once more for submit paint, capped by the readiness bound itself.
486
+ const baseVerifyMs = (DELIVERY_SETTLE_READ_ATTEMPTS - 1) * DELIVERY_SETTLE_POLL_MS;
487
+ const verifyWindowMs = Math.min(baseVerifyMs + readiness.timeoutMs, baseVerifyMs + readiness.waitedMs);
450
488
  for (let attempt = 0; attempt < DELIVERY_SUBMIT_ATTEMPTS; attempt++) {
451
489
  const enter = await this.herdr(`pane send-keys ${shq(pane)} Enter`, slot.cwd);
452
490
  if (enter.code !== 0)
@@ -454,7 +492,7 @@ export class HerdrDriver {
454
492
  // Reuse the existing settle-read window. A first-read success returns before any timer; only
455
493
  // a prompt that still occupies the delivery target spends the bounded settle window. This
456
494
  // verification always completes before a possible re-press, so a slow submit cannot duplicate.
457
- const settled = await this.settleDeliveryLine(pane, slot.cwd, transcript, inputBox, (candidate) => this.submissionRegistered(candidate, cmd, inputBox));
495
+ const settled = await this.settleDeliveryLine(pane, slot.cwd, transcript, inputBox, (candidate) => this.submissionRegistered(candidate, cmd, inputBox), verifyWindowMs);
458
496
  transcript = settled.transcript;
459
497
  if (settled.ok)
460
498
  return;
@@ -464,9 +502,12 @@ export class HerdrDriver {
464
502
  }
465
503
  throw new Error(`herdr delivery corrupted after ${DELIVERY_SUBMIT_ATTEMPTS} submit attempts — submission never registered (OBS-140); pane transcript:\n${transcript}`);
466
504
  }
467
- async settleDeliveryLine(pane, cwd, initialTranscript, inputBox, accept) {
505
+ async settleDeliveryLine(pane, cwd, initialTranscript, inputBox, accept, settleWindowMs) {
468
506
  let transcript = initialTranscript;
469
- for (let readAttempt = 0; readAttempt < DELIVERY_SETTLE_READ_ATTEMPTS; readAttempt++) {
507
+ const readAttempts = settleWindowMs === undefined
508
+ ? DELIVERY_SETTLE_READ_ATTEMPTS
509
+ : Math.max(DELIVERY_SETTLE_READ_ATTEMPTS, Math.floor(settleWindowMs / DELIVERY_SETTLE_POLL_MS) + 1);
510
+ for (let readAttempt = 0; readAttempt < readAttempts; readAttempt++) {
470
511
  const read = await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${DELIVERY_READ_LINES}`, cwd);
471
512
  if (read.code !== 0)
472
513
  return { ok: false, transcript: read.stdout || transcript, recognizedInputBox: false, readFailed: true };
@@ -485,12 +526,58 @@ export class HerdrDriver {
485
526
  };
486
527
  }
487
528
  transcript = read.stdout;
488
- if (readAttempt < DELIVERY_SETTLE_READ_ATTEMPTS - 1) {
529
+ if (readAttempt < readAttempts - 1) {
489
530
  await new Promise((resolve) => setTimeout(resolve, DELIVERY_SETTLE_POLL_MS));
490
531
  }
491
532
  }
492
533
  return { ok: false, transcript, recognizedInputBox: false };
493
534
  }
535
+ async awaitDeliveryReadiness(slot, cmd, pane) {
536
+ const inputBox = this.inputBoxes.get(slot);
537
+ const requireInputBox = inputBox !== undefined && inputBox.launchCommand?.(cmd) !== true;
538
+ const timeoutMs = inputBox?.readinessTimeoutMs ?? DELIVERY_READINESS_TIMEOUT_MS;
539
+ const started = Date.now();
540
+ let previous;
541
+ let transcript = "";
542
+ let reads = 0;
543
+ while (true) {
544
+ const elapsedBeforeRead = Date.now() - started;
545
+ const remainingBeforeRead = timeoutMs - elapsedBeforeRead;
546
+ if (remainingBeforeRead <= 0)
547
+ throw new DeliveryReadinessError(elapsedBeforeRead, transcript);
548
+ const read = await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${DELIVERY_READ_LINES}`, slot.cwd, Math.max(1, Math.ceil(remainingBeforeRead)));
549
+ reads++;
550
+ transcript = read.stdout || transcript;
551
+ const waitedMs = Date.now() - started;
552
+ if (read.code !== 0) {
553
+ // Once at least one valid frame has been observed, a read that consumes the remainder of
554
+ // the readiness budget is the bounded window expiring, not a new submission/protocol
555
+ // identity. A first-read timeout and every non-timeout read failure remain structural.
556
+ if (read.timedOut && previous !== undefined && waitedMs >= timeoutMs) {
557
+ throw new DeliveryReadinessError(waitedMs, transcript);
558
+ }
559
+ const detail = read.stderr || read.stdout || `exit ${read.code}`;
560
+ throw new Error(`herdr pane read failed during readiness${read.timedOut ? " (timed out)" : ""}: ${detail}`);
561
+ }
562
+ if (previous !== undefined) {
563
+ const stableFrame = read.stdout === previous;
564
+ const targetReady = stableFrame && (requireInputBox
565
+ ? matchesInputBox(previous, inputBox) && matchesInputBox(read.stdout, inputBox)
566
+ : !this.deliveryMatches(read.stdout, cmd));
567
+ if (targetReady)
568
+ return { waitedMs, timeoutMs, transcript: read.stdout };
569
+ }
570
+ previous = read.stdout;
571
+ const remaining = timeoutMs - waitedMs;
572
+ if (remaining <= 0)
573
+ throw new DeliveryReadinessError(waitedMs, transcript);
574
+ // The second read is immediate: an already-painted stable target proves readiness with no
575
+ // added delay. Only observed change spends a poll interval, and every path remains bounded.
576
+ if (reads > 1) {
577
+ await new Promise((resolve) => setTimeout(resolve, Math.min(DELIVERY_SETTLE_POLL_MS, remaining)));
578
+ }
579
+ }
580
+ }
494
581
  async deliveryReadMatches(pane, cmd, cwd) {
495
582
  const read = await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${DELIVERY_READ_LINES}`, cwd);
496
583
  return read.code === 0 && this.deliveryMatches(read.stdout, cmd);
@@ -680,7 +767,7 @@ export class HerdrDriver {
680
767
  const s = await this.watchSlot(cwd, name);
681
768
  this.watches.set(name, s);
682
769
  try {
683
- await this.run(s, command);
770
+ await this.deliverPersistentShellCommand(s, command);
684
771
  }
685
772
  catch (err) {
686
773
  this.watches.delete(name);
@@ -9,6 +9,7 @@ import { allAdapters, discoverChannels, getAdapter, probeAll, readDoctor } from
9
9
  import { addUsage, channelKey, matchesTrustDialog, QUOTA_RE } from "../adapters/types.js";
10
10
  import { bannerShell, paneDispatchCommand } from "../brand.js";
11
11
  import { globalConfigDir, loadConfigWithMode, readOverlayFile, repoOverlayPath, } from "../config/config.js";
12
+ import { DeliveryReadinessError } from "../drivers/herdr.js";
12
13
  import { herdrSealShellPrefix, SubprocessDriver } from "../drivers/subprocess.js";
13
14
  import { formatOwnedName } from "../drivers/types.js";
14
15
  import { captureBaseline, detectGateCommands, detectVacuousOracles } from "../gates/baseline.js";
@@ -833,6 +834,38 @@ export async function runDaemon(repoRoot, opts = {}) {
833
834
  let earlyLaunchDead = false;
834
835
  let settleParsed;
835
836
  let seedResult;
837
+ const handleDeliveryReadiness = async (error) => {
838
+ journal.append("delivery-readiness-failed", t.id, {
839
+ attempt,
840
+ waitedMs: error.waitedMs,
841
+ transcript: error.transcript,
842
+ });
843
+ if (keepOpen)
844
+ keptSlots.push(slot);
845
+ else
846
+ await closeSlot(slot);
847
+ feedback = `delivery readiness failed after ${error.waitedMs}ms; pane transcript:\n${error.transcript}`;
848
+ const step = r.ladder[Math.min(ladderIdx++, r.ladder.length - 1)];
849
+ journal.append("escalation", t.id, { step, attempt: attempt + 1 });
850
+ await driver.notify(`tickmarkr ${runId}: ${t.id} escalation: ${step}`, { tier: "attention" });
851
+ if (step === "retry")
852
+ return true;
853
+ if (step === "escalate") {
854
+ const next = failover("escalate");
855
+ if (next) {
856
+ assignment = next;
857
+ tried.push(channelKey(next));
858
+ return true;
859
+ }
860
+ // no channel left — fall through to a consult
861
+ }
862
+ if (step === "escalate" || step === "consult") {
863
+ const v = await runConsult("delivery-readiness", error.transcript, feedback, []);
864
+ return applyVerdict(v, attempt + 1, "dispatch");
865
+ }
866
+ await park(t, "escalation ladder exhausted", "ladder-exhausted", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
867
+ return false;
868
+ };
836
869
  if (interactive) {
837
870
  // v1.2 interactive: the TUI doesn't exit on completion — the trailer is the finish line.
838
871
  // The exit wrapper still fires if the TUI dies (crash/quit): fast-fail instead of burning the timeout.
@@ -842,11 +875,29 @@ export async function runDaemon(repoRoot, opts = {}) {
842
875
  // v1.69 T6: launch the real TUI without a prompt, wait for readiness, inject one seed turn,
843
876
  // then fall through to the normal trailer harvest. A failed seed is recorded as a finished
844
877
  // failure rather than allowed to race the trailer wait.
845
- seedResult = await runInteractiveSeed({ driver, slot, adapter, assignment, promptFile, taskTimeoutMinutes });
878
+ try {
879
+ seedResult = await runInteractiveSeed({ driver, slot, adapter, assignment, promptFile, taskTimeoutMinutes });
880
+ }
881
+ catch (error) {
882
+ if (!(error instanceof DeliveryReadinessError))
883
+ throw error;
884
+ if (await handleDeliveryReadiness(error))
885
+ continue attempts;
886
+ return;
887
+ }
846
888
  output = seedResult.output;
847
889
  }
848
890
  else {
849
- await driver.run(slot, paneDispatchCommand(dispatchScript));
891
+ try {
892
+ await driver.run(slot, paneDispatchCommand(dispatchScript));
893
+ }
894
+ catch (error) {
895
+ if (!(error instanceof DeliveryReadinessError))
896
+ throw error;
897
+ if (await handleDeliveryReadiness(error))
898
+ continue attempts;
899
+ return;
900
+ }
850
901
  output = await driver.read(slot, 1000);
851
902
  }
852
903
  if (seedResult?.seedFailed) {
@@ -976,7 +1027,16 @@ export async function runDaemon(repoRoot, opts = {}) {
976
1027
  }
977
1028
  }
978
1029
  else {
979
- await driver.run(slot, paneDispatchCommand(dispatchScript));
1030
+ try {
1031
+ await driver.run(slot, paneDispatchCommand(dispatchScript));
1032
+ }
1033
+ catch (error) {
1034
+ if (!(error instanceof DeliveryReadinessError))
1035
+ throw error;
1036
+ if (await handleDeliveryReadiness(error))
1037
+ continue attempts;
1038
+ return;
1039
+ }
980
1040
  // OBS-54: headless workers have the same output-inactivity budget as visible panes.
981
1041
  // v1.76: same monotonic-progress measure as the interactive site; harvest stays raw.
982
1042
  const stallWindowMs = taskTimeoutMinutes * 60_000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.76.0",
3
+ "version": "1.77.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",