@trusty-squire/mcp 1.1.13-rc.8 → 1.1.13

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 (162) hide show
  1. package/README.md +139 -62
  2. package/assets/login/vnc-input.js +9 -0
  3. package/assets/login/vnc.html +214 -202
  4. package/assets/screencaps/connect-walkthrough.png +0 -0
  5. package/assets/screencaps/connect-walkthrough.svg +1 -1
  6. package/dist/api-client.d.ts +26 -3
  7. package/dist/api-client.d.ts.map +1 -1
  8. package/dist/api-client.js +19 -31
  9. package/dist/api-client.js.map +1 -1
  10. package/dist/bin.js +2 -3
  11. package/dist/bin.js.map +1 -1
  12. package/dist/bot/browser.d.ts +134 -77
  13. package/dist/bot/browser.d.ts.map +1 -1
  14. package/dist/bot/browser.js +1788 -1543
  15. package/dist/bot/browser.js.map +1 -1
  16. package/dist/bot/compact-observation-v2.d.ts +167 -0
  17. package/dist/bot/compact-observation-v2.d.ts.map +1 -0
  18. package/dist/bot/compact-observation-v2.js +1038 -0
  19. package/dist/bot/compact-observation-v2.js.map +1 -0
  20. package/dist/bot/credential-shape.d.ts +2 -0
  21. package/dist/bot/credential-shape.d.ts.map +1 -1
  22. package/dist/bot/credential-shape.js +21 -8
  23. package/dist/bot/credential-shape.js.map +1 -1
  24. package/dist/bot/element-fingerprint.d.ts +9 -0
  25. package/dist/bot/element-fingerprint.d.ts.map +1 -0
  26. package/dist/bot/element-fingerprint.js +147 -0
  27. package/dist/bot/element-fingerprint.js.map +1 -0
  28. package/dist/bot/email-verification.d.ts +7 -1
  29. package/dist/bot/email-verification.d.ts.map +1 -1
  30. package/dist/bot/email-verification.js +48 -14
  31. package/dist/bot/email-verification.js.map +1 -1
  32. package/dist/bot/google-login.d.ts +28 -52
  33. package/dist/bot/google-login.d.ts.map +1 -1
  34. package/dist/bot/google-login.js +246 -1119
  35. package/dist/bot/google-login.js.map +1 -1
  36. package/dist/bot/install-completion.d.ts +1 -2
  37. package/dist/bot/install-completion.d.ts.map +1 -1
  38. package/dist/bot/install-completion.js +1 -2
  39. package/dist/bot/install-completion.js.map +1 -1
  40. package/dist/bot/login-state.d.ts +13 -7
  41. package/dist/bot/login-state.d.ts.map +1 -1
  42. package/dist/bot/login-state.js +50 -114
  43. package/dist/bot/login-state.js.map +1 -1
  44. package/dist/bot/oauth-providers.d.ts +1 -1
  45. package/dist/bot/oauth-providers.d.ts.map +1 -1
  46. package/dist/bot/oauth-providers.js +3 -3
  47. package/dist/bot/oauth-providers.js.map +1 -1
  48. package/dist/bot/oauth-scope.d.ts +4 -0
  49. package/dist/bot/oauth-scope.d.ts.map +1 -0
  50. package/dist/bot/oauth-scope.js +58 -0
  51. package/dist/bot/oauth-scope.js.map +1 -0
  52. package/dist/bot/operator-browser-watchdog.d.ts +127 -0
  53. package/dist/bot/operator-browser-watchdog.d.ts.map +1 -0
  54. package/dist/bot/operator-browser-watchdog.js +438 -0
  55. package/dist/bot/operator-browser-watchdog.js.map +1 -0
  56. package/dist/bot/owner-process-reaper-worker.d.ts +2 -0
  57. package/dist/bot/owner-process-reaper-worker.d.ts.map +1 -0
  58. package/dist/bot/owner-process-reaper-worker.js +17 -0
  59. package/dist/bot/owner-process-reaper-worker.js.map +1 -0
  60. package/dist/bot/owner-process-reaper.d.ts +103 -0
  61. package/dist/bot/owner-process-reaper.d.ts.map +1 -0
  62. package/dist/bot/owner-process-reaper.js +1058 -0
  63. package/dist/bot/owner-process-reaper.js.map +1 -0
  64. package/dist/bot/pay-operator.d.ts +13 -2
  65. package/dist/bot/pay-operator.d.ts.map +1 -1
  66. package/dist/bot/pay-operator.js +319 -123
  67. package/dist/bot/pay-operator.js.map +1 -1
  68. package/dist/bot/profile.d.ts +10 -1
  69. package/dist/bot/profile.d.ts.map +1 -1
  70. package/dist/bot/profile.js +137 -11
  71. package/dist/bot/profile.js.map +1 -1
  72. package/dist/bot/promote-to-skill.js +1 -1
  73. package/dist/bot/promote-to-skill.js.map +1 -1
  74. package/dist/bot/provision-session.d.ts +60 -164
  75. package/dist/bot/provision-session.d.ts.map +1 -1
  76. package/dist/bot/provision-session.js +2049 -1546
  77. package/dist/bot/provision-session.js.map +1 -1
  78. package/dist/bot/remote-login-display.d.ts +61 -0
  79. package/dist/bot/remote-login-display.d.ts.map +1 -0
  80. package/dist/bot/remote-login-display.js +765 -0
  81. package/dist/bot/remote-login-display.js.map +1 -0
  82. package/dist/bot/session/hosts.d.ts +8 -0
  83. package/dist/bot/session/hosts.d.ts.map +1 -0
  84. package/dist/bot/session/hosts.js +36 -0
  85. package/dist/bot/session/hosts.js.map +1 -0
  86. package/dist/bot/session/lifecycle.d.ts +56 -0
  87. package/dist/bot/session/lifecycle.d.ts.map +1 -0
  88. package/dist/bot/session/lifecycle.js +962 -0
  89. package/dist/bot/session/lifecycle.js.map +1 -0
  90. package/dist/bot/session/model.d.ts +175 -0
  91. package/dist/bot/session/model.d.ts.map +1 -0
  92. package/dist/bot/session/model.js +77 -0
  93. package/dist/bot/session/model.js.map +1 -0
  94. package/dist/install/cli.d.ts +29 -9
  95. package/dist/install/cli.d.ts.map +1 -1
  96. package/dist/install/cli.js +243 -322
  97. package/dist/install/cli.js.map +1 -1
  98. package/dist/install/interactive.d.ts +1 -0
  99. package/dist/install/interactive.d.ts.map +1 -1
  100. package/dist/install/interactive.js +19 -5
  101. package/dist/install/interactive.js.map +1 -1
  102. package/dist/install/ui.d.ts.map +1 -1
  103. package/dist/install/ui.js +3 -15
  104. package/dist/install/ui.js.map +1 -1
  105. package/dist/operator-env.js +1 -1
  106. package/dist/operator-env.js.map +1 -1
  107. package/dist/server-instance-registry.d.ts +103 -0
  108. package/dist/server-instance-registry.d.ts.map +1 -0
  109. package/dist/server-instance-registry.js +392 -0
  110. package/dist/server-instance-registry.js.map +1 -0
  111. package/dist/server.d.ts +15 -1
  112. package/dist/server.d.ts.map +1 -1
  113. package/dist/server.js +164 -62
  114. package/dist/server.js.map +1 -1
  115. package/dist/session-guard.d.ts +28 -0
  116. package/dist/session-guard.d.ts.map +1 -0
  117. package/dist/session-guard.js +84 -0
  118. package/dist/session-guard.js.map +1 -0
  119. package/dist/session.d.ts +57 -13
  120. package/dist/session.d.ts.map +1 -1
  121. package/dist/session.js +246 -127
  122. package/dist/session.js.map +1 -1
  123. package/dist/tools/audit-log.d.ts +16 -2
  124. package/dist/tools/audit-log.d.ts.map +1 -1
  125. package/dist/tools/audit-log.js +223 -13
  126. package/dist/tools/audit-log.js.map +1 -1
  127. package/dist/tools/audit-rollup.d.ts +80 -0
  128. package/dist/tools/audit-rollup.d.ts.map +1 -0
  129. package/dist/tools/audit-rollup.js +0 -0
  130. package/dist/tools/audit-rollup.js.map +1 -0
  131. package/dist/tools/fetch-credential.d.ts +37 -0
  132. package/dist/tools/fetch-credential.d.ts.map +1 -0
  133. package/dist/tools/fetch-credential.js +153 -0
  134. package/dist/tools/fetch-credential.js.map +1 -0
  135. package/dist/tools/index.d.ts +3 -1
  136. package/dist/tools/index.d.ts.map +1 -1
  137. package/dist/tools/index.js +7 -3
  138. package/dist/tools/index.js.map +1 -1
  139. package/dist/tools/operate-pay.d.ts +4 -4
  140. package/dist/tools/operate-pay.d.ts.map +1 -1
  141. package/dist/tools/operate-pay.js +163 -96
  142. package/dist/tools/operate-pay.js.map +1 -1
  143. package/dist/tools/provision-drive.d.ts +43 -35
  144. package/dist/tools/provision-drive.d.ts.map +1 -1
  145. package/dist/tools/provision-drive.js +268 -164
  146. package/dist/tools/provision-drive.js.map +1 -1
  147. package/dist/tools/use-credential.d.ts.map +1 -1
  148. package/dist/tools/use-credential.js +3 -2
  149. package/dist/tools/use-credential.js.map +1 -1
  150. package/package.json +4 -6
  151. package/dist/bot/operator-direct-identity.d.ts +0 -26
  152. package/dist/bot/operator-direct-identity.d.ts.map +0 -1
  153. package/dist/bot/operator-direct-identity.js +0 -120
  154. package/dist/bot/operator-direct-identity.js.map +0 -1
  155. package/dist/bot/operator-profile-pool.d.ts +0 -97
  156. package/dist/bot/operator-profile-pool.d.ts.map +0 -1
  157. package/dist/bot/operator-profile-pool.js +0 -855
  158. package/dist/bot/operator-profile-pool.js.map +0 -1
  159. package/dist/bot/xvfb.d.ts +0 -13
  160. package/dist/bot/xvfb.d.ts.map +0 -1
  161. package/dist/bot/xvfb.js +0 -146
  162. package/dist/bot/xvfb.js.map +0 -1
