@stackmemoryai/stackmemory 1.5.4 → 1.5.5

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.
@@ -60,6 +60,55 @@ function formatElapsed(ms) {
60
60
  const days = Math.floor(hours / 24);
61
61
  return `${days}d ago`;
62
62
  }
63
+ function fmtTokens(n) {
64
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
65
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
66
+ return String(n);
67
+ }
68
+ function budgetBar(pct, width = 30) {
69
+ const filled = Math.min(Math.round(pct / 100 * width), width);
70
+ const empty = width - filled;
71
+ const color = pct >= 75 ? "\x1B[31m" : pct >= 50 ? "\x1B[33m" : "\x1B[32m";
72
+ const dim = "\x1B[2m";
73
+ const rst = "\x1B[0m";
74
+ return `${color}${"\u2588".repeat(filled)}${dim}${"\u2591".repeat(empty)}${rst} ${String(pct).padStart(3)}%`;
75
+ }
76
+ function fmtMinutes(m) {
77
+ if (m < 0) return "N/A";
78
+ if (m >= 60) return `${Math.floor(m / 60)}h ${m % 60}m`;
79
+ return `${m}m`;
80
+ }
81
+ function printUsageSummary(u) {
82
+ const totalTokens = u.totalTokens || 0;
83
+ const inputTokens = u.inputTokens || 0;
84
+ const outputTokens = u.outputTokens || 0;
85
+ const estMessages = u.estimatedMessages || 0;
86
+ const tokensPerMin = u.tokensPerMin || 0;
87
+ const budgetPct5x = u.budgetPct5x || 0;
88
+ const budgetPct20x = u.budgetPct20x || 0;
89
+ const mins5x = u.minutesRemaining5x ?? -1;
90
+ const mins20x = u.minutesRemaining20x ?? -1;
91
+ const cacheHitRate = u.cacheHitRate || 0;
92
+ const b = "\x1B[1m";
93
+ const d = "\x1B[2m";
94
+ const w = "\x1B[37m";
95
+ const r = "\x1B[0m";
96
+ console.log(`${b}Token Usage${r}`);
97
+ console.log(
98
+ ` Input ${w}${fmtTokens(inputTokens)}${r} ${d}|${r} Output ${w}${fmtTokens(outputTokens)}${r} ${d}|${r} Total ${w}${fmtTokens(totalTokens)}${r}`
99
+ );
100
+ console.log(
101
+ ` Rate ${w}${fmtTokens(tokensPerMin)}/min${r} ${d}|${r} Messages ${w}${estMessages}${r} ${d}|${r} Cache hit ${w}${cacheHitRate}%${r}`
102
+ );
103
+ console.log("");
104
+ console.log(`${b}Budget (Max plan, 5h window)${r}`);
105
+ console.log(
106
+ ` 5x (225 msgs) ${budgetBar(budgetPct5x)} ${d}~${fmtMinutes(mins5x)} left${r}`
107
+ );
108
+ console.log(
109
+ ` 20x (900 msgs) ${budgetBar(budgetPct20x)} ${d}~${fmtMinutes(mins20x)} left${r}`
110
+ );
111
+ }
63
112
  function createConductorCommands() {
64
113
  const cmd = new Command("conductor");
65
114
  cmd.description("Conductor \u2014 autonomous agent orchestration via Linear");
@@ -334,6 +383,54 @@ function createConductorCommands() {
334
383
  process.on("SIGTERM", forward);
335
384
  });
336
385
  });
