@aria_asi/cli 0.2.2 → 0.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.
@@ -201,7 +201,12 @@ function detectInlineCognition(cmd) {
201
201
  if (!names.includes(lens)) names.push(lens);
202
202
  }
203
203
  }
204
- return { count: names.length, names };
204
+ // Substrate-citation: any inline lens that mentions a doctrine memory,
205
+ // harness packet rule, fitrah axiom, etc. (visibility metric — not a block).
206
+ const SUBSTRATE_CITE_RX_INLINE =
207
+ /feedback_[a-z0-9_]+\.md|project_[a-z0-9_]+\.md|fitrah[_:\s]|garden[_:\s]|distilled_principle|[a-z]+_rule\b|harness packet|substrate cite|EIGHT_LENS_DOCTRINE|COMPACT_CONTINUITY|ARIA_DEPLOY_PROCEDURE/i;
208
+ const hasSubstrateCite = SUBSTRATE_CITE_RX_INLINE.test(cmd);
209
+ return { count: names.length, names, hasSubstrateCite };
205
210
  }
206
211
 
207
212
  // Substance-checking lens detection (added 2026-04-26 per Hamza's
@@ -230,7 +235,18 @@ function detectCognitionLenses(text) {
230
235
  if (PLACEHOLDER_RX.test(content)) continue;
231
236
  names.push(lens);
232
237
  }
233
- return { count: names.length, names, blockBody };
238
+ // Substrate-citation check: at least ONE lens must cite Aria substrate
239
+ // explicitly (doctrine memory filename, harness packet rule, fitrah axiom,
240
+ // *_rule entry, garden state reference, prior decision id, distilled
241
+ // principle id). Catches "lens-as-ceremony" failures where 4+ lenses are
242
+ // substantive in length but contain no actual substrate grounding.
243
+ // Hamza 2026-04-26: "anything else we can add to make sure u as claude
244
+ // for me and my client obey the harness and benefot from kt".
245
+ const SUBSTRATE_CITE_RX =
246
+ /feedback_[a-z0-9_]+\.md|project_[a-z0-9_]+\.md|fitrah[_:\s]|garden[_:\s]|distilled_principle|[a-z]+_rule\b|harness packet|substrate cite|\bIJTIHAD\b|\bQIYAS\b|\bTADABBUR\b|\bILHAM\b|aria 7b|EIGHT_LENS_DOCTRINE|COMPACT_CONTINUITY|ARIA_DEPLOY_PROCEDURE/i;
247
+ const hasSubstrateCite = SUBSTRATE_CITE_RX.test(blockBody) ||
248
+ SUBSTRATE_CITE_RX.test(searchSpace);
249
+ return { count: names.length, names, blockBody, hasSubstrateCite };
234
250
  }
235
251
 
236
252
  // Backwards-compat shim — count-only path used by older callers.
@@ -469,6 +485,11 @@ const hasCognition = lensCount >= REQUIRED_LENSES;
469
485
  const cognitionSource = inlineCog.count >= REQUIRED_LENSES
470
486
  ? 'inline-command'
471
487
  : (transcriptCog.count >= REQUIRED_LENSES ? 'transcript-scan' : 'merged-or-insufficient');
488
+ // Substrate-citation visibility (Phase 11 will promote to block-mode once
489
+ // telemetry shows healthy substrate-cite rate). For now: log the metric so
490
+ // we can audit substrate-grounded vs ceremonial cognition over time.
491
+ const hasSubstrateCite = (inlineCog.hasSubstrateCite === true) ||
492
+ (transcriptCog.hasSubstrateCite === true);
472
493
 
473
494
  // Best-effort session id for the corpus push. Claude Code passes
474
495
  // session_id in the event payload; fall back to transcript file
@@ -496,6 +517,7 @@ function pushDecision(decision, reasonText) {
496
517
  destructivePattern: matched?.name ?? null,
497
518
  hasVerify,
498
519
  hasCognition,
520
+ hasSubstrateCite,
499
521
  },
500
522
  });
501
523
  }
