agent-dealer 1.2.2 → 1.2.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.
- package/bundle/server/dist/capacity/claude-events.js +27 -10
- package/bundle/server/dist/capacity/claude-local-cache.js +788 -0
- package/bundle/server/dist/capacity/claude-local-cache.test.js +520 -0
- package/bundle/server/dist/capacity/cursor-individual-credentials.js +203 -16
- package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +186 -2
- package/bundle/server/dist/capacity/cursor-individual.js +69 -2
- package/bundle/server/dist/capacity/cursor-individual.test.js +268 -2
- package/bundle/server/dist/capacity/muse-host.js +748 -0
- package/bundle/server/dist/capacity/muse-host.test.js +480 -0
- package/bundle/server/dist/capacity/muse-lifecycle.test.js +97 -0
- package/bundle/server/dist/capacity/muse.js +307 -60
- package/bundle/server/dist/capacity/muse.test.js +8 -51
- package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
- package/bundle/server/dist/index.js +15 -2
- package/bundle/server/dist/routes/index.js +26 -9
- package/bundle/server/dist/routes/runtime-capacity.test.js +12 -6
- package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
- package/bundle/server/dist/runners/muse-serve-session.js +313 -0
- package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
- package/bundle/server/static-ui/assets/{index-DUYzboWh.css → index-CYqRh_-S.css} +1 -1
- package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
- package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.d.ts +57 -0
- package/dist/doctor.js +206 -1
- package/dist/doctor.test.d.ts +1 -0
- package/dist/doctor.test.js +246 -0
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-0GDYfbaM.js +0 -60
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<svg viewBox="0 0 15.184 10.109" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
2
|
+
<!-- Exact Meta infinity-mark paths extracted from Meta's first-party wordmark SVG. -->
|
|
3
|
+
<path d="M4.40165 0.0546875C4.39678 0.0546875 4.39194 0.0547047 4.38708 0.0547298L4.36719 1.69809C4.37174 1.69804 4.37629 1.698 4.38085 1.698H4.38086C5.46253 1.698 6.30154 2.55086 8.12513 5.62254L8.2363 5.80954L8.24357 5.82178L9.2644 4.29012L9.25733 4.27829C9.01709 3.88756 8.78629 3.52806 8.56494 3.1998C8.30824 2.81945 8.06274 2.47967 7.8245 2.17631C6.61813 0.640297 5.60899 0.0546875 4.40165 0.0546875Z" fill="url(#meta-0)"/>
|
|
4
|
+
<path d="M4.38698 0.0547298C3.17378 0.0609714 2.10123 0.845474 1.32675 2.04593C1.32448 2.04944 1.32221 2.05296 1.31995 2.05649L2.74159 2.83027C2.7439 2.82677 2.74621 2.82327 2.74853 2.81978C3.20078 2.13886 3.76359 1.70456 4.36709 1.69809C4.37164 1.69804 4.37619 1.698 4.38075 1.698L4.40155 0.0546875C4.39668 0.0546875 4.39184 0.0547047 4.38698 0.0547298Z" fill="url(#meta-1)"/>
|
|
5
|
+
<path d="M1.32666 2.04688C1.3244 2.05039 1.32213 2.05391 1.31986 2.05744C0.810944 2.84965 0.431562 3.8218 0.226532 4.87056C0.225644 4.8751 0.224759 4.87964 0.223877 4.88419L1.82103 5.26104C1.82187 5.25649 1.82271 5.25193 1.82355 5.24738C1.99427 4.32569 2.31929 3.47086 2.7415 2.83122C2.7438 2.82772 2.74612 2.82422 2.74844 2.82073L1.32666 2.04688Z" fill="url(#meta-2)"/>
|
|
6
|
+
<path d="M1.82364 5.24791L0.226615 4.87109C0.225727 4.87563 0.224842 4.88018 0.22396 4.88472C0.11216 5.4615 0.0552821 6.04757 0.0540887 6.63508C0.0540793 6.63983 0.0540757 6.64458 0.0540777 6.64932L1.69203 6.79586C1.6919 6.79109 1.69176 6.78638 1.69165 6.7816C1.69084 6.74752 1.69043 6.71299 1.69042 6.67799C1.69124 6.20287 1.73498 5.72881 1.82112 5.26157C1.82195 5.25702 1.82279 5.25247 1.82364 5.24791Z" fill="url(#meta-3)"/>
|
|
7
|
+
<path d="M1.74184 7.30462C1.71246 7.13577 1.69581 6.96494 1.69203 6.79359C1.6919 6.78883 1.69176 6.78412 1.69165 6.77933L0.0540887 6.63281C0.0540793 6.63756 0.0540757 6.64231 0.0540777 6.64706V6.64798C0.0540777 7.01363 0.0845699 7.35598 0.143831 7.67225C0.144688 7.67682 0.145531 7.68131 0.1464 7.68588L1.74432 7.3183C1.74348 7.31376 1.74266 7.30919 1.74184 7.30462Z" fill="url(#meta-4)"/>
|
|
8
|
+
<path d="M2.11498 8.1544C1.93675 7.95985 1.81061 7.67922 1.74429 7.32032C1.74345 7.31578 1.74264 7.31121 1.74182 7.30664L0.143799 7.67426C0.144655 7.67884 0.145498 7.68333 0.146368 7.6879C0.267152 8.32205 0.504031 8.85057 0.843261 9.2504C0.846277 9.25395 0.849304 9.2575 0.852342 9.26105L2.12451 8.16464C2.12133 8.16125 2.11813 8.15783 2.11498 8.1544Z" fill="url(#meta-5)"/>
|
|
9
|
+
<path d="M6.84924 3.60742C5.88628 5.08446 5.30292 6.01088 5.30292 6.01088C4.02016 8.02169 3.57639 8.47237 2.86221 8.47237C2.5642 8.47237 2.31524 8.36626 2.12451 8.16343C2.12133 8.16005 2.11814 8.15662 2.11499 8.15319L0.843262 9.24919C0.846278 9.25274 0.849305 9.25629 0.852343 9.25984C1.32074 9.80583 1.98189 10.1088 2.79981 10.1088C4.03726 10.1088 4.92725 9.52537 6.5094 6.75973C6.5094 6.75973 7.16894 5.59504 7.62266 4.79276C7.34161 4.33898 7.08584 3.94614 6.84924 3.60742Z" fill="#0082FB"/>
|
|
10
|
+
<path d="M8.56569 1.27734C8.56244 1.28079 8.55913 1.28431 8.55589 1.28777C8.30301 1.55719 8.06032 1.85807 7.82422 2.1773C8.06247 2.48067 8.30842 2.82111 8.56513 3.20145C8.86769 2.73447 9.15011 2.35626 9.42691 2.06637C9.4302 2.06294 9.43341 2.0596 9.43669 2.05618L8.56569 1.27734Z" fill="url(#meta-6)"/>
|
|
11
|
+
<path d="M13.2408 1.13125C12.5692 0.452783 11.7685 0.0546875 10.9125 0.0546875C10.0098 0.0546875 9.25054 0.549336 8.56571 1.27604C8.56246 1.27948 8.55915 1.283 8.55591 1.28646L9.42694 2.06506C9.43022 2.06163 9.43343 2.05829 9.43672 2.05488C9.88786 1.58561 10.3245 1.35131 10.8085 1.35131H10.8084C11.3294 1.35131 11.8171 1.59654 12.2394 2.02645C12.2427 2.02982 12.246 2.03316 12.2493 2.03655L13.2508 1.14135C13.2475 1.13796 13.2441 1.13462 13.2408 1.13125Z" fill="#0082FB"/>
|
|
12
|
+
<path d="M15.1822 6.41693C15.1446 4.23851 14.3823 2.29103 13.2508 1.14096C13.2474 1.13757 13.2441 1.13423 13.2407 1.13086L12.2394 2.02606C12.2427 2.02943 12.246 2.03277 12.2493 2.03616C13.1005 2.91055 13.6844 4.53694 13.7375 6.41646C13.7376 6.42119 13.7377 6.42592 13.7379 6.43065L15.1824 6.43111C15.1824 6.42639 15.1823 6.42166 15.1822 6.41693Z" fill="url(#meta-7)"/>
|
|
13
|
+
<path d="M15.1824 6.43263C15.1824 6.4279 15.1823 6.42317 15.1822 6.41843L13.7375 6.41797C13.7376 6.4227 13.7377 6.42743 13.7379 6.43216C13.7402 6.52038 13.7414 6.60913 13.7414 6.69841C13.7414 7.21086 13.6649 7.6251 13.5092 7.92417C13.5069 7.92861 13.5045 7.93306 13.5022 7.93744L14.5793 9.05771C14.582 9.05364 14.5846 9.0496 14.5873 9.04551C14.9783 8.44213 15.1836 7.60392 15.1836 6.58747C15.1836 6.53573 15.1832 6.48411 15.1824 6.43263Z" fill="url(#meta-8)"/>
|
|
14
|
+
<path d="M13.5093 7.92188C13.507 7.92631 13.5046 7.93076 13.5023 7.93515C13.3676 8.18724 13.1753 8.35538 12.924 8.4289L13.415 9.97644C13.48 9.95436 13.5433 9.92957 13.6049 9.90207C13.6233 9.89388 13.6416 9.88545 13.6597 9.87678C13.6701 9.87178 13.6805 9.86671 13.6908 9.86157C14.0155 9.69974 14.2899 9.46106 14.5118 9.15354C14.5254 9.13476 14.5388 9.11582 14.5519 9.09654C14.5612 9.08295 14.5704 9.06925 14.5794 9.05541C14.5821 9.05135 14.5847 9.0473 14.5874 9.04322L13.5093 7.92188Z" fill="url(#meta-9)"/>
|
|
15
|
+
<path d="M12.6111 8.47085C12.4465 8.47085 12.3012 8.44634 12.159 8.38281L11.6562 9.96718C11.9388 10.0637 12.2403 10.1072 12.5765 10.1072C12.8862 10.1072 13.1705 10.0609 13.4283 9.97153L12.9374 8.42452C12.8316 8.45623 12.7216 8.47184 12.6111 8.47085Z" fill="url(#meta-10)"/>
|
|
16
|
+
<path d="M11.6051 7.93071C11.602 7.92714 11.5989 7.92355 11.5957 7.91992L10.4388 9.12313C10.4421 9.12659 10.4453 9.13004 10.4485 9.13348C10.8505 9.56188 11.2344 9.82762 11.6697 9.97347L12.1721 8.39027C11.9887 8.31148 11.8112 8.16864 11.6051 7.93071Z" fill="url(#meta-11)"/>
|
|
17
|
+
<path d="M11.5958 7.91873C11.2488 7.51518 10.8195 6.84328 10.1443 5.75677L9.26431 4.28917L9.25724 4.27734L8.23621 5.80859L8.24348 5.82083L8.86699 6.86972C9.47138 7.88111 9.96376 8.61279 10.4389 9.12193C10.4422 9.1254 10.4454 9.12885 10.4486 9.13229L11.6051 7.92952C11.602 7.92596 11.5989 7.92236 11.5958 7.91873Z" fill="url(#meta-12)"/>
|
|
18
|
+
<defs>
|
|
19
|
+
<linearGradient id="meta-0" x1="8.58586" y1="5.1989" x2="5.19669" y2="0.757905" gradientUnits="userSpaceOnUse"><stop offset="0.0006" stop-color="#0867DF"/><stop offset="0.4539" stop-color="#0668E1"/><stop offset="0.8591" stop-color="#0064E0"/></linearGradient>
|
|
20
|
+
<linearGradient id="meta-1" x1="1.98772" y1="2.32206" x2="4.31121" y2="0.558086" gradientUnits="userSpaceOnUse"><stop offset="0.1323" stop-color="#0064DF"/><stop offset="0.9988" stop-color="#0064E0"/></linearGradient>
|
|
21
|
+
<linearGradient id="meta-2" x1="1.0028" y1="4.91157" x2="1.93484" y2="2.56535" gradientUnits="userSpaceOnUse"><stop offset="0.0147" stop-color="#0072EC"/><stop offset="0.6881" stop-color="#0064DF"/></linearGradient>
|
|
22
|
+
<linearGradient id="meta-3" x1="0.876315" y1="6.60704" x2="0.984158" y2="5.17414" gradientUnits="userSpaceOnUse"><stop offset="0.0731" stop-color="#007CF6"/><stop offset="0.9943" stop-color="#0072EC"/></linearGradient>
|
|
23
|
+
<linearGradient id="meta-4" x1="0.935623" y1="7.38311" x2="0.858481" y2="6.80902" gradientUnits="userSpaceOnUse"><stop offset="0.0731" stop-color="#007FF9"/><stop offset="1" stop-color="#007CF6"/></linearGradient>
|
|
24
|
+
<linearGradient id="meta-5" x1="0.89031" y1="7.53593" x2="1.37106" y2="8.55559" gradientUnits="userSpaceOnUse"><stop offset="0.0731" stop-color="#007FF9"/><stop offset="1" stop-color="#0082FB"/></linearGradient>
|
|
25
|
+
<linearGradient id="meta-6" x1="8.27941" y1="2.60226" x2="8.91497" y2="1.72324" gradientUnits="userSpaceOnUse"><stop offset="0.2799" stop-color="#007FF8"/><stop offset="0.9141" stop-color="#0082FB"/></linearGradient>
|
|
26
|
+
<linearGradient id="meta-7" x1="13.1116" y1="1.46132" x2="14.4412" y2="6.35237" gradientUnits="userSpaceOnUse"><stop stop-color="#0082FB"/><stop offset="0.9995" stop-color="#0081FA"/></linearGradient>
|
|
27
|
+
<linearGradient id="meta-8" x1="14.7623" y1="6.541" x2="13.9208" y2="8.24143" gradientUnits="userSpaceOnUse"><stop offset="0.0619" stop-color="#0081FA"/><stop offset="1" stop-color="#0080F9"/></linearGradient>
|
|
28
|
+
<linearGradient id="meta-9" x1="13.2521" y1="9.14064" x2="14.0387" y2="8.6049" gradientUnits="userSpaceOnUse"><stop stop-color="#027AF3"/><stop offset="1" stop-color="#0080F9"/></linearGradient>
|
|
29
|
+
<linearGradient id="meta-10" x1="12.0183" y1="9.24502" x2="13.1113" y2="9.24502" gradientUnits="userSpaceOnUse"><stop stop-color="#0377EF"/><stop offset="0.9994" stop-color="#0279F1"/></linearGradient>
|
|
30
|
+
<linearGradient id="meta-11" x1="11.068" y1="8.64479" x2="11.8538" y2="9.1071" gradientUnits="userSpaceOnUse"><stop offset="0.0019" stop-color="#0471E9"/><stop offset="1" stop-color="#0377EF"/></linearGradient>
|
|
31
|
+
<linearGradient id="meta-12" x1="8.67106" y1="5.23467" x2="11.1883" y2="8.3996" gradientUnits="userSpaceOnUse"><stop offset="0.2765" stop-color="#0867DF"/><stop offset="1" stop-color="#0471E9"/></linearGradient>
|
|
32
|
+
</defs>
|
|
33
|
+
</svg>
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
9
9
|
<meta name="theme-color" content="#0a0e14" />
|
|
10
10
|
<link rel="preload" href="/fonts/Monaco.ttf" as="font" type="font/ttf" crossorigin />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-UO4lHZw4.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CYqRh_-S.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1 +1,58 @@
|
|
|
1
1
|
export declare function runDoctor(): Promise<number>;
|
|
2
|
+
/** Which local login the Individual adapter would use, if any. */
|
|
3
|
+
export type CursorIndividualLoginKind = "desktop" | "agent" | "none";
|
|
4
|
+
export interface CursorIndividualLoginReport {
|
|
5
|
+
kind: CursorIndividualLoginKind;
|
|
6
|
+
/** Static operator-facing line — no paths, tokens, or user ids, ever. */
|
|
7
|
+
line: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const CURSOR_INDIVIDUAL_LOGIN_LINES: Record<CursorIndividualLoginKind, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Map a diagnostics-safe credential status to the doctor line. Pure (no
|
|
12
|
+
* I/O): unit tests pin all three states plus the degraded inputs here.
|
|
13
|
+
*/
|
|
14
|
+
export declare function describeCursorIndividualLogin(status: {
|
|
15
|
+
present: boolean;
|
|
16
|
+
source: "desktop" | "agent" | null;
|
|
17
|
+
} | null | undefined): CursorIndividualLoginReport;
|
|
18
|
+
/**
|
|
19
|
+
* Probe the local Cursor logins the way the adapter would (desktop first,
|
|
20
|
+
* Agent auth as the fallback) and report the kind. Never throws, never
|
|
21
|
+
* prints secret material — the line is one of three static strings.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkCursorIndividualLogin(): Promise<CursorIndividualLoginReport>;
|
|
24
|
+
/** Env override for the Claude cache file (tests/smoke). */
|
|
25
|
+
export declare const CLAUDE_CAPACITY_CACHE_FILE_ENV = "AGENT_DEALER_CLAUDE_CACHE_FILE";
|
|
26
|
+
/** Paid-fallback opt-in env and its only enabling value. */
|
|
27
|
+
export declare const CLAUDE_CAPACITY_REFRESH_ENV = "AGENT_DEALER_CLAUDE_CAPACITY_REFRESH";
|
|
28
|
+
export declare const CLAUDE_CAPACITY_REFRESH_PAID_VALUE = "paid-after-1h";
|
|
29
|
+
/** Which local 5H/1W cache state the capacity ladder would read. */
|
|
30
|
+
export type ClaudeCapacitySourceKind = "fresh" | "stale" | "missing";
|
|
31
|
+
export interface ClaudeCapacitySourceReport {
|
|
32
|
+
kind: ClaudeCapacitySourceKind;
|
|
33
|
+
/** Static operator-facing line — age labels only, never values or ids. */
|
|
34
|
+
line: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Map a cache-file presence/age probe to the doctor line. Pure (no I/O):
|
|
38
|
+
* unit tests pin all three states here.
|
|
39
|
+
*/
|
|
40
|
+
export declare function describeClaudeCapacitySource(status: {
|
|
41
|
+
present: boolean;
|
|
42
|
+
ageMs: number | null;
|
|
43
|
+
} | null | undefined): ClaudeCapacitySourceReport;
|
|
44
|
+
/**
|
|
45
|
+
* Map the paid-fallback setting to an opt-in warning line, or null when the
|
|
46
|
+
* probe is disabled (the quiet default — no spend possible, nothing to say).
|
|
47
|
+
* Pure (no I/O): unit tests pin both states here.
|
|
48
|
+
*/
|
|
49
|
+
export declare function describeClaudeProbeOptIn(setting: string | undefined): string | null;
|
|
50
|
+
/**
|
|
51
|
+
* Read the `cachedUsageUtilization.fetchedAtMs` timestamp out of Claude
|
|
52
|
+
* Code's config file (override first, then `~/.claude.json`) — the same key
|
|
53
|
+
* the server adapter normalizes, so doctor and the server always agree on
|
|
54
|
+
* freshness. File mtime is meaningless here: unrelated config writes touch
|
|
55
|
+
* it. Only the timestamp is read (never utilization values, ids, or
|
|
56
|
+
* siblings) and only an age label is reported. Never throws.
|
|
57
|
+
*/
|
|
58
|
+
export declare function checkClaudeCapacitySource(nowMs?: number): Promise<ClaudeCapacitySourceReport>;
|
package/dist/doctor.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
6
|
import { cursorAuthIssueFromOutput } from "@agent-dealer/shared";
|
|
5
7
|
import { claudeAvailable, cursorAvailable } from "./cli-check.js";
|
|
6
8
|
import { loadProdEnvFile, prodEnvFilePath, prodHomeDir, resolveBundledListenPort, shortenHome, } from "./env.js";
|
|
@@ -105,11 +107,38 @@ export async function runDoctor() {
|
|
|
105
107
|
else {
|
|
106
108
|
console.warn("⚠ LINEAR_API_KEY not set — Linear inbox disabled");
|
|
107
109
|
}
|
|
110
|
+
// NOT-268: Claude account capacity source status (informational only —
|
|
111
|
+
// never fails doctor). Reports whether Claude Code's own local 5H/1W
|
|
112
|
+
// cache exists and how old it is, plus whether the paid fallback probe
|
|
113
|
+
// is armed. Age labels only: never utilization values, prompts, tokens,
|
|
114
|
+
// or account ids.
|
|
115
|
+
try {
|
|
116
|
+
console.log((await checkClaudeCapacitySource()).line);
|
|
117
|
+
const probe = describeClaudeProbeOptIn(process.env.AGENT_DEALER_CLAUDE_CAPACITY_REFRESH);
|
|
118
|
+
if (probe)
|
|
119
|
+
console.warn(probe);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Informational only: a broken probe must never fail doctor.
|
|
123
|
+
}
|
|
108
124
|
// NOT-250: experimental Cursor Individual dashboard adapter status
|
|
109
|
-
// (informational only — never fails doctor
|
|
125
|
+
// (informational only — never fails doctor). NOT-267: when enabled,
|
|
126
|
+
// also report which local login the adapter would use — desktop first,
|
|
127
|
+
// Agent auth as the fallback. The probe only runs under the explicit
|
|
128
|
+
// opt-in, and prints kind labels only: never paths, tokens, or user ids.
|
|
110
129
|
if (process.env.AGENT_DEALER_CURSOR_INDIVIDUAL_CAPACITY === "experimental") {
|
|
111
130
|
console.warn("⚠ Cursor Individual capacity: EXPERIMENTAL dashboard adapter enabled " +
|
|
112
131
|
"(undocumented API, no support guarantee — unset AGENT_DEALER_CURSOR_INDIVIDUAL_CAPACITY to disable)");
|
|
132
|
+
try {
|
|
133
|
+
const login = await checkCursorIndividualLogin();
|
|
134
|
+
if (login.kind === "none")
|
|
135
|
+
console.warn(login.line);
|
|
136
|
+
else
|
|
137
|
+
console.log(login.line);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Informational only: a broken probe must never fail doctor.
|
|
141
|
+
}
|
|
113
142
|
}
|
|
114
143
|
}
|
|
115
144
|
else {
|
|
@@ -151,6 +180,182 @@ export async function runDoctor() {
|
|
|
151
180
|
}
|
|
152
181
|
return failed ? 1 : 0;
|
|
153
182
|
}
|
|
183
|
+
export const CURSOR_INDIVIDUAL_LOGIN_LINES = {
|
|
184
|
+
desktop: "✓ Cursor Individual capacity: desktop login found (experimental dashboard adapter can use it)",
|
|
185
|
+
agent: "✓ Cursor Individual capacity: Agent login found (fallback)",
|
|
186
|
+
none: "⚠ Cursor Individual capacity: no usable local login — log in to Cursor desktop or the Cursor Agent CLI",
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Map a diagnostics-safe credential status to the doctor line. Pure (no
|
|
190
|
+
* I/O): unit tests pin all three states plus the degraded inputs here.
|
|
191
|
+
*/
|
|
192
|
+
export function describeCursorIndividualLogin(status) {
|
|
193
|
+
if (status?.present === true && status.source === "desktop") {
|
|
194
|
+
return { kind: "desktop", line: CURSOR_INDIVIDUAL_LOGIN_LINES.desktop };
|
|
195
|
+
}
|
|
196
|
+
if (status?.present === true && status.source === "agent") {
|
|
197
|
+
return { kind: "agent", line: CURSOR_INDIVIDUAL_LOGIN_LINES.agent };
|
|
198
|
+
}
|
|
199
|
+
return { kind: "none", line: CURSOR_INDIVIDUAL_LOGIN_LINES.none };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Load the server's credential-status reader without depending on the
|
|
203
|
+
* server's daemon entrypoint (which has side effects on import). The
|
|
204
|
+
* checkout layout (TypeScript source, run under tsx) is tried first, then
|
|
205
|
+
* the installed layout (built dist) — whichever answers wins. Returns null
|
|
206
|
+
* when neither is loadable; the caller then reports "no usable local
|
|
207
|
+
* login", never a crash. The loaded reader itself returns presence/source
|
|
208
|
+
* only — secret material never crosses this boundary by construction.
|
|
209
|
+
*/
|
|
210
|
+
async function loadCredentialStatusReader() {
|
|
211
|
+
const roots = [];
|
|
212
|
+
try {
|
|
213
|
+
const { resolveServerRoot } = await import("./paths.js");
|
|
214
|
+
roots.push(resolveServerRoot());
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// The installed package entry (dist) may not be built in a checkout —
|
|
218
|
+
// the workspace fallback below still locates the source.
|
|
219
|
+
}
|
|
220
|
+
// Workspace-checkout fallback: packages/cli/src → packages/server.
|
|
221
|
+
// Absent in installed layouts (no src shipped), where existsSync skips it.
|
|
222
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
223
|
+
roots.push(path.resolve(here, "..", "..", "server"));
|
|
224
|
+
const candidates = roots.flatMap((serverRoot) => [
|
|
225
|
+
path.join(serverRoot, "src", "capacity", "cursor-individual-credentials.ts"),
|
|
226
|
+
path.join(serverRoot, "dist", "capacity", "cursor-individual-credentials.js"),
|
|
227
|
+
]);
|
|
228
|
+
for (const file of candidates) {
|
|
229
|
+
try {
|
|
230
|
+
if (!fs.existsSync(file))
|
|
231
|
+
continue;
|
|
232
|
+
const mod = (await import(pathToFileURL(file).href));
|
|
233
|
+
if (typeof mod.cursorIndividualCredentialStatus === "function") {
|
|
234
|
+
return mod;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Probe the local Cursor logins the way the adapter would (desktop first,
|
|
245
|
+
* Agent auth as the fallback) and report the kind. Never throws, never
|
|
246
|
+
* prints secret material — the line is one of three static strings.
|
|
247
|
+
*/
|
|
248
|
+
export async function checkCursorIndividualLogin() {
|
|
249
|
+
try {
|
|
250
|
+
const reader = await loadCredentialStatusReader();
|
|
251
|
+
if (!reader)
|
|
252
|
+
return describeCursorIndividualLogin(null);
|
|
253
|
+
return describeCursorIndividualLogin(reader.cursorIndividualCredentialStatus());
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return describeCursorIndividualLogin(null);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// NOT-268: Claude capacity source reporting for doctor.
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
/** Env override for the Claude cache file (tests/smoke). */
|
|
263
|
+
export const CLAUDE_CAPACITY_CACHE_FILE_ENV = "AGENT_DEALER_CLAUDE_CACHE_FILE";
|
|
264
|
+
/** Paid-fallback opt-in env and its only enabling value. */
|
|
265
|
+
export const CLAUDE_CAPACITY_REFRESH_ENV = "AGENT_DEALER_CLAUDE_CAPACITY_REFRESH";
|
|
266
|
+
export const CLAUDE_CAPACITY_REFRESH_PAID_VALUE = "paid-after-1h";
|
|
267
|
+
/**
|
|
268
|
+
* Map a cache-file presence/age probe to the doctor line. Pure (no I/O):
|
|
269
|
+
* unit tests pin all three states here.
|
|
270
|
+
*/
|
|
271
|
+
export function describeClaudeCapacitySource(status) {
|
|
272
|
+
if (status?.present === true && typeof status.ageMs === "number" && status.ageMs >= 0) {
|
|
273
|
+
if (status.ageMs < 60 * 60 * 1000) {
|
|
274
|
+
return {
|
|
275
|
+
kind: "fresh",
|
|
276
|
+
line: `✓ Claude capacity: local 5H/1W cache fresh (${formatCacheAge(status.ageMs)} old)`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
kind: "stale",
|
|
281
|
+
line: `⚠ Claude capacity: local 5H/1W cache stale (${formatCacheAge(status.ageMs)} old — refreshes on the next Claude run)`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
kind: "missing",
|
|
286
|
+
line: "⚠ Claude capacity: no local 5H/1W cache yet — run Claude Code once to populate it",
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function formatCacheAge(ageMs) {
|
|
290
|
+
const minutes = Math.floor(ageMs / 60_000);
|
|
291
|
+
if (minutes < 1)
|
|
292
|
+
return "under a minute";
|
|
293
|
+
if (minutes < 60)
|
|
294
|
+
return `${minutes}m`;
|
|
295
|
+
const hours = Math.floor(minutes / 60);
|
|
296
|
+
if (hours < 48)
|
|
297
|
+
return `${hours}h`;
|
|
298
|
+
return `${Math.floor(hours / 24)}d`;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Map the paid-fallback setting to an opt-in warning line, or null when the
|
|
302
|
+
* probe is disabled (the quiet default — no spend possible, nothing to say).
|
|
303
|
+
* Pure (no I/O): unit tests pin both states here.
|
|
304
|
+
*/
|
|
305
|
+
export function describeClaudeProbeOptIn(setting) {
|
|
306
|
+
if (setting === CLAUDE_CAPACITY_REFRESH_PAID_VALUE) {
|
|
307
|
+
return ("⚠ Claude capacity paid fallback: armed (one ≤$0.01 Haiku probe after 1h stale — " +
|
|
308
|
+
`unset ${CLAUDE_CAPACITY_REFRESH_ENV} to disable)`);
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Read the `cachedUsageUtilization.fetchedAtMs` timestamp out of Claude
|
|
314
|
+
* Code's config file (override first, then `~/.claude.json`) — the same key
|
|
315
|
+
* the server adapter normalizes, so doctor and the server always agree on
|
|
316
|
+
* freshness. File mtime is meaningless here: unrelated config writes touch
|
|
317
|
+
* it. Only the timestamp is read (never utilization values, ids, or
|
|
318
|
+
* siblings) and only an age label is reported. Never throws.
|
|
319
|
+
*/
|
|
320
|
+
export async function checkClaudeCapacitySource(nowMs = Date.now()) {
|
|
321
|
+
try {
|
|
322
|
+
const override = process.env[CLAUDE_CAPACITY_CACHE_FILE_ENV]?.trim();
|
|
323
|
+
const file = override || path.join(process.env.HOME ?? os.homedir(), ".claude.json");
|
|
324
|
+
let text;
|
|
325
|
+
try {
|
|
326
|
+
text = fs.readFileSync(file, "utf8");
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return describeClaudeCapacitySource(null);
|
|
330
|
+
}
|
|
331
|
+
let parsed;
|
|
332
|
+
try {
|
|
333
|
+
parsed = JSON.parse(text);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return describeClaudeCapacitySource(null);
|
|
337
|
+
}
|
|
338
|
+
const root = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
339
|
+
? parsed
|
|
340
|
+
: null;
|
|
341
|
+
// A bare cache object (no config envelope — the shape override fixtures
|
|
342
|
+
// use) is accepted as-is; otherwise only the subtree counts.
|
|
343
|
+
const subtreeRaw = root?.cachedUsageUtilization;
|
|
344
|
+
const subtree = subtreeRaw && typeof subtreeRaw === "object" && !Array.isArray(subtreeRaw)
|
|
345
|
+
? subtreeRaw
|
|
346
|
+
: root;
|
|
347
|
+
const fetchedAtMs = subtree && typeof subtree.fetchedAtMs === "number" && Number.isFinite(subtree.fetchedAtMs)
|
|
348
|
+
? subtree.fetchedAtMs
|
|
349
|
+
: null;
|
|
350
|
+
if (fetchedAtMs === null || fetchedAtMs <= 0 || fetchedAtMs > nowMs) {
|
|
351
|
+
return describeClaudeCapacitySource(null);
|
|
352
|
+
}
|
|
353
|
+
return describeClaudeCapacitySource({ present: true, ageMs: nowMs - fetchedAtMs });
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return describeClaudeCapacitySource(null);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
154
359
|
function isPortFree(port) {
|
|
155
360
|
return new Promise((resolve) => {
|
|
156
361
|
const server = net.createServer();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// packages/cli/src/doctor.test.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-267: doctor reports which local login the experimental Cursor
|
|
4
|
+
// Individual adapter would use — desktop, Agent fallback, or none — using
|
|
5
|
+
// static lines only. Tests never touch the real local login: every case
|
|
6
|
+
// points the resolution at temporary fixtures.
|
|
7
|
+
import { test, beforeEach, afterEach } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import Database from "better-sqlite3";
|
|
13
|
+
// Local mirror of the server module's env/key contract
|
|
14
|
+
// (packages/server/src/capacity/cursor-individual-credentials.ts). A static
|
|
15
|
+
// import is impossible — CLI `rootDir` cannot include server sources — so
|
|
16
|
+
// the mirror is pinned against the live server module by the contract test
|
|
17
|
+
// below: a rename there fails that test loudly instead of letting these
|
|
18
|
+
// fixtures silently stop applying (and probe the real local login).
|
|
19
|
+
const CURSOR_DESKTOP_ACCESS_TOKEN_KEY = "cursorAuth/accessToken";
|
|
20
|
+
const CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV = "CURSOR_INDIVIDUAL_CREDENTIAL_FILE";
|
|
21
|
+
const CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV = "CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE";
|
|
22
|
+
const CURSOR_INDIVIDUAL_HOME_ENV = "CURSOR_INDIVIDUAL_HOME";
|
|
23
|
+
test("fixture env/key names match the server module's live contract", async () => {
|
|
24
|
+
const { pathToFileURL, fileURLToPath } = await import("node:url");
|
|
25
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const candidates = [
|
|
27
|
+
path.resolve(here, "..", "..", "server", "src", "capacity", "cursor-individual-credentials.ts"),
|
|
28
|
+
path.resolve(here, "..", "..", "server", "dist", "capacity", "cursor-individual-credentials.js"),
|
|
29
|
+
];
|
|
30
|
+
let mod = null;
|
|
31
|
+
for (const file of candidates) {
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(file))
|
|
34
|
+
continue;
|
|
35
|
+
mod = (await import(pathToFileURL(file).href));
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
assert.ok(mod, "the server credentials module must be loadable for the contract pin");
|
|
43
|
+
assert.equal(mod.CURSOR_DESKTOP_ACCESS_TOKEN_KEY, CURSOR_DESKTOP_ACCESS_TOKEN_KEY);
|
|
44
|
+
assert.equal(mod.CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV, CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV);
|
|
45
|
+
assert.equal(mod.CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV, CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV);
|
|
46
|
+
assert.equal(mod.CURSOR_INDIVIDUAL_HOME_ENV, CURSOR_INDIVIDUAL_HOME_ENV);
|
|
47
|
+
});
|
|
48
|
+
import { CLAUDE_CAPACITY_CACHE_FILE_ENV, CLAUDE_CAPACITY_REFRESH_ENV, CLAUDE_CAPACITY_REFRESH_PAID_VALUE, CURSOR_INDIVIDUAL_LOGIN_LINES, checkClaudeCapacitySource, checkCursorIndividualLogin, describeClaudeCapacitySource, describeClaudeProbeOptIn, describeCursorIndividualLogin, } from "./doctor.js";
|
|
49
|
+
const SECRET = "doctor-fixture-secret-must-never-print";
|
|
50
|
+
const USER_ID = "user_doctor_fixture";
|
|
51
|
+
function fixtureJwt(sub) {
|
|
52
|
+
const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
53
|
+
return `${b64url({ alg: "none" })}.${b64url({ sub })}.sig`;
|
|
54
|
+
}
|
|
55
|
+
let dir;
|
|
56
|
+
const saved = {};
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-doctor-indiv-"));
|
|
59
|
+
for (const key of [
|
|
60
|
+
CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV,
|
|
61
|
+
CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV,
|
|
62
|
+
CURSOR_INDIVIDUAL_HOME_ENV,
|
|
63
|
+
CLAUDE_CAPACITY_CACHE_FILE_ENV,
|
|
64
|
+
]) {
|
|
65
|
+
saved[key] = process.env[key];
|
|
66
|
+
delete process.env[key];
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
for (const [key, value] of Object.entries(saved)) {
|
|
71
|
+
if (value === undefined)
|
|
72
|
+
delete process.env[key];
|
|
73
|
+
else
|
|
74
|
+
process.env[key] = value;
|
|
75
|
+
}
|
|
76
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
77
|
+
});
|
|
78
|
+
test("describe maps each credential state to its static line", () => {
|
|
79
|
+
assert.deepEqual(describeCursorIndividualLogin({ present: true, source: "desktop" }), {
|
|
80
|
+
kind: "desktop",
|
|
81
|
+
line: CURSOR_INDIVIDUAL_LOGIN_LINES.desktop,
|
|
82
|
+
});
|
|
83
|
+
assert.deepEqual(describeCursorIndividualLogin({ present: true, source: "agent" }), {
|
|
84
|
+
kind: "agent",
|
|
85
|
+
line: CURSOR_INDIVIDUAL_LOGIN_LINES.agent,
|
|
86
|
+
});
|
|
87
|
+
const degraded = [
|
|
88
|
+
{ present: false, source: null },
|
|
89
|
+
{ present: false, source: "desktop" },
|
|
90
|
+
{ present: true, source: null },
|
|
91
|
+
null,
|
|
92
|
+
undefined,
|
|
93
|
+
];
|
|
94
|
+
for (const state of degraded) {
|
|
95
|
+
assert.deepEqual(describeCursorIndividualLogin(state), {
|
|
96
|
+
kind: "none",
|
|
97
|
+
line: CURSOR_INDIVIDUAL_LOGIN_LINES.none,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
test("the three lines name the three states and carry no paths or secrets", () => {
|
|
102
|
+
assert.match(CURSOR_INDIVIDUAL_LOGIN_LINES.desktop, /desktop login found/);
|
|
103
|
+
assert.match(CURSOR_INDIVIDUAL_LOGIN_LINES.agent, /Agent login found/);
|
|
104
|
+
assert.match(CURSOR_INDIVIDUAL_LOGIN_LINES.none, /no usable local login/);
|
|
105
|
+
for (const line of Object.values(CURSOR_INDIVIDUAL_LOGIN_LINES)) {
|
|
106
|
+
assert.ok(!line.includes("/"), "no filesystem paths in doctor output");
|
|
107
|
+
assert.ok(!line.includes(SECRET));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
function fixtureDesktopDb(token) {
|
|
111
|
+
const file = path.join(dir, "state.vscdb");
|
|
112
|
+
const db = new Database(file);
|
|
113
|
+
try {
|
|
114
|
+
db.exec("CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT)");
|
|
115
|
+
db.prepare("INSERT INTO ItemTable (key, value) VALUES (?, ?)").run(CURSOR_DESKTOP_ACCESS_TOKEN_KEY, token);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
db.close();
|
|
119
|
+
}
|
|
120
|
+
process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV] = file;
|
|
121
|
+
return file;
|
|
122
|
+
}
|
|
123
|
+
test("check reports desktop login found for a fixture state.vscdb", async () => {
|
|
124
|
+
process.env[CURSOR_INDIVIDUAL_HOME_ENV] = dir;
|
|
125
|
+
const jwt = fixtureJwt(USER_ID);
|
|
126
|
+
const dbFile = fixtureDesktopDb(jwt);
|
|
127
|
+
const report = await checkCursorIndividualLogin();
|
|
128
|
+
assert.equal(report.kind, "desktop");
|
|
129
|
+
assert.equal(report.line, CURSOR_INDIVIDUAL_LOGIN_LINES.desktop);
|
|
130
|
+
assert.ok(!report.line.includes(jwt), "token material must not print");
|
|
131
|
+
assert.ok(!report.line.includes(dbFile), "database paths must not print");
|
|
132
|
+
});
|
|
133
|
+
test("check reports Agent login found when only the Agent auth file answers", async () => {
|
|
134
|
+
process.env[CURSOR_INDIVIDUAL_HOME_ENV] = dir;
|
|
135
|
+
process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV] = path.join(dir, "missing.vscdb");
|
|
136
|
+
const agentFile = path.join(dir, ".cursor", "auth.json");
|
|
137
|
+
fs.mkdirSync(path.dirname(agentFile), { recursive: true });
|
|
138
|
+
fs.writeFileSync(agentFile, JSON.stringify({ token: SECRET, userId: USER_ID }));
|
|
139
|
+
const report = await checkCursorIndividualLogin();
|
|
140
|
+
assert.equal(report.kind, "agent");
|
|
141
|
+
assert.equal(report.line, CURSOR_INDIVIDUAL_LOGIN_LINES.agent);
|
|
142
|
+
assert.ok(!report.line.includes(SECRET), "token material must not print");
|
|
143
|
+
assert.ok(!report.line.includes(agentFile), "credential paths must not print");
|
|
144
|
+
});
|
|
145
|
+
test("check reports no usable local login when neither source answers", async () => {
|
|
146
|
+
process.env[CURSOR_INDIVIDUAL_HOME_ENV] = dir;
|
|
147
|
+
process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV] = path.join(dir, "missing.vscdb");
|
|
148
|
+
const report = await checkCursorIndividualLogin();
|
|
149
|
+
assert.equal(report.kind, "none");
|
|
150
|
+
assert.equal(report.line, CURSOR_INDIVIDUAL_LOGIN_LINES.none);
|
|
151
|
+
});
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// NOT-268: Claude capacity source reporting. Tests never touch the real
|
|
154
|
+
// ~/.claude.json: every case points the override at a temporary fixture.
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
test("fixture cache-file env name matches the server module's live contract", async () => {
|
|
157
|
+
const { fileURLToPath } = await import("node:url");
|
|
158
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
159
|
+
const candidates = [
|
|
160
|
+
path.resolve(here, "..", "..", "server", "src", "capacity", "claude-local-cache.ts"),
|
|
161
|
+
path.resolve(here, "..", "..", "server", "dist", "capacity", "claude-local-cache.js"),
|
|
162
|
+
];
|
|
163
|
+
const src = candidates.find((file) => fs.existsSync(file));
|
|
164
|
+
assert.ok(src, "the server cache module must exist for the contract pin");
|
|
165
|
+
const text = fs.readFileSync(src, "utf8");
|
|
166
|
+
assert.ok(text.includes(`"${CLAUDE_CAPACITY_CACHE_FILE_ENV}"`));
|
|
167
|
+
assert.ok(text.includes(`"${CLAUDE_CAPACITY_REFRESH_ENV}"`));
|
|
168
|
+
assert.ok(text.includes(`"${CLAUDE_CAPACITY_REFRESH_PAID_VALUE}"`));
|
|
169
|
+
});
|
|
170
|
+
test("describe maps each cache state to its static line", () => {
|
|
171
|
+
const fresh = describeClaudeCapacitySource({ present: true, ageMs: 5 * 60_000 });
|
|
172
|
+
assert.equal(fresh.kind, "fresh");
|
|
173
|
+
assert.match(fresh.line, /fresh/);
|
|
174
|
+
const stale = describeClaudeCapacitySource({ present: true, ageMs: 61 * 60_000 });
|
|
175
|
+
assert.equal(stale.kind, "stale");
|
|
176
|
+
assert.match(stale.line, /stale/);
|
|
177
|
+
const degraded = [
|
|
178
|
+
{ present: false, ageMs: null },
|
|
179
|
+
{ present: false, ageMs: 0 },
|
|
180
|
+
{ present: true, ageMs: null },
|
|
181
|
+
{ present: true, ageMs: -1 },
|
|
182
|
+
null,
|
|
183
|
+
undefined,
|
|
184
|
+
];
|
|
185
|
+
for (const state of degraded) {
|
|
186
|
+
const report = describeClaudeCapacitySource(state);
|
|
187
|
+
assert.equal(report.kind, "missing");
|
|
188
|
+
assert.match(report.line, /no local 5H\/1W cache/);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
test("probe opt-in line appears only under the exact paid value", () => {
|
|
192
|
+
assert.equal(describeClaudeProbeOptIn(undefined), null);
|
|
193
|
+
assert.equal(describeClaudeProbeOptIn(""), null);
|
|
194
|
+
assert.equal(describeClaudeProbeOptIn("off"), null);
|
|
195
|
+
assert.equal(describeClaudeProbeOptIn("auto"), null);
|
|
196
|
+
const armed = describeClaudeProbeOptIn(CLAUDE_CAPACITY_REFRESH_PAID_VALUE);
|
|
197
|
+
assert.ok(armed);
|
|
198
|
+
assert.match(armed, /armed/);
|
|
199
|
+
assert.match(armed, /≤\$0\.01/);
|
|
200
|
+
});
|
|
201
|
+
test("check reads fetchedAtMs from the config key, never mtime or the real home", async () => {
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
// Full config envelope with sensitive siblings: freshness comes from the
|
|
204
|
+
// subtree timestamp even though mtime is two days old (mtime must be
|
|
205
|
+
// ignored — unrelated config writes touch it). The line must not leak
|
|
206
|
+
// the sibling values.
|
|
207
|
+
const freshFile = path.join(dir, "cached-fresh.json");
|
|
208
|
+
fs.writeFileSync(freshFile, JSON.stringify({
|
|
209
|
+
userID: "user-secret-must-never-print",
|
|
210
|
+
email: "someone@example.com",
|
|
211
|
+
cachedUsageUtilization: { fetchedAtMs: now - 5 * 60_000, accountUuid: "acct-secret" },
|
|
212
|
+
}));
|
|
213
|
+
const ancient = new Date(now - 2 * 24 * 3600_000);
|
|
214
|
+
fs.utimesSync(freshFile, ancient, ancient);
|
|
215
|
+
process.env[CLAUDE_CAPACITY_CACHE_FILE_ENV] = freshFile;
|
|
216
|
+
const fresh = await checkClaudeCapacitySource(now);
|
|
217
|
+
assert.equal(fresh.kind, "fresh");
|
|
218
|
+
assert.ok(!fresh.line.includes("user-secret-must-never-print"));
|
|
219
|
+
assert.ok(!fresh.line.includes("acct-secret"));
|
|
220
|
+
// Bare cache object (no envelope) still works; a brand-new mtime cannot
|
|
221
|
+
// rescue a stale timestamp.
|
|
222
|
+
const staleFile = path.join(dir, "cached-stale.json");
|
|
223
|
+
fs.writeFileSync(staleFile, JSON.stringify({ fetchedAtMs: 1 }));
|
|
224
|
+
const justNow = new Date(now);
|
|
225
|
+
fs.utimesSync(staleFile, justNow, justNow);
|
|
226
|
+
process.env[CLAUDE_CAPACITY_CACHE_FILE_ENV] = staleFile;
|
|
227
|
+
const stale = await checkClaudeCapacitySource(now);
|
|
228
|
+
assert.equal(stale.kind, "stale");
|
|
229
|
+
// Missing file, garbage JSON, future timestamp, and a config without the
|
|
230
|
+
// key all report missing.
|
|
231
|
+
process.env[CLAUDE_CAPACITY_CACHE_FILE_ENV] = path.join(dir, "missing.json");
|
|
232
|
+
assert.equal((await checkClaudeCapacitySource(now)).kind, "missing");
|
|
233
|
+
const garbageFile = path.join(dir, "cached-garbage.json");
|
|
234
|
+
fs.writeFileSync(garbageFile, "{not json");
|
|
235
|
+
process.env[CLAUDE_CAPACITY_CACHE_FILE_ENV] = garbageFile;
|
|
236
|
+
assert.equal((await checkClaudeCapacitySource(now)).kind, "missing");
|
|
237
|
+
const futureFile = path.join(dir, "cached-future.json");
|
|
238
|
+
fs.writeFileSync(futureFile, JSON.stringify({ cachedUsageUtilization: { fetchedAtMs: now + 60_000 } }));
|
|
239
|
+
process.env[CLAUDE_CAPACITY_CACHE_FILE_ENV] = futureFile;
|
|
240
|
+
assert.equal((await checkClaudeCapacitySource(now)).kind, "missing");
|
|
241
|
+
const keylessFile = path.join(dir, "cached-keyless.json");
|
|
242
|
+
fs.writeFileSync(keylessFile, JSON.stringify({ userID: "x", projects: {} }));
|
|
243
|
+
process.env[CLAUDE_CAPACITY_CACHE_FILE_ENV] = keylessFile;
|
|
244
|
+
assert.equal((await checkClaudeCapacitySource(now)).kind, "missing");
|
|
245
|
+
delete process.env[CLAUDE_CAPACITY_CACHE_FILE_ENV];
|
|
246
|
+
});
|