386
+ cmd.command("usage").description("Show token usage, budget, and time-to-exhaustion").option("--json", "Output as JSON", false).option("--scan", "Scan Claude Code JSONL logs for historical data", false).action(async (options) => {
387
+ const statusPath = join(
388
+ process.cwd(),
389
+ ".stackmemory",
390
+ "conductor-status.json"
391
+ );
392
+ if (options.scan) {
393
+ const conductor = new Conductor({ repoRoot: process.cwd() });
394
+ await conductor.scanUsageLogs();
395
+ const summary = conductor.getUsageSummary();
396
+ if (options.json) {
397
+ console.log(JSON.stringify(summary, null, 2));
398
+ return;
399
+ }
400
+ printUsageSummary(summary);
401
+ return;
402
+ }
403
+ if (!existsSync(statusPath)) {
404
+ console.log(
405
+ "No conductor-status.json found. Run with --scan to check JSONL logs, or start the conductor first."
406
+ );
407
+ return;
408
+ }
409
+ try {
410
+ const data = JSON.parse(readFileSync(statusPath, "utf-8"));
411
+ const usage = data.usage || {};
412
+ if (options.json) {
413
+ console.log(JSON.stringify(usage, null, 2));
414
+ return;
415
+ }
416
+ printUsageSummary(usage);
417
+ const rl = data.rateLimit;
418
+ if (rl) {
419
+ console.log("");
420
+ if (rl.inBackoff) {
421
+ console.log(
422
+ `Rate limit: \x1B[31mBACKOFF\x1B[0m (${rl.backoffRemainingSec}s remaining, ${rl.totalHits} total hits)`
423
+ );
424
+ } else {
425
+ console.log(
426
+ `Rate limit: \x1B[32mOK\x1B[0m (${rl.totalHits} total hits)`
427
+ );
428
+ }
429
+ }
430
+ } catch (err) {
431
+ console.error("Failed to read status file:", err.message);
432
+ }
433
+ });
337
434
  cmd.command("start").description("Start the orchestrator daemon").option("--team <id>", "Linear team ID").option(
338
435
  "--states <states>",
339
436
  "Comma-separated issue states to pick up",
@@ -8,10 +8,13 @@ import {
8
8
  mkdirSync,
9
9
  rmSync,
10
10
  writeFileSync,
11
+ readdirSync,
11
12
  createWriteStream
12
13
  } from "fs";
13
14
  import { join, dirname } from "path";
14
15
  import { tmpdir, homedir } from "os";
16
+ import { createReadStream } from "fs";
17
+ import { createInterface } from "readline";
15
18
  import { fileURLToPath } from "url";
16
19
  import { Transform } from "stream";
17
20
  import { logger } from "../../core/monitoring/logger.js";
@@ -128,7 +131,7 @@ const DEFAULT_CONFIG = {
128
131
  inProgressState: "In Progress",
129
132
  inReviewState: "In Review",
130
133
  pollIntervalMs: 3e4,
131
- maxConcurrent: 3,
134
+ maxConcurrent: 5,
132
135
  workspaceRoot: join(tmpdir(), "conductor_workspaces"),
133
136
  repoRoot: process.cwd(),
134
137
  baseBranch: "main",
@@ -158,6 +161,23 @@ class Conductor {
158
161
  stateCache = /* @__PURE__ */ new Map();
159
162
  activeStatesLower;
160
163
  terminalStatesLower;
164
+ /** Global rate limit backoff state */
165
+ rateLimit = {
166
+ inBackoff: false,
167
+ backoffUntil: 0,
168
+ totalHits: 0,
169
+ consecutiveHits: 0,
170
+ lastHitAt: 0
171
+ };
172
+ /** Aggregated usage stats */
173
+ usage = {
174
+ inputTokens: 0,
175
+ outputTokens: 0,
176
+ cacheCreationTokens: 0,
177
+ cacheReadTokens: 0,
178
+ estimatedMessages: 0,
179
+ perAgent: /* @__PURE__ */ new Map()
180
+ };
161
181
  constructor(config = {}) {
162
182
  this.config = { ...DEFAULT_CONFIG, ...config };
163
183
  this.activeStatesLower = this.config.activeStates.map(
@@ -294,7 +314,9 @@ class Conductor {
294
314
  failed: this.failCount,
295
315
  totalAttempts: this.totalAttempts,
296
316
  uptime: Date.now() - this.startedAt,
297
- issues
317
+ issues,
318
+ rateLimit: { ...this.rateLimit },
319
+ usage: { ...this.usage, perAgent: new Map(this.usage.perAgent) }
298
320
  };
299
321
  }
300
322
  // ── Status File ──
@@ -324,7 +346,31 @@ class Conductor {
324
346
  failed: this.failCount,
325
347
  totalAttempts: this.totalAttempts,
326
348
  maxConcurrent: this.config.maxConcurrent,
327
- stopping: this.stopping
349
+ stopping: this.stopping,
350
+ rateLimit: {
351
+ inBackoff: this.rateLimit.inBackoff,
352
+ backoffUntil: this.rateLimit.backoffUntil,
353
+ backoffRemainingSec: this.rateLimit.inBackoff ? Math.max(
354
+ 0,
355
+ Math.ceil((this.rateLimit.backoffUntil - Date.now()) / 1e3)
356
+ ) : 0,
357
+ totalHits: this.rateLimit.totalHits
358
+ },
359
+ usage: (() => {
360
+ const summary = this.getUsageSummary();
361
+ return {
362
+ inputTokens: summary.inputTokens,
363
+ outputTokens: summary.outputTokens,
364
+ totalTokens: summary.totalTokens,
365
+ estimatedMessages: summary.estimatedMessages,
366
+ tokensPerMin: summary.tokensPerMin,
367
+ budgetPct5x: summary.budgetPct5x,
368
+ budgetPct20x: summary.budgetPct20x,
369
+ minutesRemaining5x: summary.minutesRemaining5x,
370
+ minutesRemaining20x: summary.minutesRemaining20x,
371
+ cacheHitRate: summary.cacheHitRate
372
+ };
373
+ })()
328
374
  };
329
375
  try {
330
376
  writeFileSync(
@@ -390,6 +436,21 @@ class Conductor {
390
436
  }
391
437
  async poll() {
392
438
  if (!this.client || this.stopping) return;
439
+ if (this.rateLimit.inBackoff) {
440
+ const remaining = this.rateLimit.backoffUntil - Date.now();
441
+ if (remaining > 0) {
442
+ logger.info("Rate limit backoff active, skipping poll", {
443
+ remainingMs: remaining,
444
+ remainingSec: Math.ceil(remaining / 1e3),
445
+ totalHits: this.rateLimit.totalHits
446
+ });
447
+ return;
448
+ }
449
+ this.rateLimit.inBackoff = false;
450
+ this.rateLimit.backoffUntil = 0;
451
+ logger.info("Rate limit backoff expired, resuming dispatch");
452
+ console.log("[rate-limit] Backoff expired, resuming dispatch");
453
+ }
393
454
  await this.reconcile();
394
455
  const available = this.config.maxConcurrent - this.running.size;
395
456
  if (available <= 0) {
@@ -407,6 +468,29 @@ class Conductor {
407
468
  if (eligible.length === 0) return;
408
469
  const sorted = eligible.sort((a, b) => (a.priority || 4) - (b.priority || 4)).slice(0, available);
409
470
  const toDispatch = this.preflightFilter(sorted);
471
+ if (this.usage.inputTokens > 0) {
472
+ const summary = this.getUsageSummary();
473
+ logger.info("Usage summary", {
474
+ totalTokens: summary.totalTokens,
475
+ estimatedMessages: summary.estimatedMessages,
476
+ tokensPerMin: summary.tokensPerMin,
477
+ budgetPct5x: `${summary.budgetPct5x}%`,
478
+ budgetPct20x: `${summary.budgetPct20x}%`,
479
+ minutesRemaining5x: summary.minutesRemaining5x,
480
+ minutesRemaining20x: summary.minutesRemaining20x,
481
+ rateLimitHits: this.rateLimit.totalHits
482
+ });
483
+ if (summary.budgetPct5x >= 75 && summary.budgetPct5x < 100) {
484
+ console.log(
485
+ `[usage] \u26A0 ${summary.budgetPct5x}% of Max 5x budget used \u2014 ~${summary.minutesRemaining5x}min remaining at ${summary.tokensPerMin} tok/min`
486
+ );
487
+ }
488
+ if (summary.budgetPct5x >= 100) {
489
+ console.log(
490
+ `[usage] \u{1F6D1} Max 5x budget likely exhausted (${summary.estimatedMessages} est. messages / 225 limit). Expect 429s.`
491
+ );
492
+ }
493
+ }
410
494
  logger.info("Dispatching issues", {
411
495
  count: toDispatch.length,
412
496
  identifiers: toDispatch.map((i) => i.identifier),
@@ -569,6 +653,9 @@ class Conductor {
569
653
  error: run.error,
570
654
  attempt: run.attempt
571
655
  });
656
+ if (this.handleRateLimitError(run.error, issue.identifier)) {
657
+ throw err;
658
+ }
572
659
  if (run.workspacePath) {
573
660
  await this.runHook(
574
661
  "after-run",
@@ -595,6 +682,179 @@ class Conductor {
595
682
  }
596
683
  }
597
684
  }
685
+ // ── Rate Limit Detection ──
686
+ /**
687
+ * Check if an error indicates a rate limit (429) or usage cap.
688
+ * Triggers global backoff so no new agents are dispatched.
689
+ *
690
+ * Claude Max limits (approximate, shared between claude.ai + Claude Code):
691
+ * - Max 5x: ~225 messages per 5h window
692
+ * - Max 20x: ~900 messages per 5h window
693
+ * Messages are token-weighted: heavy agent usage ≈ 5-10x a casual message.
694
+ */
695
+ handleRateLimitError(error, identifier) {
696
+ const rateLimitPatterns = [
697
+ "usage limits",
698
+ "rate_limit",
699
+ "rate limit",
700
+ "overloaded",
701
+ "429",
702
+ "too many requests",
703
+ "quota exceeded",
704
+ "capacity"
705
+ ];
706
+ const isRateLimit = rateLimitPatterns.some(
707
+ (p) => error.toLowerCase().includes(p)
708
+ );
709
+ if (!isRateLimit) return false;
710
+ this.rateLimit.totalHits++;
711
+ this.rateLimit.lastHitAt = Date.now();
712
+ const timeSinceLast = Date.now() - this.rateLimit.lastHitAt;
713
+ if (timeSinceLast < 6e5) {
714
+ this.rateLimit.consecutiveHits++;
715
+ } else {
716
+ this.rateLimit.consecutiveHits = 1;
717
+ }
718
+ const backoffSec = Math.min(
719
+ 60 * Math.pow(2, this.rateLimit.consecutiveHits - 1),
720
+ 900
721
+ );
722
+ this.rateLimit.inBackoff = true;
723
+ this.rateLimit.backoffUntil = Date.now() + backoffSec * 1e3;
724
+ logger.warn("Rate limit hit \u2014 global backoff", {
725
+ identifier,
726
+ backoffSec,
727
+ consecutiveHits: this.rateLimit.consecutiveHits,
728
+ totalHits: this.rateLimit.totalHits,
729
+ error: error.slice(0, 200)
730
+ });
731
+ console.log(
732
+ `[rate-limit] Hit rate limit on ${identifier} \u2014 backing off ${backoffSec}s (hit #${this.rateLimit.totalHits})`
733
+ );
734
+ return true;
735
+ }
736
+ // ── Usage Tracking ──
737
+ /**
738
+ * Track token usage from a stream-json event (CLI mode) or JSON-RPC message (adapter mode).
739
+ * Updates both global and per-agent counters.
740
+ */
741
+ trackUsage(identifier, usage) {
742
+ const input = usage.input_tokens || 0;
743
+ const output = usage.output_tokens || 0;
744
+ const cacheCreate = usage.cache_creation_input_tokens || 0;
745
+ const cacheRead = usage.cache_read_input_tokens || 0;
746
+ this.usage.inputTokens += input;
747
+ this.usage.outputTokens += output;
748
+ this.usage.cacheCreationTokens += cacheCreate;
749
+ this.usage.cacheReadTokens += cacheRead;
750
+ this.usage.estimatedMessages = Math.ceil(
751
+ (this.usage.inputTokens + this.usage.outputTokens) / 1e4
752
+ );
753
+ const agent = this.usage.perAgent.get(identifier) || {
754
+ inputTokens: 0,
755
+ outputTokens: 0
756
+ };
757
+ agent.inputTokens += input;
758
+ agent.outputTokens += output;
759
+ this.usage.perAgent.set(identifier, agent);
760
+ }
761
+ /**
762
+ * Scan Claude Code JSONL logs for token usage from conductor-spawned sessions.
763
+ * Reads logs from ~/.claude/projects/ matching conductor workspace paths.
764
+ */
765
+ async scanUsageLogs() {
766
+ const logDirs = [
767
+ join(homedir(), ".claude", "projects"),
768
+ join(homedir(), ".config", "claude", "projects")
769
+ ];
770
+ for (const logDir of logDirs) {
771
+ if (!existsSync(logDir)) continue;
772
+ try {
773
+ const entries = readdirSync(logDir);
774
+ const conductorDirs = entries.filter(
775
+ (e) => e.includes("conductor_workspaces") || e.includes("conductor-")
776
+ );
777
+ for (const dir of conductorDirs) {
778
+ const projectDir = join(logDir, dir);
779
+ if (!existsSync(projectDir)) continue;
780
+ const jsonlFiles = readdirSync(projectDir).filter(
781
+ (f) => f.endsWith(".jsonl")
782
+ );
783
+ for (const file of jsonlFiles) {
784
+ await this.parseUsageFromJsonl(join(projectDir, file));
785
+ }
786
+ }
787
+ } catch (err) {
788
+ logger.debug("Usage log scan failed", {
789
+ error: err.message
790
+ });
791
+ }
792
+ }
793
+ return this.usage;
794
+ }
795
+ async parseUsageFromJsonl(filePath) {
796
+ try {
797
+ const rl = createInterface({
798
+ input: createReadStream(filePath),
799
+ crlfDelay: Infinity
800
+ });
801
+ for await (const line of rl) {
802
+ if (!line.trim()) continue;
803
+ try {
804
+ const entry = JSON.parse(line);
805
+ if (entry.type === "assistant" && entry.message?.usage) {
806
+ const usage = entry.message.usage;
807
+ const identifier = entry.sessionId?.slice(0, 8) || "unknown";
808
+ this.trackUsage(identifier, usage);
809
+ }
810
+ } catch {
811
+ }
812
+ }
813
+ } catch {
814
+ }
815
+ }
816
+ /**
817
+ * Get current usage summary with time-to-exhaustion estimate.
818
+ *
819
+ * Claude Max limits (approximate, shared between claude.ai + Claude Code):
820
+ * - Max 5x: ~225 messages per 5h window (~10k tokens per "message")
821
+ * - Max 20x: ~900 messages per 5h window
822
+ *
823
+ * Heavy agent usage burns 5-10x faster than casual chat.
824
+ */
825
+ getUsageSummary() {
826
+ const totalCache = this.usage.cacheCreationTokens + this.usage.cacheReadTokens;
827
+ const cacheHitRate = totalCache > 0 ? this.usage.cacheReadTokens / totalCache : 0;
828
+ const uptimeMin = Math.max(1, (Date.now() - this.startedAt) / 6e4);
829
+ const totalTokens = this.usage.inputTokens + this.usage.outputTokens;
830
+ const tokensPerMin = Math.round(totalTokens / uptimeMin);
831
+ const estMessages = this.usage.estimatedMessages;
832
+ const MAX_5X_MESSAGES = 225;
833
+ const MAX_20X_MESSAGES = 900;
834
+ const budgetPct5x = Math.round(estMessages / MAX_5X_MESSAGES * 100);
835
+ const budgetPct20x = Math.round(estMessages / MAX_20X_MESSAGES * 100);
836
+ const msgsPerMin = tokensPerMin > 0 ? tokensPerMin / 1e4 : 0;
837
+ const minutesRemaining5x = msgsPerMin > 0 ? Math.round((MAX_5X_MESSAGES - estMessages) / msgsPerMin) : -1;
838
+ const minutesRemaining20x = msgsPerMin > 0 ? Math.round((MAX_20X_MESSAGES - estMessages) / msgsPerMin) : -1;
839
+ return {
840
+ totalTokens,
841
+ inputTokens: this.usage.inputTokens,
842
+ outputTokens: this.usage.outputTokens,
843
+ estimatedMessages: estMessages,
844
+ cacheHitRate: Math.round(cacheHitRate * 100),
845
+ tokensPerMin,
846
+ budgetPct5x,
847
+ budgetPct20x,
848
+ minutesRemaining5x: minutesRemaining5x < 0 ? -1 : minutesRemaining5x,
849
+ minutesRemaining20x: minutesRemaining20x < 0 ? -1 : minutesRemaining20x,
850
+ perAgent: Array.from(this.usage.perAgent.entries()).map(
851
+ ([id, stats]) => ({
852
+ id,
853
+ ...stats
854
+ })
855
+ )
856
+ };
857
+ }
598
858
  // ── Workspace Management ──
599
859
  async createWorkspace(issue) {
600
860
  const wsKey = this.sanitizeIdentifier(issue.identifier);
@@ -672,6 +932,21 @@ class Conductor {
672
932
  sanitizeIdentifier(identifier) {
673
933
  return identifier.replace(/[^A-Za-z0-9._-]/g, "_");
674
934
  }
935
+ /**
936
+ * Find the real claude binary, skipping shell wrappers (cmux, claude-smd).
937
+ * Wrappers inject --settings with hooks that block headless -p mode.
938
+ */
939
+ findClaudeBinary() {
940
+ const candidates = [
941
+ join(homedir(), ".local", "bin", "claude"),
942
+ "/usr/local/bin/claude",
943
+ "/opt/homebrew/bin/claude"
944
+ ];
945
+ for (const c of candidates) {
946
+ if (existsSync(c)) return c;
947
+ }
948
+ return "claude";
949
+ }
675
950
  // ── Agent Execution ──
676
951
  async runAgent(issue, run) {
677
952
  if (this.config.agentMode === "cli") {
@@ -679,7 +954,10 @@ class Conductor {
679
954
  return await this.runAgentCLI(issue, run);
680
955
  } catch (err) {
681
956
  const msg = err.message || "";
682
- if (msg.includes("usage limits") || msg.includes("rate_limit") || msg.includes("overloaded")) {
957
+ if (this.handleRateLimitError(msg, issue.identifier)) {
958
+ throw err;
959
+ }
960
+ if (msg.includes("usage limits") || msg.includes("overloaded")) {
683
961
  logger.warn("CLI mode hit limits, falling back to adapter", {
684
962
  identifier: issue.identifier,
685
963
  error: msg.slice(0, 200)
@@ -702,13 +980,16 @@ class Conductor {
702
980
  runAgentCLI(issue, run) {
703
981
  return new Promise((resolve, reject) => {
704
982
  const prompt = this.buildPrompt(issue, run.attempt);
983
+ const claudeBin = this.findClaudeBinary();
705
984
  const proc = spawn(
706
- "claude",
985
+ claudeBin,
707
986
  [
708
987
  "-p",
709
988
  "--output-format",
710
989
  "stream-json",
711
990
  "--dangerously-skip-permissions",
991
+ "--settings",
992
+ '{"hooks":{}}',
712
993
  prompt
713
994
  ],
714
995
  {
@@ -729,6 +1010,7 @@ class Conductor {
729
1010
  }
730
1011
  );
731
1012
  run.process = proc;
1013
+ proc.stdin.end();
732
1014
  const logStream = this.openAgentLogStream(issue.identifier);
733
1015
  run.logStream = logStream;
734
1016
  const tee = new TeeTransform(logStream);
@@ -759,6 +1041,10 @@ class Conductor {
759
1041
  }
760
1042
  if (event.type === "assistant") {
761
1043
  const message = event.message;
1044
+ const msgUsage = message?.usage;
1045
+ if (msgUsage) {
1046
+ this.trackUsage(issue.identifier, msgUsage);
1047
+ }
762
1048
  const content = message?.content || [];
763
1049
  for (const block of content) {
764
1050
  if (block.type === "tool_use") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.5.4",
3
+ "version": "1.5.5",
4
4
  "description": "Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, and automatic hooks.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",
@@ -100,6 +100,7 @@
100
100
  "postinstall": "node scripts/install-claude-hooks-auto.js || true",
101
101
  "init": "node dist/scripts/initialize.js",
102
102
  "build": "rm -rf dist && node esbuild.config.js",
103
+ "typecheck": "NODE_OPTIONS='--max-old-space-size=8192' tsc --noEmit",
103
104
  "lint": "eslint 'src/**/*.ts' 'scripts/**/*.ts'",
104
105
  "lint:fix": "eslint 'src/**/*.ts' 'scripts/**/*.ts' --fix --max-warnings=-1",
105
106
  "lint:fast": "oxlint src scripts",