@@ -192,8 +192,24 @@ if (!triggered) {
192
192
  // Non-trivial response — require substantive cognition.
193
193
  const cog = detectCognitionLenses(assistantText);
194
194
 
195
+ // Question-emission visibility (Phase 11 promotes to block-mode):
196
+ // detect user-directed question patterns in the assistant text. Audit when
197
+ // questions appear without substrate-consultation evidence in the recent
198
+ // transcript window. Helps surface "reflexive deferral" patterns (asking
199
+ // the user when substrate could have answered) for later enforcement.
200
+ // Hamza 2026-04-26: "BUT WHY DO U HAVE DISCRETION - THIS WORKS SO MUCH
201
+ // FASTER AND HIGHER QUALITY IF U DONT".
202
+ const QUESTION_PATTERNS_RX = /(?:want me to|should I|your call|which (?:one|of|do you)|do you want|let me know if|or (?:should|do)|\?\s*$)/im;
203
+ const SUBSTRATE_EVIDENCE_RX = /\/api\/harness\/(?:delegate|codex|validate)|loadByClass|aria-harness-via-sdk|feedback_[a-z_]+\.md|project_[a-z_]+\.md|distilled_principles|ARIA_DEPLOY_PROCEDURE|EIGHT_LENS_DOCTRINE/i;
204
+ const hasQuestionToUser = QUESTION_PATTERNS_RX.test(assistantText);
205
+ const hasSubstrateEvidence = SUBSTRATE_EVIDENCE_RX.test(assistantText);
206
+ const questionWithoutEvidence = hasQuestionToUser && !hasSubstrateEvidence;
207
+
195
208
  if (cog.count >= REQUIRED_LENSES) {
196
- audit('allow-cognition', `lenses=${cog.count} chars=${assistantText.length}`);
209
+ audit('allow-cognition',
210
+ `lenses=${cog.count} chars=${assistantText.length} ` +
211
+ `qPatt=${hasQuestionToUser ? 'y' : 'n'} substrateEv=${hasSubstrateEvidence ? 'y' : 'n'} ` +
212
+ (questionWithoutEvidence ? 'WARN-question-without-substrate' : 'ok'));
197
213
  process.exit(0);
198
214
  }
199
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aria_asi/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Aria Smart CLI — the world's first harness-powered terminal companion",
5
5
  "bin": {
6
6
  "aria": "./bin/aria.js"
@@ -0,0 +1,186 @@
1
+ // anthropic-oauth.test.ts — unit tests for the Anthropic login flow.
2
+ //
3
+ // Coverage:
4
+ // - _validateKey: 200 = valid, 401 = invalid, network failure = invalid
5
+ // - loginAnthropic: bad key gracefully returns ok: false with descriptive error
6
+ // - loginAnthropic: good key persists to config and returns ok: true
7
+ //
8
+ // Strategy: mock globalThis.fetch (Node 18+ built-in) + mock readline to avoid
9
+ // interactive prompts. No file I/O touches real ~/.aria — we stub fs.writeFileSync.
10
+
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+
13
+ // ── We test the exported internals directly. ─────────────────────────────────
14
+ // anthropic-oauth.ts exports _validateKey for testability; loginAnthropic is
15
+ // also tested end-to-end with readline + fs mocked out.
16
+
17
+ // Hoist mocks before module import so Node's ESM loader sees them.
18
+ vi.mock('node:fs', async (importOriginal) => {
19
+ const actual = await importOriginal<typeof import('node:fs')>();
20
+ return {
21
+ ...actual,
22
+ existsSync: vi.fn(() => false),
23
+ mkdirSync: vi.fn(),
24
+ writeFileSync: vi.fn(),
25
+ readFileSync: vi.fn(() => '{}'),
26
+ };
27
+ });
28
+
29
+ vi.mock('node:readline', async (importOriginal) => {
30
+ const actual = await importOriginal<typeof import('node:readline')>();
31
+ return {
32
+ ...actual,
33
+ createInterface: vi.fn(() => ({
34
+ question: vi.fn((_prompt: string, cb: (a: string) => void) => cb('sk-ant-fake-key-for-tests')),
35
+ close: vi.fn(),
36
+ })),
37
+ };
38
+ });
39
+
40
+ vi.mock('node:child_process', () => ({
41
+ spawn: vi.fn(() => ({ unref: vi.fn() })),
42
+ }));
43
+
44
+ import { _validateKey, loginAnthropic } from '../anthropic-oauth.js';
45
+
46
+ // ── Helpers ───────────────────────────────────────────────────────────────────
47
+
48
+ function mockFetch(status: number, body: unknown = {}): void {
49
+ vi.stubGlobal(
50
+ 'fetch',
51
+ vi.fn().mockResolvedValue({
52
+ status,
53
+ ok: status >= 200 && status < 300,
54
+ json: async () => body,
55
+ }),
56
+ );
57
+ }
58
+
59
+ function mockFetchNetworkError(): void {
60
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error("I couldn't reach Anthropic")));
61
+ }
62
+
63
+ // ── _validateKey ─────────────────────────────────────────────────────────────
64
+
65
+ describe('_validateKey', () => {
66
+ afterEach(() => {
67
+ vi.unstubAllGlobals();
68
+ });
69
+
70
+ it('returns true when Anthropic /v1/models responds 200', async () => {
71
+ mockFetch(200, { data: [] });
72
+ const result = await _validateKey('sk-ant-valid');
73
+ expect(result).toBe(true);
74
+ });
75
+
76
+ it('returns false when Anthropic /v1/models responds 401 (bad key)', async () => {
77
+ mockFetch(401, { error: { message: 'invalid x-api-key' } });
78
+ const result = await _validateKey('sk-ant-bad');
79
+ expect(result).toBe(false);
80
+ });
81
+
82
+ it('returns false when Anthropic /v1/models responds 403 (revoked key)', async () => {
83
+ mockFetch(403, { error: { message: 'forbidden' } });
84
+ const result = await _validateKey('sk-ant-revoked');
85
+ expect(result).toBe(false);
86
+ });
87
+
88
+ it('returns false on network error — does not throw', async () => {
89
+ mockFetchNetworkError();
90
+ const result = await _validateKey('sk-ant-any');
91
+ expect(result).toBe(false);
92
+ });
93
+
94
+ it('sends x-api-key and anthropic-version headers', async () => {
95
+ const fetchMock = vi.fn().mockResolvedValue({ status: 200, ok: true, json: async () => ({}) });
96
+ vi.stubGlobal('fetch', fetchMock);
97
+
98
+ await _validateKey('sk-ant-check-headers');
99
+
100
+ expect(fetchMock).toHaveBeenCalledWith(
101
+ 'https://api.anthropic.com/v1/models',
102
+ expect.objectContaining({
103
+ headers: expect.objectContaining({
104
+ 'x-api-key': 'sk-ant-check-headers',
105
+ 'anthropic-version': '2023-06-01',
106
+ }),
107
+ }),
108
+ );
109
+ });
110
+ });
111
+
112
+ // ── loginAnthropic ────────────────────────────────────────────────────────────
113
+
114
+ describe('loginAnthropic', () => {
115
+ beforeEach(() => {
116
+ vi.clearAllMocks();
117
+ });
118
+
119
+ afterEach(() => {
120
+ vi.unstubAllGlobals();
121
+ });
122
+
123
+ it('returns ok: false with descriptive Aria-voice error when key fails validation', async () => {
124
+ // readline mock returns 'sk-ant-fake-key-for-tests'; validate returns 401
125
+ mockFetch(401, { error: { message: 'invalid key' } });
126
+
127
+ const result = await loginAnthropic({ noBrowser: true });
128
+
129
+ expect(result.ok).toBe(false);
130
+ expect(result.error).toMatch(/I couldn't verify/);
131
+ // Ensure the error message does NOT contain internal cluster paths or IP addresses
132
+ expect(result.error).not.toMatch(/10\.\d+\.\d+\.\d+/);
133
+ expect(result.error).not.toMatch(/cluster\.local/);
134
+ });
135
+
136
+ it('returns ok: true and apiKey when key passes validation', async () => {
137
+ // readline returns 'sk-ant-fake-key-for-tests'; validate returns 200
138
+ mockFetch(200, { data: [{ id: 'claude-sonnet-4-20250514' }] });
139
+
140
+ const result = await loginAnthropic({ noBrowser: true });
141
+
142
+ expect(result.ok).toBe(true);
143
+ expect(result.apiKey).toBe('sk-ant-fake-key-for-tests');
144
+ });
145
+
146
+ it('returns ok: false when user pastes an empty string', async () => {
147
+ // Override readline mock to return empty string for this test
148
+ const readline = await import('node:readline');
149
+ vi.mocked(readline.createInterface).mockReturnValueOnce({
150
+ question: vi.fn((_: string, cb: (a: string) => void) => cb('')),
151
+ close: vi.fn(),
152
+ } as any);
153
+
154
+ mockFetch(200, {});
155
+
156
+ const result = await loginAnthropic({ noBrowser: true });
157
+
158
+ expect(result.ok).toBe(false);
159
+ // Should not proceed to validation when key is empty
160
+ expect(result.error).toBeTruthy();
161
+ });
162
+
163
+ it('returns ok: false gracefully on network failure during validation', async () => {
164
+ mockFetchNetworkError();
165
+
166
+ const result = await loginAnthropic({ noBrowser: true });
167
+
168
+ expect(result.ok).toBe(false);
169
+ // Should surface a human-readable error, not a raw Error object
170
+ expect(typeof result.error).toBe('string');
171
+ expect(result.error!.length).toBeGreaterThan(10);
172
+ });
173
+
174
+ it('persists apiKey to config when validation succeeds', async () => {
175
+ mockFetch(200, { data: [] });
176
+ const fs = await import('node:fs');
177
+
178
+ await loginAnthropic({ noBrowser: true });
179
+
180
+ expect(vi.mocked(fs.writeFileSync)).toHaveBeenCalledWith(
181
+ expect.stringContaining('config.json'),
182
+ expect.stringContaining('sk-ant-fake-key-for-tests'),
183
+ expect.objectContaining({ mode: 0o600 }),
184
+ );
185
+ });
186
+ });
@@ -0,0 +1,216 @@
1
+ // anthropic-oauth.ts — Anthropic login flow for `aria login --anthropic`
2
+ //
3
+ // OAuth path decision (2026-04-26):
4
+ // Anthropic does not publish a public PKCE/OAuth endpoint for end-user
5
+ // API-key issuance via the console.anthropic.com auth surface. The only
6
+ // standardised way to obtain an API key is through the Anthropic Console
7
+ // web UI. Therefore we use the browser-paste fallback (path b):
8
+ // 1. Open https://console.anthropic.com/account/keys in the user's browser.
9
+ // 2. Prompt them to paste the new key back into the CLI.
10
+ // 3. Validate the key against GET https://api.anthropic.com/v1/models.
11
+ // 4. Return the validated key to the caller.
12
+ //
13
+ // If Anthropic ships a public OAuth/PKCE spec in the future, replace the
14
+ // body of `_oauthPKCEFlow` below with the real implementation and call it
15
+ // instead of `_browserPasteFlow`.
16
+ //
17
+ // Voice: first-person Aria ("I'll open", "I see", "I couldn't reach").
18
+ // ESM: all internal imports use .js extensions.
19
+
20
+ import * as http from 'node:http';
21
+ import * as readline from 'node:readline';
22
+ import * as fs from 'node:fs';
23
+ import * as path from 'node:path';
24
+ import { homedir } from 'node:os';
25
+ import { spawn } from 'node:child_process';
26
+
27
+ const ANTHROPIC_KEYS_URL = 'https://console.anthropic.com/account/keys';
28
+ const ANTHROPIC_MODELS_URL = 'https://api.anthropic.com/v1/models';
29
+ const ANTHROPIC_API_VERSION = '2023-06-01';
30
+ const CONFIG_PATH = path.join(homedir(), '.aria', 'config.json');
31
+ const ARIA_DIR = path.join(homedir(), '.aria');
32
+
33
+ export interface AnthropicLoginResult {
34
+ ok: boolean;
35
+ apiKey?: string;
36
+ error?: string;
37
+ }
38
+
39
+ /** Options accepted by loginAnthropic. */
40
+ export interface AnthropicLoginOptions {
41
+ /** If true, skip the browser-open attempt and just prompt for the key. */
42
+ noBrowser?: boolean;
43
+ }
44
+
45
+ // ── Public API ───────────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Run the Anthropic login flow.
49
+ *
50
+ * Today this is the browser-paste flow (path b) because Anthropic does not
51
+ * expose a public PKCE/OAuth token endpoint for console API keys.
52
+ *
53
+ * On success, the validated API key is persisted to ~/.aria/config.json under
54
+ * `model.apiKey` and returned in the result object. The harness license token
55
+ * in ~/.aria/license.json is NOT modified — that belongs to the Aria harness,
56
+ * not Anthropic.
57
+ */
58
+ export async function loginAnthropic(
59
+ opts: AnthropicLoginOptions = {},
60
+ ): Promise<AnthropicLoginResult> {
61
+ try {
62
+ const apiKey = await _browserPasteFlow(opts.noBrowser ?? false);
63
+ if (!apiKey) {
64
+ return { ok: false, error: "I didn't receive a key — aborting." };
65
+ }
66
+
67
+ console.log(" I'm validating your key against the Anthropic API...");
68
+ const valid = await _validateKey(apiKey);
69
+ if (!valid) {
70
+ return {
71
+ ok: false,
72
+ error:
73
+ "I couldn't verify that key with Anthropic. Double-check that it starts with sk-ant- and hasn't been revoked.",
74
+ };
75
+ }
76
+
77
+ _persistApiKey(apiKey);
78
+ return { ok: true, apiKey };
79
+ } catch (err: unknown) {
80
+ const msg = err instanceof Error ? err.message : String(err);
81
+ return { ok: false, error: `I hit an unexpected error: ${msg}` };
82
+ }
83
+ }
84
+
85
+ // ── Internal: browser-paste flow (path b) ────────────────────────────────────
86
+
87
+ async function _browserPasteFlow(noBrowser: boolean): Promise<string | null> {
88
+ console.log('');
89
+ console.log(
90
+ " I'll open https://console.anthropic.com/account/keys in your browser.",
91
+ );
92
+ console.log(
93
+ ' Create a new API key there, then paste it back here.',
94
+ );
95
+ console.log('');
96
+
97
+ if (!noBrowser) {
98
+ _openBrowser(ANTHROPIC_KEYS_URL);
99
+ }
100
+
101
+ const rl = readline.createInterface({
102
+ input: process.stdin,
103
+ output: process.stdout,
104
+ terminal: true,
105
+ });
106
+
107
+ const rawKey = await new Promise<string>((resolve) => {
108
+ rl.question(' Paste your Anthropic API key: ', (answer) => {
109
+ rl.close();
110
+ resolve(answer.trim());
111
+ });
112
+ });
113
+
114
+ return rawKey || null;
115
+ }
116
+
117
+ // ── Internal: browser open (cross-platform) ───────────────────────────────────
118
+
119
+ function _openBrowser(url: string): void {
120
+ const platform = process.platform;
121
+ let cmd: string;
122
+ let args: string[];
123
+
124
+ if (platform === 'darwin') {
125
+ cmd = 'open';
126
+ args = [url];
127
+ } else if (platform === 'win32') {
128
+ cmd = 'cmd';
129
+ args = ['/c', 'start', '', url];
130
+ } else {
131
+ // Linux / *BSD / WSL
132
+ cmd = 'xdg-open';
133
+ args = [url];
134
+ }
135
+
136
+ try {
137
+ const child = spawn(cmd, args, {
138
+ detached: true,
139
+ stdio: 'ignore',
140
+ });
141
+ child.unref();
142
+ } catch {
143
+ // If browser open fails, the user can still paste — not fatal.
144
+ console.log(
145
+ ` I couldn't open your browser automatically. Please visit:\n ${url}`,
146
+ );
147
+ }
148
+ }
149
+
150
+ // ── Internal: key validation ──────────────────────────────────────────────────
151
+
152
+ /**
153
+ * Validate an Anthropic API key by hitting GET /v1/models.
154
+ * Returns true when the server responds 2xx; false on 401/403/network error.
155
+ */
156
+ export async function _validateKey(apiKey: string): Promise<boolean> {
157
+ try {
158
+ const resp = await fetch(ANTHROPIC_MODELS_URL, {
159
+ method: 'GET',
160
+ headers: {
161
+ 'x-api-key': apiKey,
162
+ 'anthropic-version': ANTHROPIC_API_VERSION,
163
+ },
164
+ });
165
+ // 200 = valid key; anything else = invalid or revoked
166
+ return resp.status === 200;
167
+ } catch {
168
+ // Network failure — we can't confirm validity
169
+ return false;
170
+ }
171
+ }
172
+
173
+ // ── Internal: config persistence ──────────────────────────────────────────────
174
+
175
+ function _persistApiKey(apiKey: string): void {
176
+ if (!fs.existsSync(ARIA_DIR)) {
177
+ fs.mkdirSync(ARIA_DIR, { recursive: true, mode: 0o700 });
178
+ }
179
+
180
+ let config: Record<string, unknown> = {};
181
+ if (fs.existsSync(CONFIG_PATH)) {
182
+ try {
183
+ config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
184
+ } catch {
185
+ config = {};
186
+ }
187
+ }
188
+
189
+ const model = (config.model as Record<string, unknown>) ?? {};
190
+ model.provider = model.provider ?? 'anthropic';
191
+ model.model = model.model ?? 'claude-sonnet-4-20250514';
192
+ model.apiKey = apiKey;
193
+ config.model = model;
194
+
195
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', {
196
+ mode: 0o600,
197
+ encoding: 'utf-8',
198
+ });
199
+ }
200
+
201
+ // ── Placeholder for future PKCE flow ─────────────────────────────────────────
202
+ // When Anthropic ships a public OAuth/PKCE endpoint for console key issuance,
203
+ // implement the full flow here and call it from loginAnthropic() instead of
204
+ // _browserPasteFlow():
205
+ //
206
+ // async function _oauthPKCEFlow(): Promise<string | null> {
207
+ // const port = await _findFreePort();
208
+ // const { verifier, challenge } = _pkceChallenge();
209
+ // const authUrl = `https://console.anthropic.com/oauth/authorize?` +
210
+ // `response_type=code&client_id=<CLIENT_ID>&redirect_uri=http://localhost:${port}/callback` +
211
+ // `&code_challenge=${challenge}&code_challenge_method=S256`;
212
+ // _openBrowser(authUrl);
213
+ // const code = await _waitForCallback(port);
214
+ // const token = await _exchangeCode(code, verifier, port);
215
+ // return token;
216
+ // }
@@ -1,5 +1,6 @@
1
1
  import { loadLicense, saveLicense } from './auth.js';