@@ -1,4 +1,11 @@
1
- // Setup CLI — connect / settings / logout / login subcommands.
1
+ // Setup CLI — connect / settings / logout subcommands.
2
+ //
3
+ // `connect` is the ONE onboarding AND re-auth pathway. There is no separate
4
+ // `login` command: it ran its Google OAuth through a CDP-attached Chrome,
5
+ // which Google's "secure browser" check rejects (STATE.md 2026-07-20), and a
6
+ // second command that could seed a provider session independently of the
7
+ // account claim is exactly how an install ended up "connected" with no live
8
+ // Google session.
2
9
  //
3
10
  // npx @trusty-squire/mcp connect --target=claude-code
4
11
  // Issues a machine token, then opens the trustysquire install-
@@ -8,19 +15,18 @@
8
15
  // the user's account, and
9
16
  // (b) the bot's Chrome profile gains a provider session it can
10
17
  // ride on future signups (Resend, Postmark, etc.).
11
- // One Google login, both jobs done.
18
+ // One Google login, both jobs done — and connect only reports success
19
+ // once it has re-probed the profile and seen that session LIVE.
12
20
  //
13
- // npx @trusty-squire/mcp login [--provider=google|github]
14
- // Add an additional provider session to the bot's profile. If you
15
- // signed in to install with Google but want the bot to also sign
16
- // you up via GitHub-only services, run this.
21
+ // npx @trusty-squire/mcp connect --force-relogin[=google|github]
22
+ // Re-auth. Clears the selected provider (or the whole profile) and
23
+ // re-runs the same ceremony. This replaces `login --provider=…`.
17
24
  //
18
25
  // npx @trusty-squire/mcp logout
19
26
  //
20
27
  // Flags:
21
28
  // --target=<agent> skip auto-detection
22
29
  // --api-base=<url> override the API base URL
23
- // --provider=google|github choose provider for `login`
24
30
  // --skip-browser don't launch the bot's Chrome; just print the
25
31
  // confirm URL and expect the user to open it in
26
32
  // their own browser (CI / scripted installs)
@@ -38,9 +44,9 @@ import { installInitiate, installPoll, issueMachineToken } from "../api-client.j
38
44
  import { openSessionStorage } from "../session.js";
39
45
  import { AGENTS, detectInstalledAgents, writeClaudeCodePermissions, } from "./agents.js";
40
46
  import { detectAsn } from "../bot/index.js";
41
- import { detectActiveProviderSessions, ensureOAuthSession, openInstallConfirmInBotChrome, profileHasProviderCookies, } from "../bot/google-login.js";
42
- import { isOAuthProviderId } from "../bot/oauth-providers.js";
43
- import { clearAllProviderMarkers, clearBrowserProfile, clearProviderCookies, clearProviderLoggedIn, loggedInProviders, markProviderLoggedIn, } from "../bot/login-state.js";
47
+ import { detectActiveProviderSessions, openInstallConfirmInBotChrome, } from "../bot/google-login.js";
48
+ import {} from "../bot/oauth-providers.js";
49
+ import { clearBrowserProfile, clearProviderCookies } from "../bot/login-state.js";
44
50
  import { CHROME_PROFILE_DIR, PROFILE_BUSY_MESSAGE, ProfileBusyError, profilePathIdentity, withProfileOperationGuard, } from "../bot/profile.js";
45
51
  import { VERSION } from "../version.js";
46
52
  import { ensureLatestVersion } from "./version-check.js";
