@trusty-squire/mcp 0.9.19-rc.2 → 0.9.19-rc.21

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 (106) hide show
  1. package/README.md +15 -33
  2. package/dist/api-client.d.ts +6 -0
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js.map +1 -1
  5. package/dist/bin.js +4 -10
  6. package/dist/bin.js.map +1 -1
  7. package/dist/bot/agent.d.ts +24 -2
  8. package/dist/bot/agent.d.ts.map +1 -1
  9. package/dist/bot/agent.js +634 -55
  10. package/dist/bot/agent.js.map +1 -1
  11. package/dist/bot/browser.d.ts +7 -0
  12. package/dist/bot/browser.d.ts.map +1 -1
  13. package/dist/bot/browser.js +182 -3
  14. package/dist/bot/browser.js.map +1 -1
  15. package/dist/bot/credential-extraction-flow.d.ts +2 -0
  16. package/dist/bot/credential-extraction-flow.d.ts.map +1 -1
  17. package/dist/bot/credential-extraction-flow.js +71 -1
  18. package/dist/bot/credential-extraction-flow.js.map +1 -1
  19. package/dist/bot/form-fill.d.ts.map +1 -1
  20. package/dist/bot/form-fill.js +11 -0
  21. package/dist/bot/form-fill.js.map +1 -1
  22. package/dist/bot/google-login.d.ts.map +1 -1
  23. package/dist/bot/google-login.js +37 -1
  24. package/dist/bot/google-login.js.map +1 -1
  25. package/dist/bot/index.d.ts +1 -0
  26. package/dist/bot/index.d.ts.map +1 -1
  27. package/dist/bot/index.js +1 -0
  28. package/dist/bot/index.js.map +1 -1
  29. package/dist/bot/login-state.d.ts +2 -1
  30. package/dist/bot/login-state.d.ts.map +1 -1
  31. package/dist/bot/login-state.js +22 -5
  32. package/dist/bot/login-state.js.map +1 -1
  33. package/dist/bot/nav-search.d.ts.map +1 -1
  34. package/dist/bot/nav-search.js +9 -0
  35. package/dist/bot/nav-search.js.map +1 -1
  36. package/dist/bot/post-signup-flow.d.ts.map +1 -1
  37. package/dist/bot/post-signup-flow.js +21 -0
  38. package/dist/bot/post-signup-flow.js.map +1 -1
  39. package/dist/bot/post-signup-recovery-state.d.ts +3 -0
  40. package/dist/bot/post-signup-recovery-state.d.ts.map +1 -1
  41. package/dist/bot/post-signup-recovery-state.js +3 -0
  42. package/dist/bot/post-signup-recovery-state.js.map +1 -1
  43. package/dist/bot/provision-session.d.ts +116 -1
  44. package/dist/bot/provision-session.d.ts.map +1 -1
  45. package/dist/bot/provision-session.js +885 -41
  46. package/dist/bot/provision-session.js.map +1 -1
  47. package/dist/bot/redact.d.ts.map +1 -1
  48. package/dist/bot/redact.js +25 -2
  49. package/dist/bot/redact.js.map +1 -1
  50. package/dist/bot/replay-skill.d.ts +6 -0
  51. package/dist/bot/replay-skill.d.ts.map +1 -1
  52. package/dist/bot/replay-skill.js +39 -5
  53. package/dist/bot/replay-skill.js.map +1 -1
  54. package/dist/bot/skill-hint.d.ts +7 -0
  55. package/dist/bot/skill-hint.d.ts.map +1 -0
  56. package/dist/bot/skill-hint.js +105 -0
  57. package/dist/bot/skill-hint.js.map +1 -0
  58. package/dist/bot/terminal-gate.d.ts +3 -1
  59. package/dist/bot/terminal-gate.d.ts.map +1 -1
  60. package/dist/bot/terminal-gate.js +19 -0
  61. package/dist/bot/terminal-gate.js.map +1 -1
  62. package/dist/install/agents.d.ts.map +1 -1
  63. package/dist/install/agents.js +12 -2
  64. package/dist/install/agents.js.map +1 -1
  65. package/dist/install/cli.d.ts +14 -2
  66. package/dist/install/cli.d.ts.map +1 -1
  67. package/dist/install/cli.js +346 -150
  68. package/dist/install/cli.js.map +1 -1
  69. package/dist/install/interactive.d.ts +9 -3
  70. package/dist/install/interactive.d.ts.map +1 -1
  71. package/dist/install/interactive.js +80 -140
  72. package/dist/install/interactive.js.map +1 -1
  73. package/dist/install/proxy-url.d.ts +2 -0
  74. package/dist/install/proxy-url.d.ts.map +1 -0
  75. package/dist/install/proxy-url.js +20 -0
  76. package/dist/install/proxy-url.js.map +1 -0
  77. package/dist/install/ui.js +1 -1
  78. package/dist/install/ui.js.map +1 -1
  79. package/dist/session.d.ts +3 -0
  80. package/dist/session.d.ts.map +1 -1
  81. package/dist/session.js.map +1 -1
  82. package/dist/skill-registry-client.d.ts +8 -0
  83. package/dist/skill-registry-client.d.ts.map +1 -1
  84. package/dist/skill-registry-client.js +70 -53
  85. package/dist/skill-registry-client.js.map +1 -1
  86. package/dist/tools/index.d.ts +1 -2
  87. package/dist/tools/index.d.ts.map +1 -1
  88. package/dist/tools/index.js +10 -19
  89. package/dist/tools/index.js.map +1 -1
  90. package/dist/tools/provision-any.d.ts +3 -0
  91. package/dist/tools/provision-any.d.ts.map +1 -1
  92. package/dist/tools/provision-any.js +162 -32
  93. package/dist/tools/provision-any.js.map +1 -1
  94. package/dist/tools/provision-drive.d.ts +121 -5
  95. package/dist/tools/provision-drive.d.ts.map +1 -1
  96. package/dist/tools/provision-drive.js +339 -48
  97. package/dist/tools/provision-drive.js.map +1 -1
  98. package/dist/tools/store-credential.d.ts +5 -0
  99. package/dist/tools/store-credential.d.ts.map +1 -1
  100. package/dist/tools/store-credential.js +5 -0
  101. package/dist/tools/store-credential.js.map +1 -1
  102. package/package.json +1 -3
  103. package/dist/bot/telegram-notify.d.ts +0 -8
  104. package/dist/bot/telegram-notify.d.ts.map +0 -1
  105. package/dist/bot/telegram-notify.js +0 -134
  106. package/dist/bot/telegram-notify.js.map +0 -1