2
2
  import { harnessClient } from './harness-client.js';
3
+ import { loginAnthropic } from './anthropic-oauth.js';
3
4
  import * as fs from 'node:fs';
4
5
  import * as path from 'node:path';
5
6
  import { homedir } from 'node:os';
@@ -19,6 +20,8 @@ interface LoginResult {
19
20
  tier?: string;
20
21
  jti?: string;
21
22
  expiresAt?: string;
23
+ /** Set when loginAnthropic() is used instead of the harness license path. */
24
+ anthropic?: boolean;
22
25
  error?: string;
23
26
  }
24
27
 
@@ -40,7 +43,31 @@ interface RevokeResult {
40
43
  error?: string;
41
44
  }
42
45
 
46
+ /**
47
+ * Log in to the Aria harness with a license token, OR authenticate via
48
+ * Anthropic's console (browser-paste flow) when token === '--anthropic' or
49
+ * token === '-a'.
50
+ *
51
+ * Anthropic path: opens console.anthropic.com/account/keys, waits for the
52
+ * user to paste their API key, validates it against /v1/models, then persists
53
+ * it to ~/.aria/config.json under `model.apiKey`. The harness license file
54
+ * (~/.aria/license.json) is NOT modified — it belongs to the Aria license,
55
+ * not Anthropic credentials.
56
+ *
57
+ * Harness path: unchanged — validates via heartbeat / issue endpoint and
58
+ * writes license claims to ~/.aria/license.json.
59
+ */
43
60
  export async function login(token: string): Promise<LoginResult> {
61
+ // ── Anthropic OAuth/paste branch ────────────────────────────────────────────
62
+ if (token === '--anthropic' || token === '-a') {
63
+ const result = await loginAnthropic();
64
+ if (!result.ok) {
65
+ return { ok: false, error: result.error };
66
+ }
67
+ return { ok: true, anthropic: true };
68
+ }
69
+
70
+ // ── Original harness-license branch (preserved verbatim) ────────────────────
44
71
  try {
45
72
  // Validate token via heartbeat; if unverified, issue endpoint
46
73
  let response;