@@ -60,16 +66,22 @@ function parseArgs(argv) {
60
66
  if (command === "install") {
61
67
  rejectDeprecatedCli("`install` has been removed. Use `npx @trusty-squire/mcp connect`.");
62
68
  }
69
+ if (command === "login") {
70
+ // ONE pathway. `login` seeded a provider session independently of the
71
+ // account claim (so an install could look connected with no live Google
72
+ // session) and drove Google's OAuth through CDP, which Google rejects.
73
+ rejectDeprecatedCli("`login` has been removed. Use `npx @trusty-squire/mcp connect` — " +
74
+ "add `--force-relogin=google` or `--force-relogin=github` to refresh one provider session.");
75
+ }
63
76
  let target;
64
77
  let apiBase = DEFAULT_API_BASE;
65
78
  let noRegistry = false;
66
79
  let registryConfigured = false;
67
- let providerArg;
68
- let profileDir;
69
80
  let skipBrowser = false;
70
81
  let forceRelogin = false;
71
82
  let forceReloginProvider;
72
83
  let noInteractive = false;
84
+ let account;
73
85
  for (const arg of argv) {
74
86
  if (arg.startsWith("--target=")) {
75
87
  const t = arg.slice("--target=".length);
@@ -96,12 +108,21 @@ function parseArgs(argv) {
96
108
  rejectDeprecatedCli("`--registry` has been removed because the managed registry is enabled by default.");
97
109
  }
98
110
  else if (arg.startsWith("--provider=")) {
99
- const p = arg.slice("--provider=".length);
100
- if (p === "google" || p === "github")
101
- providerArg = p;
111
+ rejectDeprecatedCli("`--provider` has been removed with `login`. Use `connect --force-relogin=google|github`.");
112
+ }
113
+ else if (arg.startsWith("--account=")) {
114
+ // An empty value must not fall through to "the most recent account":
115
+ // silently clearing a different account than the one named is the
116
+ // silent-destruction class this whole change removes.
117
+ const value = arg.slice("--account=".length).trim();
118
+ if (value.length === 0) {
119
+ console.error("--account requires an account id (e.g. --account=01ABC...)");
120
+ process.exit(64);
121
+ }
122
+ account = value;
102
123
  }
103
124
  else if (arg.startsWith("--profile-dir=")) {
104
- profileDir = arg.slice("--profile-dir=".length);
125
+ rejectDeprecatedCli("`--profile-dir` has been removed with `login`. `connect` always uses the bot's Chrome profile.");
105
126
  }
106
127
  else if (arg === "--skip-browser") {
107
128
  skipBrowser = true;
@@ -137,10 +158,12 @@ function parseArgs(argv) {
137
158
  };
138
159
  if (target !== undefined)
139
160
  args.target = target;
140
- if (providerArg !== undefined)
141
- args.providerArg = providerArg;
142
- if (profileDir !== undefined && profileDir.length > 0)
143
- args.profileDir = profileDir;
161
+ if (account !== undefined) {
162
+ if (account.length === 0) {
163
+ rejectDeprecatedCli("`--account` requires a non-empty account ID.");
164
+ }
165
+ args.account = account;
166
+ }
144
167
  return args;
145
168
  }
146
169
  function rejectDeprecatedCli(message) {
@@ -262,10 +285,7 @@ export async function runCli(argv) {
262
285
  await connect(args);
263
286
  return;
264
287
  case "logout":
265
- await logout();
266
- return;
267
- case "login":
268
- await login(args);
288
+ await logout(args);
269
289
  return;
270
290
  case "settings":
271
291
  await settings(args);
@@ -284,13 +304,11 @@ export async function runCli(argv) {
284
304
  // the install — the bot just won't have the Tier-3 solver. Runs after pairing,
285
305
  // so the session carries a usable agent_session_token. Clears args.twoCaptchaKey
286
306
  // on success so the secret doesn't linger in memory longer than needed.
287
- async function maybeStoreTwoCaptchaKey(args) {
307
+ async function maybeStoreTwoCaptchaKey(args, session) {
288
308
  const key = args.twoCaptchaKey?.trim();
289
309
  if (key === undefined || key.length === 0)
290
310
  return;
291
- const storage = await openSessionStorage();
292
- const session = await storage.read();
293
- if (session?.agent_session_token === undefined) {
311
+ if (session.agent_session_token === undefined) {
294
312
  ui.warn("Couldn't vault the 2Captcha key — no active session yet. Re-run connect to retry.");
295
313
  return;
296
314
  }
@@ -337,7 +355,7 @@ async function settings(args) {
337
355
  const picker = await runSettingsSetup({
338
356
  ...(args.target !== undefined ? { initialTarget: args.target } : {}),
339
357
  initialRegistryEnabled: session.consent_skillify_telemetry === true,
340
- initialConsentOperatorInboxOtp: session.consent_operator_inbox_otp === true,
358
+ initialConsentOperatorInboxOtp: session.consent_operator_inbox_otp !== false,
341
359
  });
342
360
  args.target = picker.target;
343
361
  args.noRegistry = !picker.registryEnabled;
@@ -354,6 +372,9 @@ async function settings(args) {
354
372
  if (args.registryConfigured !== true) {
355
373
  args.noRegistry = session.consent_skillify_telemetry !== true;
356
374
  }
375
+ if (args.consentOperatorInboxOtp === undefined) {
376
+ args.consentOperatorInboxOtp = session.consent_operator_inbox_otp !== false;
377
+ }
357
378
  }
358
379
  const target = await resolveTarget(args.target);
359
380
  const agent = AGENTS[target];
@@ -361,11 +382,11 @@ async function settings(args) {
361
382
  ...session,
362
383
  saved_at: new Date().toISOString(),
363
384
  consent_skillify_telemetry: !args.noRegistry,
364
- consent_operator_inbox_otp: args.consentOperatorInboxOtp === true,
385
+ consent_operator_inbox_otp: args.consentOperatorInboxOtp !== false,
365
386
  };
366
387
  await storage.write(updated);
367
- await writeAgentConfig(target, agent, args);
368
- await maybeStoreTwoCaptchaKey(args);
388
+ await writeAgentConfig(target, agent, args, updated);
389
+ await maybeStoreTwoCaptchaKey(args, updated);
369
390
  ui.success(`${agent.display_name} settings saved.`);
370
391
  }
371
392
  async function connect(args) {
@@ -426,15 +447,18 @@ async function connectWithProfileGuard(args, profileDir) {
426
447
  // browser confirm. Pass --force-relogin to bypass (e.g. to switch Google).
427
448
  if (!args.forceRelogin) {
428
449
  const preflight = await checkAlreadyProvisioned();
429
- if (preflight !== null) {
450
+ if (preflight.kind !== "ceremony") {
430
451
  ui.divider();
431
452
  await hydrateArgsFromStoredPreferences(args);
432
453
  await ensureConsentRecorded(consentFromArgs(args), args.advancedConfigured === true);
433
- await writeAgentConfig(target, agent, args);
434
- await maybeStoreTwoCaptchaKey(args);
435
- ui.success(`Already connected (${preflight.providers.join(" + ")}). ` +
436
- `${agent.display_name} config refreshed.`);
437
- printProviderState(preflight.providers);
454
+ if (preflight.kind === "unverified") {
455
+ await writeAgentConfig(target, agent, args, preflight.session);
456
+ await maybeStoreTwoCaptchaKey(args, preflight.session);
457
+ ui.warn(preflightUnverifiedMessage(preflight.detail));
458
+ ui.hint(`Close any other Trusty Squire session and re-run ` +
459
+ `${ui.code("npx @trusty-squire/mcp connect --force-relogin")} to verify it.`);
460
+ return;
461
+ }
438
462
  // Backfill connected_providers from the bot-side marker on
439
463
  // pre-rc.5 sessions, so the preflight cache is current.
440
464
  for (const p of preflight.providers)
@@ -443,36 +467,31 @@ async function connectWithProfileGuard(args, profileDir) {
443
467
  // valid + bound, but if the bot's GitHub session validated DEAD, proactively
444
468
  // offer to reconnect it — a dead GitHub session is exactly why people re-run
445
469
  // connect (GitHub-OAuth signups fail). Skippable; non-interactive notices.
446
- if (!preflight.providers.includes("github")) {
447
- await offerGithubReloginIfDead(args, target, agent);
470
+ // Saying yes falls THROUGH into the same ceremony below rather than
471
+ // branching into a second sign-in command.
472
+ const reconnectGithub = !preflight.providers.includes("github") && (await offerGithubReloginIfDead(args));
473
+ if (!reconnectGithub) {
474
+ await writeAgentConfig(target, agent, args, preflight.session);
475
+ await maybeStoreTwoCaptchaKey(args, preflight.session);
476
+ ui.success(`Already connected (${preflight.providers.join(" + ")}). ` +
477
+ `${agent.display_name} config refreshed.`);
478
+ printProviderState(preflight.providers);
479
+ ui.hint(`Pass ${ui.code("--force-relogin")} to switch accounts or to refresh a ` +
480
+ `stale/expired session (this "connected" check reads cached cookies, ` +
481
+ `which can outlive the real session).`);
482
+ return;
448
483
  }
449
- ui.hint(`Pass ${ui.code("--force-relogin")} to switch accounts or to refresh a ` +
450
- `stale/expired session (this "connected" check reads cached cookies, ` +
451
- `which can outlive the real session).`);
452
- return;
484
+ args.forceRelogin = true;
485
+ args.forceReloginProvider = "github";
453
486
  }
454
487
  }
455
488
  console.warn("");
456
489
  console.warn("Opening the Trusty Squire install page in a browser. " +
457
490
  "The page walks you through signing in with Google and (optionally) GitHub.");
458
- // If the preflight keeps missing a valid session, connect re-opens this page
459
- // every run. The usual cause on a headless box is an ephemeral OS keychain
460
- // that doesn't persist the session between logins — point people at the
461
- // durable file backend so they don't loop.
462
- if ((await openSessionStorage()).backendName() === "keytar") {
463
- ui.hint(`If ${ui.code("connect")} re-opens this page on every run, your OS keychain may not be ` +
464
- `persisting the session. Set ${ui.code("TRUSTY_SQUIRE_SESSION_FILE=1")} (globally) to use a durable session file.`);
465
- }
466
491
  // --force-relogin means "redo the OAuth dance from scratch". The scoped
467
492
  // form clears only one provider; bare --force-relogin is the full-profile
468
493
  // account-switch escape hatch.
469
494
  if (args.forceRelogin) {
470
- if (args.forceReloginProvider !== undefined) {
471
- clearProviderLoggedIn(args.forceReloginProvider, profileDir);
472
- }
473
- else {
474
- clearAllProviderMarkers(profileDir);
475
- }
476
495
  let cookiesCleared;
477
496
  if (args.forceReloginProvider !== undefined) {
478
497
  cookiesCleared = await clearProviderCookies(profileDir, args.forceReloginProvider);
@@ -487,21 +506,6 @@ async function connectWithProfileGuard(args, profileDir) {
487
506
  process.exit(1);
488
507
  }
489
508
  }
490
- // Connect session validation: a SCOPED force-relogin=github on an
491
- // already-bound account is a GitHub-only login — Google's gate is already
492
- // satisfied (the account is bound + its session is what we'd re-bind), so we
493
- // must NOT drag the Google-first account-binding confirm page into it. Route
494
- // straight to the provider-scoped login (the `login --provider=github` path):
495
- // it opens a GitHub-only login (account chooser, since cookies were cleared
496
- // above), never touching Google. Falls through to the full confirm flow only
497
- // when the account isn't bound yet (nothing to skip) or the scope is google
498
- // (which can re-bind the account and so needs the claim).
499
- if (args.forceReloginProvider === "github" && (await checkAlreadyBound())) {
500
- const ok = await reloginGithubOnly(args, target, agent, { writeConfig: true });
501
- if (ok)
502
- return;
503
- process.exit(1);
504
- }
505
509
  const consent = consentFromArgs(args);
506
510
  // Detect egress class so the asn rides along in the install payload
507
511
  // (API uses it to correlate captcha failures with network class).
@@ -528,9 +532,9 @@ async function connectWithProfileGuard(args, profileDir) {
528
532
  if (asn !== null) {
529
533
  printAsnWarning(asn);
530
534
  }
531
- // Browser confirm: bind this machine + seed the bot's Chrome.
535
+ // Browser confirm: bind this machine in the bot's real Chrome profile.
532
536
  // The user signs into trustysquire from inside the bot's persistent
533
- // Chrome profile (with display, or noVNC on a headless box). That
537
+ // Chrome profile on a visible display. That
534
538
  // single sign-in does TWO things at once: trustysquire claims the
535
539
  // install (sets agent_session_token), AND the provider session
536
540
  // lands in the bot's Chrome profile so future OAuth-based signups
@@ -544,7 +548,6 @@ async function connectWithProfileGuard(args, profileDir) {
544
548
  };
545
549
  const session = await runInstallClaim(args.apiBase, target, baseSession, args.skipBrowser, {
546
550
  applyServerPrefs: !wantInteractive,
547
- ...connectCompletionOptions(args),
548
551
  });
549
552
  if (session === null) {
550
553
  ui.fail(`Install didn't complete — browser confirm never finished. ` +
@@ -553,56 +556,40 @@ async function connectWithProfileGuard(args, profileDir) {
553
556
  }
554
557
  const storage = await openSessionStorage();
555
558
  await storage.write(session);
556
- ui.success(`Session saved (${storage.backendName()})`);
559
+ ui.success(`Session saved (${storage.path})`);
557
560
  args.noRegistry = session.consent_skillify_telemetry !== true;
558
- args.consentOperatorInboxOtp = session.consent_operator_inbox_otp === true;
559
- // 0.8.1 — the bot's persistent profile may have a stale provider
560
- // marker from a previous install (the marker is sticky on disk).
561
- // The install confirm above only seeded one provider (whichever
562
- // OAuth button the user clicked on trustysquire.com), so trusting
563
- // the marker would make the step-2 secondary-provider prompt
564
- // short-circuit incorrectly. Probe live cookies + rewrite the
565
- // marker so loggedInProviders() returns ground truth.
561
+ args.consentOperatorInboxOtp = session.consent_operator_inbox_otp !== false;
562
+ // Probe the real profile. Cookie/session state is the source of truth; no
563
+ // persisted provider marker is allowed to outlive the session it describes.
564
+ // This probe is also the SUCCESS GATE: the machine claim alone proves the
565
+ // account plumbing, not that the bot can wear the user's identity at a third-
566
+ // party site. `null` means the probe itself failed, which is not a pass.
567
+ let providers = null;
566
568
  try {
567
- const actual = await ui.withSpinner({
569
+ providers = await ui.withSpinner({
568
570
  start: "Checking provider sessions",
569
571
  done: "Provider sessions checked",
570
- fail: () => "Provider session check failed (continuing)",
572
+ fail: () => "Provider session check failed",
571
573
  // validate=true: confirm each session is LIVE (not just cookie-present),
572
574
  // so a dead-but-present GitHub session isn't shown as connected.
573
575
  task: () => detectActiveProviderSessions(),
574
576
  });
575
- if (actual !== null) {
576
- clearAllProviderMarkers();
577
- for (const p of actual)
578
- markProviderLoggedIn(p);
579
- }
580
577
  }
581
578
  catch (err) {
582
- // Best-effort: a probe failure (rare — playwright launch should
583
- // succeed if the install confirm just opened Chrome there) just
584
- // leaves the marker as-is. The downstream secondary prompt's
585
- // logic still has the maybeOfferSecondaryProvider escape hatch
586
- // (yes/no prompt with the default-yes), so the user can still
587
- // reach GitHub even if we mis-identified the live state.
588
- //
589
- // Surface the reason on stderr so this never recurs invisibly: the
590
- // empty catch once hid a launch error (probe missing channel:"chrome",
591
- // reaching for an absent bundled Chromium) behind the bare "(continuing)"
592
- // ✗ for months, while the stale marker still printed "connected".
593
579
  console.error(`[connect] provider-session probe failed: ${err instanceof Error ? err.message : String(err)}`);
594
580
  }
595
- // Backfill connected_providers from the (now-fresh) bot-side marker.
596
- const providers = loggedInProviders();
597
- for (const p of providers)
581
+ // Persist the current live probe only as connect UX data.
582
+ for (const p of providers ?? [])
598
583
  await recordConnectedProvider(p);
599
- printProviderState(providers);
600
- await writeAgentConfig(target, agent, args);
601
- await maybeStoreTwoCaptchaKey(args);
602
- if (args.skipBrowser) {
603
- ui.panel(`--skip-browser was set, so the bot's Chrome didn't observe your sign-in.\n` +
604
- `Before your first OAuth signup, run:\n` +
605
- ` ${ui.code("npx @trusty-squire/mcp login [--provider=google|github]")}`, { title: "Heads up", color: "yellow" });
584
+ printProviderState(providers ?? []);
585
+ // Config + key land either way: the session is real and re-running connect
586
+ // must be able to pick up from here. Only the SUCCESS claim is gated.
587
+ await writeAgentConfig(target, agent, args, session);
588
+ await maybeStoreTwoCaptchaKey(args, session);
589
+ const complete = decideConnectComplete(providers, args.forceReloginProvider);
590
+ if (!complete.ok) {
591
+ ui.fail(connectIncompleteMessage(complete.reason, args.skipBrowser));
592
+ process.exit(1);
606
593
  }
607
594
  // Visual consistency: when the picker was running, close with
608
595
  // clack's `outro` so the bookends match. The flag-driven path keeps
@@ -626,10 +613,10 @@ async function hydrateArgsFromStoredPreferences(args) {
626
613
  if (session === null)
627
614
  return;
628
615
  args.noRegistry = session.consent_skillify_telemetry !== true;
629
- args.consentOperatorInboxOtp = session.consent_operator_inbox_otp === true;
616
+ args.consentOperatorInboxOtp = session.consent_operator_inbox_otp !== false;
630
617
  }
631
618
  catch {
632
- // Best-effort. Missing preferences fall back to the privacy-safe defaults.
619
+ // Best-effort. Missing inbox preference uses the default-on setting.
633
620
  }
634
621
  }
635
622
  function printProviderState(providers) {
@@ -641,16 +628,17 @@ function printProviderState(providers) {
641
628
  //
642
629
  // Default path (`skipBrowser=false`): opens the trustysquire confirm
643
630
  // URL in the bot's OWN persistent Chrome profile. The user signs in
644
- // once — that single sign-in claims the install AND seeds the bot's
631
+ // once — that single sign-in claims the install and establishes the bot's
645
632
  // profile with a provider session for future OAuth signups. The
646
633
  // pollUntilClaimed callback closes the Chrome window as soon as the
647
634
  // API flips the install to claimed.
648
635
  //
649
636
  // Fallback (`skipBrowser=true`): prints the URL, attempts a best-
650
637
  // effort `open()` to the user's default browser, polls the API for
651
- // claim. The bot's Chrome never starts, so the bot won't have a
652
- // provider session afterwards — the user must run `mcp login` before
653
- // their first OAuth signup. This path is for CI / scripted installs.
638
+ // claim. If that ceremony leaves no live provider session in the bot's Chrome,
639
+ // connect's success gate reports the install as incomplete rather than claiming
640
+ // an OAuth-capable install. This path is for CI / scripted installs that only
641
+ // need the config written.
654
642
  // True when the local session + bot profile already carry everything
655
643
  // connect would establish. Returns the list of confirmed provider sessions, or
656
644
  // null when anything's missing. Best-effort: any read/probe error returns null
@@ -698,6 +686,70 @@ export function decideProvisioned(session, tokenValid, providers) {
698
686
  return null;
699
687
  return { providers };
700
688
  }
689
+ export function decideConnectPreflight(session, tokenValid, providers) {
690
+ if (session === null ||
691
+ session.machine_token === undefined ||
692
+ session.agent_session_token === undefined ||
693
+ session.account_id === undefined) {
694
+ return { kind: "ceremony" };
695
+ }
696
+ // Deliberately precede probe-null: an expired agent token must re-pair, not
697
+ // silently refresh config into an install that 401s on every MCP call.
698
+ // Do not flip this ordering.
699
+ if (!tokenValid)
700
+ return { kind: "ceremony" };
701
+ if (providers === null)
702
+ return { kind: "unverified" };
703
+ const provisioned = decideProvisioned(session, tokenValid, providers);
704
+ return provisioned === null
705
+ ? { kind: "ceremony" }
706
+ : { kind: "provisioned", providers: provisioned.providers };
707
+ }
708
+ // Pure SUCCESS GATE for a connect run that went through the ceremony. The
709
+ // machine claim is not the product: an install is only connected when the
710
+ // bot's Chrome profile holds a session it can wear at third-party sites, and
711
+ // the only thing that proves it is the post-ceremony LIVE probe. `providers`
712
+ // is null when that probe failed, which fails closed — an unverifiable session
713
+ // must never be reported as connected (that is how connect used to print
714
+ // "Squire on duty" over an install with no Google session at all).
715
+ export function decideConnectComplete(providers, requestedProvider) {
716
+ if (providers === null)
717
+ return { ok: false, reason: "probe_failed" };
718
+ if (!providers.includes("google"))
719
+ return { ok: false, reason: "no_google_session" };
720
+ // A scoped --force-relogin=<provider> is an explicit ask; silently landing
721
+ // only Google would report success for work the user didn't get.
722
+ if (requestedProvider !== undefined && !providers.includes(requestedProvider)) {
723
+ return { ok: false, reason: "requested_provider_missing" };
724
+ }
725
+ return { ok: true };
726
+ }
727
+ export function connectIncompleteMessage(reason, skipBrowser) {
728
+ const retry = "npx @trusty-squire/mcp connect --force-relogin";
729
+ const skipBrowserNote = skipBrowser
730
+ ? " --skip-browser signs you in outside the bot's Chrome, so its profile never " +
731
+ "gains the session; re-run connect without it on a machine with a display " +
732
+ "(headless hosts get a noVNC URL)."
733
+ : "";
734
+ switch (reason) {
735
+ case "probe_failed":
736
+ return (`This machine is bound to your account, but I couldn't verify a live Google session ` +
737
+ `in the bot's Chrome profile, so I won't call this connected. ` +
738
+ `Close any other Trusty Squire session and re-run ${retry}.`);
739
+ case "no_google_session":
740
+ return (`This machine is bound to your account, but the bot's Chrome profile has no live ` +
741
+ `Google session, so the operator cannot act as you.${skipBrowserNote} ` +
742
+ `Re-run ${retry}.`);
743
+ case "requested_provider_missing":
744
+ return (`This machine is connected, but the provider sign-in you asked to refresh didn't ` +
745
+ `complete. Re-run ${retry}=github and finish the GitHub step in the browser.`);
746
+ }
747
+ }
748
+ export function preflightUnverifiedMessage(detail) {
749
+ return (`This machine is bound to your account, but I couldn't verify a live provider session ` +
750
+ `in the bot's Chrome profile (${detail}), so I won't call this connected. ` +
751
+ `Your agent config was refreshed.`);
752
+ }
701
753
  async function checkAlreadyProvisioned() {
702
754
  try {
703
755
  const storage = await openSessionStorage();
@@ -708,7 +760,7 @@ async function checkAlreadyProvisioned() {
708
760
  session.machine_token === undefined ||
709
761
  session.agent_session_token === undefined ||
710
762
  session.account_id === undefined) {
711
- return null;
763
+ return { kind: "ceremony" };
712
764
  }
713
765
  // Don't short-circuit on a present-but-expired token — re-pair.
714
766
  const stillValid = await agentTokenStillValid(session.api_base_url, session.agent_session_token);
@@ -718,81 +770,45 @@ async function checkAlreadyProvisioned() {
718
770
  // ability to wear the user's Google identity. validate=true so a dead-but-
719
771
  // present GitHub session isn't persisted into connected_providers.
720
772
  //
721
- // BUT a BUSY profile (another Chromium already using it — a live operate
722
- // session, a background heal run, an orphaned Chrome) makes the probe throw.
723
- // That must NOT read as "not provisioned": forcing a re-pair (the noVNC
724
- // dance) on a transient lock is the connect-loops-forever bug. Fall back to
725
- // the session's cached connected_providers instead — a busy profile, if
726
- // anything, means the bot IS wearing its browser session.
773
+ // A busy profile or any other probe failure must not force a re-pair: that
774
+ // is the connect-loops-forever bug. It also must not become a connected
775
+ // claim based on cached markers, because only a live probe proves the
776
+ // provider session. Refresh config with an explicit unverified warning.
727
777
  let providers;
728
778
  try {
729
779
  providers = await detectActiveProviderSessions();
730
780
  await syncConnectedProviders(providers);
731
781
  }
732
- catch {
733
- providers = (session.connected_providers ?? []).filter(isOAuthProviderId);
734
- }
735
- return decideProvisioned(session, stillValid, providers);
736
- }
737
- catch {
738
- return null;
739
- }
740
- }
741
- // Is the account already BOUND? — a valid session (machine + agent token +
742
- // account id) whose agent token still validates. Unlike checkAlreadyProvisioned
743
- // this does NOT require a live Google session: it answers "is the install
744
- // claimed to an account", which is what a scoped GitHub force-relogin needs to
745
- // know it can skip the Google-binding confirm page.
746
- async function checkAlreadyBound() {
747
- try {
748
- const storage = await openSessionStorage();
749
- const session = await storage.read();
750
- if (session === null ||
751
- session.machine_token === undefined ||
752
- session.agent_session_token === undefined ||
753
- session.account_id === undefined) {
754
- return false;
782
+ catch (err) {
783
+ const preflight = decideConnectPreflight(session, stillValid, null);
784
+ if (preflight.kind === "unverified") {
785
+ return {
786
+ kind: "unverified",
787
+ detail: err instanceof Error ? err.message : String(err),
788
+ session,
789
+ };
790
+ }
791
+ return preflight;
755
792
  }
756
- return await agentTokenStillValid(session.api_base_url, session.agent_session_token);
793
+ const preflight = decideConnectPreflight(session, stillValid, providers);
794
+ return preflight.kind === "provisioned" ? { ...preflight, session } : preflight;
757
795
  }
758
796
  catch {
759
- return false;
760
- }
761
- }
762
- // A GitHub-only login in the bot's profile (the provider-scoped path) — clears
763
- // the GitHub session and opens a fresh GitHub login (account chooser). Used by
764
- // `--force-relogin=github` and by the proactive prompt when connect detects a
765
- // dead GitHub session. Returns true on success.
766
- async function reloginGithubOnly(args, target, agent, opts) {
767
- ui.heading("Sign in to GitHub");
768
- clearProviderLoggedIn("github");
769
- const result = await ensureOAuthSession({
770
- provider: "github",
771
- apiBaseUrl: args.apiBase,
772
- forceOpen: true,
773
- });
774
- if (result.status === "logged_in" || result.status === "already_valid") {
775
- await recordConnectedProvider("github");
776
- if (opts.writeConfig)
777
- await writeAgentConfig(target, agent, args);
778
- ui.success("Signed in to GitHub. The bot is ready.");
779
- return true;
797
+ return { kind: "ceremony" };
780
798
  }
781
- ui.fail(result.status === "timeout"
782
- ? "GitHub sign-in timed out."
783
- : `GitHub sign-in failed: ${result.detail ?? "unknown error"}`);
784
- return false;
785
799
  }
786
800
  // Connect short-circuited (Google valid + bound) but the bot's GitHub session
787
801
  // validated DEAD. GitHub is optional, but a dead session is exactly why a user
788
802
  // re-runs connect (GitHub-OAuth signups were failing), so PROACTIVELY offer to
789
- // fix it rather than just noticing. Skippable; non-interactive falls back to a
790
- // notice so scripted installs never block.
791
- async function offerGithubReloginIfDead(args, target, agent) {
803
+ // fix it rather than just noticing. Returns true when the caller should fall
804
+ // through into the normal ceremony with a GitHub-scoped relogin — there is one
805
+ // sign-in pathway, so "yes" continues this run rather than starting a second
806
+ // command. Skippable; non-interactive notices so scripted installs never block.
807
+ async function offerGithubReloginIfDead(args) {
792
808
  const reconnectHint = `run ${ui.code("npx @trusty-squire/mcp connect --force-relogin=github")} when a service needs GitHub`;
793
809
  if (process.stdout.isTTY !== true || args.noInteractive) {
794
810
  ui.hint(`GitHub session is not active — ${reconnectHint}.`);
795
- return;
811
+ return false;
796
812
  }
797
813
  const answer = await confirm({
798
814
  message: "Your GitHub session looks dead (GitHub-only signups will fail). Reconnect GitHub now?",
@@ -800,14 +816,11 @@ async function offerGithubReloginIfDead(args, target, agent) {
800
816
  });
801
817
  if (isCancel(answer) || answer !== true) {
802
818
  ui.hint(`Skipped GitHub — ${reconnectHint}.`);
803
- return;
819
+ return false;
804
820
  }
805
- await reloginGithubOnly(args, target, agent, { writeConfig: false });
821
+ return true;
806
822
  }
807
823
  async function syncConnectedProviders(providers) {
808
- clearAllProviderMarkers();
809
- for (const p of providers)
810
- markProviderLoggedIn(p);
811
824
  try {
812
825
  const storage = await openSessionStorage();
813
826
  const session = await storage.read();
@@ -821,13 +834,13 @@ async function syncConnectedProviders(providers) {
821
834
  }
822
835
  catch {
823
836
  // Best-effort — marker/session drift only affects fast-path UX. The next
824
- // connect/login/provision probe can repair it.
837
+ // connect or provision probe can repair it.
825
838
  }
826
839
  }
827
840
  // Persist `provider` into session.connected_providers (idempotent).
828
- // Called after a successful ensureOAuthSession so the install
829
- // preflight on the next run can read both providers off the session
830
- // file without having to load the bot's profile-dir marker.
841
+ // Called after a successful live provider probe so the install preflight on the
842
+ // next run can read both providers from the session file without loading a
843
+ // profile-dir marker.
831
844
  async function recordConnectedProvider(provider) {
832
845
  try {
833
846
  const storage = await openSessionStorage();
@@ -855,7 +868,7 @@ async function recordConnectedProvider(provider) {
855
868
  function consentFromArgs(args) {
856
869
  return {
857
870
  skillifyTelemetry: !args.noRegistry,
858
- operatorInboxOtp: args.consentOperatorInboxOtp === true,
871
+ operatorInboxOtp: args.consentOperatorInboxOtp !== false,
859
872
  };
860
873
  }
861
874
  async function ensureConsentRecorded(consent, overwrite) {
@@ -877,21 +890,28 @@ async function ensureConsentRecorded(consent, overwrite) {
877
890
  });
878
891
  }
879
892
  catch {
880
- // Best-effort. If we can't persist consent, runtime treats missing
881
- // fields as not approved.
893
+ // Best-effort. Missing preferences use the runtime default (inbox reads on).
882
894
  }
883
895
  }
884
896
  // Writes the host agent's MCP config — extracted so both the normal
885
897
  // install path and the preflight-already-provisioned shortcut share
886
898
  // one implementation.
887
- async function writeAgentConfig(target, agent, args) {
899
+ async function writeAgentConfig(target, agent, args, session) {
888
900
  // Tokens themselves are NOT in the env — the MCP server reads them
889
- // from session storage (keychain / file), which keeps them out of
901
+ // from the account's 0600 JSON session file, which keeps them out of
890
902
  // any child-process listing or shell history.
891
903
  const launch = resolveServerLaunch();
892
904
  const env = {
893
905
  TRUSTY_SQUIRE_AGENT_IDENTITY: target,
894
906
  };
907
+ // Which account this host agent's server serves. Sessions are stored one per
908
+ // account, so pinning it here is what keeps an ALREADY-RUNNING server on the
909
+ // account it was launched for after someone connects a different one: the old
910
+ // process keeps its launch env, the new config names the new account.
911
+ const boundAccount = session.account_id;
912
+ if (boundAccount !== undefined && boundAccount.length > 0) {
913
+ env.TRUSTY_SQUIRE_ACCOUNT_ID = boundAccount;
914
+ }
895
915
  // Skill registry URL. The endpoint is not user-configurable; Advanced setup
896
916
  // controls whether it is written at all. Registry participation is also the
897
917
  // user's consent to contribute successful non-personal signup recipes.
@@ -920,32 +940,12 @@ async function writeAgentConfig(target, agent, args) {
920
940
  "(no shared skill replay).");
921
941
  }
922
942
  }
923
- // First-time setup stays open after the account claim so the user can finish
924
- // optional setup. A forced re-login has no remaining onboarding contract — but
925
- // the account claim (agent token) is NOT the end of the interactive sign-in.
926
- // The API flips to `claimed` the moment the OAuth identity lands, which on a
927
- // cold profile is BEFORE the provider's browser session is fully seeded (Google
928
- // can still be mid-flow with a second cold-profile challenge). Tearing down on
929
- // the bare claim killed the noVNC out from under that challenge — the "two
930
- // number picks with a red-close between them" bug. So force-relogin now waits
931
- // for the requested provider's post-clear cookie presence before it closes;
932
- // neither an explicit terminal page nor another provider can substitute. The
933
- // deadline still bounds the wait.
934
- export function shouldCompleteInstallClaim(claimed, completeOnClaim, sessionSeeded, wizardCompleted = false) {
935
- if (!claimed)
936
- return false;
937
- // A forced provider relogin is successful only when its provider-specific
938
- // completion evidence arrives. Finish alone is not an override: the user may
939
- // skip optional GitHub and still send the normal completion callback.
940
- if (completeOnClaim)
941
- return sessionSeeded;
942
- return wizardCompleted;
943
- }
944
- export function connectCompletionOptions(args) {
945
- return {
946
- completeOnClaim: args.forceRelogin,
947
- completionProvider: args.forceReloginProvider ?? "google",
948
- };
943
+ // The browser remains open until both the account claim and its explicit Finish
944
+ // callback arrive. The callback is emitted after the visible browser flow
945
+ // completes, so it works for onboarding and forced re-login without inspecting
946
+ // Chrome's on-disk cookie database.
947
+ export function shouldCompleteInstallClaim(claimed, wizardCompleted = false) {
948
+ return claimed && wizardCompleted;
949
949
  }
950
950
  // During normal onboarding, claim happens before the browser's Finish step.
951
951
  // Keep the terminal message aligned with that two-phase flow.
@@ -963,15 +963,9 @@ async function runInstallClaim(apiBase, target, baseSession, skipBrowser, option
963
963
  // check at the call site — bare closure-captured `let` doesn't.
964
964
  const state = { value: null };
965
965
  // The normal wizard's Finish button invokes the nonce-scoped loopback
966
- // callback. First-time onboarding waits for that signal so the user gets a
967
- // chance to complete optional setup. Forced re-login instead ends after the
968
- // API claim and requested provider seed because no setup remains to wait for.
969
- // Plain-login predicate: the connect claim browser runs plain (no CDP — a CDP
970
- // attach fails Google's OAuth "secure browser" check). The API delivers the
971
- // account claim, the SQLite cookie store proves a forced re-login landed,
972
- // and a per-run loopback callback carries the normal wizard's explicit
973
- // Finish signal.
974
- const pollOnce = async (profileDir, wizardCompleted) => {
966
+ // callback. Every install path waits for it. Plain login deliberately has no
967
+ // CDP attach, so this callback is the sole browser-completion authority.
968
+ const pollOnce = async (wizardCompleted) => {
975
969
  let claimedThisPoll = false;
976
970
  // Keep state.value warm — the install moves to "claimed" the instant the
977
971
  // user finishes signing in.
@@ -991,22 +985,10 @@ async function runInstallClaim(apiBase, target, baseSession, skipBrowser, option
991
985
  return "expired";
992
986
  }
993
987
  }
994
- // Tear down once the account is claimed AND the provider session has
995
- // actually seeded — not on the bare claim, which can land while Google is
996
- // still writing cookies on a cold profile.
997
988
  const claimed = state.value !== null;
998
- const sessionSeeded = claimed &&
999
- options.completeOnClaim &&
1000
- profileHasProviderCookies(profileDir, options.completionProvider);
1001
- // No browser URL to watch in plain mode. Normal onboarding keys off the
1002
- // explicit loopback Finish callback; forced re-login finishes once its
1003
- // requested provider session is safely seeded.
1004
- const tearDown = shouldCompleteInstallClaim(claimed, options.completeOnClaim, sessionSeeded, wizardCompleted);
989
+ const tearDown = shouldCompleteInstallClaim(claimed, wizardCompleted);
1005
990
  if (tearDown) {
1006
- return {
1007
- status: "claimed",
1008
- provider: options.completeOnClaim ? options.completionProvider : null,
1009
- };
991
+ return { status: "claimed", provider: null };
1010
992
  }
1011
993
  if (claimedThisPoll) {
1012
994
  console.error(chalk.dim(` ✓ ${claimHeartbeatMessage(true)}`));
@@ -1046,21 +1028,16 @@ async function runInstallClaim(apiBase, target, baseSession, skipBrowser, option
1046
1028
  }
1047
1029
  // Default: run the confirm INSIDE the bot's Chrome. The user signs
1048
1030
  // The wizard page reads provider state from /v1/auth/whoami so no
1049
- // CLI-side hint is needed. apiBaseUrl threads through to the
1050
- // headless rig so it can shorten the cloudflared tunnel URL to
1051
- // `trustysquire.ai/g/<slug>` before printing it in the banner (G15).
1031
+ // CLI-side hint is needed.
1052
1032
  const result = await openInstallConfirmInBotChrome({
1053
1033
  confirmUrl: initiate.confirm_url,
1054
1034
  pollUntilClaimed: pollOnce,
1055
- apiBaseUrl: apiBase,
1056
1035
  heartbeatMessage: () => claimHeartbeatMessage(state.value !== null),
1057
1036
  });
1058
1037
  // rc.33 — surface the underlying error instead of letting the outer
1059
1038
  // wrapper print a generic "browser confirm step never finished."
1060
- // Most common case: a fresh headless box without the noVNC stack
1061
- // (x11vnc/novnc/websockify/cloudflared) — the runHeadlessChrome
1062
- // requireBinaries() throw already names the missing binaries and
1063
- // the apt-get install line, but the message was getting swallowed.
1039
+ // Surface the underlying browser-launch error rather than replacing it
1040
+ // with a generic confirmation timeout.
1064
1041
  if (result.status === "error") {
1065
1042
  ui.fail(`Couldn't open the confirm page: ${result.detail ?? "unknown error"}`);
1066
1043
  process.exit(1);
@@ -1087,7 +1064,7 @@ export function applyInstallPreferences(baseSession, preferences, applyServerPre
1087
1064
  return {
1088
1065
  ...baseSession,
1089
1066
  consent_skillify_telemetry: preferences.registry_enabled === true,
1090
- consent_operator_inbox_otp: preferences.consent_operator_inbox_otp === true,
1067
+ consent_operator_inbox_otp: preferences.consent_operator_inbox_otp !== false,
1091
1068
  };
1092
1069
  }
1093
1070
  async function resolveTarget(explicit) {
@@ -1110,75 +1087,20 @@ async function resolveTarget(explicit) {
1110
1087
  }
1111
1088
  process.exit(2);
1112
1089
  }
1113
- async function logout() {
1090
+ // Logs out ONE account — the one most recently connected, or `--account=<id>`.
1091
+ // Other accounts installed on this machine keep their sessions, and the servers
1092
+ // serving them keep working.
1093
+ async function logout(args) {
1114
1094
  const storage = await openSessionStorage();
1115
- await storage.clear();
1116
- console.warn(`✓ Cleared local session (${storage.backendName()}).`);
1117
- }
1118
- // Establish (or confirm) a provider session in the bot's persistent
1119
- // Chrome profile — the one-time interactive login the OAuth-first
1120
- // signup path needs. With a display this opens a Chrome window;
1121
- // headless, it prints a URL to log in from any browser. Defaults to
1122
- // Google; `login --provider=github` logs the same profile into GitHub.
1123
- // Unlike the login stage inside `install`, this command fails loud on
1124
- // timeout/error — it's the explicit retry path.
1125
- async function login(args) {
1126
- const profileDir = args.profileDir ?? CHROME_PROFILE_DIR;
1127
- try {
1128
- await withProfileOperationGuard(profileDir, () => loginWithProfileGuard(args, profileDir));
1129
- }
1130
- catch (err) {
1131
- if (err instanceof ProfileBusyError) {
1132
- ui.fail(PROFILE_BUSY_MESSAGE);
1133
- process.exit(1);
1134
- }
1135
- throw err;
1136
- }
1137
- }
1138
- async function loginWithProfileGuard(args, profileDir) {
1139
- const provider = args.providerArg ?? args.forceReloginProvider ?? "google";
1140
- const label = provider === "github" ? "GitHub" : "Google";
1141
- ui.heading(`Sign in to ${label}`);
1142
- // --force-relogin wipes this provider's cookies (via forceOpen below),
1143
- // so drop its marker up front too. Otherwise a stale marker from a
1144
- // prior successful login survives a re-login that the user abandons or
1145
- // that times out (e.g. GitHub's 2FA "verify it's you" never finished) —
1146
- // leaving logged-in-providers.json claiming a session whose auth cookie
1147
- // (user_session) no longer exists. ensureOAuthSession re-adds the marker
1148
- // only when it confirms a live cookie, so success still records it.
1149
- if (args.forceRelogin)
1150
- clearProviderLoggedIn(provider, profileDir);
1151
- const result = await ensureOAuthSession({
1152
- provider,
1153
- apiBaseUrl: args.apiBase,
1154
- // --profile-dir pins login to an isolated profile (a secondary
1155
- // personal Google identity) instead of the shared bot profile.
1156
- ...(args.profileDir !== undefined ? { profileDir: args.profileDir } : {}),
1157
- // 0.8.3-rc.1 — --force-relogin now also applies to the bare
1158
- // `login` command. Without this, a valid cached session
1159
- // short-circuits the flow and the operator has no way to open
1160
- // the noVNC URL — even when the actual problem is a service-
1161
- // side challenge (GitHub's "verify it's you" / Google's device-
1162
- // prompt drift) that only an interactive browser session can
1163
- // clear.
1164
- ...(args.forceRelogin ? { forceOpen: true } : {}),
1165
- });
1166
- switch (result.status) {
1167
- case "already_valid":
1168
- await recordConnectedProvider(provider);
1169
- ui.success(`Already signed in to ${label}.`);
1170
- return;
1171
- case "logged_in":
1172
- await recordConnectedProvider(provider);
1173
- ui.success(`Signed in to ${label}. The bot is ready.`);
1174
- return;
1175
- case "timeout":
1176
- ui.fail(`Sign-in timed out. Retry: ${ui.code("npx @trusty-squire/mcp login")}`);
1177
- process.exit(1);
1178
- case "error":
1179
- ui.fail(`Sign-in failed: ${result.detail ?? "unknown error"}`);
1180
- process.exit(1);
1095
+ const target = args.account ?? (await storage.currentAccountId());
1096
+ if (target === null || (args.account !== undefined && (await storage.read(target)) === null)) {
1097
+ console.warn("✓ No local session to clear.");
1098
+ return;
1181
1099
  }
1100
+ await storage.clear(target);
1101
+ const remaining = await storage.listAccounts();
1102
+ console.warn(`✓ Cleared local session for account ${target} (${storage.path}).` +
1103
+ (remaining.length > 0 ? ` Still installed: ${remaining.join(", ")}.` : ""));
1182
1104
  }
1183
1105
  function printHelp() {
1184
1106
  ui.heading("Trusty Squire");
@@ -1186,14 +1108,13 @@ function printHelp() {
1186
1108
  console.warn("");
1187
1109
  console.warn(`${chalk.bold("Commands")}`);
1188
1110
  console.warn(` ${ui.code("connect")} set up this machine (default)`);
1189
- console.warn(` ${ui.code("login --provider=<p>")} add a Google or GitHub session`);
1190
1111
  console.warn(` ${ui.code("settings")} edit registry and OTP choices`);
1191
- console.warn(` ${ui.code("logout")} clear the local session`);
1112
+ console.warn(` ${ui.code("logout [--account=<id>]")} clear ONE account's local session`);
1192
1113
  console.warn("");
1193
1114
  console.warn(`${chalk.bold("Flags for connect")}`);
1194
1115
  console.warn(` --target=<${Object.keys(AGENTS).join("|")}>`);
1195
1116
  console.warn(` --skip-browser don't launch a browser (CI mode)`);
1196
- console.warn(` --force-relogin[=google|github] switch the bound account or one provider`);
1117
+ console.warn(` --force-relogin[=google|github] re-sign-in: switch the bound account or refresh one provider`);
1197
1118
  console.warn(` --no-registry disable managed registry participation`);
1198
1119
  console.warn(` --no-interactive skip the TUI picker (use flag defaults only)`);
1199
1120
  console.warn("");
@@ -1250,5 +1171,5 @@ function printAsnWarning(asn) {
1250
1171
  return;
1251
1172
  }
1252
1173
  }
1253
- export { connect, logout, login, parseArgs, pollForClaim, printAsnWarning, resolveCopiedNpxServerLaunch, resolveServerLaunch, };
1174
+ export { connect, logout, parseArgs, pollForClaim, printAsnWarning, resolveCopiedNpxServerLaunch, resolveServerLaunch, };
1254
1175
  //# sourceMappingURL=cli.js.map