@timo972/cc-router 0.7.0 → 0.9.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.
@@ -11,6 +11,87 @@ const EMPTY_RL = {
11
11
  sevenDayUtil: 0, sevenDayReset: 0, claim: "", plan: "",
12
12
  requestsLimit: 0, lastUpdated: 0,
13
13
  };
14
+ /** Match TokenPool's source precedence for the dashboard's global windows. */
15
+ export function getGlobalCapacityView(rateLimits) {
16
+ const usage = rateLimits.usage;
17
+ const snapshotIsCurrent = usage !== undefined &&
18
+ usage.fetchStatus !== "unavailable" &&
19
+ usage.fetchedAt >= rateLimits.lastUpdated;
20
+ const usageFetchStatus = usage?.fetchStatus === "fresh" && !snapshotIsCurrent
21
+ ? "stale"
22
+ : usage?.fetchStatus;
23
+ return {
24
+ fiveHour: snapshotIsCurrent && usage.fiveHour
25
+ ? usage.fiveHour
26
+ : { utilization: rateLimits.fiveHourUtil, resetAt: rateLimits.fiveHourReset },
27
+ sevenDay: snapshotIsCurrent && usage.sevenDay
28
+ ? usage.sevenDay
29
+ : { utilization: rateLimits.sevenDayUtil, resetAt: rateLimits.sevenDayReset },
30
+ usageFetchStatus,
31
+ };
32
+ }
33
+ /** Turn the safe account payload into compact dynamic model/cooldown rows. */
34
+ export function getAccountCapacityRows(account) {
35
+ const usage = account.rateLimits?.usage;
36
+ const modelCooldowns = account.modelCooldowns ?? [];
37
+ const rows = [];
38
+ const matchedCooldowns = new Set();
39
+ const now = Date.now();
40
+ if (usage?.modelLimits.length) {
41
+ const usageFetchStatus = usage.fetchStatus === "fresh" &&
42
+ usage.fetchedAt < (account.rateLimits?.lastUpdated ?? 0)
43
+ ? "stale"
44
+ : usage.fetchStatus;
45
+ const usageState = usageFetchStatus === "fresh" ? undefined : `usage ${usageFetchStatus}`;
46
+ const paidExtraAvailable = usage.extraUsage?.usable === true;
47
+ for (const limit of usage.modelLimits) {
48
+ const requestedCooldown = modelCooldowns.find(cooldown => cooldown.modelFamily === limit.modelFamily && cooldown.untilMs > now);
49
+ const exhausted = limit.utilization >= 1;
50
+ const capacityState = usageState
51
+ ? usageState
52
+ : !limit.active
53
+ ? "inactive"
54
+ : exhausted && paidExtraAvailable
55
+ ? "paid extra active"
56
+ : exhausted
57
+ ? "exhausted"
58
+ : "included available";
59
+ const state = requestedCooldown
60
+ ? `${capacityState} · requested-model cooldown`
61
+ : capacityState;
62
+ if (requestedCooldown)
63
+ matchedCooldowns.add(requestedCooldown.modelFamily);
64
+ const color = requestedCooldown ? "yellow"
65
+ : usageState ? usageFetchStatus === "stale" ? "yellow" : "gray"
66
+ : !limit.active ? "gray"
67
+ : exhausted && paidExtraAvailable ? "yellow"
68
+ : exhausted || limit.severity === "critical" ? "red"
69
+ : limit.severity === "warning" || limit.utilization >= 0.7 ? "yellow"
70
+ : "green";
71
+ rows.push({
72
+ label: limit.displayName,
73
+ state,
74
+ color,
75
+ utilization: limit.utilization,
76
+ resetAt: limit.resetAt,
77
+ });
78
+ }
79
+ }
80
+ for (const cooldown of modelCooldowns) {
81
+ if (matchedCooldowns.has(cooldown.modelFamily) || cooldown.untilMs <= now)
82
+ continue;
83
+ rows.push({
84
+ label: `cooldown ${cooldown.modelFamily}`,
85
+ state: "requested-model cooldown",
86
+ color: "yellow",
87
+ resetAt: Math.floor(cooldown.untilMs / 1_000),
88
+ });
89
+ }
90
+ if (account.globalCooldownUntilMs && account.globalCooldownUntilMs > now) {
91
+ rows.push({ label: "cooldown", state: "global", color: "red", resetAt: Math.floor(account.globalCooldownUntilMs / 1_000) });
92
+ }
93
+ return rows;
94
+ }
14
95
  export function Dashboard({ port, baseUrl, authToken, onIntent }) {
15
96
  const { exit } = useApp();
16
97
  const [data, setData] = useState(null);
@@ -453,6 +534,9 @@ function ProviderBadge({ label, status, ready, }) {
453
534
  // ─── Account row (two-line: status + utilization bars) ───────────────────────
454
535
  function AccountRow({ account: a, selected }) {
455
536
  const rl = a.rateLimits ?? EMPTY_RL;
537
+ const usage = rl.usage;
538
+ const globalCapacity = getGlobalCapacityView(rl);
539
+ const capacityRows = getAccountCapacityRows(a);
456
540
  const isLimited = rl.status === "rate_limited";
457
541
  const isDisabled = a.enabled === false;
458
542
  const dot = isDisabled ? "⊘" : isLimited ? "⊘" : a.busy ? "◌" : a.healthy ? "●" : "●";
@@ -475,7 +559,7 @@ function AccountRow({ account: a, selected }) {
475
559
  : "";
476
560
  const pointer = selected ? "▶" : " ";
477
561
  const nameColor = isDisabled ? "gray" : undefined;
478
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: pointer }), _jsxs(Text, { color: dotColor, children: [" ", dot, " "] }), _jsx(Text, { color: nameColor, dimColor: isDisabled, children: a.id.slice(0, 20).padEnd(20) }), _jsx(Text, { color: statusColor, children: statusLabel }), providerTag && _jsx(Text, { color: a.provider === "openai_subscription" ? "cyan" : "magenta", children: providerTag.padEnd(10) }), !providerTag && _jsx(Text, { children: "".padEnd(10) }), _jsx(Text, { color: "gray", children: " req " }), _jsx(Text, { color: "white", children: String(a.requestCount).padStart(5) }), _jsx(Text, { color: "gray", children: " err " }), _jsx(Text, { color: a.errorCount > 0 ? "red" : "gray", children: String(a.errorCount).padStart(3) }), _jsx(Text, { color: "gray", children: " tok " }), _jsx(Text, { color: expiryColor, children: expiryLabel.padEnd(8) }), _jsx(Text, { color: "gray", children: " last " }), _jsx(Text, { color: "gray", children: formatAgo(a.lastUsedMs) }), a.provider !== "openai_subscription" && (_jsxs(Text, { color: "gray", children: [" ", a.activeSessions ?? 0, " active / ", a.inFlightRequests ?? 0, " streams"] })), capsHint && _jsx(Text, { color: "yellow", children: capsHint })] }), rl.lastUpdated > 0 && (_jsxs(Box, { paddingLeft: 4, children: [_jsx(UtilBar, { label: "5h", util: rl.fiveHourUtil, resetTs: rl.fiveHourReset, isActive: rl.claim === "five_hour", cap: s5 }), _jsx(Text, { children: " " }), _jsx(UtilBar, { label: "7d", util: rl.sevenDayUtil, resetTs: rl.sevenDayReset, isActive: rl.claim === "seven_day", cap: w7 })] }))] }));
562
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : undefined, children: pointer }), _jsxs(Text, { color: dotColor, children: [" ", dot, " "] }), _jsx(Text, { color: nameColor, dimColor: isDisabled, children: a.id.slice(0, 20).padEnd(20) }), _jsx(Text, { color: statusColor, children: statusLabel }), providerTag && _jsx(Text, { color: a.provider === "openai_subscription" ? "cyan" : "magenta", children: providerTag.padEnd(10) }), !providerTag && _jsx(Text, { children: "".padEnd(10) }), _jsx(Text, { color: "gray", children: " req " }), _jsx(Text, { color: "white", children: String(a.requestCount).padStart(5) }), _jsx(Text, { color: "gray", children: " err " }), _jsx(Text, { color: a.errorCount > 0 ? "red" : "gray", children: String(a.errorCount).padStart(3) }), _jsx(Text, { color: "gray", children: " tok " }), _jsx(Text, { color: expiryColor, children: expiryLabel.padEnd(8) }), _jsx(Text, { color: "gray", children: " last " }), _jsx(Text, { color: "gray", children: formatAgo(a.lastUsedMs) }), a.provider !== "openai_subscription" && (_jsxs(Text, { color: "gray", children: [" ", a.activeSessions ?? 0, " active / ", a.inFlightRequests ?? 0, " streams"] })), capsHint && _jsx(Text, { color: "yellow", children: capsHint })] }), (rl.lastUpdated > 0 || usage) && (_jsxs(Box, { paddingLeft: 4, children: [_jsx(UtilBar, { label: "5h", util: globalCapacity.fiveHour.utilization, resetTs: globalCapacity.fiveHour.resetAt, isActive: rl.claim === "five_hour", cap: s5 }), _jsx(Text, { children: " " }), _jsx(UtilBar, { label: "7d all-model", util: globalCapacity.sevenDay.utilization, resetTs: globalCapacity.sevenDay.resetAt, isActive: rl.claim === "seven_day", cap: w7 }), usage && _jsx(Text, { color: globalCapacity.usageFetchStatus === "fresh" ? "gray" : "yellow", children: ` usage ${globalCapacity.usageFetchStatus} ${usage.fetchedAt > 0 ? formatAgo(usage.fetchedAt) : ""}` })] })), capacityRows.map((row, index) => (_jsxs(Box, { paddingLeft: 4, children: [_jsxs(Text, { color: row.color, children: [" ", row.label] }), _jsxs(Text, { color: "gray", children: [" \u00B7 ", row.state] }), row.utilization !== undefined && _jsx(Text, { color: row.color, children: ` ${Math.round(row.utilization * 100)}%` }), row.resetAt !== undefined && row.resetAt > 0 && (_jsxs(Text, { color: "gray", children: [" ", `↻${formatResetIn(row.resetAt)}`] }))] }, `${row.label}-${index}`)))] }));
479
563
  }
