aismemory 0.6.0 → 0.6.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.
Files changed (33) hide show
  1. package/README.md +16 -0
  2. package/dist/__tests__/agent-load-persistence.test.d.ts +1 -0
  3. package/dist/__tests__/agent-load-persistence.test.js +69 -0
  4. package/dist/__tests__/agent-load-persistence.test.js.map +1 -0
  5. package/dist/__tests__/device-flow-pending-400.test.js +1 -1
  6. package/dist/__tests__/device-flow-pending-400.test.js.map +1 -1
  7. package/dist/__tests__/doctor.test.d.ts +1 -0
  8. package/dist/__tests__/doctor.test.js +248 -0
  9. package/dist/__tests__/doctor.test.js.map +1 -0
  10. package/dist/__tests__/keyauth-error-surface-e2e.test.d.ts +1 -0
  11. package/dist/__tests__/keyauth-error-surface-e2e.test.js +209 -0
  12. package/dist/__tests__/keyauth-error-surface-e2e.test.js.map +1 -0
  13. package/dist/__tests__/list-all-agents.test.d.ts +1 -0
  14. package/dist/__tests__/list-all-agents.test.js +45 -0
  15. package/dist/__tests__/list-all-agents.test.js.map +1 -0
  16. package/dist/__tests__/pipeline-ingestion.test.js +18 -4
  17. package/dist/__tests__/pipeline-ingestion.test.js.map +1 -1
  18. package/dist/__tests__/provisional-token-reauth.test.d.ts +1 -0
  19. package/dist/__tests__/provisional-token-reauth.test.js +156 -0
  20. package/dist/__tests__/provisional-token-reauth.test.js.map +1 -0
  21. package/dist/__tests__/resolve-agent-preference-e2e.test.js +10 -10
  22. package/dist/__tests__/resolve-agent-preference-e2e.test.js.map +1 -1
  23. package/dist/__tests__/token-expiry-reauth.test.js +1 -1
  24. package/dist/__tests__/token-expiry-reauth.test.js.map +1 -1
  25. package/dist/cli/doctor.d.ts +57 -0
  26. package/dist/cli/doctor.js +332 -0
  27. package/dist/cli/doctor.js.map +1 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.js +81 -56
  30. package/dist/index.js.map +1 -1
  31. package/dist/prompt-resource.js +1 -1
  32. package/dist/prompt-resource.js.map +1 -1
  33. package/package.json +13 -5