@@ -1,9 +1,9 @@
1
- // Setup CLI — install / logout / login subcommands.
1
+ // Setup CLI — connect / settings / logout / login subcommands.
2
2
  //
3
- // npx @trusty-squire/mcp install --target=claude-code
3
+ // npx @trusty-squire/mcp connect --target=claude-code
4
4
  // Issues a machine token, then opens the trustysquire install-
5
5
  // confirm page in the bot's own Chrome. The user signs in there
6
- // once (Google or GitHub) — that single sign-in does TWO things:
6
+ // once with Google — that single sign-in does TWO things:
7
7
  // (a) trustysquire claims the install and binds the machine to
8
8
  // the user's account, and
9
9
  // (b) the bot's Chrome profile gains a provider session it can
@@ -26,14 +26,7 @@
26
26
  // their own browser (CI / scripted installs)
27
27
  // --proxy-url=<url> bake a residential proxy into the MCP config's
28
28
  // env (UNIVERSAL_BOT_PROXY_URL)
29
- // --registry-url=<url> override the default skill registry URL
30
- // (default: https://registry.trustysquire.ai).
31
- // Baked into the MCP config's env as
32
- // TRUSTY_SQUIRE_REGISTRY_URL so the Tier-2
33
- // router is on out of the box.
34
- // --no-registry omit TRUSTY_SQUIRE_REGISTRY_URL from the
35
- // config → mcp skips the router entirely and
36
- // every signup goes through the universal bot
29
+ // --no-registry disable managed registry participation
37
30
  //
38
31
  // Pure module — `runCli()` is invoked by bin.ts. No shebang, no
39
32
  // entrypoint guard, no top-level execution.
@@ -41,7 +34,6 @@ import process from "node:process";
41
34
  import { cpSync, rmSync } from "node:fs";
42
35
  import { homedir } from "node:os";
43
36
  import { dirname, join } from "node:path";
44
- import { createInterface } from "node:readline";
45
37
  import { loadHarvesterEnvFile } from "../operator-env.js";
46
38
  import { fileURLToPath } from "node:url";
47
39
  import { installInitiate, installPoll, issueMachineToken } from "../api-client.js";
@@ -50,42 +42,35 @@ import { AGENTS, detectInstalledAgents, writeClaudeCodePermissions, } from "./ag
50
42
  import { detectAsn } from "../bot/index.js";
51
43
  import { detectActiveProviderSessions, ensureOAuthSession, openInstallConfirmInBotChrome, } from "../bot/google-login.js";
52
44
  import {} from "../bot/oauth-providers.js";
53
- import { clearAllProviderMarkers, clearProviderCookies, clearProviderLoggedIn, loggedInProviders, markProviderLoggedIn, } from "../bot/login-state.js";
45
+ import { clearAllProviderMarkers, clearBrowserProfile, clearProviderCookies, clearProviderLoggedIn, loggedInProviders, markProviderLoggedIn, } from "../bot/login-state.js";
46
+ import { waitForProfileFree } from "../bot/profile.js";
54
47
  import { VERSION } from "../version.js";
55
48
  import * as ui from "./ui.js";
56
- import { runInteractiveSetup, shouldRunInteractive, showOutro } from "./interactive.js";
49
+ import { runInteractiveSetup, runSettingsSetup, shouldRunInteractive, showOutro, } from "./interactive.js";
57
50
  import chalk from "chalk";
51
+ import { normalizeProxyUrl } from "./proxy-url.js";
58
52
  const DEFAULT_API_BASE = process.env.TRUSTY_SQUIRE_API_BASE ?? "https://trusty-squire-api.fly.dev";