480
564
  // ─── Utilization bar ─────────────────────────────────────────────────────────
481
565
  function UtilBar({ label, util, resetTs, isActive, cap }) {
@@ -507,8 +591,9 @@ function LogRow({ log, selected }) {
507
591
  const time = new Date(log.ts).toLocaleTimeString("en-GB", { hour12: false });
508
592
  const isError = log.type === "error";
509
593
  const isRefresh = log.type === "refresh";
510
- const typeColor = isError ? "red" : isRefresh ? "yellow" : "gray";
511
- const typeIcon = isError ? "" : isRefresh ? "" : "";
594
+ const isWarn = log.type === "warn";
595
+ const typeColor = isError ? "red" : isWarn ? "yellow" : isRefresh ? "yellow" : "gray";
596
+ const typeIcon = isError ? "✗" : isWarn ? "⚠" : isRefresh ? "↻" : "→";
512
597
  const statusColor = log.statusCode === undefined ? undefined
513
598
  : log.statusCode >= 500 ? "red"
514
599
  : log.statusCode >= 400 ? "yellow"
@@ -543,6 +628,7 @@ function DetailPanel({ log }) {
543
628
  hour: "2-digit", minute: "2-digit", second: "2-digit",
544
629
  });
545
630
  const isError = log.type === "error";
631
+ const isWarn = log.type === "warn";
546
632
  const statusLabel = log.statusCode === undefined ? "—"
547
633
  : log.statusCode === 0 ? "connection error"
548
634
  : `${log.statusCode} ${httpStatusText(log.statusCode)}`;
@@ -551,7 +637,7 @@ function DetailPanel({ log }) {
551
637
  : log.statusCode >= 500 ? "red"
552
638
  : log.statusCode >= 400 ? "yellow"
553
639
  : "green";
554
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : "cyan", children: " DETAILS " }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Time", value: time }), _jsx(Field, { label: "Account", value: log.accountId })] }), _jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Method", value: log.method ?? "—" }), _jsx(Field, { label: "Path", value: log.path ?? "—" })] }), _jsxs(Box, { gap: 2, children: [_jsx(FieldColored, { label: "Status", value: statusLabel, color: statusColor }), _jsx(Field, { label: "Duration", value: log.durationMs !== undefined ? `${log.durationMs}ms` : "—" }), _jsx(Field, { label: "Type", value: log.type }), _jsx(Field, { label: "Source", value: sourceFullLabel(log.source) })] }), log.details && (_jsx(Box, { children: _jsx(Field, { label: "Details", value: log.details }) })), log.cacheReadTokens !== undefined && (_jsx(Box, { gap: 2, children: _jsx(CacheBreakdown, { read: log.cacheReadTokens, created: log.cacheCreationTokens ?? 0, input: log.inputTokens ?? 0, output: log.outputTokens ?? 0 }) }))] })] }));
640
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, color: isError ? "red" : isWarn ? "yellow" : "cyan", children: " DETAILS " }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 0, children: [_jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Time", value: time }), _jsx(Field, { label: "Account", value: log.accountId })] }), _jsxs(Box, { gap: 2, children: [_jsx(Field, { label: "Method", value: log.method ?? "—" }), _jsx(Field, { label: "Path", value: log.path ?? "—" })] }), _jsxs(Box, { gap: 2, children: [_jsx(FieldColored, { label: "Status", value: statusLabel, color: statusColor }), _jsx(Field, { label: "Duration", value: log.durationMs !== undefined ? `${log.durationMs}ms` : "—" }), _jsx(Field, { label: "Type", value: log.type }), _jsx(Field, { label: "Source", value: sourceFullLabel(log.source) })] }), log.details && (_jsx(Box, { children: _jsx(Field, { label: "Details", value: log.details }) })), log.cacheReadTokens !== undefined && (_jsx(Box, { gap: 2, children: _jsx(CacheBreakdown, { read: log.cacheReadTokens, created: log.cacheCreationTokens ?? 0, input: log.inputTokens ?? 0, output: log.outputTokens ?? 0 }) }))] })] }));
555
641
  }
