pi-extension-observational-memory 0.1.2 → 0.1.3

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.
Files changed (3) hide show
  1. package/index.ts +95 -0
  2. package/overlay.ts +387 -0
  3. package/package.json +4 -2
package/index.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  type SessionEntry,
28
28
  serializeConversation,
29
29
  } from "@mariozechner/pi-coding-agent";
30
+ import { ObservationMemoryOverlay, type ObservationMemoryOverlaySnapshot } from "./overlay.js";
30
31
 
31
32
  const DETAILS_SCHEMA_VERSION = 2;
32
33
 
@@ -35,6 +36,7 @@ const OBS_REFLECT_COMMAND = "obs-reflect";
35
36
  const OBS_AUTO_COMPACT_COMMAND = "obs-auto-compact";
36
37
  const OBS_MODE_COMMAND = "obs-mode";
37
38
  const OBS_VIEW_COMMAND = "obs-view";
39
+ const OBS_STATUS_SHORTCUT = "ctrl+shift+o";
38
40
 
39
41
  const DEFAULT_RESERVE_TOKENS = 16384;
40
42
 
@@ -735,6 +737,7 @@ export default function observationalMemoryExtension(pi: ExtensionAPI) {
735
737
  let rawTailRetainTokens = DEFAULT_RAW_TAIL_RETAIN_TOKENS;
736
738
  let autoCompactInFlight = false;
737
739
  let lastAutoCompactAt = 0;
740
+ let statusOverlayOpen = false;
738
741
 
739
742
  pi.registerFlag("obs-auto-compact", {
740
743
  description: "Enable observational auto observer trigger",
@@ -827,6 +830,86 @@ export default function observationalMemoryExtension(pi: ExtensionAPI) {
827
830
  });
828
831
  };
829
832
 
833
+ const buildStatusSnapshot = (ctx: ExtensionContext): ObservationMemoryOverlaySnapshot => {
834
+ const branchEntries = ctx.sessionManager.getBranch();
835
+ const lastCompaction = [...branchEntries].reverse().find((entry) => entry.type === "compaction");
836
+ const lastBranchSummary = [...branchEntries].reverse().find((entry) => entry.type === "branch_summary");
837
+ const rawTailTokens = estimateRawTailTokens(branchEntries);
838
+ const observationTokens = estimateObservationTokens(lastCompaction?.summary);
839
+
840
+ const compactionDetails =
841
+ lastCompaction && isObservationalCompactionDetails(lastCompaction.details)
842
+ ? {
843
+ strategy: lastCompaction.details.strategy,
844
+ model: lastCompaction.details.model,
845
+ observationCount: lastCompaction.details.observationCount,
846
+ reflectorRan: lastCompaction.details.reflectorRan,
847
+ reflectionMode: lastCompaction.details.reflectionMode,
848
+ observationsDropped: lastCompaction.details.observationsDropped,
849
+ isSplitTurn: lastCompaction.details.isSplitTurn,
850
+ usedPreviousSummary: lastCompaction.details.usedPreviousSummary,
851
+ generatedAt: lastCompaction.details.generatedAt,
852
+ }
853
+ : undefined;
854
+
855
+ const branchSummaryDetails =
856
+ lastBranchSummary && isObservationalBranchDetails(lastBranchSummary.details)
857
+ ? {
858
+ strategy: lastBranchSummary.details.strategy,
859
+ model: lastBranchSummary.details.model,
860
+ observationCount: lastBranchSummary.details.observationCount,
861
+ entryCount: lastBranchSummary.details.entryCount,
862
+ generatedAt: lastBranchSummary.details.generatedAt,
863
+ }
864
+ : undefined;
865
+
866
+ return {
867
+ autoObserverEnabled,
868
+ observerTriggerTokens,
869
+ rawTailTokens,
870
+ reflectorTriggerTokens,
871
+ observationTokens,
872
+ autoCompactInFlight,
873
+ forceReflectPending: forceReflectNextCompaction,
874
+ lastCompaction: lastCompaction
875
+ ? {
876
+ id: lastCompaction.id,
877
+ timestamp: lastCompaction.timestamp,
878
+ tokensBefore: lastCompaction.tokensBefore,
879
+ fromExtension: lastCompaction.fromHook,
880
+ details: compactionDetails,
881
+ }
882
+ : undefined,
883
+ lastBranchSummary: lastBranchSummary
884
+ ? {
885
+ id: lastBranchSummary.id,
886
+ timestamp: lastBranchSummary.timestamp,
887
+ details: branchSummaryDetails,
888
+ }
889
+ : undefined,
890
+ observations: lastCompaction?.summary ? stripFileTags(lastCompaction.summary) : undefined,
891
+ };
892
+ };
893
+
894
+ const showStatusOverlay = async (ctx: ExtensionContext): Promise<void> => {
895
+ if (!ctx.hasUI) return;
896
+ if (statusOverlayOpen) return;
897
+
898
+ const snapshot = buildStatusSnapshot(ctx);
899
+ statusOverlayOpen = true;
900
+ try {
901
+ await ctx.ui.custom<null>(
902
+ (_tui, _theme, _keys, done) => new ObservationMemoryOverlay(snapshot, done),
903
+ { overlay: true },
904
+ );
905
+ } catch (error) {
906
+ const message = error instanceof Error ? error.message : String(error);
907
+ ctx.ui.notify(`Unable to render obs overlay: ${message}`, "error");
908
+ } finally {
909
+ statusOverlayOpen = false;
910
+ }
911
+ };
912
+
830
913
  pi.on("session_start", async (_event, ctx) => {
831
914
  const enabledFlag = pi.getFlag("obs-auto-compact");
832
915
  if (typeof enabledFlag === "boolean") {
@@ -1050,6 +1133,11 @@ export default function observationalMemoryExtension(pi: ExtensionAPI) {
1050
1133
  pi.registerCommand(OBS_STATUS_COMMAND, {
1051
1134
  description: "Show observational-memory compaction and tree summary status",
1052
1135
  handler: async (_args, ctx) => {
1136
+ if (ctx.hasUI) {
1137
+ await showStatusOverlay(ctx);
1138
+ return;
1139
+ }
1140
+
1053
1141
  const branchEntries = ctx.sessionManager.getBranch();
1054
1142
  const lastCompaction = [...branchEntries].reverse().find((entry) => entry.type === "compaction");
1055
1143
  const lastBranchSummary = [...branchEntries].reverse().find((entry) => entry.type === "branch_summary");
@@ -1122,6 +1210,13 @@ export default function observationalMemoryExtension(pi: ExtensionAPI) {
1122
1210
  },
1123
1211
  });
1124
1212
 
1213
+ pi.registerShortcut(OBS_STATUS_SHORTCUT, {
1214
+ description: "Open observational-memory status overlay",
1215
+ handler: async (ctx) => {
1216
+ await showStatusOverlay(ctx);
1217
+ },
1218
+ });
1219
+
1125
1220
  pi.registerCommand(OBS_AUTO_COMPACT_COMMAND, {
1126
1221
  description: "Show or set observer/reflector thresholds, mode, and raw-tail retention",
1127
1222
  handler: async (args, ctx) => {
package/overlay.ts ADDED
@@ -0,0 +1,387 @@
1
+ import { Key, matchesKey } from "@mariozechner/pi-tui";
2
+
3
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
4
+
5
+ function color(code: string, text: string): string {
6
+ return `\x1b[${code}m${text}\x1b[0m`;
7
+ }
8
+
9
+ function bold(text: string): string {
10
+ return `\x1b[1m${text}\x1b[22m`;
11
+ }
12
+
13
+ function dim(text: string): string {
14
+ return color("2", text);
15
+ }
16
+
17
+ function visibleLength(text: string): number {
18
+ return text.replace(ANSI_RE, "").length;
19
+ }
20
+
21
+ function clipAnsi(text: string, width: number): string {
22
+ if (visibleLength(text) <= width) return text;
23
+ let visible = 0;
24
+ let i = 0;
25
+ let out = "";
26
+ while (i < text.length && visible < width) {
27
+ const ch = text[i];
28
+ if (ch === "\u001b" && text[i + 1] === "[") {
29
+ let j = i + 2;
30
+ while (j < text.length && text[j] !== "m") j++;
31
+ if (j < text.length) {
32
+ out += text.slice(i, j + 1);
33
+ i = j + 1;
34
+ continue;
35
+ }
36
+ }
37
+ out += ch;
38
+ visible += 1;
39
+ i += 1;
40
+ }
41
+ return `${out}\x1b[0m`;
42
+ }
43
+
44
+ function padRight(text: string, width: number): string {
45
+ const clipped = clipAnsi(text, width);
46
+ const pad = Math.max(0, width - visibleLength(clipped));
47
+ return clipped + " ".repeat(pad);
48
+ }
49
+
50
+ function wrapPlain(text: string, width: number): string[] {
51
+ if (!text) return [""];
52
+ if (text.length <= width) return [text];
53
+
54
+ const words = text.split(/\s+/).filter((word) => word.length > 0);
55
+ if (words.length === 0) return [""];
56
+
57
+ const lines: string[] = [];
58
+ let current = "";
59
+
60
+ for (const word of words) {
61
+ if (!current) {
62
+ if (word.length <= width) {
63
+ current = word;
64
+ continue;
65
+ }
66
+ for (let i = 0; i < word.length; i += width) {
67
+ lines.push(word.slice(i, i + width));
68
+ }
69
+ continue;
70
+ }
71
+
72
+ const candidate = `${current} ${word}`;
73
+ if (candidate.length <= width) {
74
+ current = candidate;
75
+ continue;
76
+ }
77
+
78
+ lines.push(current);
79
+ if (word.length <= width) {
80
+ current = word;
81
+ } else {
82
+ for (let i = 0; i < word.length; i += width) {
83
+ lines.push(word.slice(i, i + width));
84
+ }
85
+ current = "";
86
+ }
87
+ }
88
+
89
+ if (current) lines.push(current);
90
+ return lines;
91
+ }
92
+
93
+ function formatTokenCount(tokens: number): string {
94
+ return `${tokens.toLocaleString()} tokens`;
95
+ }
96
+
97
+ function meter(current: number, total: number, width = 26): string {
98
+ const ratio = total > 0 ? Math.min(current / total, 1) : 0;
99
+ const filled = Math.max(0, Math.min(width, Math.round(ratio * width)));
100
+ const colorCode = ratio >= 0.95 ? "31" : ratio >= 0.7 ? "33" : "32";
101
+ return `${color("2", "[")}${color(colorCode, "█".repeat(filled))}${color("2", "░".repeat(width - filled))}${color("2", "]")} ${color(colorCode, `${Math.round(ratio * 100)}%`)}`;
102
+ }
103
+
104
+ type Tab = "status" | "observations";
105
+
106
+ type Severity = "normal" | "heading" | "red" | "yellow" | "green" | "muted";
107
+
108
+ interface StyledLine {
109
+ text: string;
110
+ severity?: Severity;
111
+ }
112
+
113
+ interface CompactionOverlayDetails {
114
+ strategy?: string;
115
+ model?: string;
116
+ observationCount?: number;
117
+ reflectorRan?: boolean;
118
+ reflectionMode?: string;
119
+ observationsDropped?: number;
120
+ isSplitTurn?: boolean;
121
+ usedPreviousSummary?: boolean;
122
+ generatedAt?: string;
123
+ }
124
+
125
+ interface BranchOverlayDetails {
126
+ strategy?: string;
127
+ model?: string;
128
+ observationCount?: number;
129
+ entryCount?: number;
130
+ generatedAt?: string;
131
+ }
132
+
133
+ export interface ObservationMemoryOverlaySnapshot {
134
+ autoObserverEnabled: boolean;
135
+ observerTriggerTokens: number;
136
+ rawTailTokens: number;
137
+ reflectorTriggerTokens: number;
138
+ observationTokens: number;
139
+ autoCompactInFlight: boolean;
140
+ forceReflectPending: boolean;
141
+ lastCompaction?: {
142
+ id: string;
143
+ timestamp: number | string;
144
+ tokensBefore: number;
145
+ fromExtension: boolean;
146
+ details?: CompactionOverlayDetails;
147
+ };
148
+ lastBranchSummary?: {
149
+ id: string;
150
+ timestamp: number | string;
151
+ details?: BranchOverlayDetails;
152
+ };
153
+ observations?: string;
154
+ }
155
+
156
+ function styleLine(line: StyledLine): string {
157
+ switch (line.severity) {
158
+ case "heading":
159
+ return bold(color("36", line.text));
160
+ case "red":
161
+ return color("31", line.text);
162
+ case "yellow":
163
+ return color("33", line.text);
164
+ case "green":
165
+ return color("32", line.text);
166
+ case "muted":
167
+ return dim(line.text);
168
+ default:
169
+ return line.text;
170
+ }
171
+ }
172
+
173
+ function buildStatusLines(snapshot: ObservationMemoryOverlaySnapshot): StyledLine[] {
174
+ const lines: StyledLine[] = [
175
+ { text: "Observer/Reflector" },
176
+ { text: `Observer trigger: ${snapshot.autoObserverEnabled ? "on" : "off"}` },
177
+ { text: `Observer threshold: ${formatTokenCount(snapshot.observerTriggerTokens)}` },
178
+ { text: `Raw tail now: ${formatTokenCount(snapshot.rawTailTokens)}` },
179
+ { text: meter(snapshot.rawTailTokens, snapshot.observerTriggerTokens), severity: "normal" },
180
+ { text: "" },
181
+ { text: `Reflector threshold: ${formatTokenCount(snapshot.reflectorTriggerTokens)}` },
182
+ { text: `Observation block: ${formatTokenCount(snapshot.observationTokens)}` },
183
+ { text: meter(snapshot.observationTokens, snapshot.reflectorTriggerTokens), severity: "normal" },
184
+ { text: "" },
185
+ { text: `Auto-compact in flight: ${snapshot.autoCompactInFlight ? "yes" : "no"}` },
186
+ { text: `Force-reflect pending: ${snapshot.forceReflectPending ? "yes" : "no"}` },
187
+ { text: "" },
188
+ ];
189
+
190
+ if (snapshot.lastCompaction) {
191
+ lines.push(
192
+ { text: "Last compaction", severity: "heading" },
193
+ { text: `id: ${snapshot.lastCompaction.id}` },
194
+ { text: `timestamp: ${new Date(snapshot.lastCompaction.timestamp).toLocaleString()}` },
195
+ { text: `tokensBefore: ${snapshot.lastCompaction.tokensBefore.toLocaleString()}` },
196
+ { text: `fromExtension: ${snapshot.lastCompaction.fromExtension ? "yes" : "no"}` },
197
+ );
198
+
199
+ if (snapshot.lastCompaction.details) {
200
+ const details = snapshot.lastCompaction.details;
201
+ lines.push(
202
+ { text: `strategy: ${details.strategy ?? "unknown"}`, severity: "muted" },
203
+ { text: `model: ${details.model ?? "unknown"}`, severity: "muted" },
204
+ { text: `observations: ${details.observationCount ?? 0}`, severity: "muted" },
205
+ {
206
+ text: `reflector: ${details.reflectorRan ? "yes" : "no"}${details.reflectionMode ? ` (${details.reflectionMode})` : ""}`,
207
+ severity: "muted",
208
+ },
209
+ { text: `dropped: ${details.observationsDropped ?? 0}`, severity: "muted" },
210
+ { text: `splitTurn: ${details.isSplitTurn ? "yes" : "no"}`, severity: "muted" },
211
+ { text: `usedPreviousSummary: ${details.usedPreviousSummary ? "yes" : "no"}`, severity: "muted" },
212
+ );
213
+ if (details.generatedAt) {
214
+ lines.push({ text: `generatedAt: ${details.generatedAt}`, severity: "muted" });
215
+ }
216
+ }
217
+
218
+ lines.push({ text: "" });
219
+ } else {
220
+ lines.push({ text: "No compaction entries found in current branch.", severity: "yellow" }, { text: "" });
221
+ }
222
+
223
+ if (snapshot.lastBranchSummary) {
224
+ lines.push(
225
+ { text: "Last branch summary", severity: "heading" },
226
+ { text: `id: ${snapshot.lastBranchSummary.id}` },
227
+ { text: `timestamp: ${new Date(snapshot.lastBranchSummary.timestamp).toLocaleString()}` },
228
+ );
229
+ if (snapshot.lastBranchSummary.details) {
230
+ const details = snapshot.lastBranchSummary.details;
231
+ lines.push(
232
+ { text: `strategy: ${details.strategy ?? "unknown"}`, severity: "muted" },
233
+ { text: `model: ${details.model ?? "unknown"}`, severity: "muted" },
234
+ { text: `observations: ${details.observationCount ?? 0}`, severity: "muted" },
235
+ { text: `entryCount: ${details.entryCount ?? 0}`, severity: "muted" },
236
+ );
237
+ if (details.generatedAt) {
238
+ lines.push({ text: `generatedAt: ${details.generatedAt}`, severity: "muted" });
239
+ }
240
+ }
241
+ }
242
+
243
+ return lines;
244
+ }
245
+
246
+ function buildObservationLines(summary: string | undefined): StyledLine[] {
247
+ if (!summary || summary.trim().length === 0) {
248
+ return [{ text: "No observations in the latest compaction yet.", severity: "yellow" }];
249
+ }
250
+
251
+ return summary.split("\n").map((line) => {
252
+ if (line.startsWith("## ")) return { text: line, severity: "heading" as const };
253
+ if (line.startsWith("Date:")) return { text: line, severity: "muted" as const };
254
+ if (line.startsWith("- 🔴")) return { text: line, severity: "red" as const };
255
+ if (line.startsWith("- 🟡")) return { text: line, severity: "yellow" as const };
256
+ if (line.startsWith("- 🟢")) return { text: line, severity: "green" as const };
257
+ if (/^\d+\.\s+/.test(line)) return { text: line, severity: "green" as const };
258
+ if (line.trim().length === 0) return { text: "" };
259
+ return { text: line };
260
+ });
261
+ }
262
+
263
+ function wrapStyledLines(lines: StyledLine[], width: number): string[] {
264
+ const wrapped: string[] = [];
265
+ for (const line of lines) {
266
+ const parts = wrapPlain(line.text, width);
267
+ for (const part of parts) {
268
+ wrapped.push(styleLine({ text: part, severity: line.severity }));
269
+ }
270
+ }
271
+ return wrapped;
272
+ }
273
+
274
+ export class ObservationMemoryOverlay {
275
+ private readonly maxWidth = 98;
276
+ private readonly contentRows = 20;
277
+ private tab: Tab = "status";
278
+ private scrollOffset = 0;
279
+ private cacheWidth = 0;
280
+ private statusLines: string[] = [];
281
+ private observationLines: string[] = [];
282
+
283
+ constructor(
284
+ private snapshot: ObservationMemoryOverlaySnapshot,
285
+ private done: (result: null) => void,
286
+ ) {}
287
+
288
+ handleInput(data: string): void {
289
+ if (matchesKey(data, Key.escape) || data === "q") {
290
+ this.done(null);
291
+ return;
292
+ }
293
+
294
+ if (matchesKey(data, Key.tab) || data === "1" || data === "2") {
295
+ this.tab = data === "1" ? "status" : data === "2" ? "observations" : this.tab === "status" ? "observations" : "status";
296
+ this.scrollOffset = 0;
297
+ return;
298
+ }
299
+
300
+ if (matchesKey(data, Key.up) || data === "k") {
301
+ this.scrollOffset = Math.max(0, this.scrollOffset - 1);
302
+ return;
303
+ }
304
+ if (matchesKey(data, Key.down) || data === "j") {
305
+ this.scrollOffset += 1;
306
+ return;
307
+ }
308
+ if (matchesKey(data, "pageup") || matchesKey(data, Key.ctrl("u"))) {
309
+ this.scrollOffset = Math.max(0, this.scrollOffset - 8);
310
+ return;
311
+ }
312
+ if (matchesKey(data, "pagedown") || matchesKey(data, Key.ctrl("d"))) {
313
+ this.scrollOffset += 8;
314
+ return;
315
+ }
316
+ if (data === "g") {
317
+ this.scrollOffset = 0;
318
+ return;
319
+ }
320
+ if (data === "G") {
321
+ const maxScroll = Math.max(0, this.activeLines().length - this.contentRows);
322
+ this.scrollOffset = maxScroll;
323
+ }
324
+ }
325
+
326
+ render(width: number): string[] {
327
+ if (width < 20) {
328
+ return [padRight("Obs Memory", width)];
329
+ }
330
+
331
+ const frameWidth = Math.min(this.maxWidth, width);
332
+ const innerWidth = frameWidth - 2;
333
+ const contentWidth = Math.max(1, innerWidth - 2);
334
+
335
+ if (this.cacheWidth !== contentWidth) {
336
+ this.cacheWidth = contentWidth;
337
+ this.statusLines = wrapStyledLines(buildStatusLines(this.snapshot), contentWidth);
338
+ this.observationLines = wrapStyledLines(buildObservationLines(this.snapshot.observations), contentWidth);
339
+ }
340
+
341
+ const lines = this.activeLines();
342
+ const maxScroll = Math.max(0, lines.length - this.contentRows);
343
+ if (this.scrollOffset > maxScroll) this.scrollOffset = maxScroll;
344
+
345
+ const visible = lines.slice(this.scrollOffset, this.scrollOffset + this.contentRows);
346
+ const statusTab = this.tab === "status" ? bold(color("36", "● Status")) : dim("○ Status");
347
+ const obsTab = this.tab === "observations" ? bold(color("36", "● Observations")) : dim("○ Observations");
348
+
349
+ const out: string[] = [];
350
+ const title = bold(color("36", " 🧠 Observational Memory "));
351
+ const sidePad = Math.max(0, innerWidth - visibleLength(title));
352
+ const leftPad = Math.floor(sidePad / 2);
353
+ const rightPad = sidePad - leftPad;
354
+
355
+ out.push(dim("╭") + dim("─".repeat(leftPad)) + title + dim("─".repeat(rightPad)) + dim("╮"));
356
+ out.push(dim("│") + " " + padRight(`${statusTab} ${obsTab}`, innerWidth - 1) + dim("│"));
357
+ out.push(dim("├") + dim("─".repeat(innerWidth)) + dim("┤"));
358
+
359
+ for (let i = 0; i < this.contentRows; i++) {
360
+ const line = visible[i] ?? "";
361
+ out.push(dim("│") + " " + padRight(line, innerWidth - 1) + dim("│"));
362
+ }
363
+
364
+ const rangeStart = lines.length === 0 ? 0 : this.scrollOffset + 1;
365
+ const rangeEnd = Math.min(lines.length, this.scrollOffset + this.contentRows);
366
+ const footer = dim(` ${rangeStart}-${rangeEnd} / ${lines.length} `);
367
+ const footerPad = Math.max(0, innerWidth - visibleLength(footer));
368
+ out.push(dim("├") + dim("─".repeat(Math.floor(footerPad / 2))) + footer + dim("─".repeat(Math.ceil(footerPad / 2))) + dim("┤"));
369
+
370
+ const hints = dim("↑↓/jk scroll PgUp/PgDn page tab switch esc close");
371
+ out.push(dim("│") + " " + padRight(hints, innerWidth - 1) + dim("│"));
372
+ out.push(dim("╰") + dim("─".repeat(innerWidth)) + dim("╯"));
373
+ return out;
374
+ }
375
+
376
+ invalidate(): void {
377
+ this.cacheWidth = 0;
378
+ this.statusLines = [];
379
+ this.observationLines = [];
380
+ }
381
+
382
+ dispose(): void {}
383
+
384
+ private activeLines(): string[] {
385
+ return this.tab === "status" ? this.statusLines : this.observationLines;
386
+ }
387
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extension-observational-memory",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Observational-memory compaction strategy for pi with observer/reflector token thresholds",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,6 +15,7 @@
15
15
  ],
16
16
  "files": [
17
17
  "index.ts",
18
+ "overlay.ts",
18
19
  "README.md",
19
20
  "DESIGN.md",
20
21
  "package.json"
@@ -31,7 +32,8 @@
31
32
  },
32
33
  "peerDependencies": {
33
34
  "@mariozechner/pi-ai": "*",
34
- "@mariozechner/pi-coding-agent": "*"
35
+ "@mariozechner/pi-coding-agent": "*",
36
+ "@mariozechner/pi-tui": "*"
35
37
  },
36
38
  "devDependencies": {
37
39
  "@biomejs/biome": "^2.3.5"