agent-dealer 1.2.1 → 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.
Files changed (57) hide show
  1. package/bundle/server/dist/adapters/linear-inbox.js +9 -5
  2. package/bundle/server/dist/capacity/adapter.js +2 -0
  3. package/bundle/server/dist/capacity/claude-events.js +45 -10
  4. package/bundle/server/dist/capacity/claude-events.test.js +7 -0
  5. package/bundle/server/dist/capacity/claude-local-cache.js +788 -0
  6. package/bundle/server/dist/capacity/claude-local-cache.test.js +520 -0
  7. package/bundle/server/dist/capacity/codex-app-server.js +100 -10
  8. package/bundle/server/dist/capacity/codex-app-server.test.js +195 -2
  9. package/bundle/server/dist/capacity/cursor-individual-credentials.js +425 -0
  10. package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +369 -0
  11. package/bundle/server/dist/capacity/cursor-individual.js +1037 -0
  12. package/bundle/server/dist/capacity/cursor-individual.test.js +1081 -0
  13. package/bundle/server/dist/capacity/muse-host.js +748 -0
  14. package/bundle/server/dist/capacity/muse-host.test.js +480 -0
  15. package/bundle/server/dist/capacity/muse-lifecycle.test.js +97 -0
  16. package/bundle/server/dist/capacity/muse.js +389 -91
  17. package/bundle/server/dist/capacity/muse.test.js +100 -61
  18. package/bundle/server/dist/capacity/service.js +1 -0
  19. package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
  20. package/bundle/server/dist/db/index.js +30 -0
  21. package/bundle/server/dist/db/schema.sql +27 -0
  22. package/bundle/server/dist/index.js +17 -2
  23. package/bundle/server/dist/repository/cursor-individual-billing.js +84 -0
  24. package/bundle/server/dist/repository/repository-mappings.js +46 -0
  25. package/bundle/server/dist/repository/repository-mappings.test.js +83 -0
  26. package/bundle/server/dist/repository/runtime-capacity.js +8 -3
  27. package/bundle/server/dist/repository/runtime-capacity.test.js +4 -0
  28. package/bundle/server/dist/routes/cursor-individual-billing.test.js +80 -0
  29. package/bundle/server/dist/routes/index.js +58 -9
  30. package/bundle/server/dist/routes/intake-linear.test.js +55 -1
  31. package/bundle/server/dist/routes/repository-mappings.js +22 -0
  32. package/bundle/server/dist/routes/repository-mappings.test.js +110 -0
  33. package/bundle/server/dist/routes/runtime-capacity.test.js +84 -8
  34. package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
  35. package/bundle/server/dist/runners/muse-serve-session.js +313 -0
  36. package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
  37. package/bundle/server/package.json +2 -2
  38. package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
  39. package/bundle/server/static-ui/assets/index-CYqRh_-S.css +1 -0
  40. package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
  41. package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
  42. package/bundle/server/static-ui/index.html +2 -2
  43. package/bundle/shared/dist/index.d.ts +6 -6
  44. package/bundle/shared/dist/linear-intake.d.ts +71 -0
  45. package/bundle/shared/dist/linear-intake.js +90 -0
  46. package/bundle/shared/dist/linear-intake.test.js +80 -1
  47. package/bundle/shared/dist/runtime-capacity.d.ts +89 -0
  48. package/bundle/shared/dist/runtime-capacity.js +60 -0
  49. package/bundle/shared/dist/runtime-capacity.test.js +1 -0
  50. package/bundle/shared/package.json +1 -1
  51. package/dist/doctor.d.ts +57 -0
  52. package/dist/doctor.js +211 -0
  53. package/dist/doctor.test.d.ts +1 -0
  54. package/dist/doctor.test.js +246 -0
  55. package/package.json +1 -1
  56. package/bundle/server/static-ui/assets/index-ChnI0F02.css +0 -1
  57. package/bundle/server/static-ui/assets/index-CyB1573Q.js +0 -60
@@ -34,6 +34,16 @@ export const CapacityUnavailableReason = z.enum([
34
34
  "unparsable",
35
35
  "stale",
36
36
  ]);