@@ -0,0 +1,332 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `aismemory doctor` — read-only self-diagnosis for "it doesn't work" reports.
4
+ *
5
+ * A new user whose MCP client can't reach AIS has no way to tell whether the
6
+ * problem is DNS, a corporate proxy, an unbonded agent, an expired token, or
7
+ * clock skew. This command runs a fixed set of read-only checks against the
8
+ * local environment and the AIS host and prints a PASS/WARN/FAIL report with
9
+ * a one-line remediation hint per problem — no writes to the API, no agent
10
+ * creation.
11
+ *
12
+ * Usage:
13
+ * npx aismemory doctor
14
+ * npx aismemory doctor --json
15
+ * AIS_URL=https://other.example.com npx aismemory doctor
16
+ *
17
+ * Exit codes:
18
+ * 0 — no FAIL checks (WARNs are still reported but don't fail the command)
19
+ * 1 — at least one FAIL check
20
+ */
21
+ import { lookup as dnsLookupDefault } from 'node:dns/promises';
22
+ import { connect as tlsConnectDefault } from 'node:tls';
23
+ import { loadOAuthCredentials, credentialsFilePath } from '../oauth-credentials.js';
24
+ import { isCredsStale } from '../auth-staleness.js';
25
+ /** Minimum Node major version this package requires (per @modelcontextprotocol/sdk engines). */
26
+ const MIN_NODE_MAJOR = 18;
27
+ const PROXY_ENV_KEYS = ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'NO_PROXY', 'no_proxy'];
28
+ const CLOCK_SKEW_WARN_MS = 5 * 60 * 1000;
29
+ function errMsg(err) {
30
+ return err instanceof Error ? err.message : String(err);
31
+ }
32
+ // ── Individual checks ────────────────────────────────────
33
+ export function checkNodeVersion(nodeVersion = process.version, minMajor = MIN_NODE_MAJOR) {
34
+ const match = /^v?(\d+)\./.exec(nodeVersion);
35
+ const major = match ? Number(match[1]) : NaN;
36
+ if (Number.isNaN(major)) {
37
+ return {
38
+ name: 'Node version',
39
+ status: 'WARN',
40
+ message: `Could not parse Node version string "${nodeVersion}".`,
41
+ hint: `aismemory requires Node >= ${minMajor}.`,
42
+ };
43
+ }
44
+ if (major < minMajor) {
45
+ return {
46
+ name: 'Node version',
47
+ status: 'FAIL',
48
+ message: `Node ${nodeVersion} is below the required minimum (>= ${minMajor}).`,
49
+ hint: `Upgrade Node (e.g. via nvm: "nvm install ${minMajor}") and retry.`,
50
+ };
51
+ }
52
+ return {
53
+ name: 'Node version',
54
+ status: 'PASS',
55
+ message: `Node ${nodeVersion} meets the minimum requirement (>= ${minMajor}).`,
56
+ };
57
+ }
58
+ export async function checkDnsResolution(host, lookup = dnsLookupDefault) {
59
+ try {
60
+ const { address } = await lookup(host);
61
+ return {
62
+ name: 'DNS resolution',
63
+ status: 'PASS',
64
+ message: `${host} resolves to ${address}.`,
65
+ };
66
+ }
67
+ catch (err) {
68
+ return {
69
+ name: 'DNS resolution',
70
+ status: 'FAIL',
71
+ message: `Could not resolve DNS for ${host}: ${errMsg(err)}.`,
72
+ hint: 'Check your internet connection and DNS settings (or /etc/resolv.conf).',
73
+ };
74
+ }
75
+ }
76
+ function defaultTlsProbe(host, port, timeoutMs) {
77
+ return new Promise((resolve, reject) => {
78
+ let settled = false;
79
+ const socket = tlsConnectDefault({ host, port, servername: host, timeout: timeoutMs }, () => {
80
+ if (settled)
81
+ return;
82
+ settled = true;
83
+ socket.end();
84
+ resolve();
85
+ });
86
+ socket.on('error', (err) => {
87
+ if (settled)
88
+ return;
89
+ settled = true;
90
+ reject(err);
91
+ });
92
+ socket.on('timeout', () => {
93
+ if (settled)
94
+ return;
95
+ settled = true;
96
+ socket.destroy();
97
+ reject(new Error(`connection to ${host}:${port} timed out after ${timeoutMs}ms`));
98
+ });
99
+ });
100
+ }
101
+ export async function checkTlsReachability(host, probe = defaultTlsProbe, port = 443, timeoutMs = 5000) {
102
+ try {
103
+ await probe(host, port, timeoutMs);
104
+ return {
105
+ name: 'TLS reachability',
106
+ status: 'PASS',
107
+ message: `${host}:${port} accepts TLS connections.`,
108
+ };
109
+ }
110
+ catch (err) {
111
+ return {
112
+ name: 'TLS reachability',
113
+ status: 'FAIL',
114
+ message: `Could not open a TLS connection to ${host}:${port}: ${errMsg(err)}.`,
115
+ hint: 'A firewall, VPN, or corporate proxy may be blocking outbound HTTPS. See the proxy check below.',
116
+ };
117
+ }
118
+ }
119
+ export async function checkRestHealth(baseUrl, fetchImpl = fetch) {
120
+ const url = `${baseUrl}/livez`;
121
+ try {
122
+ const res = await fetchImpl(url);
123
+ const dateHeader = res.headers.get('date') ?? undefined;
124
+ if (res.ok) {
125
+ return {
126
+ name: 'REST API health',
127
+ status: 'PASS',
128
+ message: `${url} responded HTTP ${res.status} (alive).`,
129
+ dateHeader,
130
+ };
131
+ }
132
+ return {
133
+ name: 'REST API health',
134
+ status: 'WARN',
135
+ message: `${url} responded HTTP ${res.status} — server reachable but reporting a problem.`,
136
+ hint: 'The AIS backend may be degraded. Check again shortly.',
137
+ dateHeader,
138
+ };
139
+ }
140
+ catch (err) {
141
+ return {
142
+ name: 'REST API health',
143
+ status: 'FAIL',
144
+ message: `Could not reach ${url}: ${errMsg(err)}.`,
145
+ hint: 'Check network connectivity — see the DNS/TLS checks above.',
146
+ };
147
+ }
148
+ }
149
+ export async function checkMcpEndpoint(baseUrl, fetchImpl = fetch) {
150
+ const url = `${baseUrl}/mcp`;
151
+ try {
152
+ const res = await fetchImpl(url);
153
+ // Any resolved HTTP response (even 401 "auth required") proves the MCP
154
+ // endpoint is reachable — only a network-level failure means "down".
155
+ const authNote = res.status === 401 || res.status === 403 ? ' (auth required, as expected without a key)' : '';
156
+ return {
157
+ name: 'MCP HTTP endpoint',
158
+ status: 'PASS',
159
+ message: `${url} responded HTTP ${res.status}${authNote}.`,
160
+ };
161
+ }
162
+ catch (err) {
163
+ return {
164
+ name: 'MCP HTTP endpoint',
165
+ status: 'FAIL',
166
+ message: `Could not reach ${url}: ${errMsg(err)}.`,
167
+ hint: 'Check network connectivity — see the DNS/TLS checks above.',
168
+ };
169
+ }
170
+ }
171
+ export async function checkCredentials(baseUrl, deps = {}) {
172
+ const load = deps.load ?? loadOAuthCredentials;
173
+ const now = deps.now ?? Date.now;
174
+ const fetchImpl = deps.fetchImpl ?? fetch;
175
+ const home = deps.home;
176
+ const path = credentialsFilePath(home);
177
+ const creds = load(home);
178
+ if (!creds) {
179
+ return {
180
+ name: 'Credentials',
181
+ status: 'WARN',
182
+ message: `No credentials found at ${path} — this device has not bonded to an agent yet.`,
183
+ hint: 'Run any aismemory tool from your MCP client once and follow the printed bond URL/code (takes ~30s).',
184
+ };
185
+ }
186
+ if (isCredsStale(creds.expiresAt, now())) {
187
+ return {
188
+ name: 'Credentials',
189
+ status: 'WARN',
190
+ message: `Credentials at ${path} are expired or expiring within 5 minutes (expiresAt: ${creds.expiresAt}).`,
191
+ hint: 'No action needed — the next tool call re-authenticates automatically (silently if a device key is enrolled).',
192
+ };
193
+ }
194
+ try {
195
+ const res = await fetchImpl(`${baseUrl}/v1/agents/${creds.agentId}`, {
196
+ headers: { Authorization: `Bearer ${creds.token}` },
197
+ });
198
+ if (res.ok) {
199
+ return {
200
+ name: 'Credentials',
201
+ status: 'PASS',
202
+ message: `Bonded as agent ${creds.agentId}; AIS accepted the saved token.`,
203
+ };
204
+ }
205
+ return {
206
+ name: 'Credentials',
207
+ status: 'WARN',
208
+ message: `Saved credentials found, but AIS returned HTTP ${res.status} for agent ${creds.agentId}.`,
209
+ hint: 'The token may be revoked or the agent archived. Re-run any aismemory tool to trigger re-authentication.',
210
+ };
211
+ }
212
+ catch (err) {
213
+ return {
214
+ name: 'Credentials',
215
+ status: 'FAIL',
216
+ message: `Could not verify saved credentials with AIS: ${errMsg(err)}.`,
217
+ hint: 'Check network connectivity — see the REST API health check above.',
218
+ };
219
+ }
220
+ }
221
+ export function checkProxyEnv(env = process.env) {
222
+ const set = PROXY_ENV_KEYS.filter((key) => Boolean(env[key]));
223
+ if (set.length === 0) {
224
+ return {
225
+ name: 'Proxy environment',
226
+ status: 'PASS',
227
+ message: 'No HTTP(S)_PROXY / NO_PROXY environment variables set.',
228
+ };
229
+ }
230
+ return {
231
+ name: 'Proxy environment',
232
+ status: 'WARN',
233
+ message: `Proxy environment variable(s) detected: ${set.join(', ')}.`,
234
+ hint: 'A corporate proxy can block or MITM TLS to ais.agentsandswarms.ai. If aismemory only fails on this network, ask IT to allowlist the host or trust the proxy CA.',
235
+ };
236
+ }
237
+ export function checkClockSkew(dateHeader, now = Date.now, warnThresholdMs = CLOCK_SKEW_WARN_MS) {
238
+ if (!dateHeader) {
239
+ return {
240
+ name: 'Clock skew',
241
+ status: 'WARN',
242
+ message: 'No server Date header available to compare (REST API health check did not return one).',
243
+ };
244
+ }
245
+ const remoteMs = Date.parse(dateHeader);
246
+ if (Number.isNaN(remoteMs)) {
247
+ return {
248
+ name: 'Clock skew',
249
+ status: 'WARN',
250
+ message: `Could not parse the server Date header: "${dateHeader}".`,
251
+ };
252
+ }
253
+ const skewMs = Math.abs(now() - remoteMs);
254
+ const skewMinutes = (skewMs / 60000).toFixed(1);
255
+ if (skewMs > warnThresholdMs) {
256
+ return {
257
+ name: 'Clock skew',
258
+ status: 'WARN',
259
+ message: `Local clock differs from the AIS server by ~${skewMinutes} minutes.`,
260
+ hint: 'Large clock skew can invalidate OAuth tokens. Sync your system clock (e.g. enable automatic time/NTP).',
261
+ };
262
+ }
263
+ return {
264
+ name: 'Clock skew',
265
+ status: 'PASS',
266
+ message: `Local clock is within ${skewMinutes} min of the AIS server.`,
267
+ };
268
+ }
269
+ export async function runDoctor(options = {}) {
270
+ const env = options.env ?? process.env;
271
+ const aisUrl = options.aisUrl ?? env['AIS_URL'] ?? 'https://ais.agentsandswarms.ai';
272
+ const host = new URL(aisUrl).hostname;
273
+ const fetchImpl = options.fetchImpl ?? fetch;
274
+ const now = options.now ?? Date.now;
275
+ const checks = [];
276
+ checks.push(checkNodeVersion(options.nodeVersion));
277
+ checks.push(await checkDnsResolution(host, options.dnsLookup));
278
+ checks.push(await checkTlsReachability(host, options.tlsProbe));
279
+ const health = await checkRestHealth(aisUrl, fetchImpl);
280
+ checks.push(health);
281
+ checks.push(await checkMcpEndpoint(aisUrl, fetchImpl));
282
+ checks.push(await checkCredentials(aisUrl, {
283
+ fetchImpl,
284
+ load: options.loadCreds,
285
+ home: options.home,
286
+ now,
287
+ }));
288
+ checks.push(checkProxyEnv(env));
289
+ checks.push(checkClockSkew(health.dateHeader, now));
290
+ const problemCount = checks.filter((c) => c.status !== 'PASS').length;
291
+ const hasFail = checks.some((c) => c.status === 'FAIL');
292
+ return {
293
+ aisUrl,
294
+ checks,
295
+ problemCount,
296
+ verdict: problemCount === 0 ? 'pass' : 'problems',
297
+ exitCode: hasFail ? 1 : 0,
298
+ };
299
+ }
300
+ // ── CLI formatting + entrypoint ──────────────────────────
301
+ function formatText(report) {
302
+ const lines = [];
303
+ lines.push(`aismemory doctor — ${report.aisUrl}`);
304
+ lines.push('');
305
+ for (const c of report.checks) {
306
+ lines.push(`[${c.status}] ${c.name}: ${c.message}`);
307
+ if (c.hint && c.status !== 'PASS') {
308
+ lines.push(` hint: ${c.hint}`);
309
+ }
310
+ }
311
+ lines.push('');
312
+ lines.push(report.problemCount === 0
313
+ ? 'doctor: all checks passed'
314
+ : `doctor: ${report.problemCount} problem(s) found`);
315
+ return lines.join('\n');
316
+ }
317
+ export async function main(argv = process.argv.slice(2)) {
318
+ const json = argv.includes('--json');
319
+ const report = await runDoctor();
320
+ if (json) {
321
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
322
+ }
323
+ else {
324
+ process.stdout.write(`${formatText(report)}\n`);
325
+ }
326
+ process.exit(report.exitCode);
327
+ }
328
+ main().catch((err) => {
329
+ process.stderr.write(`[aismemory doctor] Fatal: ${errMsg(err)}\n`);
330
+ process.exit(1);
331
+ });
332
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,MAAM,IAAI,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAkB,MAAM,UAAU,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAyB,MAAM,yBAAyB,CAAC;AAC3G,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAqBpD,gGAAgG;AAChG,MAAM,cAAc,GAAG,EAAE,CAAC;AAE1B,MAAM,cAAc,GAAG,CAAC,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AAE1G,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEzC,SAAS,MAAM,CAAC,GAAY;IAC1B,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED,4DAA4D;AAE5D,MAAM,UAAU,gBAAgB,CAC9B,cAAsB,OAAO,CAAC,OAAO,EACrC,WAAmB,cAAc;IAEjC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC7C,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,wCAAwC,WAAW,IAAI;YAChE,IAAI,EAAE,8BAA8B,QAAQ,GAAG;SAChD,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;QACrB,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,QAAQ,WAAW,sCAAsC,QAAQ,IAAI;YAC9E,IAAI,EAAE,4CAA4C,QAAQ,eAAe;SAC1E,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,QAAQ,WAAW,sCAAsC,QAAQ,IAAI;KAC/E,CAAC;AACJ,CAAC;AAMD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,IAAY,EACZ,SAAsB,gBAAgB;IAEtC,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;QACvC,OAAO;YACL,IAAI,EAAE,gBAAgB;YACtB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,GAAG,IAAI,gBAAgB,OAAO,GAAG;SAC3C,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,IAAI,EAAE,gBAAgB;YACtB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,6BAA6B,IAAI,KAAK,MAAM,CAAC,GAAG,CAAC,GAAG;YAC7D,IAAI,EAAE,wEAAwE;SAC/E,CAAC;IACJ,CAAC;AACH,CAAC;AAMD,SAAS,eAAe,CAAC,IAAY,EAAE,IAAY,EAAE,SAAiB;IACpE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAc,iBAAiB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE;YACrG,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,CAAC,GAAG,EAAE,CAAC;YACb,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACzB,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,CAAC,GAAG,CAAC,CAAC;QACd,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;YACxB,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,MAAM,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,CAAC,IAAI,KAAK,CAAC,iBAAiB,IAAI,IAAI,IAAI,oBAAoB,SAAS,IAAI,CAAC,CAAC,CAAC;QACpF,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,IAAY,EACZ,QAAoB,eAAe,EACnC,IAAI,GAAG,GAAG,EACV,SAAS,GAAG,IAAI;IAEhB,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;QACnC,OAAO;YACL,IAAI,EAAE,kBAAkB;YACxB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,GAAG,IAAI,IAAI,IAAI,2BAA2B;SACpD,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,IAAI,EAAE,kBAAkB;YACxB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,sCAAsC,IAAI,IAAI,IAAI,KAAK,MAAM,CAAC,GAAG,CAAC,GAAG;YAC9E,IAAI,EAAE,gGAAgG;SACvG,CAAC;IACJ,CAAC;AACH,CAAC;AASD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAe,EAAE,YAAqB,KAAK;IAC/E,MAAM,GAAG,GAAG,GAAG,OAAO,QAAQ,CAAC;IAC/B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC;QACxD,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;YACX,OAAO;gBACL,IAAI,EAAE,iBAAiB;gBACvB,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,GAAG,GAAG,mBAAmB,GAAG,CAAC,MAAM,WAAW;gBACvD,UAAU;aACX,CAAC;QACJ,CAAC;QACD,OAAO;YACL,IAAI,EAAE,iBAAiB;YACvB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,GAAG,GAAG,mBAAmB,GAAG,CAAC,MAAM,8CAA8C;YAC1F,IAAI,EAAE,uDAAuD;YAC7D,UAAU;SACX,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,IAAI,EAAE,iBAAiB;YACvB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,mBAAmB,GAAG,KAAK,MAAM,CAAC,GAAG,CAAC,GAAG;YAClD,IAAI,EAAE,4DAA4D;SACnE,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAe,EAAE,YAAqB,KAAK;IAChF,MAAM,GAAG,GAAG,GAAG,OAAO,MAAM,CAAC;IAC7B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;QACjC,uEAAuE;QACvE,qEAAqE;QACrE,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,6CAA6C,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/G,OAAO;YACL,IAAI,EAAE,mBAAmB;YACzB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,GAAG,GAAG,mBAAmB,GAAG,CAAC,MAAM,GAAG,QAAQ,GAAG;SAC3D,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,IAAI,EAAE,mBAAmB;YACzB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,mBAAmB,GAAG,KAAK,MAAM,CAAC,GAAG,CAAC,GAAG;YAClD,IAAI,EAAE,4DAA4D;SACnE,CAAC;IACJ,CAAC;AACH,CAAC;AASD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,OAAe,EAAE,OAA6B,EAAE;IACrF,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,oBAAoB,CAAC;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACjC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;IAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACvB,MAAM,IAAI,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAEvC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;IACzB,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,2BAA2B,IAAI,gDAAgD;YACxF,IAAI,EAAE,qGAAqG;SAC5G,CAAC;IACJ,CAAC;IAED,IAAI,YAAY,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;QACzC,OAAO;YACL,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,kBAAkB,IAAI,yDAAyD,KAAK,CAAC,SAAS,IAAI;YAC3G,IAAI,EAAE,8GAA8G;SACrH,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,OAAO,cAAc,KAAK,CAAC,OAAO,EAAE,EAAE;YACnE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,CAAC,KAAK,EAAE,EAAE;SACpD,CAAC,CAAC;QACH,IAAI,GAAG,CAAC,EAAE,EAAE,CAAC;YACX,OAAO;gBACL,IAAI,EAAE,aAAa;gBACnB,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,mBAAmB,KAAK,CAAC,OAAO,iCAAiC;aAC3E,CAAC;QACJ,CAAC;QACD,OAAO;YACL,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,kDAAkD,GAAG,CAAC,MAAM,cAAc,KAAK,CAAC,OAAO,GAAG;YACnG,IAAI,EAAE,yGAAyG;SAChH,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,gDAAgD,MAAM,CAAC,GAAG,CAAC,GAAG;YACvE,IAAI,EAAE,mEAAmE;SAC1E,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAyB,OAAO,CAAC,GAAG;IAChE,MAAM,GAAG,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO;YACL,IAAI,EAAE,mBAAmB;YACzB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,wDAAwD;SAClE,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,mBAAmB;QACzB,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,2CAA2C,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;QACrE,IAAI,EAAE,iKAAiK;KACxK,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,UAA8B,EAC9B,MAAoB,IAAI,CAAC,GAAG,EAC5B,kBAA0B,kBAAkB;IAE5C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO;YACL,IAAI,EAAE,YAAY;YAClB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,wFAAwF;SAClG,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3B,OAAO;YACL,IAAI,EAAE,YAAY;YAClB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,4CAA4C,UAAU,IAAI;SACpE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAChD,IAAI,MAAM,GAAG,eAAe,EAAE,CAAC;QAC7B,OAAO;YACL,IAAI,EAAE,YAAY;YAClB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,+CAA+C,WAAW,WAAW;YAC9E,IAAI,EAAE,wGAAwG;SAC/G,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,YAAY;QAClB,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,yBAAyB,WAAW,yBAAyB;KACvE,CAAC;AACJ,CAAC;AAgBD,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAyB,EAAE;IACzD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,CAAC,IAAI,gCAAgC,CAAC;IACpF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;IACtC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC;IAC7C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IAEpC,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IACnD,MAAM,CAAC,IAAI,CAAC,MAAM,kBAAkB,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/D,MAAM,CAAC,IAAI,CAAC,MAAM,oBAAoB,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEhE,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACxD,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAEpB,MAAM,CAAC,IAAI,CAAC,MAAM,gBAAgB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IACvD,MAAM,CAAC,IAAI,CACT,MAAM,gBAAgB,CAAC,MAAM,EAAE;QAC7B,SAAS;QACT,IAAI,EAAE,OAAO,CAAC,SAAS;QACvB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,GAAG;KACJ,CAAC,CACH,CAAC;IACF,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;IAChC,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC;IAEpD,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,MAAM,CAAC;IACtE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;IAExD,OAAO;QACL,MAAM;QACN,MAAM;QACN,YAAY;QACZ,OAAO,EAAE,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU;QACjD,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAC1B,CAAC;AACJ,CAAC;AAED,4DAA4D;AAE5D,SAAS,UAAU,CAAC,MAAoB;IACtC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,sBAAsB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACpD,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAClC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACR,MAAM,CAAC,YAAY,KAAK,CAAC;QACvB,CAAC,CAAC,2BAA2B;QAC7B,CAAC,CAAC,WAAW,MAAM,CAAC,YAAY,mBAAmB,CACtD,CAAC;IACF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,OAAiB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,MAAM,SAAS,EAAE,CAAC;IACjC,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAC/D,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -7,4 +7,15 @@
7
7
  *
