@tokenoftrust/cli 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/auth.mjs +66 -1
- package/src/commands/checkout.mjs +41 -3
- package/src/commands/dev.mjs +8 -7
- package/src/commands/feedback.mjs +8 -3
- package/src/commands/start.mjs +78 -17
- package/src/commands/submit.mjs +4 -3
- package/src/commands/whoami.mjs +10 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Token of Trust developer CLI — check out a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Token of Trust",
|
package/src/auth.mjs
CHANGED
|
@@ -132,5 +132,70 @@ export async function resolveDeveloperSession(client, env, deps = {}) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
if (client && typeof client.setToken === "function") client.setToken(creds.accessToken);
|
|
135
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
identity: "developer",
|
|
137
|
+
appDomain: null,
|
|
138
|
+
token: creds.accessToken,
|
|
139
|
+
email: emailFromJwt(creds.accessToken),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Establish a validated session on `client`, attaching auth in the CORRECT order
|
|
145
|
+
* relative to the MCP handshake — the one thing every command must get right:
|
|
146
|
+
*
|
|
147
|
+
* developer → attach the cached bearer via setToken() BEFORE client.initialize(),
|
|
148
|
+
* so the server binds the session to this identity AT initialize time.
|
|
149
|
+
* An init-then-attach order leaves the session anonymous for its whole
|
|
150
|
+
* life on a server that only binds identity at initialize — this is the
|
|
151
|
+
* invited-developer "no stores you can build on" dead-end.
|
|
152
|
+
* operator → client.initialize() FIRST, then credential_validate, which is an
|
|
153
|
+
* in-session tool call that requires a completed handshake.
|
|
154
|
+
*
|
|
155
|
+
* Every command calls THIS instead of hand-ordering initialize() + resolveSession(),
|
|
156
|
+
* so the ordering rule lives in exactly one place. `opts.initialize` lets a caller
|
|
157
|
+
* inject its own handshake (to wrap the unreachable-MCP error, or pass custom
|
|
158
|
+
* clientInfo); it defaults to `() => client.initialize()`.
|
|
159
|
+
*
|
|
160
|
+
* @param {ReturnType<import("./mcp.mjs").createMcpClient>} client
|
|
161
|
+
* @param {{ env?: NodeJS.ProcessEnv, prefer?: "operator"|"developer",
|
|
162
|
+
* initialize?: () => Promise<any> }} [opts]
|
|
163
|
+
* @returns {Promise<{ identity: "operator"|"developer", appDomain: string|null, email?: string|null }>}
|
|
164
|
+
*/
|
|
165
|
+
export async function establishSession(client, opts = {}) {
|
|
166
|
+
const env = opts.env || process.env;
|
|
167
|
+
const prefer = opts.prefer || (hasOperatorCreds(env) ? "operator" : "developer");
|
|
168
|
+
const initialize = opts.initialize || (() => client.initialize());
|
|
169
|
+
|
|
170
|
+
if (prefer === "developer") {
|
|
171
|
+
// Attach the bearer, THEN handshake — the handshake carries the Authorization
|
|
172
|
+
// header so the server binds this identity at initialize time.
|
|
173
|
+
const session = await resolveDeveloperSession(client, env);
|
|
174
|
+
await initialize();
|
|
175
|
+
return session;
|
|
176
|
+
}
|
|
177
|
+
// Operator: handshake FIRST, then credential_validate (an in-session tool call).
|
|
178
|
+
await initialize();
|
|
179
|
+
return resolveSession(client, { env, prefer });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Best-effort email from an OAuth access token when it's a JWT — for DISPLAY ONLY
|
|
184
|
+
* (self-diagnosing dead-ends: "signed in as <email> via <origin>"), never for trust.
|
|
185
|
+
* Decodes the unverified payload segment; returns null for an opaque token or one
|
|
186
|
+
* with no email-ish claim. Never throws.
|
|
187
|
+
* @param {string} token
|
|
188
|
+
* @returns {string|null}
|
|
189
|
+
*/
|
|
190
|
+
export function emailFromJwt(token) {
|
|
191
|
+
try {
|
|
192
|
+
const seg = String(token).split(".")[1];
|
|
193
|
+
if (!seg) return null;
|
|
194
|
+
const json = JSON.parse(
|
|
195
|
+
Buffer.from(seg.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"),
|
|
196
|
+
);
|
|
197
|
+
return json.email || json.preferred_username || null;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
136
201
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { execFileSync } from "node:child_process";
|
|
22
22
|
import { createMcpClient } from "../mcp.mjs";
|
|
23
|
-
import {
|
|
23
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
24
24
|
import { CliError, fail, formatError } from "../errors.mjs";
|
|
25
25
|
|
|
26
26
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
@@ -94,8 +94,9 @@ export async function run(argv, ctx) {
|
|
|
94
94
|
const client = createMcpClient(baseUrl);
|
|
95
95
|
|
|
96
96
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// Attach auth in the right order relative to the handshake (developer bearer
|
|
98
|
+
// BEFORE initialize; operator credential_validate after) — see establishSession.
|
|
99
|
+
const session = await establishSession(client, {
|
|
99
100
|
env,
|
|
100
101
|
prefer: args.identity || undefined,
|
|
101
102
|
});
|
|
@@ -218,10 +219,47 @@ export function normalizeStores(list) {
|
|
|
218
219
|
.filter((r) => r.id);
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Detect whether a `client_list` tool result is an ERROR result rather than a
|
|
224
|
+
* genuinely empty-but-successful store list — so callers surface it (identity +
|
|
225
|
+
* origin + reason, per fb-1783905718950-f45zg9) instead of collapsing it to an
|
|
226
|
+
* empty list and dead-ending at "ask for an invite". callTool unwraps a tool
|
|
227
|
+
* result to its structuredContent / parsed text, so an error surfaces as a
|
|
228
|
+
* status other than ok/success, an `error`/`isError` field, or an unparseable
|
|
229
|
+
* `raw` text blob. A response that carries a resolvable store array (even empty)
|
|
230
|
+
* is always a success. Returns a short human reason, or null when it's not an
|
|
231
|
+
* error. Pure + exported so it's unit-tested without any I/O.
|
|
232
|
+
* @param {unknown} list
|
|
233
|
+
* @returns {string|null}
|
|
234
|
+
*/
|
|
235
|
+
export function storeListError(list) {
|
|
236
|
+
if (list == null || typeof list !== "object" || Array.isArray(list)) return null;
|
|
237
|
+
// A resolvable store array present → it succeeded, never an error.
|
|
238
|
+
if (Array.isArray(list.clients) || Array.isArray(list.tenants)) return null;
|
|
239
|
+
const msg =
|
|
240
|
+
list.message ||
|
|
241
|
+
(typeof list.error === "string" ? list.error : list.error?.message) ||
|
|
242
|
+
null;
|
|
243
|
+
if (list.isError) return msg || "the store list request returned an error";
|
|
244
|
+
if (typeof list.status === "string" && !/^(ok|success)$/i.test(list.status)) {
|
|
245
|
+
return msg || `the store list request returned status "${list.status}"`;
|
|
246
|
+
}
|
|
247
|
+
if (list.error) return msg || "the store list request returned an error";
|
|
248
|
+
if (typeof list.raw === "string" && list.raw.trim()) return list.raw.trim();
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
221
252
|
function printClientList(list) {
|
|
253
|
+
const err = storeListError(list);
|
|
222
254
|
const stores = normalizeStores(list);
|
|
255
|
+
if (err && stores.length === 0) {
|
|
256
|
+
console.log(`\nCouldn't list your stores: ${err}`);
|
|
257
|
+
console.log("Run `tot whoami` to check your session, or `tot login` again.");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
223
260
|
if (stores.length === 0) {
|
|
224
261
|
console.log("\nNo stores available to build on for this identity yet.");
|
|
262
|
+
console.log("If you were just invited, it may still be propagating — try again in a minute.");
|
|
225
263
|
if (list && !Array.isArray(list)) console.log(JSON.stringify(list, null, 2));
|
|
226
264
|
return;
|
|
227
265
|
}
|
package/src/commands/dev.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { Readable } from "node:stream";
|
|
|
40
40
|
import { pipeline } from "node:stream/promises";
|
|
41
41
|
import { setTimeout as delay } from "node:timers/promises";
|
|
42
42
|
import { createMcpClient } from "../mcp.mjs";
|
|
43
|
-
import {
|
|
43
|
+
import { establishSession } from "../auth.mjs";
|
|
44
44
|
import { CliError, fail, formatError } from "../errors.mjs";
|
|
45
45
|
import { openBrowser, waitForServer } from "../open.mjs";
|
|
46
46
|
import {
|
|
@@ -328,8 +328,9 @@ export async function resolveEntitledRendererSource(args, { client: providedClie
|
|
|
328
328
|
const baseUrl = args.mcp || process.env.MCP_BASE_URL || process.env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
329
329
|
const client = providedClient || createMcpClient(baseUrl);
|
|
330
330
|
if (!providedClient) {
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
// Auth before the entitlement-gated tool call, in handshake order (developer
|
|
332
|
+
// bearer pre-initialize) — see establishSession.
|
|
333
|
+
await establishSession(client, { env: process.env });
|
|
333
334
|
}
|
|
334
335
|
const res = await client.callTool("dev_renderer_artifact", {});
|
|
335
336
|
if (!res?.url || !res?.version) {
|
|
@@ -360,8 +361,8 @@ export async function ensureRendererArtifact(args, { client: providedClient } =
|
|
|
360
361
|
try {
|
|
361
362
|
const client = providedClient || createMcpClient(baseUrl);
|
|
362
363
|
if (!providedClient) {
|
|
363
|
-
|
|
364
|
-
await
|
|
364
|
+
// Auth in handshake order before the entitlement-gated tool call.
|
|
365
|
+
await establishSession(client, { env: process.env });
|
|
365
366
|
}
|
|
366
367
|
const res = await client.callTool("dev_renderer_artifact", {});
|
|
367
368
|
if (!res?.url || !res?.version) {
|
|
@@ -765,8 +766,8 @@ export async function ensureRegistryLogin(image, args, { client: providedClient
|
|
|
765
766
|
try {
|
|
766
767
|
const client = providedClient || createMcpClient(baseUrl);
|
|
767
768
|
if (!providedClient) {
|
|
768
|
-
|
|
769
|
-
await
|
|
769
|
+
// Auth in handshake order before the entitlement-gated tool call.
|
|
770
|
+
await establishSession(client, { env: process.env });
|
|
770
771
|
}
|
|
771
772
|
const tok = await client.callTool("dev_image_pull_token", {});
|
|
772
773
|
const username = tok?.username || "AWS";
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { createInterface } from "node:readline/promises";
|
|
24
24
|
import { createMcpClient } from "../mcp.mjs";
|
|
25
|
-
import {
|
|
25
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
26
26
|
import { readActivity, formatActivity } from "../activity-log.mjs";
|
|
27
27
|
import { fail } from "../errors.mjs";
|
|
28
28
|
|
|
@@ -145,8 +145,13 @@ export async function run(argv) {
|
|
|
145
145
|
const mcpUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
146
146
|
const client = createMcpClient(mcpUrl);
|
|
147
147
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
// Attach auth in the right order relative to the handshake (developer bearer
|
|
149
|
+
// pre-initialize, operator credential_validate post-initialize). We keep the
|
|
150
|
+
// feedback-specific clientInfo by injecting our own initialize.
|
|
151
|
+
await establishSession(client, {
|
|
152
|
+
env,
|
|
153
|
+
initialize: () => client.initialize({ name: "tot-cli", version: "feedback" }),
|
|
154
|
+
});
|
|
150
155
|
const payload = {
|
|
151
156
|
type: args.type,
|
|
152
157
|
category: args.category,
|
package/src/commands/start.mjs
CHANGED
|
@@ -50,12 +50,12 @@ import { createInterface } from "node:readline/promises";
|
|
|
50
50
|
|
|
51
51
|
import { detectContext } from "../context.mjs";
|
|
52
52
|
import { createMcpClient } from "../mcp.mjs";
|
|
53
|
-
import {
|
|
53
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
54
54
|
import { CliError, fail, formatError, exitCodeFor } from "../errors.mjs";
|
|
55
55
|
import { openBrowser, waitForServer } from "../open.mjs";
|
|
56
56
|
import { defaultLastTenantPath, readLastTenant, writeLastTenant } from "../last-tenant.mjs";
|
|
57
57
|
import { collectChecks } from "./doctor.mjs";
|
|
58
|
-
import { normalizeStores, checkoutTenant } from "./checkout.mjs";
|
|
58
|
+
import { normalizeStores, storeListError, checkoutTenant } from "./checkout.mjs";
|
|
59
59
|
import {
|
|
60
60
|
buildContainerPlan, spawnDevContainer, dockerAvailable, tryStartDocker,
|
|
61
61
|
resolveDevImage, isPrivateRegistryImage, ensureRegistryLogin,
|
|
@@ -193,11 +193,17 @@ export async function run(argv, ctx) {
|
|
|
193
193
|
// Authenticated path. Preflight (F) now that we know we're taking it — the
|
|
194
194
|
// local machine checks (node/git/Docker) that the authed loop needs.
|
|
195
195
|
await preflight(ctx, env);
|
|
196
|
-
console.log(` ✓ signed in as ${session
|
|
196
|
+
console.log(` ✓ signed in as ${describeIdentity(session)} via ${mcpOrigin(baseUrl)}`);
|
|
197
197
|
|
|
198
198
|
// 3. store — auto-pick, use --tenant, use the remembered one, or choose (A4).
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
// Keep the raw client_list so a tool ERROR (unauthenticated / not-entitled) is
|
|
200
|
+
// surfaced with who + where, not collapsed to an empty list (fb-...f45zg9).
|
|
201
|
+
const listResp = await client.callTool("client_list", {});
|
|
202
|
+
const stores = normalizeStores(listResp);
|
|
203
|
+
const tenant = await resolveTenant(stores, args, env, baseUrl, {
|
|
204
|
+
session,
|
|
205
|
+
listErr: storeListError(listResp),
|
|
206
|
+
});
|
|
201
207
|
|
|
202
208
|
// 4. checkout → ./<tenant> (reuse an existing checkout on a re-run),
|
|
203
209
|
// OVERLAPPED (C1) with prefetching the runner: the native artifact by
|
|
@@ -340,14 +346,24 @@ async function runSampleStart(args, ctx, env, startedAt) {
|
|
|
340
346
|
* AuthUnavailableError (no creds) or a CliError (can't reach the MCP).
|
|
341
347
|
*/
|
|
342
348
|
async function loginStep(client, env, args) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
// establishSession attaches auth in the right order relative to the handshake:
|
|
350
|
+
// the developer bearer BEFORE initialize (so the server binds this identity at
|
|
351
|
+
// initialize time — otherwise the session is anonymous and client_list is empty),
|
|
352
|
+
// the operator credential_validate after it. We inject our own initialize so the
|
|
353
|
+
// unreachable-MCP case still surfaces the actionable CliError below.
|
|
354
|
+
return establishSession(client, {
|
|
355
|
+
env,
|
|
356
|
+
prefer: args.identity || undefined,
|
|
357
|
+
initialize: async () => {
|
|
358
|
+
try {
|
|
359
|
+
await client.initialize();
|
|
360
|
+
} catch (e) {
|
|
361
|
+
throw new CliError(`can't reach the Token of Trust MCP at ${client.mcpUrl} (${String(e?.message || e)})`, {
|
|
362
|
+
next: "check your network, then re-run — or point elsewhere with --mcp <url>",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
});
|
|
351
367
|
}
|
|
352
368
|
|
|
353
369
|
/**
|
|
@@ -436,12 +452,59 @@ async function prefetchDockerLogin(client, devArgs, env) {
|
|
|
436
452
|
if (!dockerAvailable()) await tryStartDocker();
|
|
437
453
|
}
|
|
438
454
|
|
|
455
|
+
/** A human label for a resolved session — the email when we could read it from
|
|
456
|
+
* the OAuth token, else the operator app domain, else the identity kind. */
|
|
457
|
+
function describeIdentity(session) {
|
|
458
|
+
if (!session) return "an unknown identity";
|
|
459
|
+
if (session.email) return session.email;
|
|
460
|
+
if (session.identity === "operator") {
|
|
461
|
+
return session.appDomain ? `operator (${session.appDomain})` : "operator";
|
|
462
|
+
}
|
|
463
|
+
return "your developer identity";
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The MCP host for user-facing messages (never the full /mcp URL). */
|
|
467
|
+
function mcpOrigin(baseUrl) {
|
|
468
|
+
try {
|
|
469
|
+
return new URL(baseUrl).host;
|
|
470
|
+
} catch {
|
|
471
|
+
return String(baseUrl);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Build the CliError for an authenticated session that surfaced NO usable store,
|
|
477
|
+
* making the dead-end SELF-DIAGNOSING (fb-1783905718950-f45zg9): always report who
|
|
478
|
+
* we're signed in as and against which MCP, and distinguish an ERRORED client_list
|
|
479
|
+
* (surface the reason — likely an auth/entitlement problem) from a genuinely empty
|
|
480
|
+
* result (invite may still be propagating, or you need one). Points at `tot whoami`.
|
|
481
|
+
*/
|
|
482
|
+
function noStoresError({ session, baseUrl, listErr }) {
|
|
483
|
+
const who = describeIdentity(session);
|
|
484
|
+
const origin = mcpOrigin(baseUrl);
|
|
485
|
+
if (listErr) {
|
|
486
|
+
return new CliError(
|
|
487
|
+
`signed in as ${who} via ${origin}, but listing your stores failed: ${listErr}`,
|
|
488
|
+
{ next: "run `tot whoami` to check your session, or `tot login` again — then re-run `tot start`" },
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return new CliError(
|
|
492
|
+
`signed in as ${who} via ${origin}, but you have no stores to build on yet`,
|
|
493
|
+
{
|
|
494
|
+
next:
|
|
495
|
+
"if you were just invited, it may still be propagating — try again in a minute; " +
|
|
496
|
+
"otherwise ask your Token of Trust contact for a store invite (see `tot whoami`), " +
|
|
497
|
+
"or `tot start --sample` for the free local preview",
|
|
498
|
+
},
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
439
502
|
/**
|
|
440
503
|
* Resolve the tenant to work on from the (normalized) store list, args, and
|
|
441
504
|
* the remembered last tenant (A4) — then remember whatever was decided so the
|
|
442
505
|
* next bare `tot start` doesn't have to ask again.
|
|
443
506
|
*/
|
|
444
|
-
async function resolveTenant(stores, args, env, baseUrl) {
|
|
507
|
+
async function resolveTenant(stores, args, env, baseUrl, { session = null, listErr = null } = {}) {
|
|
445
508
|
const lastTenantPath = defaultLastTenantPath(env);
|
|
446
509
|
const pick = pickTenant(stores, {
|
|
447
510
|
explicit: args.tenant || null,
|
|
@@ -450,9 +513,7 @@ async function resolveTenant(stores, args, env, baseUrl) {
|
|
|
450
513
|
|
|
451
514
|
let tenant;
|
|
452
515
|
if (pick.kind === "none") {
|
|
453
|
-
throw
|
|
454
|
-
next: "ask your Token of Trust contact for a store invite, then re-run — or `tot start --sample` for the free local preview",
|
|
455
|
-
});
|
|
516
|
+
throw noStoresError({ session, baseUrl, listErr });
|
|
456
517
|
} else if (pick.kind === "explicit") {
|
|
457
518
|
tenant = pick.tenant;
|
|
458
519
|
console.log(` → your store: ${tenant} (--tenant)`);
|
package/src/commands/submit.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { execFileSync } from "node:child_process";
|
|
34
34
|
import { setTimeout as delay } from "node:timers/promises";
|
|
35
35
|
import { createMcpClient } from "../mcp.mjs";
|
|
36
|
-
import {
|
|
36
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
37
37
|
import { validateTenant, ERROR } from "../validate.mjs";
|
|
38
38
|
import { openBrowser } from "../open.mjs";
|
|
39
39
|
import { fail } from "../errors.mjs";
|
|
@@ -138,8 +138,9 @@ export async function run(argv, ctx) {
|
|
|
138
138
|
const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
139
139
|
const client = createMcpClient(baseUrl);
|
|
140
140
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
// Attach auth before the first server call (developer bearer pre-initialize,
|
|
142
|
+
// operator credential_validate post-initialize) — see establishSession.
|
|
143
|
+
await establishSession(client, { env, prefer: args.identity || undefined });
|
|
143
144
|
// Set the active tenant so preview_status reads the right scope (it keys on
|
|
144
145
|
// the session's tenant + the commit — no tenant arg of its own).
|
|
145
146
|
await client.callTool("client_switch", { tenant });
|
package/src/commands/whoami.mjs
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* Dependency-free.
|
|
11
11
|
*/
|
|
12
12
|
import { defaultCredentialsPath, readCredentials, isExpired } from "../token-store.mjs";
|
|
13
|
-
import { hasOperatorCreds,
|
|
13
|
+
import { hasOperatorCreds, establishSession } from "../auth.mjs";
|
|
14
14
|
import { createMcpClient } from "../mcp.mjs";
|
|
15
|
-
import { normalizeStores } from "./checkout.mjs";
|
|
15
|
+
import { normalizeStores, storeListError } from "./checkout.mjs";
|
|
16
16
|
import { recordServerPolicy } from "../update-check.mjs";
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -55,17 +55,22 @@ export async function run(argv, _ctx) {
|
|
|
55
55
|
// identity or an unreachable MCP just shows the cached status above.
|
|
56
56
|
try {
|
|
57
57
|
const client = createMcpClient(status.mcpUrl || env.MCP_BASE_URL || env.TOT_MCP_URL || "https://mcp.tokenoftrust.com");
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// Developer bearer must be attached BEFORE initialize so the server binds this
|
|
59
|
+
// identity at handshake time — otherwise client_list resolves anonymously and
|
|
60
|
+
// (misleadingly) reports no stores (establishSession orders this correctly).
|
|
61
|
+
await establishSession(client, { env, prefer: "developer" });
|
|
60
62
|
const listResp = await client.callTool("client_list", {});
|
|
61
63
|
// Update-awareness Layer 2: an authed response may carry a version-support
|
|
62
64
|
// policy (wire contract: `cliPolicy`). Safe no-op when absent.
|
|
63
65
|
recordServerPolicy(listResp?.cliPolicy, env);
|
|
66
|
+
const listErr = storeListError(listResp);
|
|
64
67
|
const stores = normalizeStores(listResp);
|
|
65
68
|
if (stores.length) {
|
|
66
69
|
console.log(` stores you can build on: ${stores.map((s) => s.id).join(", ")}`);
|
|
70
|
+
} else if (listErr) {
|
|
71
|
+
console.log(` (couldn't list your stores: ${listErr} — try \`tot login\` again)`);
|
|
67
72
|
} else {
|
|
68
|
-
console.log(" (no stores resolved yet
|
|
73
|
+
console.log(" (no stores resolved yet — if you were just invited, it may still be propagating)");
|
|
69
74
|
}
|
|
70
75
|
} catch {
|
|
71
76
|
console.log(" (couldn't reach the MCP to list your stores right now — your cached session is above)");
|