37
+ /**
38
+ * Which account-wide critical window this is, set once by the adapter that
39
+ * knows the provider's own identity for it (e.g. Codex's `rateLimits`
40
+ * aggregate, or whichever detailed limit pair it collapsed into) — never
41
+ * re-derived downstream from a window key, label, or duration, none of
42
+ * which can disambiguate multiple same-duration buckets a provider reports.
43
+ * Null means "not the account-wide pair" (a non-critical extra bucket, or a
44
+ * provider/window this contract has no opinion on).
45
+ */
46
+ export const CapacityCriticalRole = z.enum(["five_hour", "weekly"]);
37
47
  export const CapacityWindowSnapshot = z.object({
38
48
  /** Stable per-runtime key for this window (e.g. `weekly_all_models`). */
39
49
  windowKey: z.string().min(1),
@@ -58,6 +68,7 @@ export const CapacityWindowSnapshot = z.object({
58
68
  expiresAt: z.string().nullable(),
59
69
  source: CapacitySource,
60
70
  unavailableReason: CapacityUnavailableReason.nullable(),
71
+ criticalRole: CapacityCriticalRole.nullable(),
61
72
  });
62
73
  export const RuntimeCapacityEntry = z.object({
63
74
  runtime: Runtime,
@@ -189,6 +200,55 @@ export function isTeamBillingKnown(b, nowMs = Date.now()) {
189
200
  }
190
201
  return true;
191
202
  }
203
+ /**
204
+ * NOT-250: monthly/billing-cycle capacity for Cursor Individual accounts via
205
+ * an opt-in *experimental* dashboard adapter. Cursor documents monthly
206
+ * individual usage (Spending dashboard) but exposes no supported usage API,
207
+ * so this surface reads authenticated dashboard endpoints using the existing
208
+ * local Cursor login. Those endpoints and credential formats are not a
209
+ * supported public contract and may change without notice — hence disabled
210
+ * by default, `experimental_api` source, and N/A degradation on any drift.
211
+ * Billing-cycle values only (label/start/end, reported usage, remaining
212
+ * percent): no 5H/1W-style windows are invented, and money is never rendered
213
+ * as a token percentage. N/A reasons reuse CapacityUnavailableReason.
214
+ */
215
+ export const CursorIndividualBilling = z.object({
216
+ /** True only with explicit opt-in (`AGENT_DEALER_CURSOR_INDIVIDUAL_CAPACITY=experimental`). */
217
+ enabled: z.boolean(),
218
+ /** True only when opted in AND a usable local credential was found. */
219
+ configured: z.boolean(),
220
+ /** Billing-cycle label exactly as reported (e.g. `September 2026`), never guessed. */
221
+ cycleLabel: z.string().nullable(),
222
+ /** Billing-cycle start as reported (ISO-8601, nullable). */
223
+ cycleStart: z.string().nullable(),
224
+ /** Billing-cycle reset/end as reported (ISO-8601, nullable). */
225
+ cycleEnd: z.string().nullable(),
226
+ /** Reported usage value/unit as reported (never normalized away). */
227
+ usageValue: z.number().nullable(),
228
+ usageUnit: z.string().nullable(),
229
+ /** Normalized remaining percent 0–100 when derivable; null renders N/A. */
230
+ remainingPercent: z.number().min(0).max(100).nullable(),
231
+ source: CapacitySource,
232
+ unavailableReason: CapacityUnavailableReason.nullable(),
233
+ /** When the backing dashboard read was observed (ISO-8601, nullable). */
234
+ observedAt: z.string().nullable(),
235
+ generatedAt: z.string(),
236
+ });
237
+ /** True when the individual billing snapshot carries current, renderable values. */
238
+ export function isIndividualBillingKnown(b, nowMs = Date.now()) {
239
+ if (!b.enabled || !b.configured)
240
+ return false;
241
+ if (b.source === "unavailable" || b.unavailableReason !== null)
242
+ return false;
243
+ if (b.usageValue === null && b.remainingPercent === null)
244
+ return false;
245
+ if (b.observedAt) {
246
+ const obsMs = Date.parse(b.observedAt);
247
+ if (!Number.isFinite(obsMs) || obsMs > nowMs + 60_000)
248
+ return false;
249
+ }
250
+ return true;
251
+ }
192
252
  /** True when the window carries a current, renderable remaining value. */
193
253
  export function isWindowKnown(w, nowMs = Date.now()) {
194
254
  if (w.remainingPercent === null)
@@ -17,6 +17,7 @@ function window(over = {}) {
17
17
  expiresAt: new Date(Date.now() + 3600_000).toISOString(),
18
18
  source: "supported_protocol",
19
19
  unavailableReason: null,
20
+ criticalRole: null,
20
21
  ...over,
21
22
  };
22
23
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-dealer/shared",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
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,6 +107,39 @@ 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
+ }
124
+ // NOT-250: experimental Cursor Individual dashboard adapter status
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.
129
+ if (process.env.AGENT_DEALER_CURSOR_INDIVIDUAL_CAPACITY === "experimental") {
130
+ console.warn("⚠ Cursor Individual capacity: EXPERIMENTAL dashboard adapter enabled " +
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
+ }
142
+ }
108
143
  }
109
144
  else {
110
145
  console.warn(`⚠ no config — run: agent-dealer setup`);
@@ -145,6 +180,182 @@ export async function runDoctor() {
145
180
  }
146
181
  return failed ? 1 : 0;
147
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
+ }
148
359
  function isPortFree(port) {
149
360
  return new Promise((resolve) => {
150
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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dealer",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Human control plane for agent execution — queue, plan approval, audit",
5
5
  "license": "MIT",
6
6
  "type": "module",