8
8
  * Usage: npx aismemory
9
9
  */
10
+ type AgentSummary = {
11
+ id: string;
12
+ name: string;
13
+ status: string;
14
+ };
15
+ /**
16
+ * Fetch EVERY agent in the tenant, following server pagination.
17
+ * The server defaults to limit=20 and silently truncates (2026-08-24
18
+ * lockout, ticket 72ab7f4e) — never call GET /v1/agents without this.
19
+ */
20
+ export declare function listAllAgents(token: string): Promise<AgentSummary[]>;
10
21
  export {};
package/dist/index.js CHANGED
@@ -124,6 +124,25 @@ async function aisGet(path, token) {
124
124
  }
125
125
  return data;
126
126
  }
127
+ /**
128
+ * Fetch EVERY agent in the tenant, following server pagination.
129
+ * The server defaults to limit=20 and silently truncates (2026-08-24
130
+ * lockout, ticket 72ab7f4e) — never call GET /v1/agents without this.
131
+ */
132
+ export async function listAllAgents(token) {
133
+ const all = [];
134
+ let page = 1;
135
+ // hard stop at 50 pages (1000 agents) as a runaway guard
136
+ for (; page <= 50; page += 1) {
137
+ const res = (await aisGet(`/v1/agents?limit=100&page=${page}`, token));
138
+ const agents = res.data?.agents ?? [];
139
+ all.push(...agents);
140
+ const hasMore = res.data?.pagination?.hasMore ?? false;
141
+ if (!hasMore)
142
+ break;
143
+ }
144
+ return all;
145
+ }
127
146
  async function resolveAgentForLoad(token, query) {
128
147
  const resolveRes = (await aisGet(`/v1/resolve?query=${encodeURIComponent(query)}`, token));
129
148
  if (resolveRes.data?.status === 'resolved' && resolveRes.data.agent) {
@@ -137,8 +156,7 @@ async function resolveAgentForLoad(token, query) {
137
156
  const available = resolveRes.data.available?.map((ag) => ag.name).join(', ');
138
157
  throw new Error(resolveRes.data.message ?? `agent_load: no agent named "${query}" in this tenant. Available: ${available || '(none)'}`);
139
158
  }
140
- const listRes = (await aisGet('/v1/agents', token));
141
- const agents = listRes.data?.agents ?? [];
159
+ const agents = await listAllAgents(token);
142
160
  const matches = agents.filter((ag) => ag.name.toLowerCase() === query.toLowerCase() || ag.id === query);
143
161
  if (matches.length === 0) {
144
162
  const available = agents.map((ag) => ag.name).join(', ');
@@ -160,8 +178,7 @@ async function resolveAgent(token, fallbackAgentId, fallbackTenantId, preferredA
160
178
  // Explicit env var overrides everything — must belong to this tenant.
161
179
  const envAgentId = process.env['AIS_AGENT_ID'];
162
180
  if (envAgentId) {
163
- const listRes = (await aisGet('/v1/agents', token));
164
- const agents = listRes.data?.agents ?? [];
181
+ const agents = await listAllAgents(token);
165
182
  try {
166
183
  const match = assertEnvAgentInTenant(envAgentId, agents);
167
184
  stderr(`Using agent from AIS_AGENT_ID: ${match.name} (${match.id})`);
@@ -174,8 +191,7 @@ async function resolveAgent(token, fallbackAgentId, fallbackTenantId, preferredA
174
191
  }
175
192
  }
176
193
  try {
177
- const res = await aisGet('/v1/agents', token);
178
- const agents = res.data?.agents?.filter((a) => a.status === 'active') ?? [];
194
+ const agents = (await listAllAgents(token)).filter((a) => a.status === 'active');
179
195
  const selection = selectAgentForSession({
180
196
  agents,
181
197
  envAgentName: process.env['AIS_AGENT_NAME'],
@@ -224,31 +240,31 @@ class BondPendingError extends Error {
224
240
  userCode;
225
241
  bondPending = true;
226
242
  constructor(verificationUri, userCode) {
227
- super(`Memory bond required.\n\n` +
243
+ super(`Memory bond required — ask your human to approve now.\n\n` +
228
244
  `Visit: ${verificationUri}\n` +
229
245
  `Code: ${userCode}\n\n` +
230
- `Enter your email and tap Approve, then retry this tool. Takes about 30 seconds.`);
246
+ `Enter your email and tap Approve, then retry this tool. Takes about 30 seconds.\n` +
247
+ `Heads up: this code expires in 30 minutes, and unclaimed agents (and their memories) are archived after 7 days.`);
231
248
  this.verificationUri = verificationUri;
232
249
  this.userCode = userCode;
233
250
  this.name = 'BondPendingError';
234
251
  }
235
252
  }
236
- async function startDeviceFlow() {
253
+ async function startDeviceFlow(priorProvisionalToken) {
237
254
  emitTelemetry({ event: 'auth.device_flow.start' });
238
255
  stderr('Starting authentication...');
239
- const authRes = (await aisPost('/v1/oauth/device/authorize', {
240
- name: `agent-${homedir().split('/').pop() ?? 'user'}`,
241
- }));
256
+ const authRes = (await aisPost('/v1/oauth/device/authorize', { name: `agent-${homedir().split('/').pop() ?? 'user'}` }, priorProvisionalToken));
242
257
  if (!authRes.success || !authRes.data) {
243
258
  throw new Error(`Authentication failed: ${authRes.error?.message ?? 'Unknown error'}`);
244
259
  }
245
- const { device_code, user_code, verification_uri_complete, interval, agent_id, provisional_tenant_id } = authRes.data;
260
+ const { device_code, user_code, verification_uri_complete, interval, agent_id, provisional_tenant_id, provisional_token } = authRes.data;
246
261
  stderr('');
247
262
  stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
248
263
  stderr(' aismemory — memory bond required');
249
264
  stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
250
265
  stderr(` Bond URL : ${verification_uri_complete}`);
251
266
  stderr(` Code : ${user_code}`);
267
+ stderr(' Code expires in 30 min; unclaimed agents are archived after 7 days.');
252
268
  stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
253
269
  const flow = {
254
270
  device_code,
@@ -256,6 +272,7 @@ async function startDeviceFlow() {
256
272
  verification_uri_complete,
257
273
  agent_id,
258
274
  provisional_tenant_id,
275
+ provisional_token,
259
276
  // poll is assigned below after flowRef is captured so pollForToken holds
260
277
  // a reference to the live flow object for URL updates on auto-restart.
261
278
  dead: false,
@@ -276,7 +293,7 @@ async function startDeviceFlow() {
276
293
  return flow;
277
294
  }
278
295
  async function pollForToken(device_code, interval, agent_id, provisional_tenant_id,
279
- /** Mutable ref so auto-restart can update the surfaced URL in ActiveDeviceFlow */
296
+ /** Mutable ref so auto-restart can update the surfaced URL and provisional token in ActiveDeviceFlow */
280
297
  flowRef) {
281
298
  const pollIntervalMs = Math.max(interval, 5) * 1000;
282
299
  const handle = {
@@ -307,9 +324,7 @@ flowRef) {
307
324
  }
308
325
  },
309
326
  startNewFlow: async () => {
310
- const authRes = (await aisPost('/v1/oauth/device/authorize', {
311
- name: `agent-${homedir().split('/').pop() ?? 'user'}`,
312
- }));
327
+ const authRes = (await aisPost('/v1/oauth/device/authorize', { name: `agent-${homedir().split('/').pop() ?? 'user'}` }, flowRef?.provisional_token));
313
328
  if (!authRes.success || !authRes.data) {
314
329
  throw new Error(`Re-authorization failed: ${authRes.error?.message ?? 'Unknown error'}`);
315
330
  }
@@ -319,10 +334,13 @@ flowRef) {
319
334
  user_code: authRes.data.user_code,
320
335
  };
321
336
  // Update the mutable ref so the caller (ensureCredentials) can surface
322
- // the new URL to the human without restarting the MCP process.
337
+ // the new URL to the human without restarting the MCP process, and so
338
+ // a THIRD expiry in the same flow re-sends the latest provisional
339
+ // token rather than a stale, already-superseded one.
323
340
  if (flowRef) {
324
341
  flowRef.verification_uri_complete = newHandle.verification_uri_complete;
325
342
  flowRef.user_code = newHandle.user_code;
343
+ flowRef.provisional_token = authRes.data.provisional_token;
326
344
  }
327
345
  stderr('');
328
346
  stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
@@ -404,7 +422,7 @@ const tools = [
404
422
  },
405
423
  {
406
424
  name: 'dream',
407
- description: 'Trigger a memory consolidation dream cycle — promotes important memories and archives old ones.',
425
+ description: 'Trigger a memory consolidation dream cycle — promotes important memories and archives old ones. Promotion only scans the session tier; active-tier memories move via context-clear or handoff, not dream.',
408
426
  inputSchema: { type: 'object', properties: {} },
409
427
  },
410
428
  {
@@ -523,6 +541,10 @@ async function main() {
523
541
  await import('./cli/sync-memory.js');
524
542
  return; // sync-memory.ts calls process.exit() on failure
525
543
  }
544
+ if (process.argv[2] === 'doctor') {
545
+ await import('./cli/doctor.js');
546
+ return; // doctor.ts calls process.exit() itself
547
+ }
526
548
  // Agent API key auth mode (CORBOT-B1D3244C) — unattended machines. Checked
527
549
  // before any device-flow / OAuth-credential / DID-key-auth setup below so
528
550
  // this mode NEVER falls back to those paths (that fallback is what used to
@@ -565,51 +587,50 @@ async function main() {
565
587
  if (!isCredsStale(creds.expiresAt, Date.now()))
566
588
  return creds;
567
589
  // CORBOT-A5BBD580: tokens live 7 days but the process can outlive
568
- // them. Drop stale creds and fall through to silent key auth.
590
+ // them. Before falling through to key/device auth, re-read the file —
591
+ // an external repair may have already refreshed it. loadCredentials()
592
+ // returns null for a still-stale file, so this simplifies to a
593
+ // re-read + early return (27FA5CDD).
594
+ creds = loadCredentials();
595
+ if (creds)
596
+ return creds;
569
597
  stderr('Cached credentials are expired or expiring soon — re-authenticating...');
570
- creds = null;
571
598
  }
572
599
  // ── Key auth (Phase 1 DID auth) ───────────────────────────────────────
573
- // If the user has enrolled a private key for this device, sign a
574
- // server-issued challenge and skip the device-flow ceremony entirely.
575
- // tryKeyAuth() returns null if no key file is present (clean fallback
576
- // to device flow) and throws only on real failures (e.g. server says
577
- // INVALID_SIGNATURE). On throw we log and continue to device flow —
578
- // device flow is the recovery path for "user wiped their key file".
600
+ let keyResult = null;
579
601
  try {
580
- const keyResult = await tryKeyAuth({ aisUrl: AIS_URL });
581
- if (keyResult) {
582
- const claims = decodeJwtClaims(keyResult.token);
583
- const tenantId = claims.tenantId ?? '';
584
- const resolved = await resolveAgent(keyResult.token, '', tenantId, loadOAuthCredentials()?.agentId);
585
- creds = {
586
- token: keyResult.token,
587
- agentId: resolved.agentId,
588
- tenantId: resolved.tenantId || tenantId,
589
- expiresAt: keyResult.expiresAt,
590
- };
591
- saveCredentials(creds);
592
- emitTelemetry({
593
- event: 'auth.key.success',
594
- agentIdHash: hashAgentId(creds.agentId),
595
- });
596
- stderr('Authenticated via DID key auth (no browser required)');
597
- if (!postAuthDone) {
598
- postAuthDone = true;
599
- void runPostAuth(creds);
600
- }
601
- return creds;
602
- }
602
+ keyResult = await tryKeyAuth({ aisUrl: AIS_URL });
603
603
  }
604
604
  catch (err) {
605
- emitTelemetry({
606
- event: 'auth.key.fallback',
607
- error: err instanceof Error ? err.message : String(err),
608
- });
605
+ // Real key-auth failure (e.g. INVALID_SIGNATURE / server unreachable).
606
+ // Device flow remains the recovery path ONLY for this case.
607
+ emitTelemetry({ event: 'auth.key.fallback', error: err instanceof Error ? err.message : String(err) });
609
608
  stderr(`Key auth failed: ${err instanceof Error ? err.message : String(err)}. Falling back to device flow.`);
610
609
  }
610
+ if (keyResult) {
611
+ // Post-auth failures (agent resolution, claim decode) are REAL errors —
612
+ // surface them to the caller. They must never route to device flow
613
+ // (2026-08-24 lockout: a pagination bug here minted ghost agents).
614
+ const claims = decodeJwtClaims(keyResult.token);
615
+ const tenantId = claims.tenantId ?? '';
616
+ const resolved = await resolveAgent(keyResult.token, '', tenantId, loadOAuthCredentials()?.agentId);
617
+ creds = {
618
+ token: keyResult.token,
619
+ agentId: resolved.agentId,
620
+ tenantId: resolved.tenantId || tenantId,
621
+ expiresAt: keyResult.expiresAt,
622
+ };
623
+ saveCredentials(creds);
624
+ emitTelemetry({ event: 'auth.key.success', agentIdHash: hashAgentId(creds.agentId) });
625
+ stderr('Authenticated via DID key auth (no browser required)');
626
+ if (!postAuthDone) {
627
+ postAuthDone = true;
628
+ void runPostAuth(creds);
629
+ }
630
+ return creds;
631
+ }
611
632
  if (!activeFlow || activeFlow.dead) {
612
- activeFlow = await startDeviceFlow();
633
+ activeFlow = await startDeviceFlow(activeFlow?.provisional_token);
613
634
  }
614
635
  const sentinel = Symbol('bond-pending');
615
636
  // poll is always set by startDeviceFlow before it returns; this guard
@@ -630,7 +651,7 @@ async function main() {
630
651
  // Always say WHY the old flow died: a silent discard here hid three
631
652
  // weeks of broken cold-start bonding (CORBOT-9CE03C6E).
632
653
  stderr(`Device flow discarded (${err instanceof Error ? err.message : String(err)}); starting a fresh one.`);
633
- activeFlow = await startDeviceFlow();
654
+ activeFlow = await startDeviceFlow(activeFlow.provisional_token);
634
655
  throw new BondPendingError(activeFlow.verification_uri_complete, activeFlow.user_code);
635
656
  }
636
657
  if (got === sentinel) {
@@ -766,8 +787,12 @@ async function main() {
766
787
  const target = await resolveAgentForLoad(token, query);
767
788
  agentId = target.id;
768
789
  // Persist the switch so subsequent tool calls use the new agent.
790
+ // saveCredentials() MUST be called here so that a process restart
791
+ // (silent MCP reconnect) restores the switched agent from disk
792
+ // instead of silently reverting to the default (CORBOT-497C7FA3).
769
793
  if (creds) {
770
794
  creds = { ...creds, agentId };
795
+ saveCredentials(creds);
771
796
  }
772
797
  agentContext = await hydrateIdentity(AIS_URL, agentId, token);
773
798
  applyMemoryRefresh(agentId, token);