@echomem/mcp 1.3.1 → 1.3.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/dist/forensics.js +717 -0
- package/dist/index.js +61 -4
- package/dist/migrate.js +417 -99
- package/dist/report.js +1 -0
- package/dist/setup-page.js +852 -306
- package/dist/setup.js +606 -75
- package/package.json +1 -1
package/dist/setup.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import http from "node:http";
|
|
16
16
|
import { randomUUID } from "node:crypto";
|
|
17
17
|
import { spawn } from "node:child_process";
|
|
18
|
+
import { Worker } from "node:worker_threads";
|
|
18
19
|
import fs from "node:fs";
|
|
19
20
|
import os from "node:os";
|
|
20
21
|
import path from "node:path";
|
|
@@ -22,8 +23,8 @@ import readline from "node:readline";
|
|
|
22
23
|
import axios from "axios";
|
|
23
24
|
import { KeyStore } from "./keystore.js";
|
|
24
25
|
import { fetchEncryptionConfig, deriveAndVerifyKey, verifyKeyB64 } from "./encryption.js";
|
|
25
|
-
import { collect, runReport,
|
|
26
|
-
import { cmdMigrate, discoverMigratableSessions, estimateMigrationEta, startMigration } from "./migrate.js";
|
|
26
|
+
import { collect, runReport, buildStatsPayload } from "./report.js";
|
|
27
|
+
import { cmdMigrate, applyAccountImportStatus, applyFastAccountImportStatus, discoverMigratableFastDiscovery, discoverMigratableSessions, estimateMigrationEta, fetchProcessedImportKeys, isImportStatusUnsupported, markAccountImportStatusFailed, markAccountImportStatusUnavailable, markFastAccountImportStatusUnavailable, startMigration, summarizeFastMigratableDiscovery, MIGRATE_CONCURRENCY, } from "./migrate.js";
|
|
27
28
|
import { syncCodexUsage } from "./codex-sync.js";
|
|
28
29
|
import { renderSetupPage } from "./setup-page.js";
|
|
29
30
|
// The hosted connect-device page is now only the account-auth/token courier. The dashboard itself is
|
|
@@ -149,6 +150,50 @@ function openBrowser(url) {
|
|
|
149
150
|
/* headless — caller prints the URL */
|
|
150
151
|
}
|
|
151
152
|
}
|
|
153
|
+
function migratableFromDiscovery(disc) {
|
|
154
|
+
const eta = estimateMigrationEta(disc.pending, disc.skippedActive);
|
|
155
|
+
return {
|
|
156
|
+
pending: disc.pending.length,
|
|
157
|
+
pendingTotal: disc.pendingTotal,
|
|
158
|
+
alreadyMigrated: disc.alreadyMigrated,
|
|
159
|
+
skippedActive: disc.skippedActive,
|
|
160
|
+
limited: disc.limited,
|
|
161
|
+
eta,
|
|
162
|
+
buckets: eta.buckets,
|
|
163
|
+
totalChars: eta.totalChars,
|
|
164
|
+
approxInputTokens: eta.approxInputTokens,
|
|
165
|
+
estimatedSeconds: eta.estimatedSeconds,
|
|
166
|
+
estimatedLabel: eta.estimatedLabel,
|
|
167
|
+
accountChecked: disc.accountChecked === true,
|
|
168
|
+
accountCheckFailed: disc.accountCheckFailed === true,
|
|
169
|
+
accountCheckUnavailable: disc.accountCheckUnavailable === true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function sessionsFromDiscovery(disc) {
|
|
173
|
+
return {
|
|
174
|
+
total: disc.sessions.length,
|
|
175
|
+
codex: disc.codexCount,
|
|
176
|
+
claudeCode: disc.claudeCount,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function migratableFromFastSummary(summary) {
|
|
180
|
+
return {
|
|
181
|
+
pending: summary.pending,
|
|
182
|
+
pendingTotal: summary.pendingTotal,
|
|
183
|
+
alreadyMigrated: summary.alreadyMigrated,
|
|
184
|
+
skippedActive: summary.skippedActive,
|
|
185
|
+
eta: summary.eta,
|
|
186
|
+
buckets: summary.eta.buckets,
|
|
187
|
+
totalChars: summary.eta.totalChars,
|
|
188
|
+
approxInputTokens: summary.eta.approxInputTokens,
|
|
189
|
+
estimatedSeconds: summary.eta.estimatedSeconds,
|
|
190
|
+
estimatedLabel: summary.eta.estimatedLabel,
|
|
191
|
+
quickEstimate: true,
|
|
192
|
+
accountChecked: summary.accountChecked === true,
|
|
193
|
+
accountCheckFailed: summary.accountCheckFailed === true,
|
|
194
|
+
accountCheckUnavailable: summary.accountCheckUnavailable === true,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
152
197
|
function deferred() {
|
|
153
198
|
let done = false;
|
|
154
199
|
let resolveInner;
|
|
@@ -214,6 +259,100 @@ function withTimeout(promise, ms, code, onTimeout) {
|
|
|
214
259
|
clearTimeout(timer);
|
|
215
260
|
});
|
|
216
261
|
}
|
|
262
|
+
function delay(ms) {
|
|
263
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
264
|
+
}
|
|
265
|
+
function discoverMigratableSessionsOffThread() {
|
|
266
|
+
const migrateUrl = new URL("./migrate.js", import.meta.url).href;
|
|
267
|
+
const code = `
|
|
268
|
+
import { parentPort } from "node:worker_threads";
|
|
269
|
+
import { discoverMigratableSessions } from ${JSON.stringify(migrateUrl)};
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
parentPort?.postMessage({ ok: true, discovery: discoverMigratableSessions() });
|
|
273
|
+
} catch (error) {
|
|
274
|
+
parentPort?.postMessage({
|
|
275
|
+
ok: false,
|
|
276
|
+
message: error instanceof Error ? error.message : String(error),
|
|
277
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
`;
|
|
281
|
+
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`));
|
|
282
|
+
return new Promise((resolve, reject) => {
|
|
283
|
+
let settled = false;
|
|
284
|
+
worker.once("message", (message) => {
|
|
285
|
+
settled = true;
|
|
286
|
+
const msg = message;
|
|
287
|
+
if (msg.ok === true && msg.discovery && typeof msg.discovery === "object") {
|
|
288
|
+
resolve(msg.discovery);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const err = new Error(typeof msg.message === "string" ? msg.message : "Exact local discovery failed");
|
|
292
|
+
if (typeof msg.stack === "string")
|
|
293
|
+
err.stack = msg.stack;
|
|
294
|
+
reject(err);
|
|
295
|
+
});
|
|
296
|
+
worker.once("error", (error) => {
|
|
297
|
+
if (settled)
|
|
298
|
+
return;
|
|
299
|
+
settled = true;
|
|
300
|
+
reject(error);
|
|
301
|
+
});
|
|
302
|
+
worker.once("exit", (code) => {
|
|
303
|
+
// Reject on ANY unsettled exit (incl. code 0): a worker that exits without posting a result must
|
|
304
|
+
// not leave this promise pending forever (that was the "stuck at finishing local job sizing" hang).
|
|
305
|
+
if (settled)
|
|
306
|
+
return;
|
|
307
|
+
settled = true;
|
|
308
|
+
reject(new Error(`Exact local discovery worker exited (code ${code}) without a result`));
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/** Build the local forensic "Context Doctor" report on a worker thread so the multi-file scan never
|
|
313
|
+
* blocks the bridge's event loop (the freeze postmortem: any unbounded sync work on this path is risky). */
|
|
314
|
+
function buildForensicReportOffThread(onProgress) {
|
|
315
|
+
const forensicsUrl = new URL("./forensics.js", import.meta.url).href;
|
|
316
|
+
const code = `
|
|
317
|
+
import { parentPort } from "node:worker_threads";
|
|
318
|
+
import { buildForensicReport } from ${JSON.stringify(forensicsUrl)};
|
|
319
|
+
try {
|
|
320
|
+
const report = buildForensicReport({ onProgress: (done, total) => parentPort?.postMessage({ progress: { done, total } }) });
|
|
321
|
+
parentPort?.postMessage({ ok: true, report });
|
|
322
|
+
} catch (error) {
|
|
323
|
+
parentPort?.postMessage({ ok: false, message: error instanceof Error ? error.message : String(error) });
|
|
324
|
+
}
|
|
325
|
+
`;
|
|
326
|
+
const worker = new Worker(new URL(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`));
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
let settled = false;
|
|
329
|
+
worker.on("message", (message) => {
|
|
330
|
+
const msg = message;
|
|
331
|
+
if (msg.progress) {
|
|
332
|
+
onProgress?.(msg.progress.done, msg.progress.total);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
settled = true;
|
|
336
|
+
if (msg.ok === true && msg.report && typeof msg.report === "object")
|
|
337
|
+
resolve(msg.report);
|
|
338
|
+
else
|
|
339
|
+
reject(new Error(typeof msg.message === "string" ? msg.message : "Local forensic report failed"));
|
|
340
|
+
void worker.terminate();
|
|
341
|
+
});
|
|
342
|
+
worker.once("error", (error) => {
|
|
343
|
+
if (settled)
|
|
344
|
+
return;
|
|
345
|
+
settled = true;
|
|
346
|
+
reject(error);
|
|
347
|
+
});
|
|
348
|
+
worker.once("exit", (code) => {
|
|
349
|
+
if (settled)
|
|
350
|
+
return;
|
|
351
|
+
settled = true;
|
|
352
|
+
reject(new Error(`Forensic report worker exited (code ${code}) without a result`));
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
}
|
|
217
356
|
export function respondMigrate(res, body, status = 200) {
|
|
218
357
|
if (res.writableEnded)
|
|
219
358
|
return;
|
|
@@ -226,6 +365,7 @@ export function respondMigrate(res, body, status = 200) {
|
|
|
226
365
|
*/
|
|
227
366
|
export function startCallbackServer(opts = {}) {
|
|
228
367
|
const timeoutMs = opts.timeoutMs ?? 300_000;
|
|
368
|
+
const dashboardTimeoutMs = 4 * 60 * 60 * 1000;
|
|
229
369
|
const expectedNonce = opts.nonce;
|
|
230
370
|
return new Promise((resolveOuter, rejectOuter) => {
|
|
231
371
|
const onToken = deferred();
|
|
@@ -233,9 +373,12 @@ export function startCallbackServer(opts = {}) {
|
|
|
233
373
|
const migrateRequest = deferred();
|
|
234
374
|
let stats = null;
|
|
235
375
|
let authUrl = "";
|
|
376
|
+
let switchAccountUrl = "";
|
|
236
377
|
let connected = false;
|
|
237
378
|
let progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
238
379
|
let migrateStarted = false;
|
|
380
|
+
let tokenRefreshHandler = null;
|
|
381
|
+
let logoutHandler = null;
|
|
239
382
|
let timer;
|
|
240
383
|
let closed = false;
|
|
241
384
|
let server;
|
|
@@ -254,6 +397,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
254
397
|
const armTimeout = () => {
|
|
255
398
|
if (timer)
|
|
256
399
|
clearTimeout(timer);
|
|
400
|
+
const waitMs = onToken.settled() ? dashboardTimeoutMs : timeoutMs;
|
|
257
401
|
timer = setTimeout(() => {
|
|
258
402
|
if (!onToken.settled()) {
|
|
259
403
|
onToken.reject(new Error("timed out waiting for browser approval"));
|
|
@@ -262,16 +406,27 @@ export function startCallbackServer(opts = {}) {
|
|
|
262
406
|
decision.resolve("timeout");
|
|
263
407
|
}
|
|
264
408
|
close();
|
|
265
|
-
},
|
|
409
|
+
}, waitMs);
|
|
410
|
+
timer.unref?.();
|
|
266
411
|
};
|
|
267
412
|
const handleCallback = (res, token, key, nonce) => {
|
|
268
413
|
if (!checkNonce(nonce))
|
|
269
414
|
return void text(res, 403, "bad nonce");
|
|
270
415
|
if (!token)
|
|
271
416
|
return void text(res, 400, "missing token");
|
|
417
|
+
const firstToken = !onToken.settled();
|
|
272
418
|
connected = true;
|
|
273
|
-
|
|
274
|
-
|
|
419
|
+
const setupPath = `/setup?nonce=${encodeURIComponent(nonce || "")}&connected=1`;
|
|
420
|
+
res.writeHead(200, { "Content-Type": "text/html" }).end(`<!doctype html><html><head><meta charset="utf-8"></head><body style="font-family:system-ui;padding:3rem;text-align:center"><h2>EchoMem connected</h2><p>Returning to the local setup page...</p><p><a href="${setupPath}">Continue</a></p><script>(function(){var target=${JSON.stringify(setupPath)};try{if(window.opener&&!window.opener.closed){window.opener.postMessage({type:"echomem:connected",nonce:${JSON.stringify(nonce || "")}},window.location.origin);window.close();setTimeout(function(){window.location.href=target;},500);return;}}catch(_){}window.location.href=target;})();</script></body></html>`);
|
|
421
|
+
const callbackToken = { token, key };
|
|
422
|
+
if (firstToken) {
|
|
423
|
+
onToken.resolve(callbackToken);
|
|
424
|
+
}
|
|
425
|
+
else if (tokenRefreshHandler) {
|
|
426
|
+
Promise.resolve(tokenRefreshHandler(callbackToken)).catch((e) => {
|
|
427
|
+
console.error(`Could not refresh local login: ${e instanceof Error ? e.message : String(e)}`);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
275
430
|
armTimeout();
|
|
276
431
|
};
|
|
277
432
|
server = http.createServer((req, res) => {
|
|
@@ -295,7 +450,7 @@ export function startCallbackServer(opts = {}) {
|
|
|
295
450
|
if (route === "/config" && req.method === "GET") {
|
|
296
451
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
297
452
|
return void text(res, 403, "bad nonce");
|
|
298
|
-
json(res, 200, { connected, authUrl, localOnly: true });
|
|
453
|
+
json(res, 200, { connected, authUrl, switchAccountUrl: switchAccountUrl || authUrl, localOnly: true });
|
|
299
454
|
return;
|
|
300
455
|
}
|
|
301
456
|
if (route === "/callback" && req.method === "GET") {
|
|
@@ -323,12 +478,58 @@ export function startCallbackServer(opts = {}) {
|
|
|
323
478
|
json(res, 200, payload);
|
|
324
479
|
return;
|
|
325
480
|
}
|
|
481
|
+
if (route === "/report" && req.method === "GET") {
|
|
482
|
+
// Local forensic "Context Doctor" report — computed locally, served BEFORE auth (scan-first).
|
|
483
|
+
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
484
|
+
return void text(res, 403, "bad nonce");
|
|
485
|
+
const payload = opts.getReport ? opts.getReport() : null;
|
|
486
|
+
if (payload == null) {
|
|
487
|
+
// 202 carries scan progress so the page can show a live "scanned N/total" indicator.
|
|
488
|
+
const prog = opts.getReportProgress ? opts.getReportProgress() : { scanned: 0, total: 0 };
|
|
489
|
+
return void res.writeHead(202, { "Content-Type": "application/json" }).end(JSON.stringify(prog));
|
|
490
|
+
}
|
|
491
|
+
json(res, 200, payload);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
326
494
|
if (route === "/progress" && req.method === "GET") {
|
|
327
495
|
if (!checkNonce(url.searchParams.get("nonce") || undefined))
|
|
328
496
|
return void text(res, 403, "bad nonce");
|
|
329
497
|
json(res, 200, progress);
|
|
330
498
|
return;
|
|
331
499
|
}
|
|
500
|
+
if (route === "/logout" && req.method === "POST") {
|
|
501
|
+
let body;
|
|
502
|
+
try {
|
|
503
|
+
body = await readJsonBody(req);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
text(res, 400, "bad json");
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (!checkNonce(asString(body.nonce)))
|
|
510
|
+
return void text(res, 403, "bad nonce");
|
|
511
|
+
if (migrateStarted && (progress.status === "starting" || progress.status === "running")) {
|
|
512
|
+
json(res, 409, { error: "MIGRATION_RUNNING", message: "Wait for extraction to finish before signing out locally." });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
fs.rmSync(new KeyStore().path(), { force: true });
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
/* already logged out locally */
|
|
520
|
+
}
|
|
521
|
+
connected = false;
|
|
522
|
+
stats = null;
|
|
523
|
+
migrateStarted = false;
|
|
524
|
+
progress = { status: "idle", total: 0, completed: 0, running: 0, queued: 0, failed: 0, extracted: 0 };
|
|
525
|
+
json(res, 200, { ok: true, authUrl, switchAccountUrl: switchAccountUrl || authUrl, localOnly: true });
|
|
526
|
+
Promise.resolve()
|
|
527
|
+
.then(() => logoutHandler?.())
|
|
528
|
+
.catch((e) => {
|
|
529
|
+
console.error(`Could not reset local login state: ${e instanceof Error ? e.message : String(e)}`);
|
|
530
|
+
});
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
332
533
|
if (route === "/migrate" && req.method === "POST") {
|
|
333
534
|
let body;
|
|
334
535
|
try {
|
|
@@ -385,8 +586,15 @@ export function startCallbackServer(opts = {}) {
|
|
|
385
586
|
wait: onToken.promise,
|
|
386
587
|
decision: decision.promise,
|
|
387
588
|
migrateRequest: migrateRequest.promise,
|
|
388
|
-
|
|
589
|
+
setTokenRefreshHandler: (handler) => {
|
|
590
|
+
tokenRefreshHandler = handler;
|
|
591
|
+
},
|
|
592
|
+
setLogoutHandler: (handler) => {
|
|
593
|
+
logoutHandler = handler;
|
|
594
|
+
},
|
|
595
|
+
setAuthUrl: (url, nextSwitchAccountUrl) => {
|
|
389
596
|
authUrl = url;
|
|
597
|
+
switchAccountUrl = nextSwitchAccountUrl || url;
|
|
390
598
|
},
|
|
391
599
|
setStats: (s) => {
|
|
392
600
|
stats = s;
|
|
@@ -408,6 +616,42 @@ function authedAxios(token) {
|
|
|
408
616
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
409
617
|
});
|
|
410
618
|
}
|
|
619
|
+
function formatVerificationError(error) {
|
|
620
|
+
if (axios.isAxiosError(error)) {
|
|
621
|
+
const status = typeof error.response?.status === "number" ? error.response.status : null;
|
|
622
|
+
const statusText = error.response?.statusText ? ` ${error.response.statusText}` : "";
|
|
623
|
+
const requestPath = typeof error.config?.url === "string" ? error.config.url : "";
|
|
624
|
+
const endpoint = requestPath
|
|
625
|
+
? requestPath.startsWith("http")
|
|
626
|
+
? requestPath
|
|
627
|
+
: `${API_BASE_URL}${requestPath}`
|
|
628
|
+
: API_BASE_URL;
|
|
629
|
+
const statusLabel = status ? `HTTP ${status}${statusText}` : error.code || error.message;
|
|
630
|
+
const localApi = /^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?/.test(API_BASE_URL);
|
|
631
|
+
const routeHint = status === 404 && requestPath.includes("/api/extension/account/encryption")
|
|
632
|
+
? localApi
|
|
633
|
+
? "That usually means ECHO_API_BASE_URL is pointing at a different local app or an older EchoMem API server."
|
|
634
|
+
: "That usually means the EchoMem API route is missing on the configured server."
|
|
635
|
+
: null;
|
|
636
|
+
const localHint = localApi ? "Start the EchoMem-Chrome Next API on that port, or unset ECHO_API_BASE_URL to use production." : null;
|
|
637
|
+
return [
|
|
638
|
+
`Could not verify this device token against ${endpoint}: ${statusLabel}.`,
|
|
639
|
+
routeHint,
|
|
640
|
+
localHint,
|
|
641
|
+
].filter(Boolean).join(" ");
|
|
642
|
+
}
|
|
643
|
+
return `Could not verify this device token: ${error instanceof Error ? error.message : String(error)}`;
|
|
644
|
+
}
|
|
645
|
+
async function verifyAndPrint(input) {
|
|
646
|
+
try {
|
|
647
|
+
console.log(await verifyAndStore(input));
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
console.error(`❌ ${formatVerificationError(error)}`);
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
411
655
|
/**
|
|
412
656
|
* Verify supplied secrets and persist them. Given a token (required), and EITHER a base64 key or a
|
|
413
657
|
* passphrase (optional — only for encrypted accounts), this verifies the key against the server's
|
|
@@ -415,8 +659,8 @@ function authedAxios(token) {
|
|
|
415
659
|
*/
|
|
416
660
|
export async function verifyAndStore(input) {
|
|
417
661
|
const store = new KeyStore();
|
|
418
|
-
store.saveToken(input.token);
|
|
419
662
|
const config = await fetchEncryptionConfig(authedAxios(input.token));
|
|
663
|
+
store.saveToken(input.token);
|
|
420
664
|
if (!config.enabled) {
|
|
421
665
|
return input.key || input.passphrase
|
|
422
666
|
? "Token saved. (Account is not encrypted — the supplied key was ignored.)"
|
|
@@ -505,23 +749,17 @@ async function cmdSetup(flags) {
|
|
|
505
749
|
}
|
|
506
750
|
console.log("");
|
|
507
751
|
await cmdLogin(flags);
|
|
508
|
-
// Onboarding reveal: show the local usage audit right after connecting (proactive trigger).
|
|
509
|
-
try {
|
|
510
|
-
console.log("\n" + (await buildReportText(true)));
|
|
511
|
-
}
|
|
512
|
-
catch {
|
|
513
|
-
/* report is best-effort — never block setup */
|
|
514
|
-
}
|
|
515
752
|
}
|
|
516
753
|
async function cmdLogin(flags) {
|
|
517
754
|
// Manual path (also the headless path): secrets supplied as flags.
|
|
518
755
|
if (typeof flags.token === "string") {
|
|
519
|
-
const
|
|
756
|
+
const ok = await verifyAndPrint({
|
|
520
757
|
token: flags.token,
|
|
521
758
|
key: typeof flags.key === "string" ? flags.key : undefined,
|
|
522
759
|
passphrase: typeof flags.passphrase === "string" ? flags.passphrase : undefined,
|
|
523
760
|
});
|
|
524
|
-
|
|
761
|
+
if (!ok)
|
|
762
|
+
process.exitCode = 1;
|
|
525
763
|
return;
|
|
526
764
|
}
|
|
527
765
|
// Browser path: open a localhost dashboard. It briefly leaves for hosted auth, then returns here
|
|
@@ -529,13 +767,36 @@ async function cmdLogin(flags) {
|
|
|
529
767
|
console.log("Opening your browser to approve this device…");
|
|
530
768
|
const nonce = randomUUID();
|
|
531
769
|
let stats = null;
|
|
532
|
-
|
|
770
|
+
let forensicReport = null;
|
|
771
|
+
let forensicProgress = { scanned: 0, total: 0 };
|
|
772
|
+
const srv = await startCallbackServer({
|
|
773
|
+
nonce,
|
|
774
|
+
getStats: () => stats,
|
|
775
|
+
getReport: () => forensicReport,
|
|
776
|
+
getReportProgress: () => forensicProgress,
|
|
777
|
+
});
|
|
533
778
|
const callbackUrl = `http://127.0.0.1:${srv.port}/callback`;
|
|
534
779
|
const localSetupUrl = `http://127.0.0.1:${srv.port}/setup?nonce=${nonce}`;
|
|
535
780
|
const connectUrl = `${WEB_URL}/connect-device?callback=${encodeURIComponent(callbackUrl)}&nonce=${nonce}&return_to=${encodeURIComponent(localSetupUrl)}`;
|
|
536
|
-
|
|
781
|
+
const switchAccountUrl = new URL(connectUrl);
|
|
782
|
+
// The hosted connect-device page should clear its own Supabase/browser session before minting
|
|
783
|
+
// the localhost token when this hint is present. Localhost cannot safely clear yeahecho.com auth.
|
|
784
|
+
switchAccountUrl.searchParams.set("force_signout", "1");
|
|
785
|
+
switchAccountUrl.searchParams.set("prompt", "login");
|
|
786
|
+
srv.setAuthUrl(connectUrl, switchAccountUrl.toString());
|
|
537
787
|
openBrowser(localSetupUrl);
|
|
538
788
|
console.log(`If it didn't open, visit:\n ${localSetupUrl}\n`);
|
|
789
|
+
// Scan-first: build the local forensic "Context Doctor" report off-thread so the page shows it
|
|
790
|
+
// BEFORE the user connects an account (the scan is local-only; nothing leaves the machine).
|
|
791
|
+
buildForensicReportOffThread((done, total) => {
|
|
792
|
+
forensicProgress = { scanned: done, total };
|
|
793
|
+
})
|
|
794
|
+
.then((r) => {
|
|
795
|
+
forensicReport = r;
|
|
796
|
+
})
|
|
797
|
+
.catch((e) => {
|
|
798
|
+
console.error(`Could not build the local report: ${e instanceof Error ? e.message : String(e)}`);
|
|
799
|
+
});
|
|
539
800
|
let token;
|
|
540
801
|
let key;
|
|
541
802
|
try {
|
|
@@ -547,61 +808,331 @@ async function cmdLogin(flags) {
|
|
|
547
808
|
process.exitCode = 1;
|
|
548
809
|
return;
|
|
549
810
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
811
|
+
if (!await verifyAndPrint({ token, key })) {
|
|
812
|
+
srv.close();
|
|
813
|
+
process.exitCode = 1;
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
let disc = null;
|
|
817
|
+
let exactDiscovery = Promise.resolve(null);
|
|
818
|
+
let refreshGeneration = 0;
|
|
819
|
+
let latestPendingEstimate = 0;
|
|
820
|
+
const resetLocalLoginState = () => {
|
|
821
|
+
refreshGeneration++;
|
|
822
|
+
stats = null;
|
|
823
|
+
disc = null;
|
|
824
|
+
exactDiscovery = Promise.resolve(null);
|
|
825
|
+
latestPendingEstimate = 0;
|
|
826
|
+
srv.setStats(null);
|
|
827
|
+
srv.setProgress({
|
|
828
|
+
status: "idle",
|
|
829
|
+
total: 0,
|
|
830
|
+
completed: 0,
|
|
831
|
+
running: 0,
|
|
832
|
+
queued: 0,
|
|
833
|
+
failed: 0,
|
|
834
|
+
extracted: 0,
|
|
835
|
+
});
|
|
568
836
|
};
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
837
|
+
const refreshLocalStatsForToken = async (activeToken) => {
|
|
838
|
+
const generation = ++refreshGeneration;
|
|
839
|
+
let lastProcessedImportKeys = null;
|
|
840
|
+
let importStatusUnavailable = false;
|
|
841
|
+
const quickDiscovery = discoverMigratableFastDiscovery();
|
|
842
|
+
const quick = summarizeFastMigratableDiscovery(quickDiscovery);
|
|
843
|
+
let migratable = migratableFromFastSummary(quick);
|
|
844
|
+
latestPendingEstimate = migratable.pending;
|
|
845
|
+
let sessionSummary = {
|
|
846
|
+
total: quick.sessions,
|
|
847
|
+
codex: quick.codexCount,
|
|
848
|
+
claudeCode: quick.claudeCount,
|
|
849
|
+
};
|
|
850
|
+
stats = await buildStatsPayload([], {
|
|
851
|
+
partial: true,
|
|
852
|
+
skipMemoryCount: true,
|
|
578
853
|
sessions: sessionSummary,
|
|
579
854
|
migratable,
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
855
|
+
discovery: { phase: "quick", exact: false },
|
|
856
|
+
});
|
|
857
|
+
if (generation !== refreshGeneration)
|
|
858
|
+
return;
|
|
859
|
+
srv.setStats(stats);
|
|
860
|
+
srv.setProgress({
|
|
861
|
+
status: "idle",
|
|
862
|
+
total: quick.pending,
|
|
863
|
+
completed: 0,
|
|
864
|
+
running: 0,
|
|
865
|
+
queued: quick.pending,
|
|
866
|
+
failed: 0,
|
|
867
|
+
extracted: 0,
|
|
868
|
+
});
|
|
869
|
+
const fastAccountController = new AbortController();
|
|
870
|
+
const fastAccountCheck = withTimeout(fetchProcessedImportKeys(activeToken, quickDiscovery.sessions, fastAccountController.signal), 8_000, "ACCOUNT_STATUS_TIMEOUT", () => fastAccountController.abort()).then(async (processedKeys) => {
|
|
871
|
+
if (generation !== refreshGeneration)
|
|
872
|
+
return;
|
|
873
|
+
lastProcessedImportKeys = processedKeys;
|
|
874
|
+
const cloudQuick = applyFastAccountImportStatus(quickDiscovery, processedKeys);
|
|
875
|
+
const currentPhase = stats?.discovery?.phase;
|
|
876
|
+
if (currentPhase !== "quick")
|
|
877
|
+
return;
|
|
878
|
+
const cloudSummary = summarizeFastMigratableDiscovery(cloudQuick);
|
|
879
|
+
migratable = migratableFromFastSummary(cloudSummary);
|
|
880
|
+
latestPendingEstimate = migratable.pending;
|
|
881
|
+
sessionSummary = {
|
|
882
|
+
total: cloudSummary.sessions,
|
|
883
|
+
codex: cloudSummary.codexCount,
|
|
884
|
+
claudeCode: cloudSummary.claudeCount,
|
|
885
|
+
};
|
|
886
|
+
const cloudPayload = await buildStatsPayload([], {
|
|
887
|
+
partial: true,
|
|
888
|
+
skipMemoryCount: true,
|
|
889
|
+
sessions: sessionSummary,
|
|
890
|
+
migratable,
|
|
891
|
+
discovery: { phase: "account", exact: false },
|
|
892
|
+
});
|
|
893
|
+
if (generation !== refreshGeneration)
|
|
894
|
+
return;
|
|
895
|
+
stats = cloudPayload;
|
|
896
|
+
srv.setStats(cloudPayload);
|
|
897
|
+
srv.setProgress({
|
|
898
|
+
status: "idle",
|
|
899
|
+
total: cloudSummary.pending,
|
|
900
|
+
completed: 0,
|
|
901
|
+
running: 0,
|
|
902
|
+
queued: cloudSummary.pending,
|
|
903
|
+
failed: 0,
|
|
904
|
+
extracted: 0,
|
|
905
|
+
});
|
|
906
|
+
}).catch(async (e) => {
|
|
907
|
+
if (generation !== refreshGeneration)
|
|
908
|
+
return;
|
|
909
|
+
if (isImportStatusUnsupported(e)) {
|
|
910
|
+
importStatusUnavailable = true;
|
|
911
|
+
const unavailableQuick = markFastAccountImportStatusUnavailable(quickDiscovery);
|
|
912
|
+
const currentPhase = stats?.discovery?.phase;
|
|
913
|
+
if (currentPhase !== "quick")
|
|
914
|
+
return;
|
|
915
|
+
const unavailableSummary = summarizeFastMigratableDiscovery(unavailableQuick);
|
|
916
|
+
migratable = migratableFromFastSummary(unavailableSummary);
|
|
917
|
+
latestPendingEstimate = migratable.pending;
|
|
918
|
+
sessionSummary = {
|
|
919
|
+
total: unavailableSummary.sessions,
|
|
920
|
+
codex: unavailableSummary.codexCount,
|
|
921
|
+
claudeCode: unavailableSummary.claudeCount,
|
|
922
|
+
};
|
|
923
|
+
const unavailablePayload = await buildStatsPayload([], {
|
|
924
|
+
partial: true,
|
|
925
|
+
skipMemoryCount: true,
|
|
926
|
+
sessions: sessionSummary,
|
|
927
|
+
migratable,
|
|
928
|
+
discovery: { phase: "account", exact: false },
|
|
929
|
+
});
|
|
930
|
+
if (generation !== refreshGeneration)
|
|
931
|
+
return;
|
|
932
|
+
stats = unavailablePayload;
|
|
933
|
+
srv.setStats(unavailablePayload);
|
|
934
|
+
srv.setProgress({
|
|
935
|
+
status: "idle",
|
|
936
|
+
total: unavailableSummary.pending,
|
|
937
|
+
completed: 0,
|
|
938
|
+
running: 0,
|
|
939
|
+
queued: unavailableSummary.pending,
|
|
940
|
+
failed: 0,
|
|
941
|
+
extracted: 0,
|
|
942
|
+
});
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (generation === refreshGeneration) {
|
|
946
|
+
console.error(`Could not check this EchoMem account's import status quickly: ${e instanceof Error ? e.message : String(e)}`);
|
|
947
|
+
}
|
|
585
948
|
});
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
949
|
+
exactDiscovery = new Promise((resolve, reject) => {
|
|
950
|
+
const timer = setTimeout(() => {
|
|
951
|
+
void (async () => {
|
|
952
|
+
try {
|
|
953
|
+
// Start the local sizing pass without waiting on cloud/account status. The
|
|
954
|
+
// account check is useful for tighter counts, but extraction can safely start
|
|
955
|
+
// from local candidates because the import path skips true duplicates.
|
|
956
|
+
await delay(250);
|
|
957
|
+
resolve(await discoverMigratableSessionsOffThread());
|
|
958
|
+
}
|
|
959
|
+
catch (e) {
|
|
960
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
961
|
+
}
|
|
962
|
+
})();
|
|
963
|
+
}, 250);
|
|
964
|
+
timer.unref?.();
|
|
965
|
+
}).then(async (exact) => {
|
|
966
|
+
if (generation !== refreshGeneration)
|
|
967
|
+
return disc;
|
|
968
|
+
const initialExact = lastProcessedImportKeys
|
|
969
|
+
? applyAccountImportStatus(exact, lastProcessedImportKeys)
|
|
970
|
+
: importStatusUnavailable
|
|
971
|
+
? markAccountImportStatusUnavailable(exact)
|
|
972
|
+
: exact;
|
|
973
|
+
disc = initialExact;
|
|
974
|
+
migratable = migratableFromDiscovery(initialExact);
|
|
975
|
+
latestPendingEstimate = migratable.pending;
|
|
976
|
+
sessionSummary = sessionsFromDiscovery(initialExact);
|
|
977
|
+
const partialPayload = await buildStatsPayload([], {
|
|
978
|
+
partial: true,
|
|
979
|
+
skipMemoryCount: true,
|
|
980
|
+
sessions: sessionSummary,
|
|
981
|
+
migratable,
|
|
982
|
+
discovery: { phase: "exact", exact: true },
|
|
983
|
+
});
|
|
984
|
+
if (generation !== refreshGeneration)
|
|
985
|
+
return disc;
|
|
986
|
+
stats = partialPayload;
|
|
987
|
+
srv.setStats(partialPayload);
|
|
988
|
+
srv.setProgress({
|
|
989
|
+
status: "idle",
|
|
990
|
+
total: initialExact.pending.length,
|
|
991
|
+
completed: 0,
|
|
992
|
+
running: 0,
|
|
993
|
+
queued: initialExact.pending.length,
|
|
994
|
+
failed: 0,
|
|
995
|
+
extracted: 0,
|
|
996
|
+
});
|
|
997
|
+
void (async () => {
|
|
998
|
+
let reconciled = initialExact;
|
|
999
|
+
try {
|
|
1000
|
+
await fastAccountCheck;
|
|
1001
|
+
const controller = new AbortController();
|
|
1002
|
+
const processedKeys = await withTimeout(fetchProcessedImportKeys(activeToken, exact.sessions, controller.signal), 15_000, "ACCOUNT_STATUS_TIMEOUT", () => controller.abort());
|
|
1003
|
+
lastProcessedImportKeys = processedKeys;
|
|
1004
|
+
reconciled = applyAccountImportStatus(exact, processedKeys);
|
|
1005
|
+
}
|
|
1006
|
+
catch (e) {
|
|
1007
|
+
if (lastProcessedImportKeys) {
|
|
1008
|
+
reconciled = applyAccountImportStatus(exact, lastProcessedImportKeys);
|
|
1009
|
+
if (!isImportStatusUnsupported(e)) {
|
|
1010
|
+
console.error(`Could not refresh this EchoMem account's exact import status; keeping the last successful account check. ${e instanceof Error ? e.message : String(e)}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
else if (isImportStatusUnsupported(e)) {
|
|
1014
|
+
reconciled = markAccountImportStatusUnavailable(exact);
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
reconciled = markAccountImportStatusFailed(exact);
|
|
1018
|
+
console.error(`Could not check this EchoMem account's import status: ${e instanceof Error ? e.message : String(e)}`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (generation !== refreshGeneration)
|
|
1022
|
+
return;
|
|
1023
|
+
disc = reconciled;
|
|
1024
|
+
migratable = migratableFromDiscovery(reconciled);
|
|
1025
|
+
latestPendingEstimate = migratable.pending;
|
|
1026
|
+
sessionSummary = sessionsFromDiscovery(reconciled);
|
|
1027
|
+
const reconciledPayload = await buildStatsPayload([], {
|
|
1028
|
+
partial: true,
|
|
1029
|
+
skipMemoryCount: true,
|
|
1030
|
+
sessions: sessionSummary,
|
|
1031
|
+
migratable,
|
|
1032
|
+
discovery: { phase: "exact", exact: true },
|
|
1033
|
+
});
|
|
1034
|
+
if (generation !== refreshGeneration)
|
|
1035
|
+
return;
|
|
1036
|
+
stats = reconciledPayload;
|
|
1037
|
+
srv.setStats(reconciledPayload);
|
|
1038
|
+
srv.setProgress({
|
|
1039
|
+
status: "idle",
|
|
1040
|
+
total: reconciled.pending.length,
|
|
1041
|
+
completed: 0,
|
|
1042
|
+
running: 0,
|
|
1043
|
+
queued: reconciled.pending.length,
|
|
1044
|
+
failed: 0,
|
|
1045
|
+
extracted: 0,
|
|
1046
|
+
});
|
|
1047
|
+
const fullPayload = await buildStatsPayload(collect(), {
|
|
1048
|
+
sessions: sessionSummary,
|
|
1049
|
+
migratable,
|
|
1050
|
+
discovery: { phase: "full", exact: true },
|
|
1051
|
+
});
|
|
1052
|
+
if (generation !== refreshGeneration)
|
|
1053
|
+
return;
|
|
1054
|
+
stats = fullPayload;
|
|
1055
|
+
srv.setStats(fullPayload);
|
|
1056
|
+
})();
|
|
1057
|
+
return initialExact;
|
|
1058
|
+
}).catch((e) => {
|
|
1059
|
+
if (generation === refreshGeneration) {
|
|
1060
|
+
console.error(`Could not finish exact local extraction estimate: ${e instanceof Error ? e.message : String(e)}`);
|
|
1061
|
+
}
|
|
1062
|
+
return disc;
|
|
1063
|
+
});
|
|
1064
|
+
};
|
|
1065
|
+
srv.setLogoutHandler(resetLocalLoginState);
|
|
1066
|
+
srv.setTokenRefreshHandler(async ({ token: nextToken, key: nextKey }) => {
|
|
1067
|
+
if (!await verifyAndPrint({ token: nextToken, key: nextKey }))
|
|
1068
|
+
return;
|
|
1069
|
+
await refreshLocalStatsForToken(nextToken);
|
|
596
1070
|
});
|
|
1071
|
+
await refreshLocalStatsForToken(token);
|
|
597
1072
|
const choice = await srv.decision;
|
|
598
1073
|
if (choice === "migrate") {
|
|
599
1074
|
const { res } = await srv.migrateRequest;
|
|
1075
|
+
let migrateResponded = false;
|
|
1076
|
+
const sendMigrate = (body, status = 200) => {
|
|
1077
|
+
if (migrateResponded)
|
|
1078
|
+
return;
|
|
1079
|
+
migrateResponded = true;
|
|
1080
|
+
respondMigrate(res, body, status);
|
|
1081
|
+
};
|
|
600
1082
|
let activeSessionId = "";
|
|
601
|
-
let activeJobCount =
|
|
1083
|
+
let activeJobCount = latestPendingEstimate;
|
|
602
1084
|
let progressDone = 0;
|
|
603
1085
|
let progressFailed = 0;
|
|
604
1086
|
let progressExtracted = 0;
|
|
1087
|
+
if (!disc) {
|
|
1088
|
+
srv.setProgress({
|
|
1089
|
+
status: "starting",
|
|
1090
|
+
total: activeJobCount,
|
|
1091
|
+
completed: 0,
|
|
1092
|
+
running: 0,
|
|
1093
|
+
queued: activeJobCount,
|
|
1094
|
+
failed: 0,
|
|
1095
|
+
extracted: 0,
|
|
1096
|
+
latest: "Finishing local job sizing before import starts.",
|
|
1097
|
+
});
|
|
1098
|
+
sendMigrate({
|
|
1099
|
+
status: "preparing",
|
|
1100
|
+
jobCount: activeJobCount,
|
|
1101
|
+
message: "Finishing local job sizing before import starts.",
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
let exact = disc;
|
|
1105
|
+
if (!exact) {
|
|
1106
|
+
// Off-thread sizing normally resolves in ~20s; cap the wait and fall back to an in-process pass so
|
|
1107
|
+
// a stalled/contended worker can never leave extraction stuck at "finishing local job sizing".
|
|
1108
|
+
exact = await withTimeout(exactDiscovery, 40_000, "SIZING_TIMEOUT").catch(() => null);
|
|
1109
|
+
if (!exact) {
|
|
1110
|
+
srv.setProgress({ status: "starting", total: activeJobCount, completed: 0, running: 0, queued: activeJobCount, failed: 0, extracted: 0, latest: "Sizing your sessions…" });
|
|
1111
|
+
try {
|
|
1112
|
+
exact = discoverMigratableSessions();
|
|
1113
|
+
}
|
|
1114
|
+
catch (e) {
|
|
1115
|
+
console.error(`Direct local sizing failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (!exact) {
|
|
1120
|
+
srv.setProgress({
|
|
1121
|
+
status: "failed",
|
|
1122
|
+
total: activeJobCount,
|
|
1123
|
+
completed: progressDone,
|
|
1124
|
+
running: 0,
|
|
1125
|
+
queued: Math.max(0, activeJobCount - progressDone - progressFailed),
|
|
1126
|
+
failed: progressFailed || 1,
|
|
1127
|
+
extracted: progressExtracted,
|
|
1128
|
+
error: "Local session discovery did not finish.",
|
|
1129
|
+
});
|
|
1130
|
+
sendMigrate({ error: "IMPORT_START_FAILED", message: "Local session discovery did not finish." }, 500);
|
|
1131
|
+
srv.close();
|
|
1132
|
+
process.exitCode = 1;
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
activeJobCount = exact.pending.length;
|
|
605
1136
|
const updateProgress = (patch) => {
|
|
606
1137
|
srv.setProgress({
|
|
607
1138
|
status: "running",
|
|
@@ -609,15 +1140,15 @@ async function cmdLogin(flags) {
|
|
|
609
1140
|
jobCount: activeJobCount,
|
|
610
1141
|
total: activeJobCount,
|
|
611
1142
|
completed: progressDone,
|
|
612
|
-
running:
|
|
613
|
-
queued: Math.max(0, activeJobCount - progressDone - progressFailed -
|
|
1143
|
+
running: Math.min(MIGRATE_CONCURRENCY, Math.max(0, activeJobCount - progressDone - progressFailed)),
|
|
1144
|
+
queued: Math.max(0, activeJobCount - progressDone - progressFailed - MIGRATE_CONCURRENCY),
|
|
614
1145
|
failed: progressFailed,
|
|
615
1146
|
extracted: progressExtracted,
|
|
616
1147
|
...patch,
|
|
617
1148
|
});
|
|
618
1149
|
};
|
|
619
1150
|
try {
|
|
620
|
-
if (
|
|
1151
|
+
if (exact.pending.length === 0) {
|
|
621
1152
|
srv.setProgress({
|
|
622
1153
|
status: "completed",
|
|
623
1154
|
total: 0,
|
|
@@ -628,15 +1159,15 @@ async function cmdLogin(flags) {
|
|
|
628
1159
|
extracted: 0,
|
|
629
1160
|
latest: "No unprocessed local conversations found.",
|
|
630
1161
|
});
|
|
631
|
-
|
|
1162
|
+
sendMigrate({ error: "NO_PENDING_SESSIONS" }, 409);
|
|
632
1163
|
srv.close();
|
|
633
1164
|
console.log("Setup complete — no unprocessed local conversations to extract.");
|
|
634
1165
|
return;
|
|
635
1166
|
}
|
|
636
|
-
updateProgress({ status: "starting", running: 0, queued:
|
|
1167
|
+
updateProgress({ status: "starting", running: 0, queued: exact.pending.length, latest: "Creating import session." });
|
|
637
1168
|
const controller = new AbortController();
|
|
638
1169
|
const h = await withTimeout(startMigration({
|
|
639
|
-
pending:
|
|
1170
|
+
pending: exact.pending,
|
|
640
1171
|
signal: controller.signal,
|
|
641
1172
|
onProgress: (ev) => {
|
|
642
1173
|
if (ev.error) {
|
|
@@ -654,7 +1185,7 @@ async function cmdLogin(flags) {
|
|
|
654
1185
|
activeSessionId = h.sessionId;
|
|
655
1186
|
activeJobCount = h.jobCount;
|
|
656
1187
|
updateProgress({ status: "running", sessionId: h.sessionId, jobCount: h.jobCount, total: h.jobCount, capped: h.capped, latest: "Import session created." });
|
|
657
|
-
|
|
1188
|
+
sendMigrate({ sessionId: h.sessionId, jobCount: h.jobCount, ...(h.capped ? { capped: h.capped } : {}) });
|
|
658
1189
|
console.log("Migrating your history… keep this terminal open until it completes.");
|
|
659
1190
|
console.log(`Migration metrics: ${h.metricsFile}`);
|
|
660
1191
|
const r = await h.done;
|
|
@@ -692,17 +1223,17 @@ async function cmdLogin(flags) {
|
|
|
692
1223
|
error: String(e?.message || e),
|
|
693
1224
|
});
|
|
694
1225
|
if (e?.code === "NOT_LOGGED_IN")
|
|
695
|
-
|
|
1226
|
+
sendMigrate({ error: "NOT_LOGGED_IN" }, 401);
|
|
696
1227
|
else if (e?.code === "FORBIDDEN_SCOPE")
|
|
697
|
-
|
|
1228
|
+
sendMigrate({ error: "FORBIDDEN_SCOPE" }, 403);
|
|
698
1229
|
else if (e?.code === "VAULT_LOCKED")
|
|
699
|
-
|
|
1230
|
+
sendMigrate({ error: "VAULT_LOCKED" }, 409);
|
|
700
1231
|
else if (e?.code === "NO_PENDING_SESSIONS")
|
|
701
|
-
|
|
1232
|
+
sendMigrate({ error: "NO_PENDING_SESSIONS" }, 409);
|
|
702
1233
|
else if (e?.code === "IMPORT_START_TIMEOUT")
|
|
703
|
-
|
|
1234
|
+
sendMigrate({ error: "IMPORT_START_TIMEOUT" }, 504);
|
|
704
1235
|
else
|
|
705
|
-
|
|
1236
|
+
sendMigrate({ error: "IMPORT_START_FAILED", message: String(e?.message || e) }, 500);
|
|
706
1237
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
707
1238
|
srv.close();
|
|
708
1239
|
process.exitCode = 1;
|