@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.
- package/README.md +139 -62
- package/assets/login/vnc-input.js +9 -0
- package/assets/login/vnc.html +214 -202
- package/assets/screencaps/connect-walkthrough.png +0 -0
- package/assets/screencaps/connect-walkthrough.svg +1 -1
- package/dist/api-client.d.ts +26 -3
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +19 -31
- package/dist/api-client.js.map +1 -1
- package/dist/bin.js +2 -3
- package/dist/bin.js.map +1 -1
- package/dist/bot/browser.d.ts +134 -77
- package/dist/bot/browser.d.ts.map +1 -1
- package/dist/bot/browser.js +1788 -1543
- package/dist/bot/browser.js.map +1 -1
- package/dist/bot/compact-observation-v2.d.ts +167 -0
- package/dist/bot/compact-observation-v2.d.ts.map +1 -0
- package/dist/bot/compact-observation-v2.js +1038 -0
- package/dist/bot/compact-observation-v2.js.map +1 -0
- package/dist/bot/credential-shape.d.ts +2 -0
- package/dist/bot/credential-shape.d.ts.map +1 -1
- package/dist/bot/credential-shape.js +21 -8
- package/dist/bot/credential-shape.js.map +1 -1
- package/dist/bot/element-fingerprint.d.ts +9 -0
- package/dist/bot/element-fingerprint.d.ts.map +1 -0
- package/dist/bot/element-fingerprint.js +147 -0
- package/dist/bot/element-fingerprint.js.map +1 -0
- package/dist/bot/email-verification.d.ts +7 -1
- package/dist/bot/email-verification.d.ts.map +1 -1
- package/dist/bot/email-verification.js +48 -14
- package/dist/bot/email-verification.js.map +1 -1
- package/dist/bot/google-login.d.ts +28 -52
- package/dist/bot/google-login.d.ts.map +1 -1
- package/dist/bot/google-login.js +246 -1119
- package/dist/bot/google-login.js.map +1 -1
- package/dist/bot/install-completion.d.ts +1 -2
- package/dist/bot/install-completion.d.ts.map +1 -1
- package/dist/bot/install-completion.js +1 -2
- package/dist/bot/install-completion.js.map +1 -1
- package/dist/bot/login-state.d.ts +13 -7
- package/dist/bot/login-state.d.ts.map +1 -1
- package/dist/bot/login-state.js +50 -114
- package/dist/bot/login-state.js.map +1 -1
- package/dist/bot/oauth-providers.d.ts +1 -1
- package/dist/bot/oauth-providers.d.ts.map +1 -1
- package/dist/bot/oauth-providers.js +3 -3
- package/dist/bot/oauth-providers.js.map +1 -1
- package/dist/bot/oauth-scope.d.ts +4 -0
- package/dist/bot/oauth-scope.d.ts.map +1 -0
- package/dist/bot/oauth-scope.js +58 -0
- package/dist/bot/oauth-scope.js.map +1 -0
- package/dist/bot/operator-browser-watchdog.d.ts +127 -0
- package/dist/bot/operator-browser-watchdog.d.ts.map +1 -0
- package/dist/bot/operator-browser-watchdog.js +438 -0
- package/dist/bot/operator-browser-watchdog.js.map +1 -0
- package/dist/bot/owner-process-reaper-worker.d.ts +2 -0
- package/dist/bot/owner-process-reaper-worker.d.ts.map +1 -0
- package/dist/bot/owner-process-reaper-worker.js +17 -0
- package/dist/bot/owner-process-reaper-worker.js.map +1 -0
- package/dist/bot/owner-process-reaper.d.ts +103 -0
- package/dist/bot/owner-process-reaper.d.ts.map +1 -0
- package/dist/bot/owner-process-reaper.js +1058 -0
- package/dist/bot/owner-process-reaper.js.map +1 -0
- package/dist/bot/pay-operator.d.ts +13 -2
- package/dist/bot/pay-operator.d.ts.map +1 -1
- package/dist/bot/pay-operator.js +319 -123
- package/dist/bot/pay-operator.js.map +1 -1
- package/dist/bot/profile.d.ts +10 -1
- package/dist/bot/profile.d.ts.map +1 -1
- package/dist/bot/profile.js +137 -11
- package/dist/bot/profile.js.map +1 -1
- package/dist/bot/promote-to-skill.js +1 -1
- package/dist/bot/promote-to-skill.js.map +1 -1
- package/dist/bot/provision-session.d.ts +60 -164
- package/dist/bot/provision-session.d.ts.map +1 -1
- package/dist/bot/provision-session.js +2049 -1546
- package/dist/bot/provision-session.js.map +1 -1
- package/dist/bot/remote-login-display.d.ts +61 -0
- package/dist/bot/remote-login-display.d.ts.map +1 -0
- package/dist/bot/remote-login-display.js +765 -0
- package/dist/bot/remote-login-display.js.map +1 -0
- package/dist/bot/session/hosts.d.ts +8 -0
- package/dist/bot/session/hosts.d.ts.map +1 -0
- package/dist/bot/session/hosts.js +36 -0
- package/dist/bot/session/hosts.js.map +1 -0
- package/dist/bot/session/lifecycle.d.ts +56 -0
- package/dist/bot/session/lifecycle.d.ts.map +1 -0
- package/dist/bot/session/lifecycle.js +962 -0
- package/dist/bot/session/lifecycle.js.map +1 -0
- package/dist/bot/session/model.d.ts +175 -0
- package/dist/bot/session/model.d.ts.map +1 -0
- package/dist/bot/session/model.js +77 -0
- package/dist/bot/session/model.js.map +1 -0
- package/dist/install/cli.d.ts +29 -9
- package/dist/install/cli.d.ts.map +1 -1
- package/dist/install/cli.js +243 -322
- package/dist/install/cli.js.map +1 -1
- package/dist/install/interactive.d.ts +1 -0
- package/dist/install/interactive.d.ts.map +1 -1
- package/dist/install/interactive.js +19 -5
- package/dist/install/interactive.js.map +1 -1
- package/dist/install/ui.d.ts.map +1 -1
- package/dist/install/ui.js +3 -15
- package/dist/install/ui.js.map +1 -1
- package/dist/operator-env.js +1 -1
- package/dist/operator-env.js.map +1 -1
- package/dist/server-instance-registry.d.ts +103 -0
- package/dist/server-instance-registry.d.ts.map +1 -0
- package/dist/server-instance-registry.js +392 -0
- package/dist/server-instance-registry.js.map +1 -0
- package/dist/server.d.ts +15 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +164 -62
- package/dist/server.js.map +1 -1
- package/dist/session-guard.d.ts +28 -0
- package/dist/session-guard.d.ts.map +1 -0
- package/dist/session-guard.js +84 -0
- package/dist/session-guard.js.map +1 -0
- package/dist/session.d.ts +57 -13
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +246 -127
- package/dist/session.js.map +1 -1
- package/dist/tools/audit-log.d.ts +16 -2
- package/dist/tools/audit-log.d.ts.map +1 -1
- package/dist/tools/audit-log.js +223 -13
- package/dist/tools/audit-log.js.map +1 -1
- package/dist/tools/audit-rollup.d.ts +80 -0
- package/dist/tools/audit-rollup.d.ts.map +1 -0
- package/dist/tools/audit-rollup.js +0 -0
- package/dist/tools/audit-rollup.js.map +1 -0
- package/dist/tools/fetch-credential.d.ts +37 -0
- package/dist/tools/fetch-credential.d.ts.map +1 -0
- package/dist/tools/fetch-credential.js +153 -0
- package/dist/tools/fetch-credential.js.map +1 -0
- package/dist/tools/index.d.ts +3 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +7 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/operate-pay.d.ts +4 -4
- package/dist/tools/operate-pay.d.ts.map +1 -1
- package/dist/tools/operate-pay.js +163 -96
- package/dist/tools/operate-pay.js.map +1 -1
- package/dist/tools/provision-drive.d.ts +43 -35
- package/dist/tools/provision-drive.d.ts.map +1 -1
- package/dist/tools/provision-drive.js +268 -164
- package/dist/tools/provision-drive.js.map +1 -1
- package/dist/tools/use-credential.d.ts.map +1 -1
- package/dist/tools/use-credential.js +3 -2
- package/dist/tools/use-credential.js.map +1 -1
- package/package.json +4 -6
- package/dist/bot/operator-direct-identity.d.ts +0 -26
- package/dist/bot/operator-direct-identity.d.ts.map +0 -1
- package/dist/bot/operator-direct-identity.js +0 -120
- package/dist/bot/operator-direct-identity.js.map +0 -1
- package/dist/bot/operator-profile-pool.d.ts +0 -97
- package/dist/bot/operator-profile-pool.d.ts.map +0 -1
- package/dist/bot/operator-profile-pool.js +0 -855
- package/dist/bot/operator-profile-pool.js.map +0 -1
- package/dist/bot/xvfb.d.ts +0 -13
- package/dist/bot/xvfb.d.ts.map +0 -1
- package/dist/bot/xvfb.js +0 -146
- package/dist/bot/xvfb.js.map +0 -1
package/dist/install/cli.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
// Setup CLI — connect / settings / logout
|
|
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
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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,
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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 (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 !==
|
|
450
|
+
if (preflight.kind !== "ceremony") {
|
|
430
451
|
ui.divider();
|
|
431
452
|
await hydrateArgsFromStoredPreferences(args);
|
|
432
453
|
await ensureConsentRecorded(consentFromArgs(args), args.advancedConfigured === true);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
447
|
-
|
|
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
|
-
|
|
450
|
-
|
|
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
|
|
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
|
|
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.
|
|
559
|
+
ui.success(`Session saved (${storage.path})`);
|
|
557
560
|
args.noRegistry = session.consent_skillify_telemetry !== true;
|
|
558
|
-
args.consentOperatorInboxOtp = session.consent_operator_inbox_otp
|
|
559
|
-
//
|
|
560
|
-
// marker
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
|
|
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
|
-
|
|
569
|
+
providers = await ui.withSpinner({
|
|
568
570
|
start: "Checking provider sessions",
|
|
569
571
|
done: "Provider sessions checked",
|
|
570
|
-
fail: () => "Provider session check failed
|
|
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
|
-
//
|
|
596
|
-
const providers
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
|
616
|
+
args.consentOperatorInboxOtp = session.consent_operator_inbox_otp !== false;
|
|
630
617
|
}
|
|
631
618
|
catch {
|
|
632
|
-
// Best-effort. Missing
|
|
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
|
|
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.
|
|
652
|
-
//
|
|
653
|
-
//
|
|
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
|
|
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
|
-
//
|
|
722
|
-
//
|
|
723
|
-
//
|
|
724
|
-
//
|
|
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
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
}
|
|
741
|
-
|
|
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
|
-
|
|
793
|
+
const preflight = decideConnectPreflight(session, stillValid, providers);
|
|
794
|
+
return preflight.kind === "provisioned" ? { ...preflight, session } : preflight;
|
|
757
795
|
}
|
|
758
796
|
catch {
|
|
759
|
-
return
|
|
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.
|
|
790
|
-
//
|
|
791
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
829
|
-
//
|
|
830
|
-
//
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
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.
|
|
967
|
-
//
|
|
968
|
-
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
1061
|
-
//
|
|
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
|
|
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
|
-
|
|
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.
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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")}
|
|
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,
|
|
1174
|
+
export { connect, logout, parseArgs, pollForClaim, printAsnWarning, resolveCopiedNpxServerLaunch, resolveServerLaunch, };
|
|
1254
1175
|
//# sourceMappingURL=cli.js.map
|