556
642
  function Field({ label, value }) {
557
643
  return (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [label, ": "] }), _jsx(Text, { color: "white", children: value })] }));
@@ -2,28 +2,22 @@
2
2
  * Tiny authenticated HTTP client for /cc-router/accounts.
3
3
  *
4
4
  * Used by the Ink dashboard to mutate account settings (enable/disable,
5
- * set per-account caps, delete) without exiting the TUI. The `addAccount`
6
- * flow is NOT in here that runs inquirer and must exit Ink first; see
7
- * src/cli/cmd-status.ts `runAddAccountFlow`.
5
+ * set per-account caps, delete) without exiting Ink first. The `addAccount`
6
+ * flow is not here because it runs inquirer.
8
7
  */
9
8
  const REQUEST_TIMEOUT_MS = 3_000;
9
+ const MAX_PUBLIC_ROWS = 12;
10
10
  export function createAccountsApi(baseUrl, authToken) {
11
11
  const base = baseUrl.replace(/\/+$/, "") + "/cc-router/accounts";
12
- const authHeaders = authToken
13
- ? { authorization: `Bearer ${authToken}` }
14
- : {};
12
+ const authHeaders = authToken ? { authorization: `Bearer ${authToken}` } : {};
15
13
  async function send(method, path, body) {
16
14
  const res = await fetch(base + path, {
17
15
  method,
18
- headers: {
19
- ...authHeaders,
20
- ...(body !== undefined ? { "content-type": "application/json" } : {}),
21
- },
16
+ headers: { ...authHeaders, ...(body !== undefined ? { "content-type": "application/json" } : {}) },
22
17
  body: body !== undefined ? JSON.stringify(body) : undefined,
23
18
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
24
19
  });
25
20
  if (!res.ok) {
26
- // Try to surface the server's error message if we can read one
27
21
  let detail = "";
28
22
  try {
29
23
  const data = await res.json();
@@ -34,15 +28,137 @@ export function createAccountsApi(baseUrl, authToken) {
34
28
  throw new Error(`HTTP ${res.status}${detail}`);
35
29
  }
36
30
  }
31
+ async function list() {
32
+ const res = await fetch(base, { method: "GET", headers: authHeaders, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
33
+ if (!res.ok)
34
+ throw new Error(`HTTP ${res.status}`);
35
+ const payload = await res.json();
36
+ return Array.isArray(payload.accounts) ? payload.accounts.flatMap(publicAccountSafeView) : [];
37
+ }
38
+ return {
39
+ list,
40
+ patch(id, patch) { return send("PATCH", `/${encodeURIComponent(id)}`, patch); },
41
+ setProviderEnabled(provider, enabled) { return send("PATCH", `/providers/${encodeURIComponent(provider)}`, { enabled }); },
42
+ remove(id) { return send("DELETE", `/${encodeURIComponent(id)}`); },
43
+ };
44
+ }
45
+ function isRecord(value) {
46
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
47
+ }
48
+ function publicAccountSafeView(value) {
49
+ if (!isRecord(value) || typeof value.id !== "string")
50
+ return [];
51
+ const provider = value.provider === "anthropic_subscription" || value.provider === "openai_subscription"
52
+ ? value.provider
53
+ : undefined;
54
+ const rateLimits = publicRateLimits(value.rateLimits);
55
+ const modelCooldowns = publicCooldowns(value.modelCooldowns);
56
+ return [{
57
+ id: publicText(value.id, 128, "unknown-account"),
58
+ ...(provider ? { provider } : {}),
59
+ ...(rateLimits ? { rateLimits } : {}),
60
+ globalCooldownUntilMs: publicTimestamp(value.globalCooldownUntilMs),
61
+ modelCooldowns,
62
+ }];
63
+ }
64
+ function publicRateLimits(value) {
65
+ if (!isRecord(value))
66
+ return undefined;
67
+ const usage = publicUsage(value.usage);
37
68
  return {
38
- patch(id, patch) {
39
- return send("PATCH", `/${encodeURIComponent(id)}`, patch);
40
- },
41
- setProviderEnabled(provider, enabled) {
42
- return send("PATCH", `/providers/${encodeURIComponent(provider)}`, { enabled });
43
- },
44
- remove(id) {
45
- return send("DELETE", `/${encodeURIComponent(id)}`);
46
- },
69
+ status: value.status === "allowed" || value.status === "rate_limited" ? value.status : "unknown",
70
+ fiveHourUtil: publicUtilization(value.fiveHourUtil),
71
+ fiveHourReset: publicTimestamp(value.fiveHourReset),
72
+ sevenDayUtil: publicUtilization(value.sevenDayUtil),
73
+ sevenDayReset: publicTimestamp(value.sevenDayReset),
74
+ claim: publicClaim(value.claim),
75
+ plan: value.plan === "Pro" || value.plan === "Max 5x" || value.plan === "Max 20x" ? value.plan : "",
76
+ requestsLimit: publicInteger(value.requestsLimit),
77
+ lastUpdated: publicTimestamp(value.lastUpdated),
78
+ ...(usage ? { usage } : {}),
47
79
  };
48
80
  }
81
+ function publicUsage(value) {
82
+ if (!isRecord(value))
83
+ return undefined;
84
+ const fiveHour = publicWindow(value.fiveHour);
85
+ const sevenDay = publicWindow(value.sevenDay);
86
+ const extraUsage = isRecord(value.extraUsage)
87
+ ? {
88
+ enabled: value.extraUsage.enabled === true,
89
+ spendLimitReached: value.extraUsage.spendLimitReached === true,
90
+ usable: value.extraUsage.usable === true,
91
+ }
92
+ : undefined;
93
+ return {
94
+ ...(fiveHour ? { fiveHour } : {}),
95
+ ...(sevenDay ? { sevenDay } : {}),
96
+ modelLimits: (Array.isArray(value.modelLimits) ? value.modelLimits : [])
97
+ .flatMap(publicModelLimit)
98
+ .slice(0, MAX_PUBLIC_ROWS),
99
+ ...(extraUsage ? { extraUsage } : {}),
100
+ fetchedAt: publicTimestamp(value.fetchedAt),
101
+ fetchStatus: publicFetchStatus(value.fetchStatus),
102
+ };
103
+ }
104
+ function publicWindow(value) {
105
+ if (!isRecord(value))
106
+ return undefined;
107
+ return { utilization: publicUtilization(value.utilization), resetAt: publicTimestamp(value.resetAt) };
108
+ }
109
+ function publicModelLimit(value) {
110
+ if (!isRecord(value))
111
+ return [];
112
+ return [{
113
+ modelFamily: publicModelFamily(value.modelFamily),
114
+ displayName: publicText(value.displayName, 80, "Unknown model"),
115
+ utilization: publicUtilization(value.utilization),
116
+ resetAt: publicTimestamp(value.resetAt),
117
+ active: value.active === true,
118
+ severity: publicSeverity(value.severity),
119
+ }];
120
+ }
121
+ function publicCooldowns(value) {
122
+ if (!Array.isArray(value))
123
+ return [];
124
+ return value.flatMap(cooldown => {
125
+ if (!isRecord(cooldown))
126
+ return [];
127
+ const untilMs = publicTimestamp(cooldown.untilMs);
128
+ return untilMs > 0 ? [{ modelFamily: publicModelFamily(cooldown.modelFamily), untilMs }] : [];
129
+ }).slice(0, MAX_PUBLIC_ROWS);
130
+ }
131
+ function publicUtilization(value) {
132
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
133
+ }
134
+ function publicTimestamp(value) {
135
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
136
+ }
137
+ function publicInteger(value) {
138
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
139
+ }
140
+ function publicText(value, maxLength, fallback) {
141
+ if (typeof value !== "string")
142
+ return fallback;
143
+ const normalized = value.replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, maxLength);
144
+ return normalized || fallback;
145
+ }
146
+ function publicModelFamily(value) {
147
+ return typeof value === "string" && /^[a-z0-9-]{1,64}$/.test(value) ? value : "unknown";
148
+ }
149
+ function publicSeverity(value) {
150
+ return value === "warning" || value === "critical" ? value : value ? "unknown" : "";
151
+ }
152
+ function publicFetchStatus(value) {
153
+ return value === "fresh" || value === "stale" || value === "unavailable" ? value : "unavailable";
154
+ }
155
+ function publicClaim(value) {
156
+ if (typeof value !== "string")
157
+ return "unknown";
158
+ const claim = value.trim().toLowerCase();
159
+ if (!claim)
160
+ return "";
161
+ if (claim === "five_hour" || claim === "seven_day" || claim === "seven_day_oauth_apps" || claim === "seven_day_overage_included")
162
+ return claim;
163
+ return claim.startsWith("seven_day_") ? "seven_day_model" : "unknown";
164
+ }
package/package.json CHANGED
@@ -1,19 +1,11 @@
1
1
  {
2
2
  "name": "@timo972/cc-router",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Cache-aware session router for Claude Max OAuth tokens — use multiple Claude Max accounts with Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cc-router": "dist/cli/index.js"
8
8
  },
9
- "scripts": {
10
- "build": "tsc",
11
- "dev": "tsx src/cli/index.ts",
12
- "start": "node dist/cli/index.js",
13
- "test": "vitest run",
14
- "test:watch": "vitest",
15
- "lint": "tsc --noEmit"
16
- },
17
9
  "keywords": [
18
10
  "claude",
19
11
  "anthropic",
@@ -57,6 +49,7 @@
57
49
  },
58
50
  "devDependencies": {
59
51
  "@types/express": "^4.17.21",
52
+ "@types/express-serve-static-core": "^4.17.33",
60
53
  "@types/node": "^20.0.0",
61
54
  "@types/react": "^18.3.0",
62
55
  "tsx": "^4.19.0",
@@ -64,6 +57,14 @@
64
57
  "vitest": "^4.1.2"
65
58
  },
66
59
  "engines": {
67
- "node": ">=20.0.0"
60
+ "node": ">=22.0.0"
61
+ },
62
+ "scripts": {
63
+ "build": "tsc",
64
+ "dev": "tsx src/cli/index.ts",
65
+ "start": "node dist/cli/index.js",
66
+ "test": "vitest run",
67
+ "test:watch": "vitest",
68
+ "lint": "tsc --noEmit"
68
69
  }
69
- }
70
+ }