@echomem/mcp 1.2.0 → 1.3.1

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.
@@ -0,0 +1,574 @@
1
+ export function renderSetupPage() {
2
+ return `<!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>EchoMem Device Setup</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: light;
11
+ --bg: #f7f7f4;
12
+ --ink: #171717;
13
+ --muted: #5c655f;
14
+ --line: #d8d8d2;
15
+ --panel: #fff;
16
+ --good-bg: #eff7ec;
17
+ --good-line: #cbd9c8;
18
+ --warn-bg: #fff6dc;
19
+ --warn-line: #edce79;
20
+ --error-bg: #fff1ef;
21
+ --error-line: #e7aaa2;
22
+ }
23
+ * { box-sizing: border-box; }
24
+ body {
25
+ margin: 0;
26
+ min-height: 100vh;
27
+ background: var(--bg);
28
+ color: var(--ink);
29
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
30
+ letter-spacing: 0;
31
+ }
32
+ main {
33
+ width: min(1080px, 100%);
34
+ margin: 0 auto;
35
+ padding: clamp(18px, 4vw, 34px);
36
+ }
37
+ header {
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: space-between;
41
+ gap: 16px;
42
+ flex-wrap: wrap;
43
+ border-bottom: 1px solid var(--line);
44
+ padding-bottom: 18px;
45
+ margin-bottom: 24px;
46
+ }
47
+ .kicker {
48
+ margin: 0;
49
+ color: var(--muted);
50
+ font-size: 13px;
51
+ font-weight: 800;
52
+ text-transform: uppercase;
53
+ }
54
+ h1 {
55
+ margin: 4px 0 0;
56
+ font-size: clamp(34px, 5vw, 62px);
57
+ line-height: 1;
58
+ letter-spacing: 0;
59
+ }
60
+ h2 {
61
+ margin: 0;
62
+ font-size: 22px;
63
+ line-height: 1.2;
64
+ letter-spacing: 0;
65
+ }
66
+ .pill {
67
+ min-height: 38px;
68
+ display: inline-flex;
69
+ align-items: center;
70
+ gap: 8px;
71
+ border: 1px solid #c9ccc3;
72
+ border-radius: 999px;
73
+ padding: 0 14px;
74
+ background: var(--panel);
75
+ font-weight: 800;
76
+ white-space: nowrap;
77
+ }
78
+ .state {
79
+ min-height: 220px;
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ color: #3d463f;
84
+ font-size: 18px;
85
+ text-align: center;
86
+ }
87
+ .grid {
88
+ display: grid;
89
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
90
+ gap: 12px;
91
+ margin-bottom: 18px;
92
+ }
93
+ .grid.compact {
94
+ grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
95
+ }
96
+ .metric, .panel {
97
+ border: 1px solid var(--line);
98
+ border-radius: 8px;
99
+ background: var(--panel);
100
+ padding: 18px;
101
+ }
102
+ .metric {
103
+ min-height: 132px;
104
+ display: flex;
105
+ flex-direction: column;
106
+ justify-content: space-between;
107
+ }
108
+ .label {
109
+ margin: 0;
110
+ color: #626960;
111
+ font-size: 13px;
112
+ font-weight: 800;
113
+ text-transform: uppercase;
114
+ }
115
+ .value {
116
+ display: block;
117
+ font-size: 36px;
118
+ line-height: 1;
119
+ letter-spacing: 0;
120
+ }
121
+ .detail, p {
122
+ margin: 0;
123
+ color: #4e554f;
124
+ font-size: 15px;
125
+ line-height: 1.45;
126
+ }
127
+ .extract-hero {
128
+ display: grid;
129
+ grid-template-columns: minmax(0, 1.4fr) minmax(220px, 0.6fr);
130
+ gap: 18px;
131
+ align-items: end;
132
+ margin-bottom: 12px;
133
+ }
134
+ .hero-value {
135
+ display: block;
136
+ margin-top: 10px;
137
+ font-size: clamp(42px, 7vw, 84px);
138
+ line-height: 0.95;
139
+ letter-spacing: 0;
140
+ }
141
+ .hero-copy {
142
+ max-width: 640px;
143
+ margin-top: 12px;
144
+ }
145
+ .mini-stack {
146
+ display: grid;
147
+ gap: 8px;
148
+ }
149
+ .mini-row {
150
+ display: flex;
151
+ justify-content: space-between;
152
+ gap: 12px;
153
+ border-bottom: 1px solid #ecece7;
154
+ padding-bottom: 8px;
155
+ }
156
+ .bucket-row {
157
+ align-items: center;
158
+ display: grid;
159
+ grid-template-columns: minmax(90px, 1fr) minmax(100px, auto);
160
+ gap: 12px;
161
+ }
162
+ .bucket-value {
163
+ text-align: right;
164
+ font-weight: 800;
165
+ white-space: nowrap;
166
+ }
167
+ .helper {
168
+ margin-top: 10px;
169
+ color: var(--muted);
170
+ font-size: 14px;
171
+ }
172
+ .notice, .done, .warning, .error {
173
+ border-radius: 8px;
174
+ padding: 14px 16px;
175
+ margin-bottom: 18px;
176
+ }
177
+ .notice, .done {
178
+ border: 1px solid var(--good-line);
179
+ background: var(--good-bg);
180
+ color: #203820;
181
+ }
182
+ .warning {
183
+ border: 1px solid var(--warn-line);
184
+ background: var(--warn-bg);
185
+ color: #5e4300;
186
+ }
187
+ .error {
188
+ border: 1px solid var(--error-line);
189
+ background: var(--error-bg);
190
+ color: #7d2117;
191
+ }
192
+ .details {
193
+ display: grid;
194
+ grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr));
195
+ gap: 12px;
196
+ margin-bottom: 18px;
197
+ }
198
+ dl {
199
+ margin: 14px 0 0;
200
+ display: grid;
201
+ gap: 10px;
202
+ }
203
+ .row {
204
+ display: flex;
205
+ justify-content: space-between;
206
+ gap: 12px;
207
+ border-bottom: 1px solid #ecece7;
208
+ padding-bottom: 8px;
209
+ }
210
+ dt { color: #222; }
211
+ dd { margin: 0; text-align: right; color: #222; }
212
+ ol {
213
+ list-style: none;
214
+ margin: 14px 0 0;
215
+ padding: 0;
216
+ display: grid;
217
+ gap: 10px;
218
+ }
219
+ li {
220
+ display: flex;
221
+ justify-content: space-between;
222
+ gap: 10px;
223
+ }
224
+ .actions {
225
+ display: flex;
226
+ gap: 10px;
227
+ flex-wrap: wrap;
228
+ margin-top: 18px;
229
+ }
230
+ button, .button {
231
+ min-height: 44px;
232
+ display: inline-flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ gap: 8px;
236
+ border-radius: 8px;
237
+ border: 1px solid #171717;
238
+ padding: 0 16px;
239
+ font: inherit;
240
+ font-weight: 800;
241
+ text-decoration: none;
242
+ cursor: pointer;
243
+ }
244
+ .primary {
245
+ background: #171717;
246
+ color: #fff;
247
+ }
248
+ .secondary {
249
+ background: #fff;
250
+ color: #171717;
251
+ }
252
+ button:disabled {
253
+ cursor: not-allowed;
254
+ opacity: 0.55;
255
+ }
256
+ .track {
257
+ height: 12px;
258
+ overflow: hidden;
259
+ border-radius: 999px;
260
+ background: #e8e8e1;
261
+ margin: 16px 0 10px;
262
+ }
263
+ .fill {
264
+ height: 100%;
265
+ width: 0%;
266
+ background: #171717;
267
+ transition: width 180ms ease;
268
+ }
269
+ .counts {
270
+ display: flex;
271
+ gap: 12px;
272
+ flex-wrap: wrap;
273
+ color: var(--muted);
274
+ font-size: 14px;
275
+ margin-bottom: 14px;
276
+ }
277
+ .hidden { display: none; }
278
+ @media (max-width: 720px) {
279
+ .extract-hero {
280
+ grid-template-columns: 1fr;
281
+ }
282
+ .hero-value {
283
+ font-size: clamp(38px, 13vw, 58px);
284
+ }
285
+ }
286
+ </style>
287
+ </head>
288
+ <body>
289
+ <main>
290
+ <header>
291
+ <div>
292
+ <p class="kicker">EchoMem device setup</p>
293
+ <h1 id="title">Connect this device</h1>
294
+ </div>
295
+ <div class="pill" id="status">Working</div>
296
+ </header>
297
+ <section id="app" class="state">Starting local setup...</section>
298
+ </main>
299
+ <script>
300
+ (function () {
301
+ var params = new URLSearchParams(location.search);
302
+ var nonce = params.get("nonce") || "";
303
+ var app = document.getElementById("app");
304
+ var title = document.getElementById("title");
305
+ var status = document.getElementById("status");
306
+ var stats = null;
307
+ var statsSlow = false;
308
+ var decisionMade = false;
309
+ var authUrl = "";
310
+
311
+ function esc(value) {
312
+ return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
313
+ return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
314
+ });
315
+ }
316
+ function setHead(nextTitle, nextStatus) {
317
+ title.textContent = nextTitle;
318
+ status.textContent = nextStatus;
319
+ }
320
+ function number(value) {
321
+ return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "-";
322
+ }
323
+ function compact(value) {
324
+ if (typeof value !== "number" || !isFinite(value)) return "-";
325
+ var abs = Math.abs(value);
326
+ if (abs >= 1000000000) return (value / 1000000000).toFixed(1) + "B";
327
+ if (abs >= 1000000) return (value / 1000000).toFixed(1) + "M";
328
+ if (abs >= 1000) return (value / 1000).toFixed(1) + "K";
329
+ return String(Math.round(value));
330
+ }
331
+ function hourLabel(hour) {
332
+ if (typeof hour !== "number" || hour < 0) return "-";
333
+ var h = hour % 24;
334
+ var suffix = h >= 12 ? "PM" : "AM";
335
+ var display = h % 12 || 12;
336
+ return display + " " + suffix;
337
+ }
338
+ function sleep(ms) {
339
+ return new Promise(function (resolve) { setTimeout(resolve, ms); });
340
+ }
341
+ async function getJson(path) {
342
+ var sep = path.indexOf("?") >= 0 ? "&" : "?";
343
+ var res = await fetch(path + sep + "nonce=" + encodeURIComponent(nonce), { credentials: "omit" });
344
+ if (!res.ok) throw new Error(await res.text() || ("HTTP " + res.status));
345
+ return await res.json();
346
+ }
347
+ async function postJson(path, body) {
348
+ var res = await fetch(path, {
349
+ method: "POST",
350
+ credentials: "omit",
351
+ headers: { "Content-Type": "application/json" },
352
+ body: JSON.stringify(Object.assign({ nonce: nonce }, body || {}))
353
+ });
354
+ var data = await res.json().catch(function () { return {}; });
355
+ if (!res.ok) {
356
+ var msg = data.message || data.error || ("HTTP " + res.status);
357
+ throw new Error(msg);
358
+ }
359
+ return data;
360
+ }
361
+ function metric(label, value, detail) {
362
+ return '<section class="metric"><p class="label">' + esc(label) + '</p><strong class="value">' + esc(value) + '</strong><p class="detail">' + esc(detail) + '</p></section>';
363
+ }
364
+ function row(label, value) {
365
+ return '<div class="row"><dt>' + esc(label) + '</dt><dd>' + esc(value) + '</dd></div>';
366
+ }
367
+ function migrationBuckets(migratable) {
368
+ var eta = migratable && migratable.eta ? migratable.eta : {};
369
+ var buckets = Array.isArray(migratable && migratable.buckets)
370
+ ? migratable.buckets
371
+ : (Array.isArray(eta.buckets) ? eta.buckets : []);
372
+ return buckets;
373
+ }
374
+ function bucketCount(buckets, key) {
375
+ var found = buckets.filter(function (b) { return b && b.key === key; })[0];
376
+ return found && typeof found.count === "number" ? found.count : 0;
377
+ }
378
+ function bucketList(buckets) {
379
+ if (!buckets.length) return '<p style="margin-top:10px">No pending extraction buckets.</p>';
380
+ var visible = buckets.filter(function (b) { return b && (b.count || b.chars); });
381
+ if (!visible.length) return '<p style="margin-top:10px">No pending extraction buckets.</p>';
382
+ return '<ol>' + visible.map(function (b) {
383
+ return '<li class="bucket-row"><span>' + esc(b.label) + '</span><span class="bucket-value">' + esc(number(b.count)) + ' · ' + esc(compact(b.chars)) + '</span></li>';
384
+ }).join("") + '</ol>';
385
+ }
386
+ function routeError(code) {
387
+ if (code === "NO_PENDING_SESSIONS") return "There are no unprocessed local conversations to extract right now.";
388
+ if (code === "FORBIDDEN_SCOPE") return "This device token cannot import history. Re-connect this device to mint an import-capable token.";
389
+ if (code === "VAULT_LOCKED") return "Your local vault is locked. Run echomem-mcp unlock, then retry.";
390
+ if (code === "IMPORT_START_TIMEOUT") return "Starting the import timed out. Retry when the connection settles.";
391
+ if (code === "NOT_LOGGED_IN") return "The local bridge is not logged in yet. Sign in again and retry.";
392
+ return code || "Something went wrong. Retry in a moment.";
393
+ }
394
+ function showConnect() {
395
+ setHead("Connect this device", "Local");
396
+ app.className = "panel";
397
+ app.innerHTML =
398
+ '<h2>Local setup is ready</h2>' +
399
+ '<p style="margin-top:10px">This page is served from 127.0.0.1. EchoMem will only use the web to sign in and mint a device token, then it returns here.</p>' +
400
+ '<div class="actions"><a class="button primary" href="' + esc(authUrl) + '">Connect EchoMem</a></div>';
401
+ }
402
+ async function waitForStats() {
403
+ setHead("Extract local history", "Working");
404
+ app.className = "state";
405
+ app.textContent = "Reading local extraction info from this device...";
406
+ var started = Date.now();
407
+ for (;;) {
408
+ if (decisionMade) return;
409
+ try {
410
+ var res = await fetch("/stats?nonce=" + encodeURIComponent(nonce), { credentials: "omit" });
411
+ if (res.status === 200) {
412
+ stats = await res.json();
413
+ if (decisionMade) return;
414
+ renderDashboard();
415
+ if (stats && stats.partial) {
416
+ await sleep(1200);
417
+ continue;
418
+ }
419
+ return;
420
+ }
421
+ if (res.status !== 202) throw new Error(await res.text() || ("HTTP " + res.status));
422
+ } catch (e) {
423
+ if (Date.now() - started > 20000) {
424
+ statsSlow = true;
425
+ renderDashboard();
426
+ }
427
+ }
428
+ if (Date.now() - started > 20000 && !statsSlow) {
429
+ statsSlow = true;
430
+ renderDashboard();
431
+ }
432
+ await sleep(statsSlow ? 1500 : 500);
433
+ }
434
+ }
435
+ function renderDashboard(error) {
436
+ var migratable = stats && stats.migratable ? stats.migratable : {};
437
+ var pending = stats && stats.migratable ? stats.migratable.pending : null;
438
+ var hasPendingCount = typeof pending === "number";
439
+ var knownDone = hasPendingCount && pending === 0;
440
+ setHead(knownDone ? "History is ready" : "Extract local history", "Ready");
441
+ var eta = migratable && migratable.eta ? migratable.eta : {};
442
+ var buckets = migrationBuckets(migratable);
443
+ var over1m = bucketCount(buckets, "huge") + bucketCount(buckets, "massive");
444
+ var processed = stats && stats.migratable ? stats.migratable.alreadyMigrated : null;
445
+ var skippedActive = typeof migratable.skippedActive === "number" ? migratable.skippedActive : 0;
446
+ var etaLabel = eta.estimatedLabel || migratable.estimatedLabel || (knownDone ? "complete" : "calculating");
447
+ var sessions = stats && stats.sessions ? stats.sessions : {};
448
+ var notice = pending == null
449
+ ? "The bridge can check for unprocessed conversations and extract them now."
450
+ : (knownDone && skippedActive
451
+ ? "Historical conversations are processed. " + skippedActive.toLocaleString() + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing and will be picked up later."
452
+ : (pending.toLocaleString() + " unprocessed conversation" + (pending === 1 ? "" : "s") + " can be extracted now. Already processed conversations are skipped."));
453
+ var productHint = knownDone
454
+ ? "EchoMem is ready in your editor. Active sessions will be picked up after they stop changing."
455
+ : "You can start using EchoMem in your editor while extraction runs. Keep this tab and the terminal open until it finishes.";
456
+ app.className = "";
457
+ app.innerHTML =
458
+ (statsSlow ? '<div class="warning">Local processed/unprocessed counts are taking longer than expected. You can still ask the bridge to extract anything unprocessed.</div>' : '') +
459
+ (stats && stats.partial ? '<div class="warning">Fast local counts are ready. Detailed local stats are still loading.</div>' : '') +
460
+ (error ? '<div class="error">' + esc(error) + '</div>' : '') +
461
+ '<section class="panel extract-hero">' +
462
+ '<div>' +
463
+ '<p class="label">' + esc(knownDone ? "Extraction complete" : "Estimated processing time") + '</p>' +
464
+ '<strong class="hero-value">' + esc(knownDone ? "Complete" : etaLabel) + '</strong>' +
465
+ '<p class="hero-copy">' + esc(notice) + '</p>' +
466
+ '<p class="helper">' + esc(productHint) + '</p>' +
467
+ '</div>' +
468
+ '<div class="mini-stack">' +
469
+ '<div class="mini-row"><span>Pending</span><strong>' + esc(number(pending)) + '</strong></div>' +
470
+ '<div class="mini-row"><span>Processed</span><strong>' + esc(number(processed)) + '</strong></div>' +
471
+ '<div class="mini-row"><span>Active skipped</span><strong>' + esc(number(skippedActive)) + '</strong></div>' +
472
+ '<div class="mini-row"><span>1M+ sessions</span><strong>' + esc(number(over1m)) + '</strong></div>' +
473
+ '</div>' +
474
+ '</section>' +
475
+ '<div class="grid compact">' +
476
+ metric("Sessions found", number(sessions.total), number(sessions.codex) + " Codex + " + number(sessions.claudeCode) + " Claude Code") +
477
+ metric("Pending chars", compact(migratable.totalChars || (eta && eta.totalChars)), "assembled text to process") +
478
+ metric("Approx input tokens", compact(migratable.approxInputTokens || (eta && eta.approxInputTokens)), "local estimate") +
479
+ metric("Memories captured", number(stats && stats.memoriesCaptured), "already in EchoMem") +
480
+ '</div>' +
481
+ '<div class="details">' +
482
+ '<section class="panel"><h2>Pending Size</h2>' + bucketList(buckets) + '</section>' +
483
+ '<section class="panel"><h2>Extraction Plan</h2><dl>' +
484
+ row("Mode", "local bridge queue") +
485
+ row("Uploads", "text turns only") +
486
+ row("Already processed", "skipped") +
487
+ row("Active sessions", "picked up later") +
488
+ '</dl></section>' +
489
+ '</div>' +
490
+ '<div class="actions">' +
491
+ '<button id="migrate" class="primary" ' + (knownDone ? "disabled" : "") + '>-> Extract unprocessed conversations</button>' +
492
+ '<button id="skip" class="secondary">' + (knownDone ? "Finish setup" : "Not today") + '</button>' +
493
+ '</div>';
494
+ document.getElementById("migrate").onclick = startMigrate;
495
+ document.getElementById("skip").onclick = skip;
496
+ }
497
+ async function startMigrate() {
498
+ if (decisionMade) return;
499
+ decisionMade = true;
500
+ setHead("Extraction progress", "Starting");
501
+ app.className = "panel";
502
+ app.innerHTML = '<h2>Starting extraction</h2><p style="margin-top:10px">You can start using EchoMem in your editor now. Keep this tab and the terminal open while the local bridge imports unprocessed conversations.</p>';
503
+ try {
504
+ var started = await postJson("/migrate", {});
505
+ renderProgress(Object.assign({ status: "running", completed: 0, failed: 0, extracted: 0 }, started));
506
+ await pollProgress();
507
+ } catch (e) {
508
+ decisionMade = false;
509
+ renderDashboard(routeError(e && e.message));
510
+ }
511
+ }
512
+ async function pollProgress() {
513
+ for (;;) {
514
+ var progress = await getJson("/progress");
515
+ renderProgress(progress);
516
+ if (progress.status === "completed" || progress.status === "failed") return;
517
+ await sleep(1200);
518
+ }
519
+ }
520
+ function renderProgress(progress) {
521
+ var total = progress.total || progress.jobCount || 0;
522
+ var completed = progress.completed || 0;
523
+ var failed = progress.failed || 0;
524
+ var pct = total > 0 ? Math.round((completed / total) * 100) : 0;
525
+ setHead("Extraction progress", progress.status === "completed" ? "Done" : "Extracting");
526
+ app.className = "panel";
527
+ app.innerHTML =
528
+ '<h2>Extracting local history</h2>' +
529
+ '<div class="track"><div class="fill" style="width:' + pct + '%"></div></div>' +
530
+ '<div class="counts"><span>' + completed + ' completed</span><span>' + (progress.running || 0) + ' running</span><span>' + (progress.queued || 0) + ' queued</span><span>' + failed + ' failed</span></div>' +
531
+ '<p>You can start using EchoMem now. Keep this tab and the terminal open until extraction finishes.</p>' +
532
+ (progress.latest ? '<p style="margin-top:10px">' + esc(progress.latest) + '</p>' : '') +
533
+ '<div class="' + (progress.status === "failed" ? "error" : "notice") + '" style="margin-top:16px;margin-bottom:0">' +
534
+ esc((progress.extracted || 0).toLocaleString()) + ' memories saved so far.' + (progress.error ? ' ' + esc(progress.error) : '') +
535
+ '</div>';
536
+ }
537
+ async function skip() {
538
+ if (decisionMade) return;
539
+ decisionMade = true;
540
+ try {
541
+ await postJson("/skip", {});
542
+ setHead("Setup complete", "Done");
543
+ app.className = "panel";
544
+ app.innerHTML = '<h2>All set</h2><p style="margin-top:10px">You can extract unprocessed conversations later with echomem-mcp migrate.</p>';
545
+ } catch (e) {
546
+ decisionMade = false;
547
+ renderDashboard(e && e.message);
548
+ }
549
+ }
550
+ async function init() {
551
+ if (!nonce) throw new Error("Missing setup nonce. Re-run echomem-mcp setup.");
552
+ var cfg = await getJson("/config");
553
+ authUrl = cfg.authUrl || "";
554
+ if (!cfg.connected) {
555
+ showConnect();
556
+ var key = "echomem-auth-started-" + nonce;
557
+ if (!sessionStorage.getItem(key)) {
558
+ sessionStorage.setItem(key, "1");
559
+ setTimeout(function () { location.href = authUrl; }, 700);
560
+ }
561
+ return;
562
+ }
563
+ await waitForStats();
564
+ }
565
+ init().catch(function (e) {
566
+ setHead("Connection issue", "Needs attention");
567
+ app.className = "error";
568
+ app.textContent = e && e.message ? e.message : String(e);
569
+ });
570
+ })();
571
+ </script>
572
+ </body>
573
+ </html>`;
574
+ }