agent-fuel 0.4.0 → 0.4.2

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/README.md CHANGED
@@ -86,12 +86,12 @@ type UsageSnapshot = {
86
86
  ```
87
87
  ⚡️ Agent Fuel - CLI Quota Monitor
88
88
 
89
- Claude Code [███████████████████░░░░░░░░░░░] 64% remaining (resets 01:00 PM)
90
- Codex [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0% remaining (resets in 4h 33m)
91
- AGY Gemini [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0% remaining (resets in 3h 16m) [Gemini 3.5 Flash (Medium)]
92
- AGY Other [██████████████████░░░░░░░░░░░░] 60% remaining (resets in 4h 47m) [Claude Sonnet 4.6 (Thinking)]
89
+ Claude Code [██████████████████████░░░░░░░░] 72% remaining (resets 23:10 (Europe/Copenhagen))
90
+ Codex [█████████████████████░░░░░░░░░] 69% remaining (resets 23:37)
91
+ AGY Gemini [██████████████████████████████] 100% remaining quota available [Gemini 3.5 Flash (Medium)]
92
+ AGY Other [████████████░░░░░░░░░░░░░░░░░░] 40% remaining (resets in 122h 53m) [Claude Sonnet 4.6 (Thinking)]
93
93
 
94
- agent-fuel v0.3.0
94
+ agent-fuel v0.x.y
95
95
  ```
96
96
 
97
97
  Rows appear as each adapter resolves — Claude Code (instant) prints first, Codex and AGY follow as their TUI scrapes complete.
@@ -111,18 +111,3 @@ Rows appear as each adapter resolves — Claude Code (instant) prints first, Cod
111
111
 
112
112
  > **Note on `AGENT_FUEL_CODEX_BUDGET`:** Codex quota is read directly from the Codex TUI via `expect` scraping. This variable is only used as a rough fallback estimate (shown as `[~est]`) when the TUI reports no quota warning and a percentage cannot be determined. It is a guess based on local session cost data — not an official Codex quota signal. The TUI scrape is always preferred.
113
113
 
114
- ---
115
-
116
- ## 📦 Changelog
117
-
118
- ### v0.3.0
119
- - **Codex TUI scrape**: replaced inaccurate `ccusage` cost estimate with an `expect` wrapper that reads the real Codex quota warning (`"Individual quota reached. Resets in Xh Ym"`) — same pattern as AGY. `ccusage` kept as a labelled `[~est]` fallback when quota has not yet been exhausted.
120
- - **Streaming render with fixed order**: placeholder rows print immediately; each bar overwrites in-place as its adapter resolves. Row order is always `Claude Code → Codex → AGY Gemini → AGY Other`.
121
- - **AGY split view**: Gemini and non-Gemini (Claude, etc.) quota buckets shown as separate rows
122
- - **5-minute disk cache** for AGY quota — repeated runs complete in ~1s instead of ~20s
123
- - Output size cap, typed `any` removal, env validation hardening
124
-
125
- ### v0.2.x
126
- - AGY quota now scraped live from `agy /usage` panel via `expect` wrapper (zero token cost)
127
- - Claude Code budget corrected to $20 rolling limit
128
- - Replaced token-consuming `claude -p` calls with offline `ccusage` scraping
@@ -1,13 +1,11 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import os from 'node:os';
4
- import { TuiScraper } from '../tmux.js';
4
+ import { TuiScraper, sleep } from '../tmux.js';
5
+ import { debug } from '../debug.js';
5
6
  const CACHE_PATH = path.join(os.homedir(), '.gemini/antigravity-cli/.agent-fuel-quota-cache.json');
6
7
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
7
8
  // ── Scraping ───────────────────────────────────────────────────────────────
8
- async function sleep(ms) {
9
- return new Promise(res => setTimeout(res, ms));
10
- }
11
9
  /**
12
10
  * Launches `agy` in a tmux session, opens the `/usage` panel, waits for
13
11
  * the Model Quota list to render, then returns clean rendered screen text.
@@ -16,8 +14,16 @@ async function runAgyUsage() {
16
14
  const tui = new TuiScraper('agy');
17
15
  try {
18
16
  tui.start();
19
- // Wait for AGY main menu ready
20
- await tui.waitFor(/for shortcuts/, 20_000);
17
+ // Wait for AGY main menu ready.
18
+ // On first run in a new directory, AGY shows a "Do you trust this project?"
19
+ // prompt. The "Yes, I trust this folder" option is pre-selected; press Enter.
20
+ const firstScreen = await tui.waitFor(/for shortcuts|Do you trust/i, 20_000);
21
+ if (!/for shortcuts/i.test(firstScreen)) {
22
+ debug('agy:scrape', 'trust prompt detected — confirming with Enter');
23
+ await sleep(300); // ensure app is fully interactive before sending input
24
+ tui.sendKey('Enter');
25
+ await tui.waitFor(/for shortcuts/, 15_000);
26
+ }
21
27
  // Navigate to /usage panel
22
28
  tui.send('/usage');
23
29
  await tui.waitFor(/Model Quota/, 10_000);
@@ -1,9 +1,6 @@
1
- import { TuiScraper } from '../tmux.js';
1
+ import { TuiScraper, sleep } from '../tmux.js';
2
2
  import { debug } from '../debug.js';
3
3
  // ── TUI scraper ────────────────────────────────────────────────────────────
4
- async function sleep(ms) {
5
- return new Promise(res => setTimeout(res, ms));
6
- }
7
4
  /**
8
5
  * Launches `claude` in a tmux session, opens /status, navigates to the
9
6
  * Status tab (which shows real quota usage bars), and returns the captured
@@ -33,6 +30,23 @@ async function runClaudeScrape() {
33
30
  tui.kill();
34
31
  }
35
32
  }
33
+ // ── Parser ─────────────────────────────────────────────────────────────────
34
+ /** Convert any 12h am/pm time within a string to 24h (HH:MM). */
35
+ function to24h(s) {
36
+ return s.replace(/\b(\d{1,2})(?::(\d{2}))?\s*(am|pm)\b/gi, (_, h, m, meridiem) => {
37
+ let hour = parseInt(h, 10);
38
+ const min = m ?? '00';
39
+ if (meridiem.toLowerCase() === 'am') {
40
+ if (hour === 12)
41
+ hour = 0; // 12am → 00:xx
42
+ }
43
+ else {
44
+ if (hour !== 12)
45
+ hour += 12; // 1–11pm → 13–23
46
+ }
47
+ return `${String(hour).padStart(2, '0')}:${min}`;
48
+ });
49
+ }
36
50
  function parseScrapeOutput(screen) {
37
51
  debug('claude:parse', `screen length: ${screen.length}`);
38
52
  debug('claude:parse', 'screen', screen);
@@ -42,9 +56,10 @@ function parseScrapeOutput(screen) {
42
56
  debug('claude:parse', `found ${usedMatches.length} "% used" matches`);
43
57
  const sessionUsedPct = usedMatches[0] ? parseInt(usedMatches[0][1], 10) : null;
44
58
  const weeklyUsedPct = usedMatches[1] ? parseInt(usedMatches[1][1], 10) : null;
45
- // Reset time: "Resets H:MMam" or "Resets May 30 at 6am" — grab the first occurrence
59
+ // Reset time: "Resets H:MMam" or "Resets May 30 at 6am" — grab the first occurrence,
60
+ // then normalise any 12h am/pm component to 24h (e.g. "11:10pm" → "23:10").
46
61
  const resetMatch = screen.match(/Resets\s+([^\n\r]+)/i);
47
- const sessionResetAt = resetMatch ? resetMatch[1].trim() : null;
62
+ const sessionResetAt = resetMatch ? to24h(resetMatch[1].trim()) : null;
48
63
  debug('claude:parse', 'result', { sessionUsedPct, sessionResetAt, weeklyUsedPct });
49
64
  return { sessionUsedPct, sessionResetAt, weeklyUsedPct };
50
65
  }
@@ -1,7 +1,10 @@
1
1
  import { exec, execFileSync } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
- import { readFileSync, unlinkSync } from 'node:fs';
4
- import { TuiScraper } from '../tmux.js';
3
+ import { readFileSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import os from 'node:os';
5
+ import crypto from 'node:crypto';
6
+ import path from 'node:path';
7
+ import { TuiScraper, sleep, registerTempFile, unregisterTempFile } from '../tmux.js';
5
8
  import { debug } from '../debug.js';
6
9
  const execAsync = promisify(exec);
7
10
  // Used ONLY as a rough fallback estimate when the TUI scrape cannot determine
@@ -10,9 +13,17 @@ const execAsync = promisify(exec);
10
13
  const DEFAULT_BUDGET_USD = 20.0;
11
14
  const ROLLING_WINDOW_MS = 5 * 60 * 60 * 1000;
12
15
  // ── TUI scraper (tmux) ─────────────────────────────────────────────────────
13
- async function sleep(ms) {
14
- return new Promise(res => setTimeout(res, ms));
15
- }
16
+ // Codex may show one or more blocking dialogs before its main screen ("Tip:").
17
+ // Known dialogs and their dismissal key ("2" = skip/use existing):
18
+ // • Update nag: "Update available! x.x → y.y"
19
+ // • New-model intro: "Introducing GPT-5.5"
20
+ const CODEX_READY = /Tip:/i;
21
+ const CODEX_DIALOG = /Update available|Introducing GPT|Try new model|Use existing model/i;
22
+ const CODEX_EITHER = new RegExp(`(?:${CODEX_READY.source})|(?:${CODEX_DIALOG.source})`, 'i');
23
+ const CODEX_STARTUP_MS = 25_000;
24
+ const CODEX_DIALOG_SETTLE_MS = 1_000; // wait for UI to re-render after dismissing a dialog
25
+ const CODEX_STATUS_REFRESH_MS = 2_000; // first /status just triggers a quota refresh
26
+ const CODEX_STATUS_READY_MS = 4_000; // second /status carries the live quota data
16
27
  /**
17
28
  * Launches `codex` in a tmux session, pipes all terminal bytes to a temp
18
29
  * file, sends /status twice, then reads the file and returns the raw bytes.
@@ -25,30 +36,56 @@ async function sleep(ms) {
25
36
  */
26
37
  async function runCodexScrape() {
27
38
  const tui = new TuiScraper('codex');
28
- const pipePath = `/tmp/af-codex-${Date.now()}.log`;
39
+ const tmpDir = os.tmpdir();
40
+ const randomSuffix = crypto.randomBytes(6).toString('hex');
41
+ const pipePath = path.join(tmpDir, `af-codex-${Date.now()}-${randomSuffix}.log`);
42
+ registerTempFile(pipePath);
43
+ // Create the file with restricted permissions before tmux starts writing to it
44
+ writeFileSync(pipePath, '', { mode: 0o600 });
29
45
  try {
30
46
  tui.start();
31
- // Stream all pane output to a file from the start
32
- execFileSync('tmux', ['pipe-pane', '-t', tui.sessionId, `cat >> '${pipePath}'`]);
47
+ // Stream all pane output to a file from the start.
48
+ // Single-quote escaping: safe against all shell metacharacters ($, `, \, space, etc.)
49
+ // Note: pipe-pane executes this command via tmux's `default-shell` (defaults to /bin/sh).
50
+ // If the user has set default-shell to a non-POSIX shell (e.g. fish), the `'\\''` idiom
51
+ // will fail — but pipePath is constructed from os.tmpdir() + hex, so single quotes
52
+ // cannot appear in practice, making the replace a no-op and the quoting sh-compatible.
53
+ const shellSafePath = "'" + pipePath.replace(/'/g, "'\\''") + "'";
54
+ execFileSync('tmux', ['pipe-pane', '-t', tui.sessionId, `cat >> ${shellSafePath}`]);
33
55
  debug('codex:scrape', `pipe-pane logging to ${pipePath}`);
34
- // Wait for TUI ready: current screen (historyLines=0) shows Tip, meaning MCP boot done
35
- await tui.waitFor(/Tip:/i, 20_000, 0);
56
+ // Wait for TUI ready, dismissing any blocking dialogs along the way.
57
+ const dialogDeadline = Date.now() + CODEX_STARTUP_MS;
58
+ let screen = await tui.waitFor(CODEX_EITHER, CODEX_STARTUP_MS, 0);
59
+ while (!CODEX_READY.test(screen)) {
60
+ debug('codex:scrape', 'blocking dialog detected — sending "2" to dismiss');
61
+ tui.send('2');
62
+ await sleep(CODEX_DIALOG_SETTLE_MS);
63
+ const remaining = dialogDeadline - Date.now(); // compute AFTER sleep
64
+ if (remaining < 500) {
65
+ throw new Error('Codex TUI never reached ready state after dismissing dialogs');
66
+ }
67
+ screen = await tui.waitFor(CODEX_EITHER, remaining, 0);
68
+ }
36
69
  // First /status: panel says "Limits: refresh requested; run /status again shortly"
37
70
  tui.send('/status');
38
- await sleep(2_000);
71
+ await sleep(CODEX_STATUS_REFRESH_MS);
39
72
  // Second /status: has actual 5h/weekly quota data
40
73
  tui.send('/status');
41
- await sleep(4_000);
74
+ await sleep(CODEX_STATUS_READY_MS);
42
75
  const raw = readFileSync(pipePath, 'utf-8');
43
76
  debug('codex:scrape', `pipe log size: ${raw.length} bytes`);
44
77
  return raw;
45
78
  }
46
79
  finally {
47
- tui.kill();
80
+ try {
81
+ tui.kill();
82
+ }
83
+ catch { /* already dead */ }
84
+ unregisterTempFile(pipePath); // always remove from registry, regardless of unlink success
48
85
  try {
49
86
  unlinkSync(pipePath);
50
87
  }
51
- catch { /* ok */ }
88
+ catch { /* ok if already gone */ }
52
89
  }
53
90
  }
54
91
  function stripAnsi(str) {
@@ -119,7 +156,8 @@ async function fetchCcusageEstimate(budgetLimit) {
119
156
  try {
120
157
  ({ stdout } = await execAsync('npx --no-install ccusage codex session --json'));
121
158
  }
122
- catch {
159
+ catch (err) {
160
+ debug('codex:ccusage', 'ccusage exec failed', String(err));
123
161
  return unknown();
124
162
  }
125
163
  debug('codex:ccusage', 'raw stdout', stdout);
@@ -156,7 +194,7 @@ async function fetchCcusageEstimate(budgetLimit) {
156
194
  if (latestActivity > 0) {
157
195
  try {
158
196
  resetAt = new Date(latestActivity + ROLLING_WINDOW_MS).toLocaleTimeString([], {
159
- hour: '2-digit', minute: '2-digit',
197
+ hour: '2-digit', minute: '2-digit', hour12: false,
160
198
  });
161
199
  }
162
200
  catch { /* leave null */ }
@@ -178,7 +216,8 @@ async function fetchCcusageEstimate(budgetLimit) {
178
216
  raw: { totalCost, todaySessionsCount: todaySessions.length, isEstimate: true },
179
217
  };
180
218
  }
181
- catch {
219
+ catch (err) {
220
+ debug('codex:ccusage', 'unexpected error in ccusage fallback', String(err));
182
221
  return unknown();
183
222
  }
184
223
  }
package/dist/debug.js CHANGED
@@ -1,10 +1,41 @@
1
- import { appendFileSync, mkdirSync } from 'node:fs';
1
+ import { appendFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  const enabled = process.argv.includes('--debug');
4
+ const MAX_DEBUG_LOGS = 10;
5
+ function pruneOldLogs(dir) {
6
+ try {
7
+ // Count first — stat only if pruning is actually needed.
8
+ // Use MAX_DEBUG_LOGS - 1 to reserve a slot for the new log file that is
9
+ // created after this call, so the total on disk never exceeds MAX_DEBUG_LOGS.
10
+ const names = readdirSync(dir)
11
+ .filter(f => f.startsWith('debug-') && f.endsWith('.log'));
12
+ if (names.length < MAX_DEBUG_LOGS)
13
+ return;
14
+ const files = names.map(f => {
15
+ const p = join(dir, f);
16
+ return { path: p, time: statSync(p).mtimeMs };
17
+ });
18
+ // Sort by modification time, oldest first
19
+ files.sort((a, b) => a.time - b.time);
20
+ const toDeleteCount = files.length - (MAX_DEBUG_LOGS - 1);
21
+ for (let i = 0; i < toDeleteCount; i++) {
22
+ try {
23
+ unlinkSync(files[i].path);
24
+ }
25
+ catch {
26
+ // ignore individual deletion failures
27
+ }
28
+ }
29
+ }
30
+ catch {
31
+ // ignore directory read/stat errors
32
+ }
33
+ }
4
34
  const logFile = enabled
5
35
  ? (() => {
6
36
  const dir = join(process.cwd(), '.logs');
7
37
  mkdirSync(dir, { recursive: true });
38
+ pruneOldLogs(dir);
8
39
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
9
40
  return join(dir, `debug-${ts}.log`);
10
41
  })()
package/dist/index.js CHANGED
File without changes
package/dist/tmux.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export declare function registerTempFile(filePath: string): void;
2
+ export declare function unregisterTempFile(filePath: string): void;
1
3
  export declare class TuiScraper {
2
4
  private readonly command;
3
5
  private readonly width;
@@ -11,3 +13,4 @@ export declare class TuiScraper {
11
13
  waitFor(pattern: RegExp, timeoutMs: number, historyLines?: number): Promise<string>;
12
14
  kill(): void;
13
15
  }
16
+ export declare function sleep(ms: number): Promise<void>;
package/dist/tmux.js CHANGED
@@ -1,5 +1,62 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import { unlinkSync } from 'node:fs';
3
+ import crypto from 'node:crypto';
2
4
  import { debug } from './debug.js';
5
+ // Global registries for active resources
6
+ const activeScrapers = new Set();
7
+ const activeTempFiles = new Set();
8
+ export function registerTempFile(filePath) {
9
+ activeTempFiles.add(filePath);
10
+ }
11
+ export function unregisterTempFile(filePath) {
12
+ activeTempFiles.delete(filePath);
13
+ }
14
+ function cleanupAll() {
15
+ if (activeScrapers.size === 0 && activeTempFiles.size === 0)
16
+ return;
17
+ process.stderr.write(`\n\x1b[33m[agent-fuel] Clean up triggered. Cleaning up resources...\x1b[0m\n`);
18
+ for (const scraper of activeScrapers) {
19
+ try {
20
+ scraper.kill();
21
+ }
22
+ catch {
23
+ // ignore
24
+ }
25
+ }
26
+ activeScrapers.clear();
27
+ for (const file of activeTempFiles) {
28
+ try {
29
+ unlinkSync(file);
30
+ }
31
+ catch {
32
+ // ignore
33
+ }
34
+ }
35
+ activeTempFiles.clear();
36
+ }
37
+ // Register signal handlers eagerly at module load time so that any temp files
38
+ // registered before TuiScraper.start() (e.g. between registerTempFile and start())
39
+ // are still cleaned up on SIGINT/SIGTERM/SIGHUP.
40
+ let signalsRegistered = false;
41
+ function registerSignalHandlers() {
42
+ if (signalsRegistered)
43
+ return;
44
+ signalsRegistered = true;
45
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
46
+ for (const sig of signals) {
47
+ process.on(sig, () => {
48
+ cleanupAll();
49
+ const code = sig === 'SIGINT' ? 130 : sig === 'SIGTERM' ? 143 : 129;
50
+ process.exit(code);
51
+ });
52
+ }
53
+ process.on('uncaughtException', (err) => {
54
+ process.stderr.write(`\x1b[31mUncaught Exception:\x1b[0m ${err.stack || err}\n`);
55
+ cleanupAll();
56
+ process.exit(1);
57
+ });
58
+ }
59
+ registerSignalHandlers(); // called at import time — guarded by signalsRegistered flag
3
60
  export class TuiScraper {
4
61
  command;
5
62
  width;
@@ -9,11 +66,11 @@ export class TuiScraper {
9
66
  this.command = command;
10
67
  this.width = width;
11
68
  this.height = height;
12
- this.sessionId = `af-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
69
+ this.sessionId = `af-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
13
70
  }
14
71
  start() {
15
72
  try {
16
- execFileSync('which', ['tmux'], { stdio: 'ignore' });
73
+ execFileSync('which', ['tmux'], { stdio: 'ignore', timeout: 5000 });
17
74
  }
18
75
  catch {
19
76
  throw new Error('tmux not found — install with: brew install tmux');
@@ -23,7 +80,8 @@ export class TuiScraper {
23
80
  'new-session', '-d', '-s', this.sessionId,
24
81
  '-x', String(this.width), '-y', String(this.height),
25
82
  this.command,
26
- ]);
83
+ ], { stdio: 'ignore', timeout: 5000 });
84
+ activeScrapers.add(this);
27
85
  }
28
86
  // historyLines > 0 → include that many lines of scrollback above the visible screen.
29
87
  // This catches transient overlays that rendered and then re-rendered away.
@@ -31,18 +89,18 @@ export class TuiScraper {
31
89
  const args = historyLines > 0
32
90
  ? ['capture-pane', '-t', this.sessionId, '-S', `-${historyLines}`, '-p']
33
91
  : ['capture-pane', '-t', this.sessionId, '-p'];
34
- const text = execFileSync('tmux', args).toString();
92
+ const text = execFileSync('tmux', args, { timeout: 5000 }).toString();
35
93
  debug('tmux:capture', `[${this.sessionId}] captured ${text.length} chars (history=${historyLines})`);
36
94
  return text;
37
95
  }
38
96
  send(text) {
39
97
  debug('tmux:send', `[${this.sessionId}] sending: ${JSON.stringify(text)}`);
40
- execFileSync('tmux', ['send-keys', '-t', this.sessionId, text, 'Enter']);
98
+ execFileSync('tmux', ['send-keys', '-t', this.sessionId, text, 'Enter'], { stdio: 'ignore', timeout: 5000 });
41
99
  }
42
100
  // Send a named key (Tab, Escape, Up, Down, etc.) without appending Enter.
43
101
  sendKey(key) {
44
102
  debug('tmux:send', `[${this.sessionId}] sendKey: ${key}`);
45
- execFileSync('tmux', ['send-keys', '-t', this.sessionId, key]);
103
+ execFileSync('tmux', ['send-keys', '-t', this.sessionId, key], { stdio: 'ignore', timeout: 5000 });
46
104
  }
47
105
  async waitFor(pattern, timeoutMs, historyLines = 500) {
48
106
  const deadline = Date.now() + timeoutMs;
@@ -62,11 +120,12 @@ export class TuiScraper {
62
120
  kill() {
63
121
  debug('tmux', `killing session ${this.sessionId}`);
64
122
  try {
65
- execFileSync('tmux', ['kill-session', '-t', this.sessionId], { stdio: 'ignore' });
123
+ execFileSync('tmux', ['kill-session', '-t', this.sessionId], { stdio: 'ignore', timeout: 3000 });
66
124
  }
67
125
  catch { /* already dead */ }
126
+ activeScrapers.delete(this); // delete after kill attempt so a concurrent signal doesn't see a half-killed scraper
68
127
  }
69
128
  }
70
- function sleep(ms) {
129
+ export function sleep(ms) {
71
130
  return new Promise(res => setTimeout(res, ms));
72
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-fuel",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Sleek term-based dashboard for AI coding CLI quotas",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "author": "Pedro Rodrigues",
29
29
  "license": "MIT",
30
30
  "devDependencies": {
31
- "@types/node": "^20.11.24",
32
- "typescript": "^5.3.3"
31
+ "@types/node": "^25.9.1",
32
+ "typescript": "^6.0.3"
33
33
  }
34
34
  }