@venturewild/workspace 0.5.0 → 0.5.2

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 (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +112 -112
  3. package/package.json +85 -85
  4. package/server/bin/wild-workspace.mjs +1096 -1096
  5. package/server/src/account.mjs +114 -114
  6. package/server/src/agent-login.mjs +146 -146
  7. package/server/src/agent-readiness.mjs +200 -200
  8. package/server/src/agent.mjs +468 -468
  9. package/server/src/bazaar/core.mjs +790 -730
  10. package/server/src/bazaar/index.mjs +88 -88
  11. package/server/src/bazaar/mcp-server.mjs +417 -417
  12. package/server/src/bazaar/mock-tickup.mjs +97 -97
  13. package/server/src/bazaar/preview-server.mjs +95 -95
  14. package/server/src/bazaar/seed-recipes/customer-feedback-form/know-how.md +23 -23
  15. package/server/src/bazaar/seed-recipes/customer-feedback-form/recipe.json +24 -24
  16. package/server/src/bazaar/seed-recipes/landing-page-launch/know-how.md +29 -29
  17. package/server/src/bazaar/seed-recipes/landing-page-launch/recipe.json +25 -25
  18. package/server/src/bazaar/seed-recipes/personal-portfolio/know-how.md +21 -21
  19. package/server/src/bazaar/seed-recipes/personal-portfolio/recipe.json +24 -24
  20. package/server/src/bazaar/seed-recipes/receipt-sorter/know-how.md +31 -31
  21. package/server/src/bazaar/seed-recipes/receipt-sorter/recipe.json +25 -25
  22. package/server/src/bazaar/seed-recipes/tickup-hr-matching/know-how.md +79 -79
  23. package/server/src/bazaar/seed-recipes/tickup-hr-matching/recipe.json +40 -40
  24. package/server/src/canvas/core.mjs +446 -446
  25. package/server/src/canvas/index.mjs +42 -42
  26. package/server/src/canvas/mcp-server.mjs +253 -253
  27. package/server/src/canvas-rails.mjs +108 -108
  28. package/server/src/config.mjs +404 -404
  29. package/server/src/daemon-bin.mjs +110 -110
  30. package/server/src/daemon-supervisor.mjs +285 -285
  31. package/server/src/doctor.mjs +375 -375
  32. package/server/src/inbox.mjs +86 -86
  33. package/server/src/index.mjs +3279 -3181
  34. package/server/src/listings-rails.mjs +126 -0
  35. package/server/src/logpaths.mjs +98 -98
  36. package/server/src/observability.mjs +45 -45
  37. package/server/src/operator.mjs +92 -92
  38. package/server/src/pairing.mjs +137 -137
  39. package/server/src/service.mjs +515 -515
  40. package/server/src/session-reporter.mjs +201 -201
  41. package/server/src/settings.mjs +145 -145
  42. package/server/src/share.mjs +182 -182
  43. package/server/src/skills.mjs +213 -213
  44. package/server/src/supervisor.mjs +647 -647
  45. package/server/src/support-consent.mjs +133 -133
  46. package/server/src/sync.mjs +248 -248
  47. package/server/src/transcript.mjs +121 -121
  48. package/server/src/turn-mcp.mjs +46 -46
  49. package/server/src/usage.mjs +405 -405
  50. package/server/src/workspace-registry.mjs +295 -295
  51. package/server/src/workspaces.mjs +145 -135
  52. package/web/dist/assets/index-BXq-Irj8.js +131 -0
  53. package/web/dist/assets/index-CzUrGoMW.css +32 -0
  54. package/web/dist/index.html +2 -2
  55. package/web/dist/assets/index-DWNJ55qg.css +0 -32
  56. package/web/dist/assets/index-YlSTL4Wv.js +0 -131
@@ -1,114 +1,114 @@
1
- // Per-install bmo-sync account — the user's identity in the wild-* ecosystem.
2
- //
3
- // Distinct from `agent-identity.mjs` (which holds the agent's name/tone/color)
4
- // and from `secrets.mjs` (per-install partner/share tokens). An account binds
5
- // this installation to:
6
- // - a `slug` (the `<slug>.venturewild.llc` namespace, claimed at signup)
7
- // - an `email` (human identification — NOT used as a password)
8
- // - an `accountId` (uuid)
9
- // - an `accountToken` (long-random secret that proves ownership to
10
- // `sync.venturewild.llc`; never sent to the browser)
11
- //
12
- // Persisted at `<dataDir>/account.json` (mode 0600). Absence means the user
13
- // has not run `wild-workspace login` yet; the server still works in localhost
14
- // mode but `<slug>.venturewild.llc` isn't configured.
15
- //
16
- // Login flow:
17
- // 1. User registers at `workspace.venturewild.llc` (landing).
18
- // 2. Landing displays a single opaque payload (base64url-encoded JSON).
19
- // 3. User runs `wild-workspace login <payload>`.
20
- // 4. We decode + persist here.
21
-
22
- import fs from 'node:fs';
23
- import path from 'node:path';
24
-
25
- const FILE = 'account.json';
26
-
27
- function accountPath(dataDir) {
28
- return path.join(dataDir, FILE);
29
- }
30
-
31
- export function loadAccount(dataDir) {
32
- try {
33
- const parsed = JSON.parse(fs.readFileSync(accountPath(dataDir), 'utf8'));
34
- return sanitize(parsed);
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
- function sanitize(raw) {
41
- if (!raw || typeof raw !== 'object') return null;
42
- const slug = typeof raw.slug === 'string' ? raw.slug.trim().toLowerCase() : '';
43
- const email = typeof raw.email === 'string' ? raw.email.trim().toLowerCase() : '';
44
- const accountId = typeof raw.accountId === 'string' ? raw.accountId.trim() : '';
45
- const accountToken = typeof raw.accountToken === 'string' ? raw.accountToken.trim() : '';
46
- if (!slug || !email || !accountId || !accountToken) return null;
47
- return {
48
- slug,
49
- email,
50
- accountId,
51
- accountToken,
52
- displayName: typeof raw.displayName === 'string' ? raw.displayName : null,
53
- loggedInAt: Number(raw.loggedInAt) || Date.now(),
54
- };
55
- }
56
-
57
- export function saveAccount(dataDir, account) {
58
- const merged = sanitize({ loggedInAt: Date.now(), ...account });
59
- if (!merged) throw new Error('account-invalid');
60
- fs.mkdirSync(dataDir, { recursive: true });
61
- fs.writeFileSync(accountPath(dataDir), JSON.stringify(merged, null, 2), {
62
- mode: 0o600,
63
- });
64
- return merged;
65
- }
66
-
67
- export function clearAccount(dataDir) {
68
- try {
69
- fs.unlinkSync(accountPath(dataDir));
70
- return true;
71
- } catch {
72
- return false;
73
- }
74
- }
75
-
76
- // Parse a single opaque token-blob the user pastes from the landing page.
77
- // The landing emits base64url(JSON({slug,email,accountId,token,displayName?})).
78
- // We accept either base64url or raw JSON for forward-compat / curl debugging.
79
- export function decodeLoginPayload(input) {
80
- const raw = String(input || '').trim();
81
- if (!raw) throw new Error('Empty login payload — paste the blob from your signup page.');
82
- // 1. Try base64url decode.
83
- const tryBase64 = () => {
84
- let b = raw.replace(/-/g, '+').replace(/_/g, '/');
85
- while (b.length % 4 !== 0) b += '=';
86
- return Buffer.from(b, 'base64').toString('utf8');
87
- };
88
- // 2. Try raw JSON (if it starts with `{`).
89
- let jsonText;
90
- if (raw[0] === '{') {
91
- jsonText = raw;
92
- } else {
93
- try {
94
- jsonText = tryBase64();
95
- } catch {
96
- throw new Error('Login payload is not base64url-encoded JSON.');
97
- }
98
- }
99
- let parsed;
100
- try {
101
- parsed = JSON.parse(jsonText);
102
- } catch {
103
- throw new Error('Login payload decoded but is not valid JSON.');
104
- }
105
- // Accept both `token` (what the landing emits) and `accountToken`.
106
- if (parsed.token && !parsed.accountToken) parsed.accountToken = parsed.token;
107
- if (!parsed.slug || !parsed.email || !parsed.accountId || !parsed.accountToken) {
108
- throw new Error(
109
- 'Login payload is missing one or more required fields ' +
110
- '(slug, email, accountId, token). Re-copy from the signup page?',
111
- );
112
- }
113
- return parsed;
114
- }
1
+ // Per-install bmo-sync account — the user's identity in the wild-* ecosystem.
2
+ //
3
+ // Distinct from `agent-identity.mjs` (which holds the agent's name/tone/color)
4
+ // and from `secrets.mjs` (per-install partner/share tokens). An account binds
5
+ // this installation to:
6
+ // - a `slug` (the `<slug>.venturewild.llc` namespace, claimed at signup)
7
+ // - an `email` (human identification — NOT used as a password)
8
+ // - an `accountId` (uuid)
9
+ // - an `accountToken` (long-random secret that proves ownership to
10
+ // `sync.venturewild.llc`; never sent to the browser)
11
+ //
12
+ // Persisted at `<dataDir>/account.json` (mode 0600). Absence means the user
13
+ // has not run `wild-workspace login` yet; the server still works in localhost
14
+ // mode but `<slug>.venturewild.llc` isn't configured.
15
+ //
16
+ // Login flow:
17
+ // 1. User registers at `workspace.venturewild.llc` (landing).
18
+ // 2. Landing displays a single opaque payload (base64url-encoded JSON).
19
+ // 3. User runs `wild-workspace login <payload>`.
20
+ // 4. We decode + persist here.
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+
25
+ const FILE = 'account.json';
26
+
27
+ function accountPath(dataDir) {
28
+ return path.join(dataDir, FILE);
29
+ }
30
+
31
+ export function loadAccount(dataDir) {
32
+ try {
33
+ const parsed = JSON.parse(fs.readFileSync(accountPath(dataDir), 'utf8'));
34
+ return sanitize(parsed);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function sanitize(raw) {
41
+ if (!raw || typeof raw !== 'object') return null;
42
+ const slug = typeof raw.slug === 'string' ? raw.slug.trim().toLowerCase() : '';
43
+ const email = typeof raw.email === 'string' ? raw.email.trim().toLowerCase() : '';
44
+ const accountId = typeof raw.accountId === 'string' ? raw.accountId.trim() : '';
45
+ const accountToken = typeof raw.accountToken === 'string' ? raw.accountToken.trim() : '';
46
+ if (!slug || !email || !accountId || !accountToken) return null;
47
+ return {
48
+ slug,
49
+ email,
50
+ accountId,
51
+ accountToken,
52
+ displayName: typeof raw.displayName === 'string' ? raw.displayName : null,
53
+ loggedInAt: Number(raw.loggedInAt) || Date.now(),
54
+ };
55
+ }
56
+
57
+ export function saveAccount(dataDir, account) {
58
+ const merged = sanitize({ loggedInAt: Date.now(), ...account });
59
+ if (!merged) throw new Error('account-invalid');
60
+ fs.mkdirSync(dataDir, { recursive: true });
61
+ fs.writeFileSync(accountPath(dataDir), JSON.stringify(merged, null, 2), {
62
+ mode: 0o600,
63
+ });
64
+ return merged;
65
+ }
66
+
67
+ export function clearAccount(dataDir) {
68
+ try {
69
+ fs.unlinkSync(accountPath(dataDir));
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ // Parse a single opaque token-blob the user pastes from the landing page.
77
+ // The landing emits base64url(JSON({slug,email,accountId,token,displayName?})).
78
+ // We accept either base64url or raw JSON for forward-compat / curl debugging.
79
+ export function decodeLoginPayload(input) {
80
+ const raw = String(input || '').trim();
81
+ if (!raw) throw new Error('Empty login payload — paste the blob from your signup page.');
82
+ // 1. Try base64url decode.
83
+ const tryBase64 = () => {
84
+ let b = raw.replace(/-/g, '+').replace(/_/g, '/');
85
+ while (b.length % 4 !== 0) b += '=';
86
+ return Buffer.from(b, 'base64').toString('utf8');
87
+ };
88
+ // 2. Try raw JSON (if it starts with `{`).
89
+ let jsonText;
90
+ if (raw[0] === '{') {
91
+ jsonText = raw;
92
+ } else {
93
+ try {
94
+ jsonText = tryBase64();
95
+ } catch {
96
+ throw new Error('Login payload is not base64url-encoded JSON.');
97
+ }
98
+ }
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(jsonText);
102
+ } catch {
103
+ throw new Error('Login payload decoded but is not valid JSON.');
104
+ }
105
+ // Accept both `token` (what the landing emits) and `accountToken`.
106
+ if (parsed.token && !parsed.accountToken) parsed.accountToken = parsed.token;
107
+ if (!parsed.slug || !parsed.email || !parsed.accountId || !parsed.accountToken) {
108
+ throw new Error(
109
+ 'Login payload is missing one or more required fields ' +
110
+ '(slug, email, accountId, token). Re-copy from the signup page?',
111
+ );
112
+ }
113
+ return parsed;
114
+ }
@@ -1,146 +1,146 @@
1
- // agent-login.mjs — drives an in-app "Sign in to Claude" so a non-technical user
2
- // never has to touch a terminal. See docs/user-experience.md (the cold-start gate)
3
- // + the 2026-06-01 CHANGELOG.
4
- //
5
- // WHY A PTY: `claude auth login` is an OAuth-via-browser flow. When spawned with
6
- // plain piped stdio (no TTY), Claude can't run its localhost callback server, so
7
- // the browser shows a code to paste back into the terminal — useless to us. A real
8
- // pseudo-terminal (node-pty) makes Claude behave exactly as it does in the user's
9
- // own terminal: it opens claude.ai, and the localhost callback auto-completes. The
10
- // user only ever interacts with the browser; the PTY runs server-side, headless.
11
- //
12
- // On success the credential lands in the OS store (macOS Keychain / Windows
13
- // ~/.claude/.credentials.json) — nothing for us to persist — and the existing
14
- // `claude auth status` readiness probe flips to `ready` (or `subscribe` for a
15
- // signed-in-but-no-plan account, which the gate then routes correctly).
16
- //
17
- // node-pty is loaded LAZILY + OPTIONALLY. If the prebuilt binary isn't available
18
- // on this platform, start() returns `{ status: 'unsupported' }` and the UI falls
19
- // back to the "run `claude auth login` in a new terminal" instruction — so adding
20
- // this never breaks the (no-sudo, no-native-dep) install path we proved on macOS.
21
-
22
- import { probeAgentReadiness } from './agent-readiness.mjs';
23
-
24
- // First http(s) URL in a chunk (Claude prints the OAuth URL it's opening).
25
- const URL_RE = /(https?:\/\/[^\s'"]+)/;
26
- // Claude asks for a pasted code only when the browser callback couldn't be reached
27
- // (shouldn't happen under a real PTY, but we handle it as a fallback).
28
- const CODE_HINT_RE = /paste.*code|enter the code|authoriz(at)?ion code|code:\s*$/i;
29
-
30
- let _ptyModPromise;
31
- export async function defaultPtyLoader() {
32
- if (!_ptyModPromise) {
33
- _ptyModPromise = import('@homebridge/node-pty-prebuilt-multiarch')
34
- .then((m) => m.default || m)
35
- .catch(() => null); // not installed / no prebuilt for this platform
36
- }
37
- return _ptyModPromise;
38
- }
39
-
40
- // status: idle | starting | awaiting-browser | awaiting-code | success | error | unsupported
41
- export class ClaudeLoginSession {
42
- constructor({ agent, openImpl, reprobeImpl, ptyLoader, env } = {}) {
43
- this.agent = agent;
44
- this.openImpl = openImpl || null; // (url) => void — open the OAuth URL
45
- this.ptyLoader = ptyLoader || defaultPtyLoader;
46
- this.env = env || process.env;
47
- this.reprobeImpl = reprobeImpl || (() => probeAgentReadiness(this.agent, undefined, this.env));
48
- this.status = 'idle';
49
- this.url = null;
50
- this.error = null;
51
- this.verdict = null;
52
- this.proc = null;
53
- this._buf = '';
54
- }
55
-
56
- async start() {
57
- if (this.status === 'starting' || this.status === 'awaiting-browser' || this.status === 'awaiting-code') {
58
- return this.snapshot(); // already in flight — idempotent
59
- }
60
- if (!this.agent || this.agent.id !== 'claude' || !this.agent.available) {
61
- return this._fail('unsupported', 'Claude is not installed on this machine.');
62
- }
63
- const pty = await this.ptyLoader();
64
- if (!pty || typeof pty.spawn !== 'function') {
65
- return this._fail('unsupported', 'In-app sign-in is not available on this platform.');
66
- }
67
- const command = this.agent.resolvedPath || this.agent.binary;
68
- this._reset();
69
- this.status = 'starting';
70
- try {
71
- this.proc = pty.spawn(command, ['auth', 'login'], {
72
- name: 'xterm-color',
73
- cols: 100,
74
- rows: 30,
75
- cwd: process.cwd(),
76
- env: { ...this.env },
77
- });
78
- } catch (e) {
79
- return this._fail('error', `couldn't start sign-in: ${e?.message || e}`);
80
- }
81
- this.status = 'awaiting-browser';
82
- this.proc.onData((d) => this._onData(String(d)));
83
- this.proc.onExit((ev) => this._onExit(ev && typeof ev === 'object' ? ev.exitCode : ev));
84
- return this.snapshot();
85
- }
86
-
87
- _onData(chunk) {
88
- this._buf += chunk;
89
- if (!this.url) {
90
- const m = chunk.match(URL_RE) || this._buf.match(URL_RE);
91
- if (m) {
92
- this.url = m[1].replace(/[).,*'"]+$/, '');
93
- if (this.openImpl) { try { this.openImpl(this.url); } catch { /* best-effort */ } }
94
- }
95
- }
96
- if (CODE_HINT_RE.test(chunk) && this.status === 'awaiting-browser') {
97
- this.status = 'awaiting-code';
98
- }
99
- }
100
-
101
- async _onExit(exitCode) {
102
- this.proc = null;
103
- // `claude auth login` exits once the browser flow completes. The credential
104
- // store is the source of truth — re-probe to confirm (and to learn whether
105
- // they also have an active plan vs. need to subscribe).
106
- let verdict;
107
- try { verdict = await this.reprobeImpl(); } catch { verdict = { status: 'unknown' }; }
108
- this.verdict = verdict;
109
- if (verdict.status === 'ready' || verdict.status === 'subscribe') {
110
- this.status = 'success';
111
- } else if (exitCode === 0 && verdict.status !== 'login' && verdict.status !== 'missing') {
112
- this.status = 'success';
113
- } else {
114
- this.status = 'error';
115
- this.error = this.error || (exitCode === 0 ? 'sign-in did not complete' : `sign-in exited with code ${exitCode}`);
116
- }
117
- }
118
-
119
- // Fallback only: relay a pasted code into the PTY if Claude ever asks for one.
120
- submitCode(code) {
121
- if (this.proc && this.status === 'awaiting-code' && code != null) {
122
- try { this.proc.write(`${String(code).trim()}\r`); } catch { /* dead pty */ return false; }
123
- this.status = 'awaiting-browser';
124
- return true;
125
- }
126
- return false;
127
- }
128
-
129
- cancel() {
130
- if (this.proc) { try { this.proc.kill(); } catch { /* already gone */ } this.proc = null; }
131
- if (this.status !== 'success') this.status = 'idle';
132
- return this.snapshot();
133
- }
134
-
135
- _reset() { this.url = null; this.error = null; this.verdict = null; this._buf = ''; }
136
-
137
- _fail(status, message) {
138
- this.status = status;
139
- this.error = message;
140
- return this.snapshot();
141
- }
142
-
143
- snapshot() {
144
- return { status: this.status, url: this.url, error: this.error, verdict: this.verdict };
145
- }
146
- }
1
+ // agent-login.mjs — drives an in-app "Sign in to Claude" so a non-technical user
2
+ // never has to touch a terminal. See docs/user-experience.md (the cold-start gate)
3
+ // + the 2026-06-01 CHANGELOG.
4
+ //
5
+ // WHY A PTY: `claude auth login` is an OAuth-via-browser flow. When spawned with
6
+ // plain piped stdio (no TTY), Claude can't run its localhost callback server, so
7
+ // the browser shows a code to paste back into the terminal — useless to us. A real
8
+ // pseudo-terminal (node-pty) makes Claude behave exactly as it does in the user's
9
+ // own terminal: it opens claude.ai, and the localhost callback auto-completes. The
10
+ // user only ever interacts with the browser; the PTY runs server-side, headless.
11
+ //
12
+ // On success the credential lands in the OS store (macOS Keychain / Windows
13
+ // ~/.claude/.credentials.json) — nothing for us to persist — and the existing
14
+ // `claude auth status` readiness probe flips to `ready` (or `subscribe` for a
15
+ // signed-in-but-no-plan account, which the gate then routes correctly).
16
+ //
17
+ // node-pty is loaded LAZILY + OPTIONALLY. If the prebuilt binary isn't available
18
+ // on this platform, start() returns `{ status: 'unsupported' }` and the UI falls
19
+ // back to the "run `claude auth login` in a new terminal" instruction — so adding
20
+ // this never breaks the (no-sudo, no-native-dep) install path we proved on macOS.
21
+
22
+ import { probeAgentReadiness } from './agent-readiness.mjs';
23
+
24
+ // First http(s) URL in a chunk (Claude prints the OAuth URL it's opening).
25
+ const URL_RE = /(https?:\/\/[^\s'"]+)/;
26
+ // Claude asks for a pasted code only when the browser callback couldn't be reached
27
+ // (shouldn't happen under a real PTY, but we handle it as a fallback).
28
+ const CODE_HINT_RE = /paste.*code|enter the code|authoriz(at)?ion code|code:\s*$/i;
29
+
30
+ let _ptyModPromise;
31
+ export async function defaultPtyLoader() {
32
+ if (!_ptyModPromise) {
33
+ _ptyModPromise = import('@homebridge/node-pty-prebuilt-multiarch')
34
+ .then((m) => m.default || m)
35
+ .catch(() => null); // not installed / no prebuilt for this platform
36
+ }
37
+ return _ptyModPromise;
38
+ }
39
+
40
+ // status: idle | starting | awaiting-browser | awaiting-code | success | error | unsupported
41
+ export class ClaudeLoginSession {
42
+ constructor({ agent, openImpl, reprobeImpl, ptyLoader, env } = {}) {
43
+ this.agent = agent;
44
+ this.openImpl = openImpl || null; // (url) => void — open the OAuth URL
45
+ this.ptyLoader = ptyLoader || defaultPtyLoader;
46
+ this.env = env || process.env;
47
+ this.reprobeImpl = reprobeImpl || (() => probeAgentReadiness(this.agent, undefined, this.env));
48
+ this.status = 'idle';
49
+ this.url = null;
50
+ this.error = null;
51
+ this.verdict = null;
52
+ this.proc = null;
53
+ this._buf = '';
54
+ }
55
+
56
+ async start() {
57
+ if (this.status === 'starting' || this.status === 'awaiting-browser' || this.status === 'awaiting-code') {
58
+ return this.snapshot(); // already in flight — idempotent
59
+ }
60
+ if (!this.agent || this.agent.id !== 'claude' || !this.agent.available) {
61
+ return this._fail('unsupported', 'Claude is not installed on this machine.');
62
+ }
63
+ const pty = await this.ptyLoader();
64
+ if (!pty || typeof pty.spawn !== 'function') {
65
+ return this._fail('unsupported', 'In-app sign-in is not available on this platform.');
66
+ }
67
+ const command = this.agent.resolvedPath || this.agent.binary;
68
+ this._reset();
69
+ this.status = 'starting';
70
+ try {
71
+ this.proc = pty.spawn(command, ['auth', 'login'], {
72
+ name: 'xterm-color',
73
+ cols: 100,
74
+ rows: 30,
75
+ cwd: process.cwd(),
76
+ env: { ...this.env },
77
+ });
78
+ } catch (e) {
79
+ return this._fail('error', `couldn't start sign-in: ${e?.message || e}`);
80
+ }
81
+ this.status = 'awaiting-browser';
82
+ this.proc.onData((d) => this._onData(String(d)));
83
+ this.proc.onExit((ev) => this._onExit(ev && typeof ev === 'object' ? ev.exitCode : ev));
84
+ return this.snapshot();
85
+ }
86
+
87
+ _onData(chunk) {
88
+ this._buf += chunk;
89
+ if (!this.url) {
90
+ const m = chunk.match(URL_RE) || this._buf.match(URL_RE);
91
+ if (m) {
92
+ this.url = m[1].replace(/[).,*'"]+$/, '');
93
+ if (this.openImpl) { try { this.openImpl(this.url); } catch { /* best-effort */ } }
94
+ }
95
+ }
96
+ if (CODE_HINT_RE.test(chunk) && this.status === 'awaiting-browser') {
97
+ this.status = 'awaiting-code';
98
+ }
99
+ }
100
+
101
+ async _onExit(exitCode) {
102
+ this.proc = null;
103
+ // `claude auth login` exits once the browser flow completes. The credential
104
+ // store is the source of truth — re-probe to confirm (and to learn whether
105
+ // they also have an active plan vs. need to subscribe).
106
+ let verdict;
107
+ try { verdict = await this.reprobeImpl(); } catch { verdict = { status: 'unknown' }; }
108
+ this.verdict = verdict;
109
+ if (verdict.status === 'ready' || verdict.status === 'subscribe') {
110
+ this.status = 'success';
111
+ } else if (exitCode === 0 && verdict.status !== 'login' && verdict.status !== 'missing') {
112
+ this.status = 'success';
113
+ } else {
114
+ this.status = 'error';
115
+ this.error = this.error || (exitCode === 0 ? 'sign-in did not complete' : `sign-in exited with code ${exitCode}`);
116
+ }
117
+ }
118
+
119
+ // Fallback only: relay a pasted code into the PTY if Claude ever asks for one.
120
+ submitCode(code) {
121
+ if (this.proc && this.status === 'awaiting-code' && code != null) {
122
+ try { this.proc.write(`${String(code).trim()}\r`); } catch { /* dead pty */ return false; }
123
+ this.status = 'awaiting-browser';
124
+ return true;
125
+ }
126
+ return false;
127
+ }
128
+
129
+ cancel() {
130
+ if (this.proc) { try { this.proc.kill(); } catch { /* already gone */ } this.proc = null; }
131
+ if (this.status !== 'success') this.status = 'idle';
132
+ return this.snapshot();
133
+ }
134
+
135
+ _reset() { this.url = null; this.error = null; this.verdict = null; this._buf = ''; }
136
+
137
+ _fail(status, message) {
138
+ this.status = status;
139
+ this.error = message;
140
+ return this.snapshot();
141
+ }
142
+
143
+ snapshot() {
144
+ return { status: this.status, url: this.url, error: this.error, verdict: this.verdict };
145
+ }
146
+ }