59
- // Default skill-registry URL. Wired into the MCP config's env block
60
- // so users don't have to set it manually without it, mcp skips the
61
- // Tier-2 router and every signup goes through the universal bot
62
- // (fail-open by design, but a worse experience than just using the
63
- // closed loop).
64
- const DEFAULT_REGISTRY_URL = process.env.TRUSTY_SQUIRE_REGISTRY_URL ?? "https://registry.trustysquire.ai";
53
+ // Managed skill-registry URL. Advanced setup decides whether this is written
54
+ // into the MCP config; the URL itself is product-owned and not user-editable.
55
+ const DEFAULT_REGISTRY_URL = "https://registry.trustysquire.ai";
65
56
  function parseArgs(argv) {
66
57
  const positional = argv.filter((a) => !a.startsWith("--"));
67
- // `connect` is the canonical command as of 0.6.14. `install` is kept
68
- // as a hidden alias so any docs/scripts/blog-posts published against
69
- // ≤0.6.13 still work it emits a one-line deprecation notice but
70
- // otherwise behaves identically. Default (no positional) → `connect`
71
- // because the most common invocation is `npx @trusty-squire/mcp` with
72
- // no args, and that should still kick off the setup flow.
73
- let command = positional[0] ?? "connect";
58
+ // Default (no positional) → `connect` because the most common invocation is
59
+ // `npx @trusty-squire/mcp` with no args, and that should kick off setup.
60
+ const command = positional[0] ?? "connect";
74
61
  if (command === "install") {
75
- console.warn("[trusty-squire] `install` is now `connect`. " +
76
- "This alias still works but will be removed in a future major. " +
77
- "Update your docs/scripts to: `npx @trusty-squire/mcp connect`.");
78
- command = "connect";
62
+ rejectDeprecatedCli("`install` has been removed. Use `npx @trusty-squire/mcp connect`.");
79
63
  }
80
64
  let target;
81
65
  let apiBase = DEFAULT_API_BASE;
82
66
  let proxyUrl;
83
- let registryUrl;
84
67
  let noRegistry = false;
68
+ let registryConfigured = false;
85
69
  let providerArg;
86
70
  let profileDir;
87
71
  let skipBrowser = false;
88
72
  let forceRelogin = false;
73
+ let forceReloginProvider;
89
74
  let noInteractive = false;
90
75
  for (const arg of argv) {
91
76
  if (arg.startsWith("--target=")) {
@@ -103,13 +88,23 @@ function parseArgs(argv) {
103
88
  apiBase = arg.slice("--api-base=".length);
104
89
  }
105
90
  else if (arg.startsWith("--proxy-url=")) {
106
- proxyUrl = arg.slice("--proxy-url=".length);
91
+ const rawProxyUrl = arg.slice("--proxy-url=".length);
92
+ const normalized = normalizeProxyUrl(rawProxyUrl);
93
+ if (rawProxyUrl.length > 0 && normalized === undefined) {
94
+ console.error("invalid --proxy-url. Use http://user:pass@host:port or socks5://host:port.");
95
+ process.exit(64);
96
+ }
97
+ proxyUrl = normalized;
107
98
  }
108
99
  else if (arg.startsWith("--registry-url=")) {
109
- registryUrl = arg.slice("--registry-url=".length);
100
+ rejectDeprecatedCli("`--registry-url` has been removed. Trusty Squire uses the managed skill registry.");
110
101
  }
111
102
  else if (arg === "--no-registry") {
112
103
  noRegistry = true;
104
+ registryConfigured = true;
105
+ }
106
+ else if (arg === "--registry") {
107
+ rejectDeprecatedCli("`--registry` has been removed because the managed registry is enabled by default.");
113
108
  }
114
109
  else if (arg.startsWith("--provider=")) {
115
110
  const p = arg.slice("--provider=".length);
@@ -119,18 +114,23 @@ function parseArgs(argv) {
119
114
  else if (arg.startsWith("--profile-dir=")) {
120
115
  profileDir = arg.slice("--profile-dir=".length);
121
116
  }
122
- else if (arg === "--skip-browser" || arg === "--skip-login") {
123
- // --skip-login kept as an alias for the 0.5.0 spelling so any
124
- // scripted callers still work.
117
+ else if (arg === "--skip-browser") {
125
118
  skipBrowser = true;
126
119
  }
120
+ else if (arg === "--skip-login") {
121
+ rejectDeprecatedCli("`--skip-login` has been removed. Use `--skip-browser`.");
122
+ }
127
123
  else if (arg === "--force-relogin") {
128
124
  forceRelogin = true;
129
125
  }
126
+ else if (arg.startsWith("--force-relogin=")) {
127
+ forceRelogin = true;
128
+ const p = arg.slice("--force-relogin=".length);
129
+ if (p === "google" || p === "github")
130
+ forceReloginProvider = p;
131
+ }
130
132
  else if (arg === "--skip-secondary") {
131
- // No-op: kept as a flag for backwards-compat. The 0.8.2 wizard
132
- // collapsed step 1 + step 2 into one browser session, so there
133
- // is no "secondary" stage left to skip.
133
+ rejectDeprecatedCli("`--skip-secondary` has been removed; connect is single-stage.");
134
134
  }
135
135
  else if (arg === "--no-interactive") {
136
136
  noInteractive = true;
@@ -141,21 +141,25 @@ function parseArgs(argv) {
141
141
  apiBase,
142
142
  skipBrowser,
143
143
  forceRelogin,
144
+ ...(forceReloginProvider !== undefined ? { forceReloginProvider } : {}),
144
145
  noRegistry,
146
+ ...(registryConfigured ? { registryConfigured } : {}),
145
147
  noInteractive,
146
148
  };
147
149
  if (target !== undefined)
148
150
  args.target = target;
149
151
  if (proxyUrl !== undefined && proxyUrl.length > 0)
150
152
  args.proxyUrl = proxyUrl;
151
- if (registryUrl !== undefined && registryUrl.length > 0)
152
- args.registryUrl = registryUrl;
153
153
  if (providerArg !== undefined)
154
154
  args.providerArg = providerArg;
155
155
  if (profileDir !== undefined && profileDir.length > 0)
156
156
  args.profileDir = profileDir;
157
157
  return args;
158
158
  }
159
+ function rejectDeprecatedCli(message) {
160
+ console.error(`[trusty-squire] ${message}`);
161
+ process.exit(64);
162
+ }
159
163
  function isAgentTarget(s) {
160
164
  // Source of truth is AGENTS — adding/removing a target there auto-
161
165
  // propagates here, so a new agent (or a removed one) can't drift the
@@ -168,23 +172,14 @@ function isAgentTarget(s) {
168
172
  // that points at a permanent path. Use the absolute bin.js path
169
173
  // directly. Deterministic, offline, fast.
170
174
  //
171
- // 2. Ephemeral + stable version — the CLI was invoked via
172
- // `npx @trusty-squire/mcp@X.Y.Z`, which copies the package into
173
- // npx's throwaway cache. The cache CAN get swept, so we don't
174
- // pin the cache path; instead we write `npx @trusty-squire/mcp@<version>`
175
- // so the launch re-resolves against npm every time. Works as
176
- // long as the version is on the public npm registry.
177
- //
178
- // 3. Ephemeral + prerelease version — the CLI was invoked via
179
- // `npx <tarball-url>` (the GitHub-Release test pattern). The
180
- // version isn't on npm, so case 2 would fail with `ETARGET`.
181
- // We instead copy the package out of the ephemeral cache into
182
- // `~/.trusty-squire/lib/mcp` and write a `node <stable>/dist/bin.js`
183
- // launch. The stable copy survives npx-cache cleanup; if the user
184
- // re-installs they overwrite it.
185
- //
186
- // Prerelease detection: semver prerelease versions carry a `-` (e.g.
187
- // `0.6.0-rc.1`). Stable versions don't. Cheap, reliable.
175
+ // 2. Ephemeral — the CLI was invoked via npx, which copies the package into
176
+ // npx's throwaway cache. The cache CAN get swept, so never pin the cache
177
+ // path into a host agent config. Instead write
178
+ // `npx -y @trusty-squire/mcp@<version> server`, which re-resolves the exact
179
+ // published version on each agent launch. This matters for RCs: prerelease
180
+ // versions are published to npm on the `next` tag, so treating every
181
+ // prerelease as a non-registry tarball leaves Goose pointing at dead npx
182
+ // cache paths and stale tool schemas.
188
183
  /**
189
184
  * Copy an npx-style node_modules tree to a stable location.
190
185
  *
@@ -221,11 +216,14 @@ function resolveServerLaunch() {
221
216
  if (!ephemeral) {
222
217
  return { command: process.execPath, args: [binPath, "server"] };
223
218
  }
224
- const isPrerelease = VERSION.includes("-");
225
- if (!isPrerelease) {
226
- return { command: "npx", args: ["-y", `@trusty-squire/mcp@${VERSION}`, "server"] };
227
- }
228
- // Prerelease from GitHub Releases copy the package PLUS the
219
+ return { command: "npx", args: ["-y", `@trusty-squire/mcp@${VERSION}`, "server"] };
220
+ }
221
+ // Historical fallback for GitHub-release tarball installs. The normal install
222
+ // path no longer calls this because RCs are published to npm and host configs
223
+ // must not pin npx cache paths. Kept exported for the regression tests around
224
+ // copying broken npx symlink trees.
225
+ function resolveCopiedNpxServerLaunch(binPath) {
226
+ // Copy the package PLUS the
229
227
  // ephemeral cache's entire `node_modules` to a stable location.
230
228
  //
231
229
  // We need both because the package imports @modelcontextprotocol/sdk,
@@ -286,6 +284,9 @@ export async function runCli(argv) {
286
284
  case "login":
287
285
  await login(args);
288
286
  return;
287
+ case "settings":
288
+ await settings(args);
289
+ return;
289
290
  case "help":
290
291
  printHelp();
291
292
  return;
@@ -295,11 +296,58 @@ export async function runCli(argv) {
295
296
  process.exit(64);
296
297
  }
297
298
  }
299
+ async function settings(args) {
300
+ const storage = await openSessionStorage();
301
+ const session = await storage.read();
302
+ if (session === null) {
303
+ ui.fail(`No local Trusty Squire session found. Run ${ui.code("npx @trusty-squire/mcp connect")} first.`);
304
+ process.exit(1);
305
+ }
306
+ if (!args.noInteractive && process.stdin.isTTY === true) {
307
+ const picker = await runSettingsSetup({
308
+ ...(args.target !== undefined ? { initialTarget: args.target } : {}),
309
+ ...(session.proxy_url !== undefined ? { initialProxyUrl: session.proxy_url } : {}),
310
+ initialRegistryEnabled: session.consent_skillify_telemetry === true,
311
+ initialConsentOperatorInboxOtp: session.consent_operator_inbox_otp === true,
312
+ });
313
+ args.target = picker.target;
314
+ if (picker.proxyUrl !== undefined)
315
+ args.proxyUrl = picker.proxyUrl;
316
+ args.noRegistry = !picker.registryEnabled;
317
+ args.advancedConfigured = true;
318
+ args.consentOperatorInboxOtp = picker.consentOperatorInboxOtp === true;
319
+ }
320
+ else {
321
+ if (args.target === undefined) {
322
+ ui.fail(`Pass ${ui.code("--target=<agent>")} when running settings outside an interactive terminal.`);
323
+ process.exit(64);
324
+ }
325
+ if (args.registryConfigured !== true) {
326
+ args.noRegistry = session.consent_skillify_telemetry !== true;
327
+ }
328
+ if (args.proxyUrl === undefined && session.proxy_url !== undefined) {
329
+ args.proxyUrl = session.proxy_url;
330
+ }
331
+ }
332
+ const target = await resolveTarget(args.target);
333
+ const agent = AGENTS[target];
334
+ const updated = {
335
+ ...session,
336
+ saved_at: new Date().toISOString(),
337
+ consent_skillify_telemetry: !args.noRegistry,
338
+ consent_operator_inbox_otp: args.consentOperatorInboxOtp === true,
339
+ ...(args.proxyUrl !== undefined && args.proxyUrl.trim().length > 0
340
+ ? { proxy_url: args.proxyUrl.trim() }
341
+ : {}),
342
+ };
343
+ await storage.write(updated);
344
+ await writeAgentConfig(target, agent, args);
345
+ ui.success(`${agent.display_name} settings saved.`);
346
+ }
298
347
  async function connect(args) {
299
- // 0.8.1 — interactive picker (clack), Goose-flavored. Walks the
300
- // user through agent / OAuth providers / LLM choice / advanced
301
- // before the install ceremony fires. The picker fills in args so
302
- // the rest of this function is unchanged.
348
+ // Interactive picker (clack). Walks the user through agent + advanced setup
349
+ // before the browser install ceremony fires. The picker fills in args so the
350
+ // rest of this function is unchanged.
303
351
  const wantInteractive = !args.noInteractive &&
304
352
  shouldRunInteractive({
305
353
  hasTty: process.stdin.isTTY === true,
@@ -311,12 +359,11 @@ async function connect(args) {
311
359
  // 0.8.2 — picker no longer asks about OAuth providers. The
312
360
  // install wizard rendered in the bot's Chrome handles the
313
361
  // Google + (optional) GitHub flow directly; the CLI just
314
- // surfaces agent + LLM + advanced.
362
+ // surfaces agent + advanced.
315
363
  const picker = await runInteractiveSetup({
316
364
  ...(args.target !== undefined ? { initialTarget: args.target } : {}),
317
365
  ...(args.proxyUrl !== undefined ? { initialProxyUrl: args.proxyUrl } : {}),
318
- ...(args.registryUrl !== undefined ? { initialRegistryUrl: args.registryUrl } : {}),
319
- registryEnabled: !args.noRegistry,
366
+ initialRegistryEnabled: !args.noRegistry,
320
367
  });
321
368
  args.target = picker.target;
322
369
  args.llmChoice = picker.llmChoice;
@@ -324,11 +371,10 @@ async function connect(args) {
324
371
  args.byokKey = picker.byokKey;
325
372
  if (picker.proxyUrl !== undefined)
326
373
  args.proxyUrl = picker.proxyUrl;
327
- if (!picker.registryEnabled) {
328
- args.noRegistry = true;
329
- }
330
- else if (picker.registryUrl !== undefined) {
331
- args.registryUrl = picker.registryUrl;
374
+ args.noRegistry = !picker.registryEnabled;
375
+ args.advancedConfigured = picker.advancedConfigured;
376
+ if (picker.consentOperatorInboxOtp !== undefined) {
377
+ args.consentOperatorInboxOtp = picker.consentOperatorInboxOtp;
332
378
  }
333
379
  }
334
380
  else {
@@ -337,26 +383,33 @@ async function connect(args) {
337
383
  }
338
384
  const target = await resolveTarget(args.target);
339
385
  const agent = AGENTS[target];
340
- // Preflight: an existing install that's fully provisioned (machine
341
- // token + agent session token + a bot Google session marker) doesn't
342
- // need to redo the browser confirm. Just rewrite the MCP config in
343
- // place so the host agent picks up the latest server entrypoint and
344
- // env. Pass --force-relogin to bypass (e.g. to switch Google account).
386
+ // Preflight: an existing install is "connected" only when BOTH the
387
+ // account-bound plumbing still works and the bot profile has a confirmed
388
+ // Google session. A bare machine/agent token can talk to Trusty Squire, but
389
+ // it cannot act as the user at third-party sites, so it must not skip the
390
+ // browser confirm. Pass --force-relogin to bypass (e.g. to switch Google).
345
391
  if (!args.forceRelogin) {
346
392
  const preflight = await checkAlreadyProvisioned();
347
393
  if (preflight !== null) {
348
394
  ui.divider();
395
+ await hydrateArgsFromStoredPreferences(args);
396
+ await ensureConsentRecorded(consentFromArgs(args), args.advancedConfigured === true);
349
397
  await writeAgentConfig(target, agent, args);
350
- const provNote = preflight.providers.length > 0
351
- ? `Already provisioned (${preflight.providers.join(" + ")}).`
352
- : `Session valid — no bot OAuth login on this machine yet ` +
353
- `(run ${ui.code("npx @trusty-squire/mcp login")} to enable ` +
354
- `OAuth-preferring signups).`;
355
- ui.success(`${provNote} ${agent.display_name} config refreshed.`);
398
+ ui.success(`Already connected (${preflight.providers.join(" + ")}). ` +
399
+ `${agent.display_name} config refreshed.`);
400
+ printProviderState(preflight.providers);
356
401
  // Backfill connected_providers from the bot-side marker on
357
402
  // pre-rc.5 sessions, so the preflight cache is current.
358
403
  for (const p of preflight.providers)
359
404
  await recordConnectedProvider(p);
405
+ // "Skipped but dead" notice (DESIGN-connect-session-validation): we
406
+ // short-circuited because Google is valid, but if the bot's GitHub session
407
+ // validated as dead, say so — a dead session must never be silently
408
+ // hidden. GitHub is optional, so we don't raise the ceremony for it; the
409
+ // user refreshes it explicitly when a GitHub-only service needs it.
410
+ if (!preflight.providers.includes("github")) {
411
+ ui.hint(`GitHub session is not active — run ${ui.code("npx @trusty-squire/mcp connect --force-relogin=github")} if a service needs GitHub.`);
412
+ }
360
413
  ui.hint(`Pass ${ui.code("--force-relogin")} to switch accounts.`);
361
414
  return;
362
415
  }
@@ -364,13 +417,64 @@ async function connect(args) {
364
417
  console.warn("");
365
418
  console.warn("Opening the Trusty Squire install page in a browser. " +
366
419
  "The page walks you through signing in with Google and (optionally) GitHub.");
367
- // --force-relogin means "redo the OAuth dance from scratch" wipe
368
- // the bot's profile cookies + marker so the install wizard sees a
369
- // clean slate. The user signs in fresh inside the bot Chrome.
420
+ // --force-relogin means "redo the OAuth dance from scratch". The scoped
421
+ // form clears only one provider; bare --force-relogin is the full-profile
422
+ // account-switch escape hatch.
370
423
  if (args.forceRelogin) {
371
- clearAllProviderMarkers();
372
- await clearProviderCookies();
424
+ if (args.forceReloginProvider !== undefined) {
425
+ clearProviderLoggedIn(args.forceReloginProvider);
426
+ }
427
+ else {
428
+ clearAllProviderMarkers();
429
+ }
430
+ const free = await waitForProfileFree(undefined, {
431
+ deadlineMs: 120_000,
432
+ onWait: () => ui.hint("Waiting for the bot browser to finish before clearing the old session…"),
433
+ });
434
+ if (!free) {
435
+ ui.fail("The bot browser is still using the profile, so I can't safely switch accounts yet. " +
436
+ "Close the running signup/login browser and retry with --force-relogin.");
437
+ process.exit(1);
438
+ }
439
+ if (args.forceReloginProvider !== undefined) {
440
+ await clearProviderCookies(undefined, args.forceReloginProvider);
441
+ }
442
+ else {
443
+ clearBrowserProfile();
444
+ await clearProviderCookies();
445
+ }
373
446
  }
447
+ // DESIGN-connect-session-validation: a SCOPED force-relogin=github on an
448
+ // already-bound account is a GitHub-only login — Google's gate is already
449
+ // satisfied (the account is bound + its session is what we'd re-bind), so we
450
+ // must NOT drag the Google-first account-binding confirm page into it. Route
451
+ // straight to the provider-scoped login (the `login --provider=github` path):
452
+ // it opens a GitHub-only login (account chooser, since cookies were cleared
453
+ // above), never touching Google. Falls through to the full confirm flow only
454
+ // when the account isn't bound yet (nothing to skip) or the scope is google
455
+ // (which can re-bind the account and so needs the claim).
456
+ if (args.forceReloginProvider === "github") {
457
+ const bound = await checkAlreadyBound();
458
+ if (bound) {
459
+ ui.heading("Sign in to GitHub");
460
+ const result = await ensureOAuthSession({
461
+ provider: "github",
462
+ apiBaseUrl: args.apiBase,
463
+ forceOpen: true,
464
+ });
465
+ if (result.status === "logged_in" || result.status === "already_valid") {
466
+ await recordConnectedProvider("github");
467
+ await writeAgentConfig(target, agent, args);
468
+ ui.success("Signed in to GitHub. The bot is ready; config refreshed.");
469
+ return;
470
+ }
471
+ ui.fail(result.status === "timeout"
472
+ ? `GitHub sign-in timed out. Retry: ${ui.code("npx @trusty-squire/mcp connect --force-relogin=github")}`
473
+ : `GitHub sign-in failed: ${result.detail ?? "unknown error"}`);
474
+ process.exit(1);
475
+ }
476
+ }
477
+ const consent = consentFromArgs(args);
374
478
  // Detect egress class so the asn rides along in the install payload
375
479
  // (API uses it to correlate captcha failures with network class).
376
480
  // Best-effort: a failure returns null and the install continues.
@@ -407,6 +511,8 @@ async function connect(args) {
407
511
  api_base_url: args.apiBase,
408
512
  saved_at: new Date().toISOString(),
409
513
  machine_token: machine.machine_token,
514
+ consent_skillify_telemetry: consent.skillifyTelemetry,
515
+ consent_operator_inbox_otp: consent.operatorInboxOtp,
410
516
  };
411
517
  const session = await runInstallClaim(args.apiBase, target, baseSession, args.skipBrowser);
412
518
  if (session === null) {
@@ -417,6 +523,10 @@ async function connect(args) {
417
523
  const storage = await openSessionStorage();
418
524
  await storage.write(session);
419
525
  ui.success(`Session saved (${storage.backendName()})`);
526
+ args.noRegistry = session.consent_skillify_telemetry !== true;
527
+ args.consentOperatorInboxOtp = session.consent_operator_inbox_otp === true;
528
+ if (session.proxy_url !== undefined)
529
+ args.proxyUrl = session.proxy_url;
420
530
  // 0.8.1 — the bot's persistent profile may have a stale provider
421
531
  // marker from a previous install (the marker is sticky on disk).
422
532
  // The install confirm above only seeded one provider (whichever
@@ -429,6 +539,8 @@ async function connect(args) {
429
539
  start: "Checking provider sessions",
430
540
  done: "Provider sessions checked",
431
541
  fail: () => "Provider session check failed (continuing)",
542
+ // validate=true: confirm each session is LIVE (not just cookie-present),
543
+ // so a dead-but-present GitHub session isn't shown as connected.
432
544
  task: () => detectActiveProviderSessions(),
433
545
  });
434
546
  if (actual !== null) {
@@ -446,8 +558,10 @@ async function connect(args) {
446
558
  // reach GitHub even if we mis-identified the live state.
447
559
  }
448
560
  // Backfill connected_providers from the (now-fresh) bot-side marker.
449
- for (const p of loggedInProviders())
561
+ const providers = loggedInProviders();
562
+ for (const p of providers)
450
563
  await recordConnectedProvider(p);
564
+ printProviderState(providers);
451
565
  await writeAgentConfig(target, agent, args);
452
566
  if (args.skipBrowser) {
453
567
  ui.panel(`--skip-browser was set, so the bot's Chrome didn't observe your sign-in.\n` +
@@ -468,6 +582,27 @@ async function connect(args) {
468
582
  ui.panel(closingLine, { color: "wine" });
469
583
  }
470
584
  }
585
+ async function hydrateArgsFromStoredPreferences(args) {
586
+ if (args.advancedConfigured === true)
587
+ return;
588
+ try {
589
+ const session = await (await openSessionStorage()).read();
590
+ if (session === null)
591
+ return;
592
+ args.noRegistry = session.consent_skillify_telemetry !== true;
593
+ args.consentOperatorInboxOtp = session.consent_operator_inbox_otp === true;
594
+ if (session.proxy_url !== undefined)
595
+ args.proxyUrl = session.proxy_url;
596
+ }
597
+ catch {
598
+ // Best-effort. Missing preferences fall back to the privacy-safe defaults.
599
+ }
600
+ }
601
+ function printProviderState(providers) {
602
+ const have = new Set(providers);
603
+ ui.hint(` Provider sessions: Google ${have.has("google") ? "connected" : "not connected"}; ` +
604
+ `GitHub ${have.has("github") ? "connected" : "not connected"}`);
605
+ }
471
606
  // Runs the browser-based install confirm flow.
472
607
  //
473
608
  // Default path (`skipBrowser=false`): opens the trustysquire confirm
@@ -483,9 +618,9 @@ async function connect(args) {
483
618
  // provider session afterwards — the user must run `mcp login` before
484
619
  // their first OAuth signup. This path is for CI / scripted installs.
485
620
  // True when the local session + bot profile already carry everything
486
- // install would establish. Returns the list of provider sessions
487
- // detected, or null when anything's missing. Best-effort: any read
488
- // error returns null and the caller proceeds with the normal flow.
621
+ // connect would establish. Returns the list of confirmed provider sessions, or
622
+ // null when anything's missing. Best-effort: any read/probe error returns null
623
+ // and the caller proceeds with the normal browser flow.
489
624
  // Probe whether the stored agent token still authenticates. Agent
490
625
  // sessions have a 24h absolute cap, so a token can be PRESENT in the
491
626
  // session file but already dead on the server. Treating present as
@@ -507,19 +642,15 @@ export async function agentTokenStillValid(apiBaseUrl, token, fetchImpl = fetch)
507
642
  return true;
508
643
  }
509
644
  }
510
- // Pure gate for the `connect` fast path: given the read session, whether
511
- // its agent token still validated, and the bot's provider markers, decide
512
- // whether connect can (re)write the MCP config WITHOUT a browser re-claim.
645
+ // Pure gate for the `connect` fast path: given the read session, whether its
646
+ // agent token still validated, and the bot's confirmed provider sessions,
647
+ // decide whether connect can (re)write the MCP config WITHOUT a browser
648
+ // re-claim.
513
649
  //
514
- // The provider marker is INFORMATIONAL it tells the signup bot which
515
- // providers to auto-prefer for OAuth, NOT whether the host agent can be
516
- // wired up. A valid session (machine + agent token that just validated)
517
- // is sufficient. Gating on a non-empty marker used to force a full
518
- // browser re-claim on any box whose bot profile had no login yet (a fresh
519
- // machine that restored session.json, or a headless box where the claim
520
- // browser can't run) — leaving the user with NO config written and no
521
- // clear reason. So an empty `providers` is fine; we still return
522
- // provisioned. Exported for unit tests.
650
+ // Account-bound plumbing is not enough. The product-level connection is the
651
+ // bot-profile Google session: without it the host agent may be able to call the
652
+ // Trusty Squire API, but cannot act as the user at third-party services.
653
+ // GitHub is optional headroom; Google is the required primary identity.
523
654
  export function decideProvisioned(session, tokenValid, providers) {
524
655
  if (session === null ||
525
656
  session.machine_token === undefined ||
@@ -529,6 +660,8 @@ export function decideProvisioned(session, tokenValid, providers) {
529
660
  }
530
661
  if (!tokenValid)
531
662
  return null;
663
+ if (!providers.includes("google"))
664
+ return null;
532
665
  return { providers };
533
666
  }
534
667
  async function checkAlreadyProvisioned() {
@@ -545,12 +678,60 @@ async function checkAlreadyProvisioned() {
545
678
  }
546
679
  // Don't short-circuit on a present-but-expired token — re-pair.
547
680
  const stillValid = await agentTokenStillValid(session.api_base_url, session.agent_session_token);
548
- return decideProvisioned(session, stillValid, loggedInProviders());
681
+ // Probe the profile cookies instead of trusting the marker. The marker is a
682
+ // cache that can lie after logout/expiry; connect is rare enough to pay this
683
+ // cost, and this keeps "Already connected" aligned with the bot's real
684
+ // ability to wear the user's Google identity. validate=true so a dead-but-
685
+ // present GitHub session isn't persisted into connected_providers.
686
+ const providers = await detectActiveProviderSessions();
687
+ await syncConnectedProviders(providers);
688
+ return decideProvisioned(session, stillValid, providers);
549
689
  }
550
690
  catch {
551
691
  return null;
552
692
  }
553
693
  }
694
+ // Is the account already BOUND? — a valid session (machine + agent token +
695
+ // account id) whose agent token still validates. Unlike checkAlreadyProvisioned
696
+ // this does NOT require a live Google session: it answers "is the install
697
+ // claimed to an account", which is what a scoped GitHub force-relogin needs to
698
+ // know it can skip the Google-binding confirm page.
699
+ async function checkAlreadyBound() {
700
+ try {
701
+ const storage = await openSessionStorage();
702
+ const session = await storage.read();
703
+ if (session === null ||
704
+ session.machine_token === undefined ||
705
+ session.agent_session_token === undefined ||
706
+ session.account_id === undefined) {
707
+ return false;
708
+ }
709
+ return await agentTokenStillValid(session.api_base_url, session.agent_session_token);
710
+ }
711
+ catch {
712
+ return false;
713
+ }
714
+ }
715
+ async function syncConnectedProviders(providers) {
716
+ clearAllProviderMarkers();
717
+ for (const p of providers)
718
+ markProviderLoggedIn(p);
719
+ try {
720
+ const storage = await openSessionStorage();
721
+ const session = await storage.read();
722
+ if (session === null)
723
+ return;
724
+ await storage.write({
725
+ ...session,
726
+ connected_providers: [...providers],
727
+ saved_at: new Date().toISOString(),
728
+ });
729
+ }
730
+ catch {
731
+ // Best-effort — marker/session drift only affects fast-path UX. The next
732
+ // connect/login/provision probe can repair it.
733
+ }
734
+ }
554
735
  // Persist `provider` into session.connected_providers (idempotent).
555
736
  // Called after a successful ensureOAuthSession so the install
556
737
  // preflight on the next run can read both providers off the session
@@ -579,26 +760,33 @@ async function recordConnectedProvider(provider) {
579
760
  // which is recoverable.
580
761
  }
581
762
  }
582
- // Y/n prompt with a default answer. Returns true on yes, false on no.
583
- // Default is taken when the user just hits enter or when stdin isn't
584
- // a TTY (CI / scripted contexts). Designed to match what the user
585
- // expects from common CLI tooling — `[Y/n]` means default-yes.
586
- async function promptYesNo(message, defaultYes) {
587
- if (!process.stdin.isTTY)
588
- return defaultYes;
589
- const suffix = defaultYes ? "[Y/n]" : "[y/N]";
590
- const rl = createInterface({ input: process.stdin, output: process.stderr });
763
+ function consentFromArgs(args) {
764
+ return {
765
+ skillifyTelemetry: !args.noRegistry,
766
+ operatorInboxOtp: args.consentOperatorInboxOtp === true,
767
+ };
768
+ }
769
+ async function ensureConsentRecorded(consent, overwrite) {
591
770
  try {
592
- const answer = await new Promise((resolve) => {
593
- rl.question(`${message} ${suffix} `, resolve);
771
+ const storage = await openSessionStorage();
772
+ const session = await storage.read();
773
+ if (session === null)
774
+ return;
775
+ if (!overwrite &&
776
+ session.consent_skillify_telemetry !== undefined &&
777
+ session.consent_operator_inbox_otp !== undefined) {
778
+ return;
779
+ }
780
+ await storage.write({
781
+ ...session,
782
+ saved_at: new Date().toISOString(),
783
+ consent_skillify_telemetry: consent.skillifyTelemetry,
784
+ consent_operator_inbox_otp: consent.operatorInboxOtp,
594
785
  });
595
- const trimmed = answer.trim().toLowerCase();
596
- if (trimmed.length === 0)
597
- return defaultYes;
598
- return trimmed === "y" || trimmed === "yes";
599
786
  }
600
- finally {
601
- rl.close();
787
+ catch {
788
+ // Best-effort. If we can't persist consent, runtime treats missing
789
+ // fields as not approved.
602
790
  }
603
791
  }
604
792
  // Writes the host agent's MCP config — extracted so both the normal
@@ -616,17 +804,14 @@ async function writeAgentConfig(target, agent, args) {
616
804
  if (args.proxyUrl !== undefined) {
617
805
  env.UNIVERSAL_BOT_PROXY_URL = args.proxyUrl;
618
806
  }
619
- // Skill registry URL wired by default so the Tier-2 router is on
620
- // out of the box. Override with --registry-url=<url>; opt out
621
- // entirely with --no-registry (which omits the var → router skips).
807
+ // Skill registry URL. The endpoint is not user-configurable; Advanced setup
808
+ // controls whether it is written at all. Registry participation is also the
809
+ // user's consent to contribute successful non-personal signup recipes.
622
810
  if (!args.noRegistry) {
623
- env.TRUSTY_SQUIRE_REGISTRY_URL = args.registryUrl ?? DEFAULT_REGISTRY_URL;
811
+ env.TRUSTY_SQUIRE_REGISTRY_URL = DEFAULT_REGISTRY_URL;
624
812
  }
625
- // 0.8.1 LLM choice from the interactive picker (or future
626
- // --llm/--byok-key flags). BYOK paths write the provider key as
627
- // env; the server's LLM client picks the matching path. The
628
- // managed-free path omits keys entirely so the server routes
629
- // through our proxy (the rate-limited /v1/llm/chat endpoint).
813
+ // Legacy LLM env support. Signup navigation is session-agent driven now, so
814
+ // normal installs never set these fields; keep the switch for older callers.
630
815
  switch (args.llmChoice) {
631
816
  case "byok_openrouter":
632
817
  if (args.byokKey !== undefined)
@@ -665,10 +850,7 @@ async function writeAgentConfig(target, agent, args) {
665
850
  ui.hint(` Residential proxy baked in: ${args.proxyUrl}`);
666
851
  }
667
852
  if (args.noRegistry) {
668
- ui.hint(" Skill registry disabled (--no-registry) — every signup goes through the universal bot");
669
- }
670
- else if (args.registryUrl !== undefined) {
671
- ui.hint(` Skill registry: ${args.registryUrl}`);
853
+ ui.hint(" Skill registry disabled — every signup goes through the universal bot");
672
854
  }
673
855
  }
674
856
  // A confirm-flow page URL that means the claim is finished and the
@@ -716,6 +898,9 @@ async function runInstallClaim(apiBase, target, baseSession, skipBrowser) {
716
898
  state.value = {
717
899
  token: status.agent_session_token,
718
900
  account_id: status.account_id ?? "",
901
+ ...(status.install_preferences !== undefined
902
+ ? { preferences: status.install_preferences }
903
+ : {}),
719
904
  };
720
905
  }
721
906
  else if (status.status === "expired") {
@@ -761,7 +946,7 @@ async function runInstallClaim(apiBase, target, baseSession, skipBrowser) {
761
946
  if (ok === null)
762
947
  return null;
763
948
  return {
764
- ...baseSession,
949
+ ...applyInstallPreferences(baseSession, ok.preferences),
765
950
  api_base_url: apiBase,
766
951
  saved_at: new Date().toISOString(),
767
952
  agent_session_token: ok.token,
@@ -792,13 +977,24 @@ async function runInstallClaim(apiBase, target, baseSession, skipBrowser) {
792
977
  return null;
793
978
  }
794
979
  return {
795
- ...baseSession,
980
+ ...applyInstallPreferences(baseSession, state.value.preferences),
796
981
  api_base_url: apiBase,
797
982
  saved_at: new Date().toISOString(),
798
983
  agent_session_token: state.value.token,
799
984
  account_id: state.value.account_id,
800
985
  };
801
986
  }
987
+ function applyInstallPreferences(baseSession, preferences) {
988
+ if (preferences === undefined)
989
+ return baseSession;
990
+ const proxy = preferences.proxy_url?.trim();
991
+ return {
992
+ ...baseSession,
993
+ consent_skillify_telemetry: preferences.registry_enabled === true,
994
+ consent_operator_inbox_otp: preferences.consent_operator_inbox_otp === true,
995
+ ...(proxy !== undefined && proxy.length > 0 ? { proxy_url: proxy } : {}),
996
+ };
997
+ }
802
998
  async function resolveTarget(explicit) {
803
999
  if (explicit !== undefined)
804
1000
  return explicit;
@@ -832,7 +1028,7 @@ async function logout() {
832
1028
  // Unlike the login stage inside `install`, this command fails loud on
833
1029
  // timeout/error — it's the explicit retry path.
834
1030
  async function login(args) {
835
- const provider = args.providerArg === "github" ? "github" : "google";
1031
+ const provider = args.providerArg ?? args.forceReloginProvider ?? "google";
836
1032
  const label = provider === "github" ? "GitHub" : "Google";
837
1033
  ui.heading(`Sign in to ${label}`);
838
1034
  // --force-relogin wipes this provider's cookies (via forceOpen below),
@@ -883,16 +1079,15 @@ function printHelp() {
883
1079
  console.warn(`${chalk.bold("Commands")}`);
884
1080
  console.warn(` ${ui.code("connect")} set up this machine (default)`);
885
1081
  console.warn(` ${ui.code("login --provider=<p>")} add a Google or GitHub session`);
1082
+ console.warn(` ${ui.code("settings")} edit registry, OTP, and proxy choices`);
886
1083
  console.warn(` ${ui.code("logout")} clear the local session`);
887
1084
  console.warn("");
888
1085
  console.warn(`${chalk.bold("Flags for connect")}`);
889
1086
  console.warn(` --target=<${Object.keys(AGENTS).join("|")}>`);
890
- console.warn(` --skip-secondary don't prompt for the second provider`);
891
- console.warn(` --skip-login don't launch a browser (CI mode)`);
892
- console.warn(` --force-relogin switch the bound account`);
1087
+ console.warn(` --skip-browser don't launch a browser (CI mode)`);
1088
+ console.warn(` --force-relogin[=google|github] switch the bound account or one provider`);
893
1089
  console.warn(` --proxy-url=<url> bake a residential proxy into the bot env`);
894
- console.warn(` --registry-url=<url> use a non-default skill registry`);
895
- console.warn(` --no-registry disable the Tier-2 router entirely`);
1090
+ console.warn(` --no-registry disable managed registry participation`);
896
1091
  console.warn(` --no-interactive skip the TUI picker (use flag defaults only)`);
897
1092
  console.warn("");
898
1093
  console.warn(`${chalk.bold("Example")}`);
@@ -912,6 +1107,9 @@ async function pollForClaim(apiBase, setupCode, intervalMsOrOpts = {}, timeoutMs
912
1107
  return {
913
1108
  token: status.agent_session_token,
914
1109
  account_id: status.account_id ?? "",
1110
+ ...(status.install_preferences !== undefined
1111
+ ? { preferences: status.install_preferences }
1112
+ : {}),
915
1113
  };
916
1114
  }
917
1115
  if (status.status === "expired")
@@ -945,7 +1143,5 @@ function printAsnWarning(asn) {
945
1143
  return;
946
1144
  }
947
1145
  }
948
- // Back-compat alias: external imports of `install` keep working at the module
949
- // level. The CLI command got renamed; the function is the same.
950
- export { connect, connect as install, logout, login, parseArgs, pollForClaim, printAsnWarning, resolveServerLaunch, };
1146
+ export { connect, logout, login, parseArgs, pollForClaim, printAsnWarning, resolveCopiedNpxServerLaunch, resolveServerLaunch, };
951
1147
  //# sourceMappingURL=cli.js.map