@tokenoftrust/cli 2.0.11 → 2.0.14
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/bin/tot.mjs +22 -3
- package/package.json +1 -1
- package/src/activity-log.mjs +2 -1
- package/src/activity.mjs +1 -1
- package/src/candidate-state.mjs +52 -27
- package/src/commands/clone.mjs +6 -1
- package/src/commands/dev.mjs +12 -2
- package/src/commands/feedback.mjs +18 -8
- package/src/commands/grants.mjs +5 -1
- package/src/commands/link.mjs +9 -1
- package/src/commands/submit.mjs +267 -211
- package/src/commands/sync.mjs +4 -0
- package/src/commands/whoami.mjs +5 -1
- package/src/diagnostics.mjs +224 -0
- package/src/errors.mjs +6 -1
- package/src/mcp.mjs +213 -28
- package/src/oauth.mjs +107 -37
- package/src/open.mjs +18 -3
package/bin/tot.mjs
CHANGED
|
@@ -21,6 +21,12 @@ import { printError } from "../src/errors.mjs";
|
|
|
21
21
|
import { recordActivity, redactArgs } from "../src/activity-log.mjs";
|
|
22
22
|
import { emitActivity, capRendered } from "../src/activity.mjs";
|
|
23
23
|
import { maybeNotifyUpdate } from "../src/update-check.mjs";
|
|
24
|
+
import {
|
|
25
|
+
currentInvocationDiagnostics,
|
|
26
|
+
hasRequiredInvocationDiagnostic,
|
|
27
|
+
recordDiagnostic,
|
|
28
|
+
resetInvocationDiagnostics,
|
|
29
|
+
} from "../src/diagnostics.mjs";
|
|
24
30
|
|
|
25
31
|
const BUILD_ORDER = ["clone", "validate", "dev", "preview"];
|
|
26
32
|
|
|
@@ -305,6 +311,7 @@ async function main() {
|
|
|
305
311
|
const ctx = detectContext();
|
|
306
312
|
const startedAt = Date.now();
|
|
307
313
|
const command = cmd || "(none)";
|
|
314
|
+
resetInvocationDiagnostics({ command, cliVersion: VERSION });
|
|
308
315
|
const subcommand = safeSubcommand(cmd, rest);
|
|
309
316
|
let code = 0;
|
|
310
317
|
let errMsg = null;
|
|
@@ -323,10 +330,18 @@ async function main() {
|
|
|
323
330
|
return code;
|
|
324
331
|
} catch (e) {
|
|
325
332
|
errMsg = e?.message || String(e);
|
|
333
|
+
recordDiagnostic(e, { command, operation: command, cliVersion: VERSION, required: true });
|
|
326
334
|
throw e;
|
|
327
335
|
} finally {
|
|
328
|
-
const exitCode = errMsg ? 1 : (code
|
|
336
|
+
const exitCode = errMsg ? 1 : (Number.isInteger(code) ? code : 0);
|
|
329
337
|
const durationMs = Date.now() - startedAt;
|
|
338
|
+
if (exitCode !== 0 && !hasRequiredInvocationDiagnostic()) {
|
|
339
|
+
recordDiagnostic(new Error(errMsg || `command exited ${exitCode}`), {
|
|
340
|
+
command, operation: command, category: "handled_exit", required: true,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const diagnostics = currentInvocationDiagnostics();
|
|
344
|
+
const diagnosticIds = diagnostics.map((entry) => entry.diagnosticId).filter(Boolean);
|
|
330
345
|
// Best-effort activity breadcrumb (never throws, never blocks). `feedback`'s own
|
|
331
346
|
// free-text message is omitted — it's user-typed and belongs only in the report.
|
|
332
347
|
recordActivity({
|
|
@@ -337,6 +352,7 @@ async function main() {
|
|
|
337
352
|
code: exitCode,
|
|
338
353
|
ms: durationMs,
|
|
339
354
|
...(errMsg ? { err: String(errMsg).slice(0, 200) } : {}),
|
|
355
|
+
...(diagnosticIds.length ? { diagnosticId: diagnosticIds.at(-1), diagnosticIds } : {}),
|
|
340
356
|
});
|
|
341
357
|
// Emit `cli.command.result` (exit code + duration + a bounded/redacted
|
|
342
358
|
// rendered field). The house-style error text is capped + run through the JS
|
|
@@ -347,9 +363,12 @@ async function main() {
|
|
|
347
363
|
// when there's no credential.
|
|
348
364
|
const resultEmit = emitActivity({
|
|
349
365
|
action: "cli.command.result",
|
|
350
|
-
outcome: { status:
|
|
366
|
+
outcome: { status: exitCode === 0 ? "succeeded" : "failed", durationMs },
|
|
351
367
|
payload: {
|
|
352
|
-
args: {
|
|
368
|
+
args: {
|
|
369
|
+
command, ...(subcommand ? { subcommand } : {}), cliVersion: VERSION,
|
|
370
|
+
exitCode, durationMs, ...(diagnosticIds.length ? { diagnosticId: diagnosticIds.at(-1) } : {}),
|
|
371
|
+
},
|
|
353
372
|
...(errMsg ? { rendered: { output: capRendered(errMsg) } } : {}),
|
|
354
373
|
},
|
|
355
374
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.14",
|
|
4
4
|
"description": "Token of Trust developer CLI — clone 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/activity-log.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* (sent through the Token of Trust MCP's `feedback_submit` tool).
|
|
5
5
|
*
|
|
6
6
|
* ONE file, `~/.tot/activity.log` (JSON-lines), beside the credential cache. Each
|
|
7
|
-
* line is one command invocation: { ts, v, cmd, args, code, ms, err
|
|
7
|
+
* line is one command invocation: { ts, v, cmd, args, code, ms, err?, diagnosticId?,
|
|
8
|
+
* diagnosticIds? }. Bounded to
|
|
8
9
|
* the most recent MAX_ENTRIES so it never grows without limit.
|
|
9
10
|
*
|
|
10
11
|
* NEVER records secrets: the value after `--code` / `--token` (the single-use
|
package/src/activity.mjs
CHANGED
|
@@ -63,7 +63,7 @@ export const CLI_ACTION_CATALOG = {
|
|
|
63
63
|
renderedAllow: [],
|
|
64
64
|
},
|
|
65
65
|
"cli.command.result": {
|
|
66
|
-
argsAllow: ["command", "subcommand", "cliVersion", "exitCode", "durationMs"],
|
|
66
|
+
argsAllow: ["command", "subcommand", "cliVersion", "exitCode", "durationMs", "diagnosticId"],
|
|
67
67
|
renderedAllow: [],
|
|
68
68
|
},
|
|
69
69
|
"cli.obstacle.reported": {
|
package/src/candidate-state.mjs
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* updates, per forge repo.
|
|
4
4
|
*
|
|
5
5
|
* `tot submit` is idempotent on a STABLE changeId (`deriveChangeId`) so a re-submit
|
|
6
|
-
* updates the same PR by default
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* updates the same PR by default. Every chosen identity is persisted before its
|
|
7
|
+
* ref is pushed, which makes a failed push or MCP registration retry the same ref.
|
|
8
|
+
* The stored pointer is especially important when the chosen id diverges from the
|
|
9
|
+
* stable default:
|
|
9
10
|
*
|
|
10
11
|
* - `tot submit --fork-candidate` forks a fresh candidate and remembers it here, so the
|
|
11
12
|
* NEXT plain `tot submit` keeps updating the NEW PR (like pushing more commits
|
|
@@ -18,14 +19,13 @@
|
|
|
18
19
|
* candidates): a different MCP, repo, OR non-default git branch is a different
|
|
19
20
|
* candidate namespace, so a feature branch gets its OWN candidate PR instead of
|
|
20
21
|
* fighting main's over the same handle. The default branch deliberately keeps the
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* discipline as last-tenant.mjs (0600 in a 0700 dir, write-tmp-then-rename).
|
|
22
|
+
* branch-less key. Non-default branches never inherit that pointer. Updates hold an
|
|
23
|
+
* exclusive lock across read-modify-write so concurrent submits cannot lose either
|
|
24
|
+
* identity. Files use mode 0600 in a 0700 directory.
|
|
25
25
|
* Dependency-free (node:fs/os/path). `TOT_HOME` overrides home (tests).
|
|
26
26
|
*/
|
|
27
27
|
import {
|
|
28
|
-
readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync,
|
|
28
|
+
readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync, rmdirSync, statSync,
|
|
29
29
|
} from "node:fs";
|
|
30
30
|
import { homedir } from "node:os";
|
|
31
31
|
import { join, dirname } from "node:path";
|
|
@@ -79,45 +79,70 @@ function readMap(filePath) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function writeMap(filePath, map) {
|
|
82
|
-
|
|
83
|
-
const tmp = `${filePath}.tmp`;
|
|
82
|
+
const tmp = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
84
83
|
writeFileSync(tmp, `${JSON.stringify(map, null, 2)}\n`, { mode: 0o600 });
|
|
85
84
|
renameSync(tmp, filePath);
|
|
86
85
|
chmodSync(filePath, 0o600);
|
|
87
86
|
}
|
|
88
87
|
|
|
88
|
+
const LOCK_RETRY_MS = 10;
|
|
89
|
+
const LOCK_ATTEMPTS = 200;
|
|
90
|
+
const STALE_LOCK_MS = 30_000;
|
|
91
|
+
const lockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
92
|
+
|
|
93
|
+
function updateMap(filePath, mutate) {
|
|
94
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
95
|
+
const lockPath = `${filePath}.lock`;
|
|
96
|
+
let locked = false;
|
|
97
|
+
for (let attempt = 0; attempt < LOCK_ATTEMPTS && !locked; attempt += 1) {
|
|
98
|
+
try {
|
|
99
|
+
mkdirSync(lockPath, { mode: 0o700 });
|
|
100
|
+
locked = true;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error?.code !== "EEXIST") throw error;
|
|
103
|
+
try {
|
|
104
|
+
if (Date.now() - statSync(lockPath).mtimeMs > STALE_LOCK_MS) rmdirSync(lockPath);
|
|
105
|
+
} catch (staleError) {
|
|
106
|
+
if (staleError?.code !== "ENOENT" && staleError?.code !== "ENOTEMPTY") throw staleError;
|
|
107
|
+
}
|
|
108
|
+
if (!locked) Atomics.wait(lockWait, 0, 0, LOCK_RETRY_MS);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!locked) throw new Error(`Candidate state is busy: ${lockPath}`);
|
|
112
|
+
try {
|
|
113
|
+
const map = readMap(filePath);
|
|
114
|
+
if (mutate(map)) writeMap(filePath, map);
|
|
115
|
+
} finally {
|
|
116
|
+
rmdirSync(lockPath);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
89
120
|
/**
|
|
90
121
|
* The remembered active changeId for `(mcpUrl, repo, branch)`, or null when there
|
|
91
122
|
* isn't one (absent/unreadable/malformed) — a miss means "use the stable default".
|
|
92
|
-
*
|
|
93
|
-
* branch-less key so state written before the rekey (or by the default branch)
|
|
94
|
-
* isn't orphaned. Never throws.
|
|
123
|
+
* Non-default branches never read the default branch's pointer. Never throws.
|
|
95
124
|
*/
|
|
96
125
|
export function readActiveChangeId(filePath, { mcpUrl, repo, branch }) {
|
|
97
126
|
const map = readMap(filePath);
|
|
98
|
-
|
|
99
|
-
if (primary) return primary;
|
|
100
|
-
// Legacy fallback: a branch-scoped miss reads the old branch-less key (a no-op
|
|
101
|
-
// when we're already on the default branch, which IS the legacy key).
|
|
102
|
-
if (!isDefaultBranch(branch)) return recordChangeId(map[legacyStateKey(mcpUrl, repo)]);
|
|
103
|
-
return null;
|
|
127
|
+
return recordChangeId(map[stateKey(mcpUrl, repo, branch)]);
|
|
104
128
|
}
|
|
105
129
|
|
|
106
130
|
/** Remember `changeId` as the active candidate for `(mcpUrl, repo, branch)`, atomically. */
|
|
107
131
|
export function writeActiveChangeId(filePath, { mcpUrl, repo, branch, changeId }) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
updateMap(filePath, (map) => {
|
|
133
|
+
map[stateKey(mcpUrl, repo, branch)] = { changeId, updatedAt: Date.now() };
|
|
134
|
+
return true;
|
|
135
|
+
});
|
|
111
136
|
}
|
|
112
137
|
|
|
113
138
|
/** Forget the active candidate for `(mcpUrl, repo, branch)` (e.g. after closing it). */
|
|
114
139
|
export function clearActiveChangeId(filePath, { mcpUrl, repo, branch }) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
updateMap(filePath, (map) => {
|
|
141
|
+
const key = stateKey(mcpUrl, repo, branch);
|
|
142
|
+
if (!(key in map)) return false;
|
|
118
143
|
delete map[key];
|
|
119
|
-
|
|
120
|
-
}
|
|
144
|
+
return true;
|
|
145
|
+
});
|
|
121
146
|
}
|
|
122
147
|
|
|
123
148
|
/**
|
package/src/commands/clone.mjs
CHANGED
|
@@ -55,6 +55,7 @@ import { CliError, fail, formatError } from "../errors.mjs";
|
|
|
55
55
|
import { writeNvmrc } from "../sample.mjs";
|
|
56
56
|
import { emitObstacle } from "../obstacle.mjs";
|
|
57
57
|
import { splitAuthedRemote, basicAuthExtraHeader, installForgeCredentialHelper } from "../git-credential.mjs";
|
|
58
|
+
import { recordDiagnostic } from "../diagnostics.mjs";
|
|
58
59
|
|
|
59
60
|
const execFileP = promisify(execFile);
|
|
60
61
|
|
|
@@ -364,6 +365,7 @@ async function cloneRepo(gitRemote, dir, redact) {
|
|
|
364
365
|
await emitObstacle("clone-failed");
|
|
365
366
|
throw new CliError(`clone failed: ${redact(String(e.stderr || e.message || e))}`, {
|
|
366
367
|
next: `check the target dir is empty and you can reach the remote, then re-run`,
|
|
368
|
+
cause: e,
|
|
367
369
|
});
|
|
368
370
|
}
|
|
369
371
|
// Install the HOST-SCOPED helper, not a bare `credential.helper`. A bare one is appended to
|
|
@@ -376,9 +378,12 @@ async function cloneRepo(gitRemote, dir, redact) {
|
|
|
376
378
|
const gitSync = (/** @type {string[]} */ cargs) =>
|
|
377
379
|
execFileSync("git", ["-C", dir, ...cargs], { encoding: "utf8" });
|
|
378
380
|
installForgeCredentialHelper(gitSync, { host: new URL(gitRemote).host });
|
|
379
|
-
} catch {
|
|
381
|
+
} catch (error) {
|
|
380
382
|
// Best-effort — never fail a good clone over credential plumbing. The next
|
|
381
383
|
// `tot preview`/`tot sync` installs it, and the fetch that needs it says why.
|
|
384
|
+
recordDiagnostic(error, {
|
|
385
|
+
command: "clone", operation: "credential_helper_install", required: false, degraded: true,
|
|
386
|
+
});
|
|
382
387
|
}
|
|
383
388
|
const head = (await git(["-C", dir, "log", "-1", "--oneline"])).trim();
|
|
384
389
|
writeNvmrc(dir); // version-manager hooks land on a supported Node on cd
|
package/src/commands/dev.mjs
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
import { startHeartbeatFromEnv } from "../dev-heartbeat.mjs";
|
|
54
54
|
import { streamDevLogs } from "../dev-logs.mjs";
|
|
55
55
|
import { cockpitUrlFrom } from "../banner.mjs";
|
|
56
|
+
import { recordDiagnostic } from "../diagnostics.mjs";
|
|
56
57
|
|
|
57
58
|
/** The published runner image (--docker fallback). Override with --image / TOT_DEV_IMAGE. */
|
|
58
59
|
const DEFAULT_DEV_IMAGE =
|
|
@@ -874,7 +875,10 @@ export function probeRunnerVersion(runnerDir, { timeoutMs = 4000 } = {}) {
|
|
|
874
875
|
});
|
|
875
876
|
const v = String(out).trim().split(/\s+/)[0];
|
|
876
877
|
return /^\d+\.\d+\.\d+/.test(v) ? v : null;
|
|
877
|
-
} catch {
|
|
878
|
+
} catch (error) {
|
|
879
|
+
recordDiagnostic(error, {
|
|
880
|
+
command: "dev", operation: "runner_version_probe", required: false, degraded: true,
|
|
881
|
+
});
|
|
878
882
|
return null; // old runner (no --version), timeout, or spawn failure → treat as unversioned
|
|
879
883
|
}
|
|
880
884
|
}
|
|
@@ -1323,7 +1327,10 @@ export async function publishedRunnerIntegrity(env, version, { timeoutMs = 2000,
|
|
|
1323
1327
|
if (!res.ok) return null;
|
|
1324
1328
|
const dist = (await res.json())?.versions?.[version]?.dist;
|
|
1325
1329
|
return dist?.integrity || dist?.shasum || null;
|
|
1326
|
-
} catch {
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
recordDiagnostic(error, {
|
|
1332
|
+
command: "dev", operation: "runner_integrity_probe", required: false, degraded: true,
|
|
1333
|
+
}, env);
|
|
1327
1334
|
return null;
|
|
1328
1335
|
}
|
|
1329
1336
|
}
|
|
@@ -1809,6 +1816,9 @@ export async function ensureRegistryLogin(image, args, { client: providedClient
|
|
|
1809
1816
|
});
|
|
1810
1817
|
console.error(`~ registry sign-in ok (${reg})`);
|
|
1811
1818
|
} catch (e) {
|
|
1819
|
+
recordDiagnostic(e, {
|
|
1820
|
+
command: "dev", operation: "dev_image_pull_token", required: false, degraded: true,
|
|
1821
|
+
});
|
|
1812
1822
|
console.error(
|
|
1813
1823
|
`~ (using existing docker login for ${registry} — MCP pull-token not available: ${String(e?.message || e)})`,
|
|
1814
1824
|
);
|
|
@@ -26,6 +26,7 @@ import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
|
26
26
|
import { readActivity, formatActivity } from "../activity-log.mjs";
|
|
27
27
|
import { defaultCredentialsPath, readCredentials } from "../token-store.mjs";
|
|
28
28
|
import { fail } from "../errors.mjs";
|
|
29
|
+
import { formatDiagnostics, readDiagnostics, recordDiagnostic } from "../diagnostics.mjs";
|
|
29
30
|
|
|
30
31
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
31
32
|
const TYPES = new Set(["bug", "feature", "improvement"]);
|
|
@@ -57,9 +58,14 @@ Requires a signed-in session — run \`tot login\` first if needed.`;
|
|
|
57
58
|
* `traceId` link) is unit-testable without driving the MCP handshake. Both extras
|
|
58
59
|
* are OMITTED when their source is absent — never sent as null/empty.
|
|
59
60
|
* @param {{ type: string, category: string, severity: string, title: string,
|
|
60
|
-
* description: string, activityText?: string|null,
|
|
61
|
+
* description: string, activityText?: string|null, diagnosticsText?: string|null,
|
|
62
|
+
* traceId?: string|null }} f
|
|
61
63
|
*/
|
|
62
|
-
export function buildFeedbackPayload({ type, category, severity, title, description, activityText, traceId }) {
|
|
64
|
+
export function buildFeedbackPayload({ type, category, severity, title, description, activityText, diagnosticsText, traceId }) {
|
|
65
|
+
const attachments = [
|
|
66
|
+
activityText ? `tot CLI activity (most recent last):\n${activityText}` : null,
|
|
67
|
+
diagnosticsText ? `tot CLI diagnostics (redacted, most recent last):\n${diagnosticsText}` : null,
|
|
68
|
+
].filter(Boolean);
|
|
63
69
|
return {
|
|
64
70
|
type,
|
|
65
71
|
category,
|
|
@@ -67,9 +73,9 @@ export function buildFeedbackPayload({ type, category, severity, title, descript
|
|
|
67
73
|
title,
|
|
68
74
|
description,
|
|
69
75
|
scenario: "tot-cli",
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
...(
|
|
76
|
+
// CLI context → `sensitive` (ToT-admins-only, never clustered/shared). It is
|
|
77
|
+
// redacted before attachment; `sensitive` is the belt-and-suspenders home for it.
|
|
78
|
+
...(attachments.length ? { sensitive: attachments.join("\n\n") } : {}),
|
|
73
79
|
// Invite→terminal→problem trace link (omitted when this wasn't an invite login).
|
|
74
80
|
...(traceId ? { traceId } : {}),
|
|
75
81
|
};
|
|
@@ -138,6 +144,8 @@ export async function run(argv) {
|
|
|
138
144
|
|
|
139
145
|
const entries = args.activity ? readActivity(env, { limit: 40 }) : [];
|
|
140
146
|
const activityText = entries.length ? formatActivity(entries) : null;
|
|
147
|
+
const diagnostics = args.activity ? readDiagnostics(env, { limit: 20 }) : [];
|
|
148
|
+
const diagnosticsText = diagnostics.length ? formatDiagnostics(diagnostics) : null;
|
|
141
149
|
const title = message.length > 80 ? `${message.slice(0, 79)}…` : message;
|
|
142
150
|
|
|
143
151
|
// The invite→terminal→problem trace id `tot login` cached from the pasted invite
|
|
@@ -153,9 +161,9 @@ export async function run(argv) {
|
|
|
153
161
|
console.error(` ${args.type} · ${args.category} · ${args.severity}`);
|
|
154
162
|
console.error(` "${title}"`);
|
|
155
163
|
console.error(
|
|
156
|
-
activityText
|
|
157
|
-
? ` +
|
|
158
|
-
: " (no activity
|
|
164
|
+
activityText || diagnosticsText
|
|
165
|
+
? ` + ${entries.length} recent command(s) and ${diagnostics.length} diagnostic(s) — redacted, ToT-admins-only`
|
|
166
|
+
: " (no activity or diagnostics attached)",
|
|
159
167
|
);
|
|
160
168
|
|
|
161
169
|
if (!args.yes) {
|
|
@@ -192,6 +200,7 @@ export async function run(argv) {
|
|
|
192
200
|
title,
|
|
193
201
|
description: message,
|
|
194
202
|
activityText,
|
|
203
|
+
diagnosticsText,
|
|
195
204
|
traceId,
|
|
196
205
|
});
|
|
197
206
|
const res = await client.callTool("feedback_submit", payload);
|
|
@@ -199,6 +208,7 @@ export async function run(argv) {
|
|
|
199
208
|
console.log(`\n+ sent — thank you.${id ? ` (report ${id})` : ""}`);
|
|
200
209
|
return 0;
|
|
201
210
|
} catch (e) {
|
|
211
|
+
recordDiagnostic(e, { command: "feedback", operation: "feedback_submit", required: true }, env);
|
|
202
212
|
if (e instanceof AuthUnavailableError) {
|
|
203
213
|
console.error(fail(`can't send feedback: ${e.message}`, e.hint || "run `tot login` first."));
|
|
204
214
|
return 1;
|
package/src/commands/grants.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { createMcpClient } from "../mcp.mjs";
|
|
|
29
29
|
import { storeListError, noStoresGuidance } from "./clone.mjs";
|
|
30
30
|
import { offerSignIn } from "./login.mjs";
|
|
31
31
|
import { recordServerPolicy } from "../update-check.mjs";
|
|
32
|
+
import { recordDiagnostic } from "../diagnostics.mjs";
|
|
32
33
|
|
|
33
34
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
34
35
|
|
|
@@ -169,7 +170,10 @@ async function tryIntrospect(client) {
|
|
|
169
170
|
const r = await client.callTool("grant_introspect", {});
|
|
170
171
|
if (r && typeof r === "object" && Array.isArray(r.grants)) return r;
|
|
171
172
|
return null;
|
|
172
|
-
} catch {
|
|
173
|
+
} catch (error) {
|
|
174
|
+
recordDiagnostic(error, {
|
|
175
|
+
command: "grants", operation: "grant_introspect", required: false, degraded: true,
|
|
176
|
+
});
|
|
173
177
|
return null;
|
|
174
178
|
}
|
|
175
179
|
}
|
package/src/commands/link.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { offerSignIn } from "./login.mjs";
|
|
|
25
25
|
import { openBrowser } from "../open.mjs";
|
|
26
26
|
import { CliError, fail, formatError } from "../errors.mjs";
|
|
27
27
|
import { normalizeStores } from "./clone.mjs";
|
|
28
|
+
import { recordDiagnostic } from "../diagnostics.mjs";
|
|
28
29
|
|
|
29
30
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
30
31
|
|
|
@@ -131,6 +132,7 @@ export async function run(argv, _ctx) {
|
|
|
131
132
|
} catch (e) {
|
|
132
133
|
throw new CliError(`couldn't start the identity link: ${String(e?.message || e)}`, {
|
|
133
134
|
next: "your MCP may not support `tot link` yet — run `tot whoami` for the current guidance",
|
|
135
|
+
cause: e,
|
|
134
136
|
});
|
|
135
137
|
}
|
|
136
138
|
const { authUrl, pollHandle } = linkBeginFields(begin);
|
|
@@ -172,7 +174,10 @@ export async function run(argv, _ctx) {
|
|
|
172
174
|
} else {
|
|
173
175
|
console.log(" Next: `tot start` — if it still shows no stores, ask your ToT contact for a store invite.");
|
|
174
176
|
}
|
|
175
|
-
} catch {
|
|
177
|
+
} catch (error) {
|
|
178
|
+
recordDiagnostic(error, {
|
|
179
|
+
command: "link", operation: "client_list", required: false, degraded: true,
|
|
180
|
+
}, env);
|
|
176
181
|
console.log(" Next: `tot start` to build your store.");
|
|
177
182
|
}
|
|
178
183
|
return 0;
|
|
@@ -205,6 +210,9 @@ export async function pollLink(client, pollHandle, { timeoutMs = 120000, interva
|
|
|
205
210
|
res = await client.callTool("identity_link_poll", { pollHandle });
|
|
206
211
|
} catch (e) {
|
|
207
212
|
// A transient poll error isn't fatal — keep trying until the deadline.
|
|
213
|
+
recordDiagnostic(e, {
|
|
214
|
+
command: "link", operation: "identity_link_poll", required: false, degraded: true,
|
|
215
|
+
});
|
|
208
216
|
await delay(intervalMs);
|
|
209
217
|
continue;
|
|
210
218
|
}
|