agent-dealer 0.2.0 → 1.0.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.
- package/bundle/server/dist/adapters/agent-deck-bind.js +443 -0
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +454 -0
- package/bundle/server/dist/adapters/agent-deck.js +38 -50
- package/bundle/server/dist/adapters/agent-deck.test.js +63 -0
- package/bundle/server/dist/adapters/agent-health.js +230 -33
- package/bundle/server/dist/adapters/agent-health.test.js +237 -0
- package/bundle/server/dist/adapters/authority-lifecycle.js +289 -0
- package/bundle/server/dist/adapters/codex-scoped-config.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.test.js +166 -0
- package/bundle/server/dist/adapters/external.js +0 -1
- package/bundle/server/dist/adapters/git-worktree.js +514 -0
- package/bundle/server/dist/adapters/git-worktree.test.js +502 -0
- package/bundle/server/dist/adapters/github.js +170 -0
- package/bundle/server/dist/adapters/github.test.js +205 -0
- package/bundle/server/dist/adapters/linear-inbox.js +199 -30
- package/bundle/server/dist/adapters/linear-inbox.test.js +35 -0
- package/bundle/server/dist/adapters/linear-sync.js +3 -101
- package/bundle/server/dist/adapters/managed-repo.js +168 -0
- package/bundle/server/dist/adapters/managed-repo.test.js +63 -0
- package/bundle/server/dist/adapters/outbound-delivery.js +83 -0
- package/bundle/server/dist/adapters/outbound-delivery.test.js +53 -0
- package/bundle/server/dist/adapters/reflect-authority.js +43 -0
- package/bundle/server/dist/cli-env.js +26 -0
- package/bundle/server/dist/cli-env.test.js +14 -1
- package/bundle/server/dist/coordinator/abort.integration.test.js +74 -0
- package/bundle/server/dist/coordinator/admission.js +330 -0
- package/bundle/server/dist/coordinator/admission.test.js +338 -0
- package/bundle/server/dist/coordinator/args.js +128 -0
- package/bundle/server/dist/coordinator/args.test.js +132 -0
- package/bundle/server/dist/coordinator/authority-recovery.integration.test.js +661 -0
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/auto-merge.js +286 -0
- package/bundle/server/dist/coordinator/auto-merge.timeout.test.js +14 -0
- package/bundle/server/dist/coordinator/branch-progress.js +81 -0
- package/bundle/server/dist/coordinator/clock-jump.js +98 -0
- package/bundle/server/dist/coordinator/commands.js +1088 -0
- package/bundle/server/dist/coordinator/commands.test.js +554 -0
- package/bundle/server/dist/coordinator/deck-outage-config.js +32 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +200 -0
- package/bundle/server/dist/coordinator/dependencies.js +242 -0
- package/bundle/server/dist/coordinator/dependency-readiness.integration.test.js +535 -0
- package/bundle/server/dist/coordinator/developer-effect.js +959 -0
- package/bundle/server/dist/coordinator/developer-effect.test.js +1143 -0
- package/bundle/server/dist/coordinator/dispatcher.js +58 -0
- package/bundle/server/dist/coordinator/dispatcher.test.js +81 -0
- package/bundle/server/dist/coordinator/effect-registry.js +17 -0
- package/bundle/server/dist/coordinator/execution-env.integration.test.js +122 -0
- package/bundle/server/dist/coordinator/failure-reason.js +273 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +234 -0
- package/bundle/server/dist/coordinator/guidance.js +33 -0
- package/bundle/server/dist/coordinator/guidance.test.js +110 -0
- package/bundle/server/dist/coordinator/human-resolution.js +88 -0
- package/bundle/server/dist/coordinator/human-resolution.test.js +72 -0
- package/bundle/server/dist/coordinator/intent-forecast.js +13 -0
- package/bundle/server/dist/coordinator/intent-forecast.test.js +34 -0
- package/bundle/server/dist/coordinator/latest-failure.js +100 -0
- package/bundle/server/dist/coordinator/latest-failure.test.js +125 -0
- package/bundle/server/dist/coordinator/metrics.js +35 -0
- package/bundle/server/dist/coordinator/metrics.test.js +52 -0
- package/bundle/server/dist/coordinator/permissions.js +127 -0
- package/bundle/server/dist/coordinator/permissions.test.js +184 -0
- package/bundle/server/dist/coordinator/presumed-dead-budget.integration.test.js +146 -0
- package/bundle/server/dist/coordinator/process-liveness.js +191 -0
- package/bundle/server/dist/coordinator/profile-snapshot.js +20 -0
- package/bundle/server/dist/coordinator/profile-snapshot.test.js +118 -0
- package/bundle/server/dist/coordinator/projection.js +147 -0
- package/bundle/server/dist/coordinator/projection.test.js +121 -0
- package/bundle/server/dist/coordinator/prompts.js +164 -0
- package/bundle/server/dist/coordinator/prompts.test.js +177 -0
- package/bundle/server/dist/coordinator/queue-by-default.integration.test.js +271 -0
- package/bundle/server/dist/coordinator/reclaim-republish.integration.test.js +372 -0
- package/bundle/server/dist/coordinator/recovery.js +466 -0
- package/bundle/server/dist/coordinator/recovery.test.js +164 -0
- package/bundle/server/dist/coordinator/reflect-trigger.js +218 -0
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +324 -0
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +234 -0
- package/bundle/server/dist/coordinator/restart-recovery.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +507 -0
- package/bundle/server/dist/coordinator/reviewer-effect.test.js +612 -0
- package/bundle/server/dist/coordinator/reviewer-result.js +38 -0
- package/bundle/server/dist/coordinator/reviewer-result.test.js +45 -0
- package/bundle/server/dist/coordinator/routing.js +160 -0
- package/bundle/server/dist/coordinator/routing.test.js +336 -0
- package/bundle/server/dist/coordinator/session-lifecycle.integration.test.js +127 -0
- package/bundle/server/dist/coordinator/session-lifecycle.js +280 -0
- package/bundle/server/dist/coordinator/session-lifecycle.test.js +121 -0
- package/bundle/server/dist/coordinator/session-progress.js +351 -0
- package/bundle/server/dist/coordinator/session-progress.test.js +190 -0
- package/bundle/server/dist/coordinator/session-timeouts.js +33 -0
- package/bundle/server/dist/coordinator/sleep-recovery.integration.test.js +233 -0
- package/bundle/server/dist/coordinator/spawn.js +49 -0
- package/bundle/server/dist/coordinator/sync-issue-base-branch.js +18 -0
- package/bundle/server/dist/coordinator/usage-cap-config.js +10 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +189 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +193 -0
- package/bundle/server/dist/coordinator/usage.js +40 -0
- package/bundle/server/dist/coordinator/usage.test.js +69 -0
- package/bundle/server/dist/coordinator/verification-receipt.js +333 -0
- package/bundle/server/dist/coordinator/verification-receipt.test.js +382 -0
- package/bundle/server/dist/coordinator/worker-loop.js +363 -0
- package/bundle/server/dist/coordinator/worker-loop.test.js +327 -0
- package/bundle/server/dist/coordinator/workflow-instances.test.js +45 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.js +82 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +199 -0
- package/bundle/server/dist/db/artifacts-issue-columns.test.js +18 -0
- package/bundle/server/dist/db/authority-attempts-stale-at-migration.test.js +65 -0
- package/bundle/server/dist/db/idempotency-index-migration.test.js +57 -0
- package/bundle/server/dist/db/index.js +359 -7
- package/bundle/server/dist/db/linear-inbox-unlock.test.js +67 -0
- package/bundle/server/dist/db/migrate-to-issues.js +824 -0
- package/bundle/server/dist/db/migrate-to-issues.test.js +842 -0
- package/bundle/server/dist/db/schema.sql +311 -1
- package/bundle/server/dist/db/schema.test.js +66 -0
- package/bundle/server/dist/db/worker-session-pid-migration.test.js +112 -0
- package/bundle/server/dist/dev-review-cli-happy-path.integration.test.js +338 -0
- package/bundle/server/dist/direct-start-liveness.integration.test.js +350 -0
- package/bundle/server/dist/index.js +58 -5
- package/bundle/server/dist/paths.js +40 -4
- package/bundle/server/dist/paths.test.js +42 -0
- package/bundle/server/dist/pid-marker-claim-child.js +18 -0
- package/bundle/server/dist/pid-marker-hold-arbiter-child.js +24 -0
- package/bundle/server/dist/pid-marker.js +141 -0
- package/bundle/server/dist/pid-marker.test.js +208 -0
- package/bundle/server/dist/queue/approve-deliver.js +128 -28
- package/bundle/server/dist/queue/reflect-schedule.js +25 -0
- package/bundle/server/dist/queue/result-qa.js +1 -1
- package/bundle/server/dist/queue/send-gate.test.js +172 -36
- package/bundle/server/dist/repository/agents.js +55 -18
- package/bundle/server/dist/repository/artifacts-for-issue.js +42 -0
- package/bundle/server/dist/repository/artifacts-for-issue.test.js +48 -0
- package/bundle/server/dist/repository/artifacts.js +47 -0
- package/bundle/server/dist/repository/artifacts.test.js +48 -0
- package/bundle/server/dist/repository/authority-attempts.js +189 -0
- package/bundle/server/dist/repository/findings.js +70 -0
- package/bundle/server/dist/repository/findings.test.js +68 -0
- package/bundle/server/dist/repository/human-actions.js +142 -0
- package/bundle/server/dist/repository/human-actions.test.js +123 -0
- package/bundle/server/dist/repository/intake-settings.js +5 -59
- package/bundle/server/dist/repository/issues.js +305 -0
- package/bundle/server/dist/repository/issues.test.js +69 -0
- package/bundle/server/dist/repository/outbound-drafts.js +28 -0
- package/bundle/server/dist/repository/queue-entries.js +162 -0
- package/bundle/server/dist/repository/review-publications.js +153 -0
- package/bundle/server/dist/repository/review-publications.test.js +78 -0
- package/bundle/server/dist/repository/runs-external-id.test.js +5 -3
- package/bundle/server/dist/repository/runs.js +4 -211
- package/bundle/server/dist/repository/runtime-availability.js +54 -0
- package/bundle/server/dist/repository/usage-events.js +60 -0
- package/bundle/server/dist/repository/usage-events.test.js +56 -0
- package/bundle/server/dist/repository/work-items.js +305 -0
- package/bundle/server/dist/repository/work-items.test.js +142 -0
- package/bundle/server/dist/repository/worker-sessions.js +219 -0
- package/bundle/server/dist/repository/worker-sessions.test.js +60 -0
- package/bundle/server/dist/repository/workflow-events.js +193 -0
- package/bundle/server/dist/repository/workflow-events.test.js +89 -0
- package/bundle/server/dist/resolve-tsx-bin.js +19 -0
- package/bundle/server/dist/routes/human-actions.js +68 -0
- package/bundle/server/dist/routes/human-actions.test.js +284 -0
- package/bundle/server/dist/routes/index.js +29 -496
- package/bundle/server/dist/routes/issues.js +240 -0
- package/bundle/server/dist/routes/issues.test.js +451 -0
- package/bundle/server/dist/routes/queue.js +41 -0
- package/bundle/server/dist/routes/queue.test.js +78 -0
- package/bundle/server/dist/runners/claude-args.js +18 -30
- package/bundle/server/dist/runners/claude.js +19 -163
- package/bundle/server/dist/runners/claude.test.js +15 -70
- package/bundle/server/dist/runners/codex-args.js +59 -0
- package/bundle/server/dist/runners/codex-args.test.js +72 -0
- package/bundle/server/dist/runners/codex-jsonl.js +150 -0
- package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
- package/bundle/server/dist/runners/codex-resume.test.js +19 -0
- package/bundle/server/dist/runners/codex.js +46 -0
- package/bundle/server/dist/runners/models-codex.test.js +14 -0
- package/bundle/server/dist/runners/models.js +53 -1
- package/bundle/server/dist/runners/persist.js +4 -1
- package/bundle/server/dist/runners/process-registry.js +11 -0
- package/bundle/server/dist/runners/prompts.js +13 -280
- package/bundle/server/dist/runners/prompts.test.js +22 -153
- package/bundle/server/dist/runners/qa.js +17 -1
- package/bundle/server/dist/runners/reflect.js +4 -16
- package/bundle/server/dist/runners/spawn-cli.js +197 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +130 -0
- package/bundle/server/dist/runners/stream-json.js +0 -141
- package/bundle/server/dist/runners/usage-cap.js +242 -0
- package/bundle/server/dist/runners/usage-cap.test.js +97 -0
- package/bundle/server/dist/server-liveness.js +37 -0
- package/bundle/server/dist/server-liveness.test.js +82 -0
- package/bundle/server/dist/static-ui.test.js +44 -0
- package/bundle/server/package.json +4 -2
- package/bundle/server/static-ui/assets/index-BTtPGYpY.js +60 -0
- package/bundle/server/static-ui/assets/index-BxTx4b1d.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/agents.d.ts +262 -133
- package/bundle/shared/dist/agents.js +85 -19
- package/bundle/shared/dist/cursor-auth-health.d.ts +12 -0
- package/bundle/shared/dist/cursor-auth-health.js +36 -0
- package/bundle/shared/dist/cursor-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/cursor-auth-health.test.js +40 -0
- package/bundle/shared/dist/findings.d.ts +46 -0
- package/bundle/shared/dist/findings.js +17 -0
- package/bundle/shared/dist/findings.test.d.ts +1 -0
- package/bundle/shared/dist/findings.test.js +20 -0
- package/bundle/shared/dist/github-repo.d.ts +28 -0
- package/bundle/shared/dist/github-repo.js +98 -0
- package/bundle/shared/dist/github-repo.test.d.ts +1 -0
- package/bundle/shared/dist/github-repo.test.js +32 -0
- package/bundle/shared/dist/human-actions.d.ts +66 -0
- package/bundle/shared/dist/human-actions.js +51 -0
- package/bundle/shared/dist/human-actions.test.d.ts +1 -0
- package/bundle/shared/dist/human-actions.test.js +57 -0
- package/bundle/shared/dist/index.d.ts +567 -757
- package/bundle/shared/dist/index.js +22 -36
- package/bundle/shared/dist/issues.d.ts +385 -0
- package/bundle/shared/dist/issues.js +169 -0
- package/bundle/shared/dist/issues.test.d.ts +1 -0
- package/bundle/shared/dist/issues.test.js +72 -0
- package/bundle/shared/dist/outbound-draft.d.ts +6 -0
- package/bundle/shared/dist/outbound-draft.js +4 -0
- package/bundle/shared/dist/plan-triage.d.ts +6 -6
- package/bundle/shared/dist/profile-snapshot.d.ts +154 -0
- package/bundle/shared/dist/profile-snapshot.js +145 -0
- package/bundle/shared/dist/profile-snapshot.test.d.ts +1 -0
- package/bundle/shared/dist/profile-snapshot.test.js +67 -0
- package/bundle/shared/dist/queue-entries.d.ts +89 -0
- package/bundle/shared/dist/queue-entries.js +19 -0
- package/bundle/shared/dist/result-qa.d.ts +2 -2
- package/bundle/shared/dist/runtime-auth-health.d.ts +66 -0
- package/bundle/shared/dist/runtime-auth-health.js +186 -0
- package/bundle/shared/dist/runtime-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/runtime-auth-health.test.js +200 -0
- package/bundle/shared/dist/runtime-availability.d.ts +51 -0
- package/bundle/shared/dist/runtime-availability.js +13 -0
- package/bundle/shared/dist/runtime.d.ts +1 -1
- package/bundle/shared/dist/runtime.js +1 -1
- package/bundle/shared/dist/usage-events.d.ts +36 -0
- package/bundle/shared/dist/usage-events.js +13 -0
- package/bundle/shared/dist/worker-sessions.d.ts +140 -0
- package/bundle/shared/dist/worker-sessions.js +70 -0
- package/bundle/shared/dist/worker-sessions.test.d.ts +1 -0
- package/bundle/shared/dist/worker-sessions.test.js +39 -0
- package/bundle/shared/dist/workflow.d.ts +78 -0
- package/bundle/shared/dist/workflow.js +59 -0
- package/bundle/shared/dist/workflow.test.d.ts +1 -0
- package/bundle/shared/dist/workflow.test.js +61 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +50 -0
- package/dist/action.test.d.ts +1 -0
- package/dist/action.test.js +70 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +17 -0
- package/dist/agent.test.d.ts +1 -0
- package/dist/agent.test.js +19 -0
- package/dist/cli-check.d.ts +7 -0
- package/dist/cli-check.js +40 -0
- package/dist/doctor.js +59 -1
- package/dist/http.d.ts +5 -0
- package/dist/http.js +19 -0
- package/dist/index.js +28 -0
- package/dist/issue.d.ts +35 -0
- package/dist/issue.js +143 -0
- package/dist/issue.test.d.ts +1 -0
- package/dist/issue.test.js +240 -0
- package/dist/lifecycle.contract.test.d.ts +1 -0
- package/dist/lifecycle.contract.test.js +135 -0
- package/dist/queue.d.ts +11 -0
- package/dist/queue.js +60 -0
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +46 -0
- package/dist/test-fetch-stub.d.ts +6 -0
- package/dist/test-fetch-stub.js +31 -0
- package/package.json +2 -1
- package/templates/prod.env.example +1 -1
- package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
- package/bundle/server/static-ui/assets/index-QYv3zmE6.css +0 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { AgentHealthIssue } from "./agents.js";
|
|
2
|
+
import type { Runtime } from "./runtime.js";
|
|
3
|
+
/** Shared remediation for Cursor macOS keychain stuck-auth (NOT-114 / NOT-103). */
|
|
4
|
+
export declare const CURSOR_KEYCHAIN_REMEDIATION: string;
|
|
5
|
+
export declare const CURSOR_KEYCHAIN_HEALTH_ISSUE: AgentHealthIssue;
|
|
6
|
+
/**
|
|
7
|
+
* Remediations quote both routes the CLI itself offers. The old Cursor message named only
|
|
8
|
+
* `cursor-agent login`, while the CLI's own logged-out line offers `agent login` *or*
|
|
9
|
+
* `CURSOR_API_KEY` — an operator running headless needs the second one.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CURSOR_AUTH_REMEDIATION: string;
|
|
12
|
+
export declare const CODEX_AUTH_REMEDIATION = "Run `codex login` (or set OPENAI_API_KEY for automation)";
|
|
13
|
+
export declare const CLAUDE_AUTH_REMEDIATION: string;
|
|
14
|
+
/**
|
|
15
|
+
* Used when text proves an auth failure but does not say which CLI printed it. All three
|
|
16
|
+
* runtimes print a bare `Not logged in`, so naming one of them there would be a guess — and
|
|
17
|
+
* a confidently wrong remediation (`cursor-agent login` for a Codex log) is worse than the
|
|
18
|
+
* generic crash reason it replaces.
|
|
19
|
+
*/
|
|
20
|
+
export declare const AMBIGUOUS_AUTH_REMEDIATION: string;
|
|
21
|
+
/** Operator-facing runtime names for prose built out of a classification. */
|
|
22
|
+
export declare const RUNTIME_AUTH_LABEL: Record<Runtime, string>;
|
|
23
|
+
/** True when Cursor status/login/stderr indicates a stuck macOS keychain item. */
|
|
24
|
+
export declare function isCursorKeychainStuckOutput(output: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Classify one runtime's status/login/stderr text.
|
|
27
|
+
* Cursor's keychain case wins over its plain logged-out case — same symptom, different fix.
|
|
28
|
+
*
|
|
29
|
+
* For callers that *know* what they just ran — the health preflight probes one CLI and reads
|
|
30
|
+
* its output. Callers holding a log plus an unreliable label want
|
|
31
|
+
* `runtimeAuthClassificationForLog`: several runtimes print the same `Not logged in`, so
|
|
32
|
+
* "does this read as Cursor's auth failure" is not the same question as "did Cursor write it".
|
|
33
|
+
*/
|
|
34
|
+
export declare function runtimeAuthIssueFromOutput(runtime: Runtime, output: string): AgentHealthIssue | null;
|
|
35
|
+
/**
|
|
36
|
+
* Classify Cursor auth health from status/login/stderr text.
|
|
37
|
+
* Also usable by session failure classifiers (NOT-113) for the same remediation string.
|
|
38
|
+
*/
|
|
39
|
+
export declare function cursorAuthIssueFromOutput(output: string): AgentHealthIssue | null;
|
|
40
|
+
/** `runtime: null` means "definitely an auth failure, but the text does not name the CLI". */
|
|
41
|
+
export type RuntimeAuthClassification = {
|
|
42
|
+
runtime: Runtime | null;
|
|
43
|
+
issue: AgentHealthIssue;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Classify a log and say which CLI wrote it, without trusting a caller's label over the
|
|
47
|
+
* text. The failure-reason classifier reads a spawn log by path and its idea of the runtime
|
|
48
|
+
* is a hint at best: `worker_sessions.runtime` is nullable, and older rows can disagree with
|
|
49
|
+
* what actually ran.
|
|
50
|
+
*
|
|
51
|
+
* Attribution is deliberately conservative, in this order:
|
|
52
|
+
* 1. a single vendor anchor in text that also reads as that runtime's auth failure — this
|
|
53
|
+
* outranks `recordedRuntime`, because the log naming its own CLI is stronger evidence
|
|
54
|
+
* than a row saying what was supposed to run;
|
|
55
|
+
* 2. otherwise `recordedRuntime`, when its own patterns recognise the text (nothing in the
|
|
56
|
+
* log contradicts it, so the caller's label is the best evidence available);
|
|
57
|
+
* 3. otherwise, an auth failure only one runtime's patterns recognise at all;
|
|
58
|
+
* 4. otherwise unattributed (`runtime: null`) with the generic remediation.
|
|
59
|
+
*
|
|
60
|
+
* Trying the runtimes in a fixed order — or taking `recordedRuntime` at its word — reports
|
|
61
|
+
* Claude's `Not logged in · Please run /login` as Cursor, because Cursor's list carries the
|
|
62
|
+
* shared `not logged in` too, and hands the operator `cursor-agent login` for a Claude death.
|
|
63
|
+
*/
|
|
64
|
+
export declare function runtimeAuthClassificationForLog(output: string, recordedRuntime?: Runtime | null): RuntimeAuthClassification | null;
|
|
65
|
+
/** `runtimeAuthClassificationForLog` for callers that hold no runtime label at all. */
|
|
66
|
+
export declare function anyRuntimeAuthIssueFromOutput(output: string): RuntimeAuthClassification | null;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/** Shared remediation for Cursor macOS keychain stuck-auth (NOT-114 / NOT-103). */
|
|
2
|
+
export const CURSOR_KEYCHAIN_REMEDIATION = "Cursor macOS keychain auth is stuck (errSecDuplicateItem). Delete the item and sign in again: " +
|
|
3
|
+
"`security delete-generic-password -s cursor-access-token -a cursor-user` then `agent login` " +
|
|
4
|
+
"(or `cursor-agent login`). See docs/TROUBLESHOOTING.md#cursor-macos-keychain-auth.";
|
|
5
|
+
export const CURSOR_KEYCHAIN_HEALTH_ISSUE = {
|
|
6
|
+
code: "cursor_keychain",
|
|
7
|
+
message: CURSOR_KEYCHAIN_REMEDIATION,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Remediations quote both routes the CLI itself offers. The old Cursor message named only
|
|
11
|
+
* `cursor-agent login`, while the CLI's own logged-out line offers `agent login` *or*
|
|
12
|
+
* `CURSOR_API_KEY` — an operator running headless needs the second one.
|
|
13
|
+
*/
|
|
14
|
+
export const CURSOR_AUTH_REMEDIATION = "Cursor is not authenticated — run `cursor-agent login` (the CLI calls it `agent login`), " +
|
|
15
|
+
"or set CURSOR_API_KEY for automation";
|
|
16
|
+
export const CODEX_AUTH_REMEDIATION = "Run `codex login` (or set OPENAI_API_KEY for automation)";
|
|
17
|
+
export const CLAUDE_AUTH_REMEDIATION = "Claude Code is not authenticated — run `claude auth login` (`/login` inside a session), " +
|
|
18
|
+
"or set ANTHROPIC_API_KEY for automation";
|
|
19
|
+
/**
|
|
20
|
+
* Used when text proves an auth failure but does not say which CLI printed it. All three
|
|
21
|
+
* runtimes print a bare `Not logged in`, so naming one of them there would be a guess — and
|
|
22
|
+
* a confidently wrong remediation (`cursor-agent login` for a Codex log) is worse than the
|
|
23
|
+
* generic crash reason it replaces.
|
|
24
|
+
*/
|
|
25
|
+
export const AMBIGUOUS_AUTH_REMEDIATION = "A runtime CLI reported it is not authenticated, but the log does not say which one — " +
|
|
26
|
+
"check the agent's runtime login: `cursor-agent login`, `codex login`, or `claude auth login`";
|
|
27
|
+
const KEYCHAIN_STUCK_PATTERNS = [
|
|
28
|
+
/errsecduplicateitem/i,
|
|
29
|
+
/security exit code 45/i,
|
|
30
|
+
/keychain item is stuck/i,
|
|
31
|
+
/couldn't save your login to the macOS keychain/i,
|
|
32
|
+
/could not save your login to the macOS keychain/i,
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Patterns are matched against raw CLI text, ANSI escapes and all — the captures keep
|
|
36
|
+
* theirs, and cursor-agent colourises the very lines we key on.
|
|
37
|
+
*/
|
|
38
|
+
const CURSOR_AUTH_PATTERNS = [
|
|
39
|
+
// `cursor-agent -p` when logged out (cursor-agent-print-logged-out.txt). The string
|
|
40
|
+
// NOT-133 was filed for: 12 sessions died on it while every pattern here missed.
|
|
41
|
+
/authentication required/i,
|
|
42
|
+
// Same line's remediation. Kept as a second anchor because remediation text drifts less
|
|
43
|
+
// than error prose — if the sentence above is reworded again, this still catches it.
|
|
44
|
+
/set CURSOR_API_KEY/i,
|
|
45
|
+
// `cursor-agent status` when logged out (cursor-agent-status-logged-out.txt) — exits 0.
|
|
46
|
+
/not logged in/i,
|
|
47
|
+
// `cursor-agent -p` with a bad CURSOR_API_KEY (cursor-agent-print-invalid-api-key.txt).
|
|
48
|
+
/the provided api key is invalid/i,
|
|
49
|
+
// Retained from NOT-114. No capture prints these, but removing them could only narrow
|
|
50
|
+
// coverage of CLI versions nobody has captured.
|
|
51
|
+
/login required/i,
|
|
52
|
+
/not authenticated/i,
|
|
53
|
+
];
|
|
54
|
+
const CODEX_AUTH_PATTERNS = [
|
|
55
|
+
// `codex login status` when logged out (codex-login-status-logged-out.txt) — exits 1.
|
|
56
|
+
/not logged in/i,
|
|
57
|
+
// `codex exec` when logged out (codex-exec-logged-out.txt): the session spawns and then
|
|
58
|
+
// fails every request, so this is what a dead codex session's log actually contains. Every
|
|
59
|
+
// such line in the capture names the endpoint it was refused by, and that qualifier stays
|
|
60
|
+
// in the pattern: a bare `401 Unauthorized` is not Codex's voice — the agent's own test run
|
|
61
|
+
// or curl shares the CLI's stderr, and an unrelated API's 401 must not read as a login.
|
|
62
|
+
/401 unauthorized[^\n]*api\.openai\.com/i,
|
|
63
|
+
/missing bearer or basic authentication/i,
|
|
64
|
+
];
|
|
65
|
+
const CLAUDE_AUTH_PATTERNS = [
|
|
66
|
+
// `claude -p` when logged out (claude-print-logged-out.txt): "Not logged in · Please run /login".
|
|
67
|
+
/not logged in/i,
|
|
68
|
+
/please run \/login/i,
|
|
69
|
+
// `claude auth status` when logged out (claude-auth-status-logged-out.txt) — exits 0, so
|
|
70
|
+
// the JSON body is the only signal.
|
|
71
|
+
/"loggedIn"\s*:\s*false/i,
|
|
72
|
+
// `claude -p` with an invalid ANTHROPIC_API_KEY (claude-print-invalid-api-key.txt).
|
|
73
|
+
/failed to authenticate/i,
|
|
74
|
+
/api key is invalid/i,
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* Which CLI *wrote* this text, as opposed to what it said. The auth pattern lists above
|
|
78
|
+
* overlap by design — `not logged in` is printed verbatim by cursor-agent, codex and claude
|
|
79
|
+
* alike — so they answer "is this an auth failure", never "whose failure is it". These
|
|
80
|
+
* anchors answer the second question: each names its own vendor, binary or credential
|
|
81
|
+
* variable, which no other runtime has a reason to print. Anything that matches none of them
|
|
82
|
+
* (or more than one) is left unattributed rather than guessed at.
|
|
83
|
+
*/
|
|
84
|
+
const RUNTIME_VENDOR_ANCHORS = {
|
|
85
|
+
// "set CURSOR_API_KEY environment variable" / "run `cursor-agent login`".
|
|
86
|
+
cursor_local: [/cursor[_-]api[_-]key/i, /cursor-agent\b/i],
|
|
87
|
+
// "OpenAI Codex v0.154.0", `codex login`, the `codex_api::` tracing target. A bare
|
|
88
|
+
// `openai` is not an anchor: any runtime's session can talk to api.openai.com.
|
|
89
|
+
codex_local: [/openai codex/i, /\bcodex\b/i],
|
|
90
|
+
// "Please run /login", ANTHROPIC_API_KEY, `claude auth login`. A bare `claude` is
|
|
91
|
+
// deliberately *not* an anchor: Cursor and Codex both name Claude models (`--model
|
|
92
|
+
// claude-...`), so it identifies a model, not the CLI that printed the line.
|
|
93
|
+
claude_code: [/anthropic/i, /please run \/login/i, /claude\s+(?:auth\s+)?login/i],
|
|
94
|
+
};
|
|
95
|
+
const AUTH_PATTERNS_BY_RUNTIME = {
|
|
96
|
+
cursor_local: CURSOR_AUTH_PATTERNS,
|
|
97
|
+
codex_local: CODEX_AUTH_PATTERNS,
|
|
98
|
+
claude_code: CLAUDE_AUTH_PATTERNS,
|
|
99
|
+
};
|
|
100
|
+
const ALL_RUNTIMES = ["cursor_local", "codex_local", "claude_code"];
|
|
101
|
+
const REMEDIATION_BY_RUNTIME = {
|
|
102
|
+
cursor_local: CURSOR_AUTH_REMEDIATION,
|
|
103
|
+
codex_local: CODEX_AUTH_REMEDIATION,
|
|
104
|
+
claude_code: CLAUDE_AUTH_REMEDIATION,
|
|
105
|
+
};
|
|
106
|
+
/** Operator-facing runtime names for prose built out of a classification. */
|
|
107
|
+
export const RUNTIME_AUTH_LABEL = {
|
|
108
|
+
cursor_local: "Cursor",
|
|
109
|
+
codex_local: "Codex",
|
|
110
|
+
claude_code: "Claude Code",
|
|
111
|
+
};
|
|
112
|
+
/** True when Cursor status/login/stderr indicates a stuck macOS keychain item. */
|
|
113
|
+
export function isCursorKeychainStuckOutput(output) {
|
|
114
|
+
return KEYCHAIN_STUCK_PATTERNS.some((re) => re.test(output));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Classify one runtime's status/login/stderr text.
|
|
118
|
+
* Cursor's keychain case wins over its plain logged-out case — same symptom, different fix.
|
|
119
|
+
*
|
|
120
|
+
* For callers that *know* what they just ran — the health preflight probes one CLI and reads
|
|
121
|
+
* its output. Callers holding a log plus an unreliable label want
|
|
122
|
+
* `runtimeAuthClassificationForLog`: several runtimes print the same `Not logged in`, so
|
|
123
|
+
* "does this read as Cursor's auth failure" is not the same question as "did Cursor write it".
|
|
124
|
+
*/
|
|
125
|
+
export function runtimeAuthIssueFromOutput(runtime, output) {
|
|
126
|
+
if (runtime === "cursor_local" && isCursorKeychainStuckOutput(output)) {
|
|
127
|
+
return { ...CURSOR_KEYCHAIN_HEALTH_ISSUE };
|
|
128
|
+
}
|
|
129
|
+
if (AUTH_PATTERNS_BY_RUNTIME[runtime].some((re) => re.test(output))) {
|
|
130
|
+
return { code: "runtime_auth", message: REMEDIATION_BY_RUNTIME[runtime] };
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Classify Cursor auth health from status/login/stderr text.
|
|
136
|
+
* Also usable by session failure classifiers (NOT-113) for the same remediation string.
|
|
137
|
+
*/
|
|
138
|
+
export function cursorAuthIssueFromOutput(output) {
|
|
139
|
+
return runtimeAuthIssueFromOutput("cursor_local", output);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Classify a log and say which CLI wrote it, without trusting a caller's label over the
|
|
143
|
+
* text. The failure-reason classifier reads a spawn log by path and its idea of the runtime
|
|
144
|
+
* is a hint at best: `worker_sessions.runtime` is nullable, and older rows can disagree with
|
|
145
|
+
* what actually ran.
|
|
146
|
+
*
|
|
147
|
+
* Attribution is deliberately conservative, in this order:
|
|
148
|
+
* 1. a single vendor anchor in text that also reads as that runtime's auth failure — this
|
|
149
|
+
* outranks `recordedRuntime`, because the log naming its own CLI is stronger evidence
|
|
150
|
+
* than a row saying what was supposed to run;
|
|
151
|
+
* 2. otherwise `recordedRuntime`, when its own patterns recognise the text (nothing in the
|
|
152
|
+
* log contradicts it, so the caller's label is the best evidence available);
|
|
153
|
+
* 3. otherwise, an auth failure only one runtime's patterns recognise at all;
|
|
154
|
+
* 4. otherwise unattributed (`runtime: null`) with the generic remediation.
|
|
155
|
+
*
|
|
156
|
+
* Trying the runtimes in a fixed order — or taking `recordedRuntime` at its word — reports
|
|
157
|
+
* Claude's `Not logged in · Please run /login` as Cursor, because Cursor's list carries the
|
|
158
|
+
* shared `not logged in` too, and hands the operator `cursor-agent login` for a Claude death.
|
|
159
|
+
*/
|
|
160
|
+
export function runtimeAuthClassificationForLog(output, recordedRuntime) {
|
|
161
|
+
// The keychain signature names Cursor on its own (errSecDuplicateItem / macOS keychain).
|
|
162
|
+
if (isCursorKeychainStuckOutput(output)) {
|
|
163
|
+
return { runtime: "cursor_local", issue: { ...CURSOR_KEYCHAIN_HEALTH_ISSUE } };
|
|
164
|
+
}
|
|
165
|
+
const matched = ALL_RUNTIMES.filter((r) => AUTH_PATTERNS_BY_RUNTIME[r].some((re) => re.test(output)));
|
|
166
|
+
if (matched.length === 0)
|
|
167
|
+
return null;
|
|
168
|
+
// Anchors are only consulted within the runtimes whose auth prose matched, so a log that
|
|
169
|
+
// merely mentions another CLI by name cannot steal the attribution.
|
|
170
|
+
const anchored = matched.filter((r) => RUNTIME_VENDOR_ANCHORS[r].some((re) => re.test(output)));
|
|
171
|
+
const named = anchored.length === 1
|
|
172
|
+
? anchored[0]
|
|
173
|
+
: recordedRuntime && matched.includes(recordedRuntime)
|
|
174
|
+
? recordedRuntime
|
|
175
|
+
: matched.length === 1
|
|
176
|
+
? matched[0]
|
|
177
|
+
: null;
|
|
178
|
+
if (named) {
|
|
179
|
+
return { runtime: named, issue: { code: "runtime_auth", message: REMEDIATION_BY_RUNTIME[named] } };
|
|
180
|
+
}
|
|
181
|
+
return { runtime: null, issue: { code: "runtime_auth", message: AMBIGUOUS_AUTH_REMEDIATION } };
|
|
182
|
+
}
|
|
183
|
+
/** `runtimeAuthClassificationForLog` for callers that hold no runtime label at all. */
|
|
184
|
+
export function anyRuntimeAuthIssueFromOutput(output) {
|
|
185
|
+
return runtimeAuthClassificationForLog(output, null);
|
|
186
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// packages/shared/src/runtime-auth-health.test.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-133: these assertions run against verbatim CLI captures (src/fixtures/runtime-auth/),
|
|
4
|
+
// not phrasings a test author guessed at. The bug being regression-tested is exactly the
|
|
5
|
+
// gap that hand-written expectations cannot catch — the old suite asserted "login required"
|
|
6
|
+
// while cursor-agent prints "Authentication required".
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { AMBIGUOUS_AUTH_REMEDIATION, CLAUDE_AUTH_REMEDIATION, CODEX_AUTH_REMEDIATION, CURSOR_AUTH_REMEDIATION, CURSOR_KEYCHAIN_HEALTH_ISSUE, anyRuntimeAuthIssueFromOutput, cursorAuthIssueFromOutput, isCursorKeychainStuckOutput, runtimeAuthClassificationForLog, runtimeAuthIssueFromOutput, } from "./runtime-auth-health.js";
|
|
13
|
+
const FIXTURES = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures/runtime-auth");
|
|
14
|
+
function capture(name) {
|
|
15
|
+
return fs.readFileSync(path.join(FIXTURES, name), "utf8");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The NOT-133 stderr, byte for byte. Asserting the capture's own content here means a
|
|
19
|
+
* fixture silently replaced by a paraphrase fails this test instead of passing quietly.
|
|
20
|
+
*/
|
|
21
|
+
test("the cursor-agent logged-out capture is the string the incident reported", () => {
|
|
22
|
+
assert.equal(capture("cursor-agent-print-logged-out.txt"), "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n");
|
|
23
|
+
});
|
|
24
|
+
test("cursorAuthIssueFromOutput classifies the verbatim cursor-agent logged-out stderr", () => {
|
|
25
|
+
const issue = cursorAuthIssueFromOutput(capture("cursor-agent-print-logged-out.txt"));
|
|
26
|
+
assert.equal(issue?.code, "runtime_auth");
|
|
27
|
+
assert.equal(issue?.message, CURSOR_AUTH_REMEDIATION);
|
|
28
|
+
});
|
|
29
|
+
test("the cursor remediation offers both routes the CLI itself offers", () => {
|
|
30
|
+
// The old message named only `cursor-agent login` while the CLI offers login *or* an API key.
|
|
31
|
+
assert.match(CURSOR_AUTH_REMEDIATION, /cursor-agent login/);
|
|
32
|
+
assert.match(CURSOR_AUTH_REMEDIATION, /agent login/);
|
|
33
|
+
assert.match(CURSOR_AUTH_REMEDIATION, /CURSOR_API_KEY/);
|
|
34
|
+
});
|
|
35
|
+
test("cursorAuthIssueFromOutput classifies the verbatim `cursor-agent status` logged-out capture", () => {
|
|
36
|
+
// The admission-gate half: the preflight classifies `status`, not `-p` stderr.
|
|
37
|
+
const issue = cursorAuthIssueFromOutput(capture("cursor-agent-status-logged-out.txt"));
|
|
38
|
+
assert.equal(issue?.code, "runtime_auth");
|
|
39
|
+
});
|
|
40
|
+
test("cursorAuthIssueFromOutput classifies a rejected CURSOR_API_KEY", () => {
|
|
41
|
+
const issue = cursorAuthIssueFromOutput(capture("cursor-agent-print-invalid-api-key.txt"));
|
|
42
|
+
assert.equal(issue?.code, "runtime_auth");
|
|
43
|
+
});
|
|
44
|
+
test("cursorAuthIssueFromOutput passes a logged-in `cursor-agent status` capture (ANSI included)", () => {
|
|
45
|
+
assert.equal(cursorAuthIssueFromOutput(capture("cursor-agent-status-logged-in.txt")), null);
|
|
46
|
+
});
|
|
47
|
+
test("codex logged-out captures classify as runtime_auth", () => {
|
|
48
|
+
for (const name of ["codex-login-status-logged-out.txt", "codex-exec-logged-out.txt"]) {
|
|
49
|
+
const issue = runtimeAuthIssueFromOutput("codex_local", capture(name));
|
|
50
|
+
assert.equal(issue?.code, "runtime_auth", `${name} should classify`);
|
|
51
|
+
assert.equal(issue?.message, CODEX_AUTH_REMEDIATION);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
test("someone else's 401 is not the runtime's login problem", () => {
|
|
55
|
+
// A worker shares stderr with everything it spawns, so an unrelated API refusing the
|
|
56
|
+
// *agent's own* request would otherwise be reported as "run `codex login`". The captured
|
|
57
|
+
// Codex lines all name the endpoint that refused them; that qualifier is what separates
|
|
58
|
+
// them from a test suite's 401.
|
|
59
|
+
const unrelated = " ✗ POST /v1/things 401 Unauthorized (expected 200)\n" +
|
|
60
|
+
"1 failing test: Error: request failed with 401 Unauthorized\n";
|
|
61
|
+
assert.equal(runtimeAuthIssueFromOutput("codex_local", unrelated), null);
|
|
62
|
+
assert.equal(anyRuntimeAuthIssueFromOutput(unrelated), null);
|
|
63
|
+
// The real capture is still classified.
|
|
64
|
+
assert.match(capture("codex-exec-logged-out.txt"), /401 Unauthorized/);
|
|
65
|
+
assert.equal(runtimeAuthIssueFromOutput("codex_local", capture("codex-exec-logged-out.txt"))?.code, "runtime_auth");
|
|
66
|
+
});
|
|
67
|
+
test("a logged-in `codex login status` capture is not a runtime_auth issue", () => {
|
|
68
|
+
assert.equal(runtimeAuthIssueFromOutput("codex_local", capture("codex-login-status-logged-in.txt")), null);
|
|
69
|
+
});
|
|
70
|
+
test("claude logged-out captures classify as runtime_auth", () => {
|
|
71
|
+
for (const name of [
|
|
72
|
+
"claude-print-logged-out.txt",
|
|
73
|
+
"claude-print-invalid-api-key.txt",
|
|
74
|
+
"claude-auth-status-logged-out.txt",
|
|
75
|
+
]) {
|
|
76
|
+
const issue = runtimeAuthIssueFromOutput("claude_code", capture(name));
|
|
77
|
+
assert.equal(issue?.code, "runtime_auth", `${name} should classify`);
|
|
78
|
+
assert.equal(issue?.message, CLAUDE_AUTH_REMEDIATION);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
test("a logged-in `claude auth status` capture is not a runtime_auth issue", () => {
|
|
82
|
+
assert.equal(runtimeAuthIssueFromOutput("claude_code", capture("claude-auth-status-logged-in.txt")), null);
|
|
83
|
+
});
|
|
84
|
+
test("anyRuntimeAuthIssueFromOutput names the runtime a log came from", () => {
|
|
85
|
+
const cursor = anyRuntimeAuthIssueFromOutput(capture("cursor-agent-print-logged-out.txt"));
|
|
86
|
+
assert.equal(cursor?.runtime, "cursor_local");
|
|
87
|
+
assert.equal(cursor?.issue.message, CURSOR_AUTH_REMEDIATION);
|
|
88
|
+
const codex = anyRuntimeAuthIssueFromOutput(capture("codex-exec-logged-out.txt"));
|
|
89
|
+
assert.equal(codex?.runtime, "codex_local");
|
|
90
|
+
assert.equal(codex?.issue.message, CODEX_AUTH_REMEDIATION);
|
|
91
|
+
const claude = anyRuntimeAuthIssueFromOutput(capture("claude-print-invalid-api-key.txt"));
|
|
92
|
+
assert.equal(claude?.runtime, "claude_code");
|
|
93
|
+
assert.equal(claude?.issue.message, CLAUDE_AUTH_REMEDIATION);
|
|
94
|
+
assert.equal(anyRuntimeAuthIssueFromOutput(capture("codex-login-status-logged-in.txt")), null);
|
|
95
|
+
});
|
|
96
|
+
test("a Claude log with no recorded runtime is not reported as Cursor", () => {
|
|
97
|
+
// `Not logged in · Please run /login`. Cursor's pattern list also carries "not logged in",
|
|
98
|
+
// so trying the runtimes in a fixed order attributed this capture — and Cursor's
|
|
99
|
+
// remediation — to Cursor. `/login` is Claude's own wording and settles it.
|
|
100
|
+
const claude = anyRuntimeAuthIssueFromOutput(capture("claude-print-logged-out.txt"));
|
|
101
|
+
assert.equal(claude?.runtime, "claude_code");
|
|
102
|
+
assert.equal(claude?.issue.message, CLAUDE_AUTH_REMEDIATION);
|
|
103
|
+
assert.doesNotMatch(claude.issue.message, /cursor/i);
|
|
104
|
+
});
|
|
105
|
+
test("a Claude `auth status` JSON body with no recorded runtime is attributed to Claude", () => {
|
|
106
|
+
const claude = anyRuntimeAuthIssueFromOutput(capture("claude-auth-status-logged-out.txt"));
|
|
107
|
+
assert.equal(claude?.runtime, "claude_code");
|
|
108
|
+
assert.equal(claude?.issue.message, CLAUDE_AUTH_REMEDIATION);
|
|
109
|
+
});
|
|
110
|
+
test("a Codex exec log with no recorded runtime is attributed to Codex, not Cursor", () => {
|
|
111
|
+
const codex = anyRuntimeAuthIssueFromOutput(capture("codex-exec-logged-out.txt"));
|
|
112
|
+
assert.equal(codex?.runtime, "codex_local");
|
|
113
|
+
assert.doesNotMatch(codex.issue.message, /cursor/i);
|
|
114
|
+
});
|
|
115
|
+
test("a bare `Not logged in` is classified as auth but attributed to no runtime", () => {
|
|
116
|
+
// cursor-agent status and codex login status print the identical line: the two captures are
|
|
117
|
+
// byte-for-byte the same, so nothing in the text can name the CLI that wrote it.
|
|
118
|
+
assert.equal(capture("cursor-agent-status-logged-out.txt"), capture("codex-login-status-logged-out.txt"));
|
|
119
|
+
for (const name of ["cursor-agent-status-logged-out.txt", "codex-login-status-logged-out.txt"]) {
|
|
120
|
+
const classified = anyRuntimeAuthIssueFromOutput(capture(name));
|
|
121
|
+
assert.equal(classified?.issue.code, "runtime_auth", `${name} is still an auth failure`);
|
|
122
|
+
assert.equal(classified?.runtime, null, `${name} must not be attributed to a runtime`);
|
|
123
|
+
assert.equal(classified?.issue.message, AMBIGUOUS_AUTH_REMEDIATION);
|
|
124
|
+
// The generic remediation covers every runtime rather than betting on one.
|
|
125
|
+
assert.match(classified.issue.message, /cursor-agent login/);
|
|
126
|
+
assert.match(classified.issue.message, /codex login/);
|
|
127
|
+
assert.match(classified.issue.message, /claude auth login/);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
test("a known runtime still gets its own remediation for the shared `Not logged in`", () => {
|
|
131
|
+
// The ambiguity above only applies when the caller cannot say what ran. The health
|
|
132
|
+
// preflight always can, and must keep naming the CLI it just probed.
|
|
133
|
+
const status = capture("cursor-agent-status-logged-out.txt");
|
|
134
|
+
assert.equal(runtimeAuthIssueFromOutput("cursor_local", status)?.message, CURSOR_AUTH_REMEDIATION);
|
|
135
|
+
assert.equal(runtimeAuthIssueFromOutput("codex_local", status)?.message, CODEX_AUTH_REMEDIATION);
|
|
136
|
+
});
|
|
137
|
+
test("a log that names its own CLI outranks a conflicting recorded runtime", () => {
|
|
138
|
+
// The worst outcome of trusting the row: the operator is told to run `cursor-agent login`
|
|
139
|
+
// because a session row said cursor_local, while the log is Claude's own logged-out line.
|
|
140
|
+
for (const recorded of ["cursor_local", "codex_local"]) {
|
|
141
|
+
const claude = runtimeAuthClassificationForLog(capture("claude-print-logged-out.txt"), recorded);
|
|
142
|
+
assert.equal(claude?.runtime, "claude_code", `recorded ${recorded} must not win`);
|
|
143
|
+
assert.equal(claude?.issue.message, CLAUDE_AUTH_REMEDIATION);
|
|
144
|
+
assert.doesNotMatch(claude.issue.message, /cursor|codex/i);
|
|
145
|
+
}
|
|
146
|
+
const codex = runtimeAuthClassificationForLog(capture("codex-exec-logged-out.txt"), "claude_code");
|
|
147
|
+
assert.equal(codex?.runtime, "codex_local");
|
|
148
|
+
assert.equal(codex?.issue.message, CODEX_AUTH_REMEDIATION);
|
|
149
|
+
const cursor = runtimeAuthClassificationForLog(capture("cursor-agent-print-logged-out.txt"), "claude_code");
|
|
150
|
+
assert.equal(cursor?.runtime, "cursor_local");
|
|
151
|
+
assert.equal(cursor?.issue.message, CURSOR_AUTH_REMEDIATION);
|
|
152
|
+
});
|
|
153
|
+
test("a recorded runtime settles text that names no CLI", () => {
|
|
154
|
+
// `Not logged in` on its own: no anchor either way, so the caller's label is the best
|
|
155
|
+
// evidence there is — and beats the generic three-CLI remediation.
|
|
156
|
+
const shared = capture("codex-login-status-logged-out.txt");
|
|
157
|
+
assert.equal(runtimeAuthClassificationForLog(shared, "codex_local")?.runtime, "codex_local");
|
|
158
|
+
assert.equal(runtimeAuthClassificationForLog(shared, "cursor_local")?.runtime, "cursor_local");
|
|
159
|
+
assert.equal(runtimeAuthClassificationForLog(shared, null)?.runtime, null);
|
|
160
|
+
});
|
|
161
|
+
test("a recorded runtime cannot invent an auth failure that no capture matches", () => {
|
|
162
|
+
assert.equal(runtimeAuthClassificationForLog(capture("cursor-agent-status-logged-in.txt"), "cursor_local"), null);
|
|
163
|
+
});
|
|
164
|
+
test("a stuck keychain is attributed to Cursor even with no recorded runtime", () => {
|
|
165
|
+
const classified = anyRuntimeAuthIssueFromOutput(RECONSTRUCTED_KEYCHAIN_STDERR);
|
|
166
|
+
assert.equal(classified?.runtime, "cursor_local");
|
|
167
|
+
assert.equal(classified?.issue.code, "cursor_keychain");
|
|
168
|
+
});
|
|
169
|
+
// NOT-114's keychain branch has no capture — the stuck-keychain state cannot be provoked
|
|
170
|
+
// without corrupting the operator's real keychain, so this stays a reconstruction of the
|
|
171
|
+
// reported stderr and is labelled as such rather than filed under fixtures/.
|
|
172
|
+
const RECONSTRUCTED_KEYCHAIN_STDERR = `Cursor couldn't save your login to the macOS keychain (errSecDuplicateItem, security exit code 45).
|
|
173
|
+
The keychain item is stuck. Delete it and sign in again:
|
|
174
|
+
security delete-generic-password -s cursor-access-token -a cursor-user
|
|
175
|
+
agent login
|
|
176
|
+
`;
|
|
177
|
+
test("isCursorKeychainStuckOutput detects errSecDuplicateItem", () => {
|
|
178
|
+
assert.equal(isCursorKeychainStuckOutput(RECONSTRUCTED_KEYCHAIN_STDERR), true);
|
|
179
|
+
});
|
|
180
|
+
test("isCursorKeychainStuckOutput detects security exit code 45", () => {
|
|
181
|
+
assert.equal(isCursorKeychainStuckOutput("security exit code 45 while saving token"), true);
|
|
182
|
+
});
|
|
183
|
+
test("isCursorKeychainStuckOutput detects keychain item is stuck prose", () => {
|
|
184
|
+
assert.equal(isCursorKeychainStuckOutput("The keychain item is stuck."), true);
|
|
185
|
+
});
|
|
186
|
+
test("isCursorKeychainStuckOutput ignores healthy status", () => {
|
|
187
|
+
assert.equal(isCursorKeychainStuckOutput(capture("cursor-agent-status-logged-in.txt")), false);
|
|
188
|
+
assert.equal(isCursorKeychainStuckOutput(capture("cursor-agent-status-logged-out.txt")), false);
|
|
189
|
+
});
|
|
190
|
+
test("the keychain classification still wins over the plain logged-out one", () => {
|
|
191
|
+
const issue = cursorAuthIssueFromOutput(RECONSTRUCTED_KEYCHAIN_STDERR);
|
|
192
|
+
assert.equal(issue?.code, "cursor_keychain");
|
|
193
|
+
assert.equal(issue?.message, CURSOR_KEYCHAIN_HEALTH_ISSUE.message);
|
|
194
|
+
assert.match(issue.message, /security delete-generic-password/);
|
|
195
|
+
assert.match(issue.message, /cursor-access-token/);
|
|
196
|
+
assert.match(issue.message, /agent login|cursor-agent login/);
|
|
197
|
+
});
|
|
198
|
+
test("cursorAuthIssueFromOutput returns null for clean output", () => {
|
|
199
|
+
assert.equal(cursorAuthIssueFromOutput("✓ Logged in\n"), null);
|
|
200
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Runtime } from "./runtime.js";
|
|
3
|
+
export declare const RuntimeAvailabilityAvailable: z.ZodObject<{
|
|
4
|
+
available: z.ZodLiteral<true>;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
available: true;
|
|
7
|
+
}, {
|
|
8
|
+
available: true;
|
|
9
|
+
}>;
|
|
10
|
+
export type RuntimeAvailabilityAvailable = z.infer<typeof RuntimeAvailabilityAvailable>;
|
|
11
|
+
export declare const RuntimeAvailabilityCapped: z.ZodObject<{
|
|
12
|
+
available: z.ZodLiteral<false>;
|
|
13
|
+
until: z.ZodString;
|
|
14
|
+
reason: z.ZodString;
|
|
15
|
+
}, "strip", z.ZodTypeAny, {
|
|
16
|
+
reason: string;
|
|
17
|
+
available: false;
|
|
18
|
+
until: string;
|
|
19
|
+
}, {
|
|
20
|
+
reason: string;
|
|
21
|
+
available: false;
|
|
22
|
+
until: string;
|
|
23
|
+
}>;
|
|
24
|
+
export type RuntimeAvailabilityCapped = z.infer<typeof RuntimeAvailabilityCapped>;
|
|
25
|
+
export declare const RuntimeAvailability: z.ZodDiscriminatedUnion<"available", [z.ZodObject<{
|
|
26
|
+
available: z.ZodLiteral<true>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
available: true;
|
|
29
|
+
}, {
|
|
30
|
+
available: true;
|
|
31
|
+
}>, z.ZodObject<{
|
|
32
|
+
available: z.ZodLiteral<false>;
|
|
33
|
+
until: z.ZodString;
|
|
34
|
+
reason: z.ZodString;
|
|
35
|
+
}, "strip", z.ZodTypeAny, {
|
|
36
|
+
reason: string;
|
|
37
|
+
available: false;
|
|
38
|
+
until: string;
|
|
39
|
+
}, {
|
|
40
|
+
reason: string;
|
|
41
|
+
available: false;
|
|
42
|
+
until: string;
|
|
43
|
+
}>]>;
|
|
44
|
+
export type RuntimeAvailability = z.infer<typeof RuntimeAvailability>;
|
|
45
|
+
export interface RuntimeAvailabilityRecord {
|
|
46
|
+
runtime: Runtime;
|
|
47
|
+
unavailableUntil: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
evidenceJson: string | null;
|
|
50
|
+
observedAt: string;
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const RuntimeAvailabilityAvailable = z.object({
|
|
3
|
+
available: z.literal(true),
|
|
4
|
+
});
|
|
5
|
+
export const RuntimeAvailabilityCapped = z.object({
|
|
6
|
+
available: z.literal(false),
|
|
7
|
+
until: z.string(),
|
|
8
|
+
reason: z.string(),
|
|
9
|
+
});
|
|
10
|
+
export const RuntimeAvailability = z.discriminatedUnion("available", [
|
|
11
|
+
RuntimeAvailabilityAvailable,
|
|
12
|
+
RuntimeAvailabilityCapped,
|
|
13
|
+
]);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
export const Runtime = z.enum(["claude_code", "cursor_local"]);
|
|
2
|
+
export const Runtime = z.enum(["claude_code", "cursor_local", "codex_local"]);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const UsageEvent: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
issueId: z.ZodString;
|
|
5
|
+
workerSessionId: z.ZodString;
|
|
6
|
+
role: z.ZodEnum<["developer", "reviewer", "legacy"]>;
|
|
7
|
+
runtime: z.ZodNullable<z.ZodString>;
|
|
8
|
+
tokensIn: z.ZodNullable<z.ZodNumber>;
|
|
9
|
+
tokensOut: z.ZodNullable<z.ZodNumber>;
|
|
10
|
+
costUsd: z.ZodNullable<z.ZodNumber>;
|
|
11
|
+
durationMs: z.ZodNullable<z.ZodNumber>;
|
|
12
|
+
ts: z.ZodString;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
id: string;
|
|
15
|
+
issueId: string;
|
|
16
|
+
role: "developer" | "reviewer" | "legacy";
|
|
17
|
+
runtime: string | null;
|
|
18
|
+
workerSessionId: string;
|
|
19
|
+
ts: string;
|
|
20
|
+
tokensIn: number | null;
|
|
21
|
+
tokensOut: number | null;
|
|
22
|
+
costUsd: number | null;
|
|
23
|
+
durationMs: number | null;
|
|
24
|
+
}, {
|
|
25
|
+
id: string;
|
|
26
|
+
issueId: string;
|
|
27
|
+
role: "developer" | "reviewer" | "legacy";
|
|
28
|
+
runtime: string | null;
|
|
29
|
+
workerSessionId: string;
|
|
30
|
+
ts: string;
|
|
31
|
+
tokensIn: number | null;
|
|
32
|
+
tokensOut: number | null;
|
|
33
|
+
costUsd: number | null;
|
|
34
|
+
durationMs: number | null;
|
|
35
|
+
}>;
|
|
36
|
+
export type UsageEvent = z.infer<typeof UsageEvent>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const UsageEvent = z.object({
|
|
3
|
+
id: z.string().uuid(),
|
|
4
|
+
issueId: z.string().uuid(),
|
|
5
|
+
workerSessionId: z.string().uuid(),
|
|
6
|
+
role: z.enum(["developer", "reviewer", "legacy"]),
|
|
7
|
+
runtime: z.string().nullable(),
|
|
8
|
+
tokensIn: z.number().int().nullable(),
|
|
9
|
+
tokensOut: z.number().int().nullable(),
|
|
10
|
+
costUsd: z.number().nullable(),
|
|
11
|
+
durationMs: z.number().int().nullable(),
|
|
12
|
+
ts: z.string(),
|
|
13
|
+
});
|