@echomem/mcp 1.3.0 → 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/README.md +2 -0
- package/dist/codex-sync.js +469 -0
- package/dist/encryption.js +3 -1
- package/dist/forensics.js +717 -0
- package/dist/index.js +461 -9
- package/dist/migrate.js +950 -125
- package/dist/report.js +154 -7
- package/dist/setup-page.js +1120 -0
- package/dist/setup.js +999 -76
- package/dist/v1-contract.js +68 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1120 @@
|
|
|
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
|
+
--echo-ink-primary: #1a3a8f;
|
|
12
|
+
--echo-ink-accent: #4ecdc4;
|
|
13
|
+
--echo-ink-seal: #c7372f;
|
|
14
|
+
--echo-ink-text: #2b2a26;
|
|
15
|
+
--echo-ink-mute: #6b6f69;
|
|
16
|
+
--echo-ink-faint: #9a9b94;
|
|
17
|
+
--echo-paper-canvas: #faf7f0;
|
|
18
|
+
--echo-paper-white: #ffffff;
|
|
19
|
+
--echo-paper-soft: #f0efe9;
|
|
20
|
+
--echo-line-soft: #e4e0d6;
|
|
21
|
+
--echo-font-brand: "Avenir Next", "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
22
|
+
--echo-font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
23
|
+
--echo-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
24
|
+
--bg: var(--echo-paper-canvas);
|
|
25
|
+
--ink: var(--echo-ink-text);
|
|
26
|
+
--muted: var(--echo-ink-mute);
|
|
27
|
+
--line: var(--echo-line-soft);
|
|
28
|
+
--panel: var(--echo-paper-white);
|
|
29
|
+
--good-bg: #eff7ec;
|
|
30
|
+
--good-line: #cbd9c8;
|
|
31
|
+
--warn-bg: #fff6dc;
|
|
32
|
+
--warn-line: #edce79;
|
|
33
|
+
--error-bg: #fff1ef;
|
|
34
|
+
--error-line: #e7aaa2;
|
|
35
|
+
}
|
|
36
|
+
* { box-sizing: border-box; }
|
|
37
|
+
body {
|
|
38
|
+
margin: 0;
|
|
39
|
+
min-height: 100vh;
|
|
40
|
+
background:
|
|
41
|
+
linear-gradient(180deg, rgba(245, 241, 231, 0.92), rgba(255, 255, 255, 0.96)),
|
|
42
|
+
var(--echo-paper-canvas);
|
|
43
|
+
color: var(--echo-ink-text);
|
|
44
|
+
font-family: var(--echo-font-body);
|
|
45
|
+
letter-spacing: 0;
|
|
46
|
+
}
|
|
47
|
+
main {
|
|
48
|
+
width: min(1120px, 100%);
|
|
49
|
+
margin: 0 auto;
|
|
50
|
+
padding: clamp(18px, 4vw, 34px);
|
|
51
|
+
}
|
|
52
|
+
header {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: space-between;
|
|
56
|
+
gap: 16px;
|
|
57
|
+
flex-wrap: wrap;
|
|
58
|
+
border-bottom: 1px solid var(--line);
|
|
59
|
+
padding-bottom: 18px;
|
|
60
|
+
margin-bottom: 24px;
|
|
61
|
+
}
|
|
62
|
+
.kicker {
|
|
63
|
+
margin: 0;
|
|
64
|
+
font-family: var(--echo-font-mono);
|
|
65
|
+
color: var(--echo-ink-mute);
|
|
66
|
+
font-size: 12px;
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
letter-spacing: 0.08em;
|
|
69
|
+
text-transform: uppercase;
|
|
70
|
+
}
|
|
71
|
+
h1 {
|
|
72
|
+
margin: 4px 0 0;
|
|
73
|
+
font-family: var(--echo-font-brand);
|
|
74
|
+
font-size: clamp(30px, 5vw, 56px);
|
|
75
|
+
font-weight: 900;
|
|
76
|
+
line-height: 1;
|
|
77
|
+
letter-spacing: 0;
|
|
78
|
+
color: var(--echo-ink-primary);
|
|
79
|
+
}
|
|
80
|
+
h2 {
|
|
81
|
+
margin: 0;
|
|
82
|
+
font-family: var(--echo-font-brand);
|
|
83
|
+
font-size: 22px;
|
|
84
|
+
font-weight: 900;
|
|
85
|
+
line-height: 1.2;
|
|
86
|
+
letter-spacing: 0;
|
|
87
|
+
color: var(--echo-ink-primary);
|
|
88
|
+
}
|
|
89
|
+
.pill {
|
|
90
|
+
min-height: 36px;
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 8px;
|
|
94
|
+
border: 1px solid #c9ccc3;
|
|
95
|
+
border-radius: 999px;
|
|
96
|
+
padding: 0 14px;
|
|
97
|
+
background: var(--panel);
|
|
98
|
+
font-family: var(--echo-font-mono);
|
|
99
|
+
font-size: 12px;
|
|
100
|
+
font-weight: 700;
|
|
101
|
+
letter-spacing: 0.04em;
|
|
102
|
+
text-transform: uppercase;
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
}
|
|
105
|
+
.state {
|
|
106
|
+
min-height: 220px;
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
color: var(--echo-ink-mute);
|
|
111
|
+
font-size: 16px;
|
|
112
|
+
text-align: center;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ---- pure loading skeleton (no copy) ---- */
|
|
116
|
+
.loading {
|
|
117
|
+
display: grid;
|
|
118
|
+
gap: 22px;
|
|
119
|
+
padding: clamp(24px, 6vw, 64px) 0;
|
|
120
|
+
}
|
|
121
|
+
.skeleton {
|
|
122
|
+
display: grid;
|
|
123
|
+
gap: 14px;
|
|
124
|
+
max-width: 720px;
|
|
125
|
+
}
|
|
126
|
+
.sk {
|
|
127
|
+
height: 16px;
|
|
128
|
+
border-radius: 6px;
|
|
129
|
+
background: linear-gradient(90deg, #ece9e0, #d7dbe6, #ece9e0);
|
|
130
|
+
background-size: 220% 100%;
|
|
131
|
+
animation: pulse 1.4s ease-in-out infinite;
|
|
132
|
+
}
|
|
133
|
+
.sk.title { height: 46px; width: 78%; }
|
|
134
|
+
.sk.w70 { width: 70%; }
|
|
135
|
+
.sk.w52 { width: 52%; }
|
|
136
|
+
.sk.w38 { width: 38%; }
|
|
137
|
+
.sk-grid {
|
|
138
|
+
display: grid;
|
|
139
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
140
|
+
gap: 10px;
|
|
141
|
+
max-width: 720px;
|
|
142
|
+
}
|
|
143
|
+
.sk-card {
|
|
144
|
+
height: 92px;
|
|
145
|
+
border-radius: 8px;
|
|
146
|
+
background: linear-gradient(90deg, #ece9e0, #d7dbe6, #ece9e0);
|
|
147
|
+
background-size: 220% 100%;
|
|
148
|
+
animation: pulse 1.4s ease-in-out infinite;
|
|
149
|
+
}
|
|
150
|
+
@keyframes pulse {
|
|
151
|
+
0% { background-position: 0% 50%; }
|
|
152
|
+
100% { background-position: 220% 50%; }
|
|
153
|
+
}
|
|
154
|
+
.loadProgress { display: grid; gap: 8px; max-width: 720px; margin-top: 4px; }
|
|
155
|
+
.loadBar { height: 6px; border-radius: 999px; background: var(--echo-paper-soft); overflow: hidden; }
|
|
156
|
+
.loadFill { height: 100%; width: 0%; background: var(--echo-ink-primary); transition: width 300ms ease; }
|
|
157
|
+
.loadCaption { margin: 0; font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute); }
|
|
158
|
+
|
|
159
|
+
/* ---- forensic report (ContextDoctorTwo) ---- */
|
|
160
|
+
.hero { padding: 4px 0 24px; }
|
|
161
|
+
.privacy {
|
|
162
|
+
display: grid;
|
|
163
|
+
grid-template-columns: auto 1fr;
|
|
164
|
+
gap: 10px;
|
|
165
|
+
align-items: baseline;
|
|
166
|
+
margin-bottom: 24px;
|
|
167
|
+
padding: 10px 0;
|
|
168
|
+
border-bottom: 1px solid var(--echo-line-soft);
|
|
169
|
+
}
|
|
170
|
+
.privacy span,
|
|
171
|
+
.kicker-mono,
|
|
172
|
+
.sectionHead span {
|
|
173
|
+
font-family: var(--echo-font-mono);
|
|
174
|
+
font-size: 12px;
|
|
175
|
+
font-weight: 700;
|
|
176
|
+
letter-spacing: 0.08em;
|
|
177
|
+
text-transform: uppercase;
|
|
178
|
+
color: var(--echo-ink-primary);
|
|
179
|
+
}
|
|
180
|
+
.privacy p { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.55; }
|
|
181
|
+
.heroGrid {
|
|
182
|
+
display: grid;
|
|
183
|
+
grid-template-columns: minmax(0, 1fr) 280px;
|
|
184
|
+
gap: clamp(20px, 3vw, 36px);
|
|
185
|
+
align-items: stretch;
|
|
186
|
+
}
|
|
187
|
+
.heroCopy { display: flex; flex-direction: column; justify-content: center; }
|
|
188
|
+
.heroCopy .kicker-mono { margin: 0 0 14px; color: var(--echo-ink-primary); }
|
|
189
|
+
.heroCopy h2 {
|
|
190
|
+
max-width: 720px;
|
|
191
|
+
margin: 0;
|
|
192
|
+
font-size: clamp(34px, 5vw, 58px);
|
|
193
|
+
line-height: 1.02;
|
|
194
|
+
}
|
|
195
|
+
.heroCopy p:not(.kicker-mono) {
|
|
196
|
+
margin: 18px 0 0;
|
|
197
|
+
font-size: clamp(15px, 1.25vw, 18px);
|
|
198
|
+
line-height: 1.62;
|
|
199
|
+
color: var(--echo-ink-text);
|
|
200
|
+
}
|
|
201
|
+
.scoreCard, .promiseCard, .panel {
|
|
202
|
+
border: 1px solid var(--echo-line-soft);
|
|
203
|
+
border-radius: 8px;
|
|
204
|
+
background: var(--echo-paper-white);
|
|
205
|
+
}
|
|
206
|
+
.scoreCard { display: grid; align-content: center; min-height: 282px; padding: 22px; }
|
|
207
|
+
.scoreCard span, .promiseCard span {
|
|
208
|
+
font-family: var(--echo-font-mono);
|
|
209
|
+
font-size: 12px;
|
|
210
|
+
font-weight: 700;
|
|
211
|
+
letter-spacing: 0.08em;
|
|
212
|
+
text-transform: uppercase;
|
|
213
|
+
color: var(--echo-ink-mute);
|
|
214
|
+
}
|
|
215
|
+
.scoreCard strong {
|
|
216
|
+
margin-top: 12px;
|
|
217
|
+
font-family: var(--echo-font-brand);
|
|
218
|
+
font-size: clamp(72px, 8vw, 96px);
|
|
219
|
+
font-weight: 900;
|
|
220
|
+
line-height: 0.9;
|
|
221
|
+
color: var(--echo-ink-seal);
|
|
222
|
+
font-variant-numeric: tabular-nums;
|
|
223
|
+
}
|
|
224
|
+
.scoreCard em {
|
|
225
|
+
margin-top: 4px;
|
|
226
|
+
font-style: normal;
|
|
227
|
+
font-weight: 900;
|
|
228
|
+
font-size: 22px;
|
|
229
|
+
color: var(--echo-ink-primary);
|
|
230
|
+
}
|
|
231
|
+
.scoreCard p, .promiseCard p { margin: 14px 0 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.55; }
|
|
232
|
+
.statStrip {
|
|
233
|
+
display: grid;
|
|
234
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
235
|
+
gap: 10px;
|
|
236
|
+
margin-top: 22px;
|
|
237
|
+
}
|
|
238
|
+
.stat {
|
|
239
|
+
min-height: 92px;
|
|
240
|
+
padding: 14px;
|
|
241
|
+
border: 1px solid var(--echo-line-soft);
|
|
242
|
+
border-radius: 8px;
|
|
243
|
+
background: var(--echo-paper-white);
|
|
244
|
+
}
|
|
245
|
+
.stat strong {
|
|
246
|
+
display: block;
|
|
247
|
+
font-family: var(--echo-font-brand);
|
|
248
|
+
font-size: clamp(24px, 2.8vw, 34px);
|
|
249
|
+
font-weight: 900;
|
|
250
|
+
line-height: 1;
|
|
251
|
+
color: var(--echo-ink-primary);
|
|
252
|
+
font-variant-numeric: tabular-nums;
|
|
253
|
+
}
|
|
254
|
+
.stat.alert strong { color: var(--echo-ink-seal); }
|
|
255
|
+
.stat span {
|
|
256
|
+
display: block;
|
|
257
|
+
margin-top: 8px;
|
|
258
|
+
font-family: var(--echo-font-mono);
|
|
259
|
+
font-size: 10px;
|
|
260
|
+
font-weight: 700;
|
|
261
|
+
letter-spacing: 0.06em;
|
|
262
|
+
text-transform: uppercase;
|
|
263
|
+
color: var(--echo-ink-mute);
|
|
264
|
+
}
|
|
265
|
+
.sourceNote {
|
|
266
|
+
max-width: 980px;
|
|
267
|
+
margin: 16px 0 0;
|
|
268
|
+
font-family: var(--echo-font-mono);
|
|
269
|
+
font-size: 11px;
|
|
270
|
+
line-height: 1.55;
|
|
271
|
+
color: var(--echo-ink-faint);
|
|
272
|
+
}
|
|
273
|
+
.sourceNote code {
|
|
274
|
+
padding: 1px 5px;
|
|
275
|
+
border-radius: 4px;
|
|
276
|
+
background: var(--echo-paper-soft);
|
|
277
|
+
color: var(--echo-ink-primary);
|
|
278
|
+
}
|
|
279
|
+
.section { padding: 30px 0 0; margin-top: 30px; border-top: 1px solid var(--echo-line-soft); }
|
|
280
|
+
.sectionHead { display: flex; gap: 12px; align-items: baseline; margin-bottom: 18px; }
|
|
281
|
+
.sectionHead h2 { margin: 0; font-size: clamp(24px, 3.4vw, 38px); line-height: 1.05; }
|
|
282
|
+
.twoCol {
|
|
283
|
+
display: grid;
|
|
284
|
+
grid-template-columns: minmax(0, 1fr) minmax(340px, 0.8fr);
|
|
285
|
+
gap: clamp(20px, 3.5vw, 36px);
|
|
286
|
+
align-items: start;
|
|
287
|
+
}
|
|
288
|
+
.storyText p { margin: 0 0 18px; color: var(--echo-ink-text); font-size: clamp(15px, 1.25vw, 18px); line-height: 1.62; }
|
|
289
|
+
.storyText p:last-child { margin-bottom: 0; }
|
|
290
|
+
.storyText b { color: var(--echo-ink-primary); }
|
|
291
|
+
.panel { display: grid; gap: 12px; padding: 16px; }
|
|
292
|
+
.promiseCard { padding: 22px; }
|
|
293
|
+
.promiseCard strong {
|
|
294
|
+
display: block;
|
|
295
|
+
margin-top: 12px;
|
|
296
|
+
font-family: var(--echo-font-brand);
|
|
297
|
+
font-size: clamp(34px, 4vw, 48px);
|
|
298
|
+
font-weight: 900;
|
|
299
|
+
line-height: 1;
|
|
300
|
+
color: var(--echo-ink-primary);
|
|
301
|
+
}
|
|
302
|
+
.promiseActions { display: grid; gap: 10px; margin-top: 18px; }
|
|
303
|
+
.promiseCard button, .promiseCard .button {
|
|
304
|
+
width: 100%;
|
|
305
|
+
min-height: 48px;
|
|
306
|
+
border-radius: 6px;
|
|
307
|
+
font-family: var(--echo-font-mono);
|
|
308
|
+
font-size: 13px;
|
|
309
|
+
font-weight: 800;
|
|
310
|
+
letter-spacing: 0.08em;
|
|
311
|
+
text-transform: uppercase;
|
|
312
|
+
}
|
|
313
|
+
.promiseCard .primary { border-color: var(--echo-ink-primary); background: var(--echo-ink-primary); color: #fff; }
|
|
314
|
+
.promiseCard .secondary { background: #fff; color: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
|
|
315
|
+
|
|
316
|
+
/* histogram */
|
|
317
|
+
.histo { display: flex; align-items: flex-end; gap: 3px; height: 120px; padding-top: 8px; }
|
|
318
|
+
.histoBar { flex: 1; min-height: 2px; border-radius: 2px 2px 0 0; background: var(--echo-ink-primary); }
|
|
319
|
+
.histoBar.night { background: var(--echo-ink-seal); }
|
|
320
|
+
.histoAxis {
|
|
321
|
+
display: flex; justify-content: space-between; margin-top: 8px;
|
|
322
|
+
font-family: var(--echo-font-mono); font-size: 10px; letter-spacing: 0.06em; color: var(--echo-ink-faint);
|
|
323
|
+
}
|
|
324
|
+
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
325
|
+
.chips span {
|
|
326
|
+
padding: 6px 8px; border: 1px solid var(--echo-line-soft); border-radius: 6px;
|
|
327
|
+
background: var(--echo-paper-soft); font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-primary);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* reread / repo rows */
|
|
331
|
+
.rrow2 {
|
|
332
|
+
display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: baseline;
|
|
333
|
+
padding: 11px 0; border-bottom: 1px solid var(--echo-line-soft);
|
|
334
|
+
}
|
|
335
|
+
.row2:last-child { border-bottom: 0; }
|
|
336
|
+
.row2 b { font-family: var(--echo-font-mono); font-size: 13px; color: var(--echo-ink-primary); }
|
|
337
|
+
.row2 small { display: block; margin-top: 3px; font-size: 12px; color: var(--echo-ink-mute); }
|
|
338
|
+
.row2 strong {
|
|
339
|
+
font-family: var(--echo-font-brand); font-size: 24px; font-weight: 900;
|
|
340
|
+
color: var(--echo-ink-seal); font-variant-numeric: tabular-nums; white-space: nowrap;
|
|
341
|
+
}
|
|
342
|
+
.repoRow strong { color: var(--echo-ink-primary); }
|
|
343
|
+
.tag {
|
|
344
|
+
display: inline-block; margin-left: 8px; padding: 2px 7px; border-radius: 999px;
|
|
345
|
+
background: var(--echo-paper-soft); font-family: var(--echo-font-mono); font-size: 10px;
|
|
346
|
+
font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--echo-ink-seal);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* cost split + model rows */
|
|
350
|
+
.segBar { display: flex; width: 100%; height: 22px; overflow: hidden; border-radius: 5px; background: var(--echo-paper-soft); }
|
|
351
|
+
.segBar span { display: block; min-width: 2px; }
|
|
352
|
+
.seg-output { background: var(--echo-ink-primary); }
|
|
353
|
+
.seg-cold { background: var(--echo-ink-mute); }
|
|
354
|
+
.seg-cw { background: var(--echo-ink-accent); }
|
|
355
|
+
.seg-cr { background: var(--echo-ink-seal); }
|
|
356
|
+
.costLegend { display: grid; gap: 6px; font-family: var(--echo-font-mono); font-size: 11px; color: var(--echo-ink-mute); }
|
|
357
|
+
.modelRow {
|
|
358
|
+
display: grid; grid-template-columns: 1fr auto auto; gap: 12px; align-items: baseline;
|
|
359
|
+
padding: 9px 0; border-bottom: 1px solid var(--echo-line-soft);
|
|
360
|
+
font-family: var(--echo-font-mono); font-size: 12px; color: var(--echo-ink-mute);
|
|
361
|
+
}
|
|
362
|
+
.modelRow:last-child { border-bottom: 0; }
|
|
363
|
+
.modelRow b { font-size: 13px; color: var(--echo-ink-primary); }
|
|
364
|
+
.modelRow strong { font-family: var(--echo-font-brand); font-size: 18px; font-weight: 900; color: var(--echo-ink-seal); }
|
|
365
|
+
|
|
366
|
+
.nightCard {
|
|
367
|
+
margin-top: 18px; padding: 20px 22px; border: 1px solid var(--echo-ink-seal);
|
|
368
|
+
border-radius: 8px; background: var(--echo-paper-white);
|
|
369
|
+
}
|
|
370
|
+
.nightCard span {
|
|
371
|
+
font-family: var(--echo-font-mono); font-size: 11px; font-weight: 700; letter-spacing: 0.08em;
|
|
372
|
+
text-transform: uppercase; color: var(--echo-ink-seal);
|
|
373
|
+
}
|
|
374
|
+
.nightCard p { margin: 10px 0 0; font-size: 17px; line-height: 1.5; color: var(--echo-ink-text); }
|
|
375
|
+
.nightCard b { color: var(--echo-ink-primary); }
|
|
376
|
+
|
|
377
|
+
/* ---- dashboard (post-auth extraction) ---- */
|
|
378
|
+
.loading-panel, .doctor-copy, .doctor-score, .metric, .extract-panel {
|
|
379
|
+
border: 1px solid var(--line);
|
|
380
|
+
border-radius: 8px;
|
|
381
|
+
background: var(--panel);
|
|
382
|
+
}
|
|
383
|
+
.status-strip {
|
|
384
|
+
display: flex; align-items: flex-start; gap: 12px;
|
|
385
|
+
border: 1px solid #c7d6e5; border-radius: 8px; background: #eef5fb; color: #1e3449;
|
|
386
|
+
padding: 13px 15px; margin-bottom: 18px;
|
|
387
|
+
}
|
|
388
|
+
.status-strip.good { border-color: var(--good-line); background: var(--good-bg); color: #203820; }
|
|
389
|
+
.status-strip.warn { border-color: var(--warn-line); background: var(--warn-bg); color: #5e4300; }
|
|
390
|
+
.status-strip .dot { flex: 0 0 auto; width: 12px; height: 12px; border-radius: 999px; margin-top: 4px; background: currentColor; }
|
|
391
|
+
.status-strip strong { display: block; line-height: 1.25; }
|
|
392
|
+
.status-strip p { margin-top: 3px; color: currentColor; font-size: 14px; }
|
|
393
|
+
.doctor-hero {
|
|
394
|
+
display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(260px, 0.8fr);
|
|
395
|
+
gap: 14px; margin-bottom: 14px; align-items: stretch;
|
|
396
|
+
}
|
|
397
|
+
.doctor-copy, .doctor-score { padding: clamp(18px, 3vw, 26px); }
|
|
398
|
+
.doctor-copy { display: flex; flex-direction: column; justify-content: center; min-height: 240px; }
|
|
399
|
+
.doctor-copy h2 { max-width: 760px; margin-top: 10px; font-size: clamp(30px, 4.4vw, 52px); line-height: 1.02; }
|
|
400
|
+
.doctor-copy p:not(.label):not(.helper) { max-width: 720px; margin-top: 14px; font-size: clamp(15px, 1.5vw, 18px); color: var(--echo-ink-text); }
|
|
401
|
+
.doctor-score { display: grid; align-content: center; gap: 12px; }
|
|
402
|
+
.doctor-score > span { font-family: var(--echo-font-mono); color: var(--echo-ink-mute); font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
403
|
+
.doctor-score > strong { display: block; font-family: var(--echo-font-brand); font-size: clamp(34px, 5.5vw, 60px); font-weight: 900; line-height: 0.95; color: var(--echo-ink-primary); }
|
|
404
|
+
.doctor-mini { display: grid; gap: 8px; margin-top: 12px; }
|
|
405
|
+
.mini-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--echo-line-soft); padding-bottom: 8px; }
|
|
406
|
+
.mini-row span { color: var(--echo-ink-text); font-size: 14px; }
|
|
407
|
+
.mini-row strong { font-family: var(--echo-font-brand); font-size: 16px; font-weight: 900; color: var(--echo-ink-primary); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
408
|
+
.doctor-stat-strip { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-bottom: 14px; }
|
|
409
|
+
.doctor-actions { margin: 0 0 18px; }
|
|
410
|
+
.metric { min-height: 122px; padding: 18px; display: flex; flex-direction: column; justify-content: space-between; }
|
|
411
|
+
.label { margin: 0; font-family: var(--echo-font-mono); color: var(--echo-ink-mute); font-size: 12px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
412
|
+
.value { display: block; font-family: var(--echo-font-brand); font-size: 34px; font-weight: 900; line-height: 1; color: var(--echo-ink-primary); }
|
|
413
|
+
.detail { margin: 0; color: var(--echo-ink-mute); font-size: 14px; line-height: 1.45; }
|
|
414
|
+
.extract-panel { padding: 18px; }
|
|
415
|
+
.bucket-row { align-items: center; display: grid; grid-template-columns: minmax(90px, 1fr) minmax(100px, auto); gap: 12px; }
|
|
416
|
+
.bucket-value { text-align: right; font-weight: 800; white-space: nowrap; }
|
|
417
|
+
.helper { margin-top: 10px; color: var(--echo-ink-mute); font-size: 14px; }
|
|
418
|
+
.notice, .done, .warning, .error { border-radius: 8px; padding: 14px 16px; margin-bottom: 18px; }
|
|
419
|
+
.notice, .done { border: 1px solid var(--good-line); background: var(--good-bg); color: #203820; }
|
|
420
|
+
.warning { border: 1px solid var(--warn-line); background: var(--warn-bg); color: #5e4300; }
|
|
421
|
+
.error { border: 1px solid var(--error-line); background: var(--error-bg); color: #7d2117; }
|
|
422
|
+
.details { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(290px, 100%), 1fr)); gap: 12px; margin-bottom: 18px; }
|
|
423
|
+
dl { margin: 14px 0 0; display: grid; gap: 10px; }
|
|
424
|
+
.row { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid #ecece7; padding-bottom: 8px; }
|
|
425
|
+
dt { color: var(--echo-ink-text); }
|
|
426
|
+
dd { margin: 0; text-align: right; color: var(--echo-ink-text); }
|
|
427
|
+
ol { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 10px; }
|
|
428
|
+
li { display: flex; justify-content: space-between; gap: 10px; }
|
|
429
|
+
.actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
|
430
|
+
button, .button {
|
|
431
|
+
min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
|
432
|
+
border-radius: 8px; border: 1px solid var(--echo-ink-primary); padding: 0 16px;
|
|
433
|
+
font: inherit; font-weight: 800; text-decoration: none; cursor: pointer;
|
|
434
|
+
}
|
|
435
|
+
.primary { background: var(--echo-ink-primary); color: #fff; }
|
|
436
|
+
.secondary { background: #fff; color: var(--echo-ink-primary); }
|
|
437
|
+
button:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
438
|
+
.track { height: 12px; overflow: hidden; border-radius: 999px; background: #e8e8e1; margin: 16px 0 10px; }
|
|
439
|
+
.fill { height: 100%; width: 0%; background: var(--echo-ink-primary); transition: width 180ms ease; }
|
|
440
|
+
/* indeterminate "we're working on it" bar for the prep phase (before job counts move) */
|
|
441
|
+
.track.indef .fill { width: 40%; transition: none; animation: prepSlide 1.2s ease-in-out infinite; }
|
|
442
|
+
@keyframes prepSlide { 0% { transform: translateX(-110%); } 100% { transform: translateX(350%); } }
|
|
443
|
+
.prep-steps { list-style: none; margin: 18px 0 0; padding: 0; display: grid; gap: 11px; }
|
|
444
|
+
.prep-steps li { display: grid; grid-template-columns: 16px 1fr; gap: 11px; align-items: center; font-size: 15px; color: var(--echo-ink-mute); }
|
|
445
|
+
.prep-steps .dot { width: 13px; height: 13px; border-radius: 999px; border: 2px solid #cfcfc6; box-sizing: border-box; }
|
|
446
|
+
.prep-steps li.done { color: var(--echo-ink-text); }
|
|
447
|
+
.prep-steps li.done .dot { background: var(--echo-ink-primary); border-color: var(--echo-ink-primary); }
|
|
448
|
+
.prep-steps li.active { color: var(--echo-ink-primary); font-weight: 700; }
|
|
449
|
+
.prep-steps li.active .dot { border-color: var(--echo-ink-primary); animation: prepDot 1.1s ease-in-out infinite; }
|
|
450
|
+
@keyframes prepDot { 0%, 100% { box-shadow: 0 0 0 0 rgba(26,58,143,0.4); } 60% { box-shadow: 0 0 0 6px rgba(26,58,143,0); } }
|
|
451
|
+
.counts { display: flex; gap: 14px; flex-wrap: wrap; font-family: var(--echo-font-mono); color: var(--echo-ink-mute); font-size: 13px; letter-spacing: 0.02em; margin-bottom: 6px; }
|
|
452
|
+
.finish { display: grid; gap: 6px; place-items: center; text-align: center; padding: clamp(28px, 6vw, 64px) 0; }
|
|
453
|
+
.finishMark { width: 54px; height: 54px; border-radius: 999px; display: grid; place-items: center; font-size: 26px; font-weight: 900; margin-bottom: 8px; }
|
|
454
|
+
.finishMark.ok { background: var(--good-bg); color: #2f7f4f; border: 1px solid var(--good-line); }
|
|
455
|
+
.finishMark.warn { background: var(--warn-bg); color: #5e4300; border: 1px solid var(--warn-line); }
|
|
456
|
+
.finishBig { font-family: var(--echo-font-brand); font-size: clamp(48px, 8vw, 84px); font-weight: 900; line-height: 0.9; color: var(--echo-ink-primary); font-variant-numeric: tabular-nums; }
|
|
457
|
+
.finishLabel { font-family: var(--echo-font-mono); font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--echo-ink-mute); }
|
|
458
|
+
.finish p { max-width: 560px; margin: 12px 0 0; color: var(--echo-ink-text); font-size: 16px; line-height: 1.6; }
|
|
459
|
+
.finish p.helper { color: var(--echo-ink-mute); font-size: 14px; margin-top: 6px; }
|
|
460
|
+
code { font-family: var(--echo-font-mono); font-size: 0.9em; padding: 1px 5px; border-radius: 4px; background: var(--echo-paper-soft); color: var(--echo-ink-primary); }
|
|
461
|
+
.hidden { display: none; }
|
|
462
|
+
@media (max-width: 820px) {
|
|
463
|
+
.heroGrid, .twoCol, .statStrip, .privacy, .doctor-hero, .sk-grid, .doctor-stat-strip { grid-template-columns: 1fr; }
|
|
464
|
+
.doctor-stat-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
465
|
+
.heroCopy h2 { font-size: 34px; }
|
|
466
|
+
.scoreCard { min-height: 220px; }
|
|
467
|
+
}
|
|
468
|
+
</style>
|
|
469
|
+
</head>
|
|
470
|
+
<body>
|
|
471
|
+
<main>
|
|
472
|
+
<header>
|
|
473
|
+
<div>
|
|
474
|
+
<p class="kicker">EchoMem device setup</p>
|
|
475
|
+
<h1 id="title">Context Doctor</h1>
|
|
476
|
+
</div>
|
|
477
|
+
<div class="pill" id="status">Local</div>
|
|
478
|
+
</header>
|
|
479
|
+
<section id="app" class="state">Starting local scan...</section>
|
|
480
|
+
</main>
|
|
481
|
+
<script>
|
|
482
|
+
(function () {
|
|
483
|
+
var params = new URLSearchParams(location.search);
|
|
484
|
+
var nonce = params.get("nonce") || "";
|
|
485
|
+
var app = document.getElementById("app");
|
|
486
|
+
var title = document.getElementById("title");
|
|
487
|
+
var status = document.getElementById("status");
|
|
488
|
+
var report = null;
|
|
489
|
+
var stats = null;
|
|
490
|
+
var statsSlow = false;
|
|
491
|
+
var decisionMade = false;
|
|
492
|
+
var connected = false;
|
|
493
|
+
var authUrl = "";
|
|
494
|
+
var authWindow = null;
|
|
495
|
+
var connectionPollStarted = false;
|
|
496
|
+
var statsPollStarted = false;
|
|
497
|
+
var reportPollStarted = false;
|
|
498
|
+
var NIGHT_HOURS = { 22: 1, 23: 1, 0: 1, 1: 1, 2: 1, 3: 1 };
|
|
499
|
+
|
|
500
|
+
function esc(value) {
|
|
501
|
+
return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
|
|
502
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch];
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
function setHead(nextTitle, nextStatus) {
|
|
506
|
+
title.textContent = nextTitle;
|
|
507
|
+
status.textContent = nextStatus;
|
|
508
|
+
}
|
|
509
|
+
function number(value) {
|
|
510
|
+
return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "-";
|
|
511
|
+
}
|
|
512
|
+
function countOrChecking(value) {
|
|
513
|
+
return typeof value === "number" && isFinite(value) ? Math.round(value).toLocaleString() : "checking";
|
|
514
|
+
}
|
|
515
|
+
function compact(value) {
|
|
516
|
+
if (typeof value !== "number" || !isFinite(value)) return "-";
|
|
517
|
+
var abs = Math.abs(value);
|
|
518
|
+
if (abs >= 1e9) return (value / 1e9).toFixed(1) + "B";
|
|
519
|
+
if (abs >= 1e6) return (value / 1e6).toFixed(1) + "M";
|
|
520
|
+
if (abs >= 1e3) return (value / 1e3).toFixed(1) + "K";
|
|
521
|
+
return String(Math.round(value));
|
|
522
|
+
}
|
|
523
|
+
function big(value) {
|
|
524
|
+
if (typeof value !== "number" || !isFinite(value)) return "-";
|
|
525
|
+
var n = Math.abs(value);
|
|
526
|
+
if (n >= 1e9) return (value / 1e9).toFixed(2) + "B";
|
|
527
|
+
if (n >= 1e6) return (value / 1e6).toFixed(1) + "M";
|
|
528
|
+
if (n >= 1e3) return Math.round(value / 1e3) + "K";
|
|
529
|
+
return String(Math.round(value));
|
|
530
|
+
}
|
|
531
|
+
function money(value) {
|
|
532
|
+
return "$" + number(value);
|
|
533
|
+
}
|
|
534
|
+
function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); }
|
|
535
|
+
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
536
|
+
var controller = new AbortController();
|
|
537
|
+
var timer = setTimeout(function () { controller.abort(); }, timeoutMs);
|
|
538
|
+
try {
|
|
539
|
+
return await fetch(url, Object.assign({}, options || {}, { signal: controller.signal }));
|
|
540
|
+
} finally {
|
|
541
|
+
clearTimeout(timer);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function getJson(path) {
|
|
545
|
+
var sep = path.indexOf("?") >= 0 ? "&" : "?";
|
|
546
|
+
var res = await fetch(path + sep + "nonce=" + encodeURIComponent(nonce), { credentials: "omit" });
|
|
547
|
+
if (!res.ok) throw new Error(await res.text() || ("HTTP " + res.status));
|
|
548
|
+
return await res.json();
|
|
549
|
+
}
|
|
550
|
+
async function postJson(path, body, timeoutMs) {
|
|
551
|
+
var options = {
|
|
552
|
+
method: "POST",
|
|
553
|
+
credentials: "omit",
|
|
554
|
+
headers: { "Content-Type": "application/json" },
|
|
555
|
+
body: JSON.stringify(Object.assign({ nonce: nonce }, body || {}))
|
|
556
|
+
};
|
|
557
|
+
var res = timeoutMs ? await fetchWithTimeout(path, options, timeoutMs) : await fetch(path, options);
|
|
558
|
+
var data = await res.json().catch(function () { return {}; });
|
|
559
|
+
if (!res.ok) throw new Error(data.message || data.error || ("HTTP " + res.status));
|
|
560
|
+
return data;
|
|
561
|
+
}
|
|
562
|
+
function metric(label, value, detail) {
|
|
563
|
+
return '<section class="metric"><p class="label">' + esc(label) + '</p><strong class="value">' + esc(value) + '</strong><p class="detail">' + esc(detail) + '</p></section>';
|
|
564
|
+
}
|
|
565
|
+
function row(label, value) {
|
|
566
|
+
return '<div class="row"><dt>' + esc(label) + '</dt><dd>' + esc(value) + '</dd></div>';
|
|
567
|
+
}
|
|
568
|
+
function migrationBuckets(migratable) {
|
|
569
|
+
var eta = migratable && migratable.eta ? migratable.eta : {};
|
|
570
|
+
return Array.isArray(migratable && migratable.buckets) ? migratable.buckets : (Array.isArray(eta.buckets) ? eta.buckets : []);
|
|
571
|
+
}
|
|
572
|
+
function bucketCount(buckets, key) {
|
|
573
|
+
var found = buckets.filter(function (b) { return b && b.key === key; })[0];
|
|
574
|
+
return found && typeof found.count === "number" ? found.count : 0;
|
|
575
|
+
}
|
|
576
|
+
function bucketList(buckets) {
|
|
577
|
+
var visible = buckets.filter(function (b) { return b && (b.count || b.chars); });
|
|
578
|
+
if (!visible.length) return '<p style="margin-top:10px">No pending extraction buckets.</p>';
|
|
579
|
+
return '<ol>' + visible.map(function (b) {
|
|
580
|
+
return '<li class="bucket-row"><span>' + esc(b.label) + '</span><span class="bucket-value">' + esc(number(b.count)) + ' · ' + esc(compact(b.chars)) + '</span></li>';
|
|
581
|
+
}).join("") + '</ol>';
|
|
582
|
+
}
|
|
583
|
+
function routeError(code) {
|
|
584
|
+
if (code === "NO_PENDING_SESSIONS") return "There are no unprocessed local conversations to extract right now.";
|
|
585
|
+
if (code === "FORBIDDEN_SCOPE") return "This device token cannot import history. Re-connect this device to mint an import-capable token.";
|
|
586
|
+
if (code === "VAULT_LOCKED") return "Your local vault is locked. Run echomem-mcp unlock, then retry.";
|
|
587
|
+
if (code === "IMPORT_START_TIMEOUT") return "Starting the import timed out. Retry when the connection settles.";
|
|
588
|
+
if (code === "NOT_LOGGED_IN") return "The local bridge is not logged in yet. Sign in again and retry.";
|
|
589
|
+
return code || "Something went wrong. Retry in a moment.";
|
|
590
|
+
}
|
|
591
|
+
function closeAuthWindow() {
|
|
592
|
+
try { if (authWindow && !authWindow.closed) authWindow.close(); } catch (_) {}
|
|
593
|
+
authWindow = null;
|
|
594
|
+
}
|
|
595
|
+
function notifyOpenerConnected() {
|
|
596
|
+
try {
|
|
597
|
+
if (window.opener && window.opener !== window && !window.opener.closed) {
|
|
598
|
+
window.opener.postMessage({ type: "echomem:connected", nonce: nonce }, location.origin);
|
|
599
|
+
setTimeout(function () { window.close(); }, 250);
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
} catch (_) {}
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
function openAuthWindow(url) {
|
|
606
|
+
var targetUrl = url || authUrl;
|
|
607
|
+
if (!targetUrl) return false;
|
|
608
|
+
try {
|
|
609
|
+
authWindow = window.open(targetUrl, "echomem-signin-" + nonce, "popup=yes,width=560,height=760");
|
|
610
|
+
if (!authWindow) return false;
|
|
611
|
+
authWindow.focus();
|
|
612
|
+
return true;
|
|
613
|
+
} catch (_) { return false; }
|
|
614
|
+
}
|
|
615
|
+
function showPopupFallback(message, url) {
|
|
616
|
+
var hint = document.getElementById("signin-help");
|
|
617
|
+
if (hint) hint.textContent = message || "Your browser blocked the secure sign-in window. Use the button below, then this page will resume when sign-in returns.";
|
|
618
|
+
var fallback = document.getElementById("signin-fallback");
|
|
619
|
+
if (fallback) { fallback.setAttribute("href", url || authUrl); fallback.classList.remove("hidden"); }
|
|
620
|
+
}
|
|
621
|
+
function onConnectClick() {
|
|
622
|
+
if (connected) { void waitForStats(); return; }
|
|
623
|
+
if (!openAuthWindow(authUrl)) showPopupFallback("", authUrl);
|
|
624
|
+
startConnectionPoll();
|
|
625
|
+
}
|
|
626
|
+
function bindConnect() {
|
|
627
|
+
var signin = document.getElementById("signin");
|
|
628
|
+
if (signin) signin.onclick = onConnectClick;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/* ---------- pure loading (pic 5) ---------- */
|
|
632
|
+
function renderLoading(prog) {
|
|
633
|
+
var pct = prog && prog.total ? Math.round((prog.scanned / prog.total) * 100) : 0;
|
|
634
|
+
var caption = prog && prog.total
|
|
635
|
+
? "Reading your local history — " + number(prog.scanned) + " / " + number(prog.total) + " sessions"
|
|
636
|
+
: "Reading your local history…";
|
|
637
|
+
// Update the bar in place on later polls so the skeleton animation doesn't restart/flicker.
|
|
638
|
+
var fill = document.getElementById("loadFill");
|
|
639
|
+
if (app.className === "loading" && fill) {
|
|
640
|
+
fill.style.width = pct + "%";
|
|
641
|
+
var cap = document.getElementById("loadCaption");
|
|
642
|
+
if (cap) cap.textContent = caption;
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
setHead("Context Doctor", "Reading");
|
|
646
|
+
app.className = "loading";
|
|
647
|
+
app.innerHTML =
|
|
648
|
+
'<div class="skeleton">' +
|
|
649
|
+
'<div class="sk title"></div><div class="sk w70"></div><div class="sk w52"></div>' +
|
|
650
|
+
'</div>' +
|
|
651
|
+
'<div class="sk-grid"><div class="sk-card"></div><div class="sk-card"></div><div class="sk-card"></div><div class="sk-card"></div></div>' +
|
|
652
|
+
'<div class="skeleton"><div class="sk w38"></div><div class="sk w70"></div></div>' +
|
|
653
|
+
'<div class="loadProgress"><div class="loadBar"><div id="loadFill" class="loadFill" style="width:' + pct + '%"></div></div><p id="loadCaption" class="loadCaption">' + esc(caption) + '</p></div>';
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/* ---------- forensic report (pic 1/2) ---------- */
|
|
657
|
+
function histogram(hours) {
|
|
658
|
+
var list = Array.isArray(hours) ? hours : [];
|
|
659
|
+
var max = Math.max.apply(null, [1].concat(list));
|
|
660
|
+
var bars = list.map(function (v, h) {
|
|
661
|
+
var height = Math.max(2, Math.round((v / max) * 100));
|
|
662
|
+
return '<span class="histoBar' + (NIGHT_HOURS[h] ? ' night' : '') + '" style="height:' + height + '%" title="' + h + ':00 — ' + number(v) + ' prompts"></span>';
|
|
663
|
+
}).join("");
|
|
664
|
+
return '<div class="histo">' + bars + '</div><div class="histoAxis"><span>12a</span><span>6a</span><span>12p</span><span>6p</span><span>11p</span></div>';
|
|
665
|
+
}
|
|
666
|
+
function rereadRows(files) {
|
|
667
|
+
return (files || []).slice(0, 6).map(function (f) {
|
|
668
|
+
var meta = esc(f.repo) + ' · ' + number(f.sessionsTouched) + ' sessions · ' + number(f.daysTouched) + ' days' + (f.nightReads ? ' · ' + number(f.nightReads) + ' at night' : '');
|
|
669
|
+
var tag = f.everChanged ? '' : '<span class="tag">never changed</span>';
|
|
670
|
+
return '<div class="row2"><div><b>' + esc(f.file) + '</b>' + tag + '<small>' + meta + '</small></div><strong>' + number(f.totalReads) + '×</strong></div>';
|
|
671
|
+
}).join("");
|
|
672
|
+
}
|
|
673
|
+
function repoRows(repos) {
|
|
674
|
+
return (repos || []).slice(0, 5).map(function (r) {
|
|
675
|
+
var meta = number(r.sessions) + ' sessions · ' + number(r.activeAttentionDays) + ' active days · ' + (r.dailyFocusHours || 0) + 'h/day · ' + big(r.tokens) + ' tokens';
|
|
676
|
+
return '<div class="row2 repoRow"><div><b style="font-family:var(--echo-font-brand);font-size:18px">' + esc(r.name) + '</b><small>' + esc(meta) + '</small></div><strong style="color:var(--echo-ink-primary)">' + (r.attentionHours || 0) + 'h</strong></div>';
|
|
677
|
+
}).join("");
|
|
678
|
+
}
|
|
679
|
+
function costSplit(cost) {
|
|
680
|
+
var total = cost.total || 1;
|
|
681
|
+
function seg(cls, v) { return '<span class="' + cls + '" style="width:' + Math.max(0, (v / total) * 100) + '%"></span>'; }
|
|
682
|
+
var byModel = cost.byModel || {};
|
|
683
|
+
var rows = Object.keys(byModel).map(function (k) { return [k, byModel[k]]; })
|
|
684
|
+
.sort(function (a, b) { return b[1].total - a[1].total; })
|
|
685
|
+
.map(function (e) { return '<div class="modelRow"><b>' + esc(e[0]) + '</b><span>' + big(e[1].total) + ' tok</span><strong>' + money(e[1].cost) + '</strong></div>'; })
|
|
686
|
+
.join("");
|
|
687
|
+
return '<div class="segBar">' + seg("seg-output", cost.outputCost) + seg("seg-cold", cost.coldInputCost) + seg("seg-cw", cost.cacheWriteCost) + seg("seg-cr", cost.cacheReadCost) + '</div>' +
|
|
688
|
+
'<div class="costLegend"><span>output ' + money(cost.outputCost) + '</span><span>cold input ' + money(cost.coldInputCost) + '</span><span>cache write ' + money(cost.cacheWriteCost) + '</span><span>cache read ' + money(cost.cacheReadCost) + '</span></div>' +
|
|
689
|
+
'<div style="margin-top:14px">' + rows + '</div>';
|
|
690
|
+
}
|
|
691
|
+
function renderForensicReport(message) {
|
|
692
|
+
var r = report;
|
|
693
|
+
if (!r) { showReport(); return; }
|
|
694
|
+
var s = r.scale || {}, c = r.cost || {}, mu = r.modelUsage || {}, uwh = r.userWorkingHours || {}, rh = r.rhythm || {}, cl = r.cleanliness || {}, rf = r.rereadForensics || {}, aw = r.avoidableWait || {}, cf = r.counterfactual || {};
|
|
695
|
+
var night = rf.nightExample;
|
|
696
|
+
setHead("Context Doctor", connected ? "Signed in" : "Local");
|
|
697
|
+
app.className = "";
|
|
698
|
+
app.innerHTML =
|
|
699
|
+
(message ? '<div class="notice">' + esc(message) + '</div>' : '') +
|
|
700
|
+
'<section class="hero">' +
|
|
701
|
+
'<div class="privacy"><span>Local scan</span><p>Hi, I am Echo. I read your Codex and Claude Code history from ' + esc(r.scanStartDate) + ' to ' + esc(r.scanEndDate) + ', straight off your machine. Nothing was uploaded, moved to the cloud, or shared.</p></div>' +
|
|
702
|
+
'<div class="heroGrid">' +
|
|
703
|
+
'<div class="heroCopy">' +
|
|
704
|
+
'<p class="kicker-mono">Workspace Physical-Exam</p>' +
|
|
705
|
+
'<h2>You shipped a lot. Your agent kept re-reading the same files.</h2>' +
|
|
706
|
+
'<p>Across ' + number(uwh.activeAttentionDays) + ' active days you put in ' + (uwh.userAttentionHours || 0) + ' hours of real focus, moved ' + big(s.totalTokens) + ' tokens through ' + number(s.repoCount) + ' repos, and ran ' + number(s.sessionCount) + ' sessions. That is a season of work.</p>' +
|
|
707
|
+
'</div>' +
|
|
708
|
+
'<aside class="scoreCard"><span>Context Cleanliness</span><strong>' + number(cl.score) + '</strong><em>/100</em><p>' + number(cl.staleReadSharePct) + '% of everything the agent read was re-reading files that had not changed. That is ' + number(cl.staleRereads) + ' stale rereads.</p></aside>' +
|
|
709
|
+
'</div>' +
|
|
710
|
+
'<div class="statStrip">' +
|
|
711
|
+
'<div class="stat"><strong>' + (uwh.userAttentionHours || 0) + 'h</strong><span>focused attention</span></div>' +
|
|
712
|
+
'<div class="stat"><strong>' + big(s.totalTokens) + '</strong><span>tokens moved</span></div>' +
|
|
713
|
+
'<div class="stat alert"><strong>' + money(c.total) + '</strong><span>API-equivalent worth</span></div>' +
|
|
714
|
+
'<div class="stat"><strong>' + number(rh.lateNightShare) + '%</strong><span>prompts after 10pm</span></div>' +
|
|
715
|
+
'</div>' +
|
|
716
|
+
'<p class="sourceNote">Source: <code>~/.codex/sessions</code> + <code>~/.claude/projects</code> · ' + number(s.sessionCount) + ' sessions · model ' + esc(mu.topModel) + ' ' + number(mu.topModelShare) + '%. Cost is an API-equivalent estimate at list prices, not your subscription bill. Codex reads are extracted best-effort from shell commands (~73% coverage); Claude Code reads are exact.</p>' +
|
|
717
|
+
'</section>' +
|
|
718
|
+
|
|
719
|
+
'<section class="section"><div class="sectionHead"><span>01</span><h2>This is your trail.</h2></div>' +
|
|
720
|
+
'<div class="twoCol">' +
|
|
721
|
+
'<div class="storyText">' +
|
|
722
|
+
'<p>On an active day you average ' + (uwh.averageDailyAttentionHours || 0) + ' hours of focus. Your longest single day was ' + (uwh.maxDailyAttentionHours || 0) + ' hours, on ' + esc(uwh.peakDay && uwh.peakDay.date) + '. You are a <b>' + esc(mu.identityLabel) + '</b> — ' + esc(mu.topModel) + ' carried ' + number(mu.topModelShare) + '% of the load.</p>' +
|
|
723
|
+
'<p>And I can see when you work. ' + number(rh.lateNightShare) + '% of your prompts land after 10pm — ' + number(rh.lateNightSessionCount) + ' sessions ran deep into the night. Your busiest day is ' + esc(rh.busiestWeekday) + '. The red bars are the late hours you kept going.</p>' +
|
|
724
|
+
'</div>' +
|
|
725
|
+
'<div class="panel">' + histogram(rh.hourHistogram) + '<div class="chips">' + (r.repos || []).slice(0, 6).map(function (x) { return '<span>' + esc(x.name) + ' · ' + (x.attentionHours || 0) + 'h</span>'; }).join("") + '</div></div>' +
|
|
726
|
+
'</div>' +
|
|
727
|
+
'<div class="panel" style="margin-top:18px">' + repoRows(r.repos) + '</div>' +
|
|
728
|
+
'</section>' +
|
|
729
|
+
|
|
730
|
+
'<section class="section"><div class="sectionHead"><span>02</span><h2>The money feeds input, not output.</h2></div>' +
|
|
731
|
+
'<div class="twoCol">' +
|
|
732
|
+
'<div class="storyText">' +
|
|
733
|
+
'<p>' + number(s.inputPct) + '% of your tokens are input. Output — the part you picture paying for — is ' + money(c.outputCost) + '. Cold input is only ' + money(c.coldInputCost) + '.</p>' +
|
|
734
|
+
'<p>Cache is cheap per token, so you stop worrying about it. But the volume is enormous: cache reads alone cost ' + money(c.cacheReadCost) + ', cache writes ' + money(c.cacheWriteCost) + '. You paid ' + money(c.total) + ' in API-equivalent worth, and almost all of it is the agent re-loading context, turn after turn.</p>' +
|
|
735
|
+
'</div>' +
|
|
736
|
+
'<div class="panel">' + costSplit(c) + '</div>' +
|
|
737
|
+
'</div>' +
|
|
738
|
+
'</section>' +
|
|
739
|
+
|
|
740
|
+
'<section class="section"><div class="sectionHead"><span>03</span><h2>Why the score is ' + number(cl.score) + ': it kept re-reading.</h2></div>' +
|
|
741
|
+
'<div class="twoCol">' +
|
|
742
|
+
'<div class="storyText">' +
|
|
743
|
+
(night ? '<p>Late on ' + esc(night.day) + ', around ' + esc(night.hour) + ':00, you were heads-down trying to land something. And right then your agent opened <b>' + esc(night.file) + '</b> again — the ' + number(night.priorReads) + 'th time it had pulled that file into this workspace, unchanged since the read before.</p>' : '') +
|
|
744
|
+
'<p>That is the pattern everywhere. ' + number(cl.staleReadSharePct) + '% of the content the agent pulled in was re-reading files that never changed since the last look. It re-learns your repo, your design rules, your structure — over and over — instead of remembering them.</p>' +
|
|
745
|
+
'<p>This is not your fault. The agent is stateless. Every new session is a blank page; every long session pushes earlier reads out of the window. So it reads the same files again, and you wait.</p>' +
|
|
746
|
+
'</div>' +
|
|
747
|
+
'<div class="panel">' + rereadRows(rf.topFiles) + '</div>' +
|
|
748
|
+
'</div>' +
|
|
749
|
+
(night ? '<div class="nightCard"><span>Deep-night reread</span><p><b>' + esc(night.file) + '</b> re-read at ' + esc(night.hour) + ':00 on ' + esc(night.day) + ' — the <b>' + number(night.priorReads) + 'th</b> read of that file in your workspace, unchanged since the previous one.</p></div>' : '') +
|
|
750
|
+
'</section>' +
|
|
751
|
+
|
|
752
|
+
'<section class="section"><div class="sectionHead"><span>04</span><h2>Echo remembers so it stops re-reading.</h2></div>' +
|
|
753
|
+
'<div class="twoCol">' +
|
|
754
|
+
'<div class="storyText">' +
|
|
755
|
+
'<p>I do not touch the real work — the building, the judgment, the direction. I remove the part that should never repeat: warm-up, unchanged-file rereads, re-learning your repo and your rules every session.</p>' +
|
|
756
|
+
'<p>Direct estimate from your own data: about ' + (aw.hours || 0) + ' hours of avoidable wait, roughly ' + (aw.days || 0) + ' focused days. Every file I hold becomes a compounding asset across every future session and every tool. You stop paying the re-reading tax twice.</p>' +
|
|
757
|
+
'<p id="signin-help">Connect the same EchoMem account you want memories saved to. After sign-in returns here, you choose what to extract.</p>' +
|
|
758
|
+
'</div>' +
|
|
759
|
+
'<aside class="promiseCard">' +
|
|
760
|
+
'<span>Next-week target</span>' +
|
|
761
|
+
'<strong>' + number(cl.score) + ' → ' + number(cf.targetScore) + '</strong>' +
|
|
762
|
+
'<p>Turn your repo map, hot files, design rules, and recent decisions into memory the agent never rebuilds.</p>' +
|
|
763
|
+
'<div class="promiseActions">' +
|
|
764
|
+
'<button id="signin" class="primary">' + (connected ? "Continue" : "Connect Echo") + '</button>' +
|
|
765
|
+
'<a id="signin-fallback" class="button secondary hidden" href="' + esc(authUrl) + '">Open sign-in in this tab</a>' +
|
|
766
|
+
'</div>' +
|
|
767
|
+
'</aside>' +
|
|
768
|
+
'</div>' +
|
|
769
|
+
'</section>';
|
|
770
|
+
bindConnect();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/* ---------- dashboard (post-auth extraction) ---------- */
|
|
774
|
+
function statusStrip(title, detail, kind) {
|
|
775
|
+
return '<div class="status-strip ' + esc(kind || "") + '"><span class="dot"></span><div><strong>' + esc(title) + '</strong><p>' + esc(detail) + '</p></div></div>';
|
|
776
|
+
}
|
|
777
|
+
function renderBridgeIssue(error) {
|
|
778
|
+
setHead("Local bridge issue", "Retrying");
|
|
779
|
+
app.className = "loading-panel";
|
|
780
|
+
app.innerHTML =
|
|
781
|
+
'<div style="padding:24px"><h2>Still waiting for the local bridge</h2>' +
|
|
782
|
+
'<p class="helper">Keep the terminal running. If it already exited or printed an error, close this tab and run echomem-mcp setup again.</p>' +
|
|
783
|
+
(error ? '<div class="warning" style="margin-top:16px;margin-bottom:0">' + esc(error) + '</div>' : '') + '</div>';
|
|
784
|
+
}
|
|
785
|
+
function renderLogoutPending() {
|
|
786
|
+
setHead("Signing out locally", "Working");
|
|
787
|
+
app.className = "loading-panel";
|
|
788
|
+
app.innerHTML = '<div style="padding:24px"><h2>Clearing this device token</h2><p class="helper">If the bridge is scanning local files, this can take a few seconds to answer.</p></div>';
|
|
789
|
+
}
|
|
790
|
+
function renderLogoutIssue(error) {
|
|
791
|
+
setHead("Sign out did not finish", "Needs attention");
|
|
792
|
+
app.className = "loading-panel";
|
|
793
|
+
app.innerHTML =
|
|
794
|
+
'<div style="padding:24px"><h2>The local bridge did not answer</h2>' +
|
|
795
|
+
'<p class="helper">Keep the terminal open and retry. If the command exited, close this tab and run setup again.</p>' +
|
|
796
|
+
(error ? '<div class="warning" style="margin-top:16px;margin-bottom:0">' + esc(error) + '</div>' : '') +
|
|
797
|
+
'<div class="actions"><button id="logout" class="secondary">Retry sign out</button></div></div>';
|
|
798
|
+
var lo = document.getElementById("logout"); if (lo) lo.onclick = localLogout;
|
|
799
|
+
}
|
|
800
|
+
async function waitForConnection() {
|
|
801
|
+
if (connectionPollStarted) return;
|
|
802
|
+
connectionPollStarted = true;
|
|
803
|
+
try {
|
|
804
|
+
for (;;) {
|
|
805
|
+
if (decisionMade) return;
|
|
806
|
+
try {
|
|
807
|
+
var cfg = await getJson("/config");
|
|
808
|
+
authUrl = cfg.authUrl || authUrl;
|
|
809
|
+
if (cfg.connected) { connected = true; closeAuthWindow(); await waitForStats(); return; }
|
|
810
|
+
} catch (_) {}
|
|
811
|
+
await sleep(900);
|
|
812
|
+
}
|
|
813
|
+
} finally { connectionPollStarted = false; }
|
|
814
|
+
}
|
|
815
|
+
function startConnectionPoll() { void waitForConnection(); }
|
|
816
|
+
async function waitForStats() {
|
|
817
|
+
if (statsPollStarted) return;
|
|
818
|
+
statsPollStarted = true;
|
|
819
|
+
setHead("Context Doctor", "Working");
|
|
820
|
+
app.className = "loading";
|
|
821
|
+
app.innerHTML = '<div class="skeleton"><div class="sk title"></div><div class="sk w70"></div></div><div class="sk-grid"><div class="sk-card"></div><div class="sk-card"></div><div class="sk-card"></div><div class="sk-card"></div></div>';
|
|
822
|
+
try {
|
|
823
|
+
var started = Date.now();
|
|
824
|
+
var lastError = "";
|
|
825
|
+
for (;;) {
|
|
826
|
+
if (decisionMade) return;
|
|
827
|
+
try {
|
|
828
|
+
var res = await fetchWithTimeout("/stats?nonce=" + encodeURIComponent(nonce), { credentials: "omit" }, 3000);
|
|
829
|
+
if (res.status === 200) {
|
|
830
|
+
stats = await res.json();
|
|
831
|
+
if (decisionMade) return;
|
|
832
|
+
renderDashboard();
|
|
833
|
+
if (stats && stats.partial) { await sleep(1200); continue; }
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (res.status !== 202) throw new Error(await res.text() || ("HTTP " + res.status));
|
|
837
|
+
} catch (e) {
|
|
838
|
+
lastError = e && e.message ? e.message : String(e);
|
|
839
|
+
if (!stats && Date.now() - started > 25000) renderBridgeIssue(lastError);
|
|
840
|
+
if (Date.now() - started > 20000) { statsSlow = true; if (stats) renderDashboard(); }
|
|
841
|
+
}
|
|
842
|
+
await sleep(statsSlow ? 1500 : 500);
|
|
843
|
+
}
|
|
844
|
+
} finally { statsPollStarted = false; }
|
|
845
|
+
}
|
|
846
|
+
function renderDashboard(error) {
|
|
847
|
+
var migratable = stats && stats.migratable ? stats.migratable : {};
|
|
848
|
+
var pending = stats && stats.migratable ? stats.migratable.pending : null;
|
|
849
|
+
var hasPendingCount = typeof pending === "number";
|
|
850
|
+
var discovery = stats && stats.discovery ? stats.discovery : {};
|
|
851
|
+
var isPartial = !!(stats && stats.partial);
|
|
852
|
+
var localScanReady = !isPartial || discovery.phase === "exact" || discovery.phase === "full";
|
|
853
|
+
var eta = migratable && migratable.eta ? migratable.eta : {};
|
|
854
|
+
var buckets = migrationBuckets(migratable);
|
|
855
|
+
var skippedActive = typeof migratable.skippedActive === "number" ? migratable.skippedActive : 0;
|
|
856
|
+
var knownDone = hasPendingCount && pending === 0;
|
|
857
|
+
var canExtract = hasPendingCount && pending > 0;
|
|
858
|
+
var sessions = stats && stats.sessions ? stats.sessions : {};
|
|
859
|
+
setHead(knownDone ? "History is ready" : "Context Doctor", localScanReady ? "Ready" : "Scanning");
|
|
860
|
+
var heroValue = knownDone ? "Complete" : (eta.estimatedLabel || migratable.estimatedLabel || "checking");
|
|
861
|
+
// Size profile: fold the char buckets into a simple small / medium / large summary.
|
|
862
|
+
var tierOf = function (keys) {
|
|
863
|
+
var count = 0, chars = 0;
|
|
864
|
+
keys.forEach(function (k) {
|
|
865
|
+
var b = buckets.filter(function (x) { return x && x.key === k; })[0];
|
|
866
|
+
if (b) { count += b.count || 0; chars += b.chars || 0; }
|
|
867
|
+
});
|
|
868
|
+
return { count: count, chars: chars };
|
|
869
|
+
};
|
|
870
|
+
var smallT = tierOf(["small"]);
|
|
871
|
+
var mediumT = tierOf(["routine", "large"]);
|
|
872
|
+
var largeT = tierOf(["veryLarge", "huge", "massive"]);
|
|
873
|
+
var convWord = pending === 1 ? "conversation" : "conversations";
|
|
874
|
+
var heroTitle = knownDone
|
|
875
|
+
? "Your local AI history is already covered."
|
|
876
|
+
: (pending === 1 ? "Echo found one conversation ready for memory." : "Echo found local AI history it can turn into memory.");
|
|
877
|
+
var subtext;
|
|
878
|
+
if (knownDone) {
|
|
879
|
+
subtext = skippedActive
|
|
880
|
+
? skippedActive.toLocaleString() + " active conversation" + (skippedActive === 1 ? " is" : "s are") + " still changing and will be picked up later."
|
|
881
|
+
: "Your historical conversations are already in this account.";
|
|
882
|
+
} else if (hasPendingCount) {
|
|
883
|
+
subtext = number(pending) + " " + convWord + " ready to extract — already-processed ones are skipped, so re-running stays incremental.";
|
|
884
|
+
} else {
|
|
885
|
+
subtext = "Checking your local history…";
|
|
886
|
+
}
|
|
887
|
+
var candidateDetail = knownDone ? "Nothing to extract right now." : number(pending) + " " + convWord + (localScanReady ? " · ready" : " · still sizing");
|
|
888
|
+
app.className = "";
|
|
889
|
+
app.innerHTML =
|
|
890
|
+
(statsSlow && !stats ? '<div class="warning">Local counts are taking longer than expected. You can still ask the bridge to extract anything unprocessed.</div>' : '') +
|
|
891
|
+
(error ? '<div class="error">' + esc(error) + '</div>' : '') +
|
|
892
|
+
'<section class="privacy"><span>Local scan</span><p>Echo checked your Codex and Claude Code history on this device. No transcripts leave this machine unless you start extraction.</p></section>' +
|
|
893
|
+
'<section class="doctor-hero">' +
|
|
894
|
+
'<div class="doctor-copy"><h2>' + esc(heroTitle) + '</h2><p>' + esc(subtext) + '</p></div>' +
|
|
895
|
+
'<aside class="doctor-score"><span>' + esc(knownDone ? "Extraction complete" : "Estimated extraction time") + '</span><strong>' + esc(heroValue) + '</strong><p>' + esc(candidateDetail) + '</p>' +
|
|
896
|
+
'<div class="doctor-mini">' +
|
|
897
|
+
'<div class="mini-row"><span>Small ≤30k</span><strong>' + esc(number(smallT.count)) + ' · ' + esc(compact(smallT.chars)) + '</strong></div>' +
|
|
898
|
+
'<div class="mini-row"><span>Medium 30k–350k</span><strong>' + esc(number(mediumT.count)) + ' · ' + esc(compact(mediumT.chars)) + '</strong></div>' +
|
|
899
|
+
'<div class="mini-row"><span>Large >350k</span><strong>' + esc(number(largeT.count)) + ' · ' + esc(compact(largeT.chars)) + '</strong></div>' +
|
|
900
|
+
'</div>' +
|
|
901
|
+
'</aside>' +
|
|
902
|
+
'</section>' +
|
|
903
|
+
'<div class="doctor-stat-strip">' +
|
|
904
|
+
metric("Sessions found", number(sessions.total), number(sessions.codex) + " Codex + " + number(sessions.claudeCode) + " Claude Code") +
|
|
905
|
+
metric("Memories captured", countOrChecking(stats && stats.memoriesCaptured), "already in EchoMem") +
|
|
906
|
+
'</div>' +
|
|
907
|
+
'<div class="actions doctor-actions">' +
|
|
908
|
+
'<button id="migrate" class="primary" ' + (canExtract ? "" : "disabled") + '>Extract conversations</button>' +
|
|
909
|
+
'<button id="skip" class="secondary">' + (knownDone ? "Finish setup" : "Not today") + '</button>' +
|
|
910
|
+
'<button id="logout" class="secondary">Sign out locally</button>' +
|
|
911
|
+
'</div>';
|
|
912
|
+
document.getElementById("migrate").onclick = startMigrate;
|
|
913
|
+
document.getElementById("skip").onclick = skip;
|
|
914
|
+
document.getElementById("logout").onclick = localLogout;
|
|
915
|
+
}
|
|
916
|
+
async function startMigrate() {
|
|
917
|
+
if (decisionMade) return;
|
|
918
|
+
decisionMade = true;
|
|
919
|
+
setHead("Extracting", "Starting");
|
|
920
|
+
app.className = "";
|
|
921
|
+
app.innerHTML = '<section class="privacy"><span>Extracting</span><p>Starting your extraction. Echo sizes the remaining jobs, then imports automatically — keep this terminal open.</p></section><div class="loading" style="padding:18px 0"><div class="skeleton"><div class="sk title"></div><div class="sk w70"></div></div></div>';
|
|
922
|
+
try {
|
|
923
|
+
var started = await postJson("/migrate", {});
|
|
924
|
+
renderProgress(Object.assign({ status: "starting", completed: 0, failed: 0, extracted: 0 }, started));
|
|
925
|
+
await pollProgress();
|
|
926
|
+
} catch (e) {
|
|
927
|
+
decisionMade = false;
|
|
928
|
+
renderDashboard(routeError(e && e.message));
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async function pollProgress() {
|
|
932
|
+
for (;;) {
|
|
933
|
+
var progress = await getJson("/progress");
|
|
934
|
+
renderProgress(progress);
|
|
935
|
+
if (progress.status === "completed" || progress.status === "failed") return;
|
|
936
|
+
await sleep(1200);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function renderProgress(progress) {
|
|
940
|
+
var status = progress.status;
|
|
941
|
+
var done = status === "completed";
|
|
942
|
+
var stopped = status === "failed";
|
|
943
|
+
var total = progress.total || progress.jobCount || 0;
|
|
944
|
+
var completed = progress.completed || 0;
|
|
945
|
+
var failed = progress.failed || 0;
|
|
946
|
+
var running = progress.running || 0;
|
|
947
|
+
var queued = progress.queued || 0;
|
|
948
|
+
var extracted = progress.extracted || 0;
|
|
949
|
+
var pct = total > 0 ? Math.round((completed / total) * 100) : (done ? 100 : 0);
|
|
950
|
+
app.className = "";
|
|
951
|
+
if (done || stopped) {
|
|
952
|
+
setHead(done ? "Memory captured" : "Extraction stopped", done ? "Done" : "Needs attention");
|
|
953
|
+
app.innerHTML =
|
|
954
|
+
'<section class="finish">' +
|
|
955
|
+
'<div class="finishMark ' + (done ? "ok" : "warn") + '">' + (done ? "✓" : "!") + '</div>' +
|
|
956
|
+
'<strong class="finishBig">' + esc(number(extracted)) + '</strong>' +
|
|
957
|
+
'<span class="finishLabel">' + (extracted === 1 ? "memory" : "memories") + ' captured</span>' +
|
|
958
|
+
'<p>' + (done
|
|
959
|
+
? "from " + esc(number(completed)) + " conversation" + (completed === 1 ? "" : "s") + ". Your next coding session will recall instead of re-reading — and it keeps compounding."
|
|
960
|
+
: (String(progress.error) === "key-expired"
|
|
961
|
+
? "Your encryption key wasn't accepted. Re-run extraction; if it keeps failing, run <code>echomem-mcp unlock</code> first. Already-done conversations are skipped."
|
|
962
|
+
: esc(progress.error || "Some conversations did not finish.") + " Re-run <code>echomem-mcp migrate</code> to finish the rest; already-done conversations are skipped.")) + '</p>' +
|
|
963
|
+
(done && failed ? '<p class="helper">' + esc(number(failed)) + ' could not be extracted.</p>' : "") +
|
|
964
|
+
'</section>';
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (status === "starting" || status === "preparing") {
|
|
968
|
+
// Prep phase — no conversation has been sent yet. Name the specific step we're on
|
|
969
|
+
// (with a moving bar + stepper) so this window reads as active work, not a frozen "0 of N".
|
|
970
|
+
setHead("Preparing", "Preparing");
|
|
971
|
+
var note = progress.latest || progress.message || "";
|
|
972
|
+
var activeStep = /creat/i.test(note) ? 1 : 0;
|
|
973
|
+
var prepLabels = [
|
|
974
|
+
"Reading your local sessions",
|
|
975
|
+
"Creating your EchoMem import session",
|
|
976
|
+
"Queuing conversations to extract",
|
|
977
|
+
];
|
|
978
|
+
var stepsHtml = "";
|
|
979
|
+
for (var i = 0; i < prepLabels.length; i++) {
|
|
980
|
+
var stepCls = i < activeStep ? "done" : (i === activeStep ? "active" : "");
|
|
981
|
+
stepsHtml += '<li class="' + stepCls + '"><span class="dot"></span><span>' + esc(prepLabels[i]) + '</span></li>';
|
|
982
|
+
}
|
|
983
|
+
app.innerHTML =
|
|
984
|
+
'<section class="privacy"><span>Preparing</span><p>Nothing has left your machine yet — Echo is getting your sessions ready locally. This takes a few seconds before extraction starts.</p></section>' +
|
|
985
|
+
'<section class="doctor-hero">' +
|
|
986
|
+
'<div class="doctor-copy">' +
|
|
987
|
+
'<p class="label">Now working on</p>' +
|
|
988
|
+
'<h2>' + esc(prepLabels[activeStep]) + '…</h2>' +
|
|
989
|
+
'<div class="track indef"><div class="fill"></div></div>' +
|
|
990
|
+
'<ul class="prep-steps">' + stepsHtml + '</ul>' +
|
|
991
|
+
'</div>' +
|
|
992
|
+
'<aside class="doctor-score">' +
|
|
993
|
+
'<span>Conversations</span>' +
|
|
994
|
+
'<strong>' + (total ? esc(number(total)) : "…") + '</strong>' +
|
|
995
|
+
'<p>found locally</p>' +
|
|
996
|
+
'</aside>' +
|
|
997
|
+
'</section>';
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
setHead("Extracting", "Extracting");
|
|
1001
|
+
app.innerHTML =
|
|
1002
|
+
'<section class="privacy"><span>Extracting</span><p>Echo is turning your history into memory. Keep this terminal open — it advances while the bridge runs. You can start using EchoMem in your editor now.</p></section>' +
|
|
1003
|
+
'<section class="doctor-hero">' +
|
|
1004
|
+
'<div class="doctor-copy">' +
|
|
1005
|
+
'<p class="label">Extraction progress</p>' +
|
|
1006
|
+
'<h2>' + (total ? esc(number(completed)) + ' of ' + esc(number(total)) : "Starting…") + '</h2>' +
|
|
1007
|
+
'<div class="track"><div class="fill" style="width:' + pct + '%"></div></div>' +
|
|
1008
|
+
'<div class="counts"><span>' + esc(number(completed)) + ' done</span><span>' + esc(number(running)) + ' running</span><span>' + esc(number(queued)) + ' queued</span>' + (failed ? '<span>' + esc(number(failed)) + ' failed</span>' : "") + '</div>' +
|
|
1009
|
+
(progress.latest ? '<p class="helper" style="margin-top:10px">Now: ' + esc(progress.latest) + '</p>' : "") +
|
|
1010
|
+
(progress.capped ? '<p class="helper">Importing the newest ' + esc(number(progress.capped)) + '; re-run <code>echomem-mcp migrate</code> later for older sessions.</p>' : "") +
|
|
1011
|
+
'</div>' +
|
|
1012
|
+
'<aside class="doctor-score">' +
|
|
1013
|
+
'<span>Memories captured</span>' +
|
|
1014
|
+
'<strong>' + esc(number(extracted)) + '</strong>' +
|
|
1015
|
+
'<p>' + (extracted ? "and growing" : "starting…") + '</p>' +
|
|
1016
|
+
'</aside>' +
|
|
1017
|
+
'</section>';
|
|
1018
|
+
}
|
|
1019
|
+
async function skip() {
|
|
1020
|
+
if (decisionMade) return;
|
|
1021
|
+
decisionMade = true;
|
|
1022
|
+
try {
|
|
1023
|
+
await postJson("/skip", {});
|
|
1024
|
+
setHead("Setup complete", "Done");
|
|
1025
|
+
app.className = "notice";
|
|
1026
|
+
app.innerHTML = '<h2 style="color:#203820">All set</h2><p style="margin-top:10px">You can extract unprocessed conversations later with echomem-mcp migrate.</p>';
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
decisionMade = false;
|
|
1029
|
+
renderDashboard(e && e.message);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async function localLogout() {
|
|
1033
|
+
if (decisionMade) return;
|
|
1034
|
+
decisionMade = true;
|
|
1035
|
+
renderLogoutPending();
|
|
1036
|
+
try {
|
|
1037
|
+
await postJson("/logout", {}, 12000);
|
|
1038
|
+
stats = null;
|
|
1039
|
+
statsSlow = false;
|
|
1040
|
+
connected = false;
|
|
1041
|
+
decisionMade = false;
|
|
1042
|
+
renderForensicReport("Signed out locally. Connect EchoMem again to extract.");
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
decisionMade = false;
|
|
1045
|
+
renderLogoutIssue(e && e.message ? e.message : String(e));
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/* ---------- scan-first poll for the forensic report ---------- */
|
|
1050
|
+
async function showReport() {
|
|
1051
|
+
if (reportPollStarted) return;
|
|
1052
|
+
reportPollStarted = true;
|
|
1053
|
+
if (!report) renderLoading();
|
|
1054
|
+
try {
|
|
1055
|
+
var fails = 0;
|
|
1056
|
+
for (;;) {
|
|
1057
|
+
if (decisionMade) return;
|
|
1058
|
+
try {
|
|
1059
|
+
var res = await fetchWithTimeout("/report?nonce=" + encodeURIComponent(nonce), { credentials: "omit" }, 12000);
|
|
1060
|
+
if (res.status === 200) { report = await res.json(); renderForensicReport(); return; }
|
|
1061
|
+
if (res.status === 202) {
|
|
1062
|
+
fails = 0;
|
|
1063
|
+
var prog = await res.json().catch(function () { return null; });
|
|
1064
|
+
renderLoading(prog);
|
|
1065
|
+
} else {
|
|
1066
|
+
throw new Error(await res.text() || ("HTTP " + res.status));
|
|
1067
|
+
}
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
// Only a RUN of consecutive failures is a real bridge problem. A long scan that keeps
|
|
1070
|
+
// answering 202 must never trip this (that was the old false "Local bridge issue").
|
|
1071
|
+
fails++;
|
|
1072
|
+
if (!report && fails >= 8) renderBridgeIssue(e && e.message ? e.message : String(e));
|
|
1073
|
+
}
|
|
1074
|
+
await sleep(700);
|
|
1075
|
+
}
|
|
1076
|
+
} finally { reportPollStarted = false; }
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
window.addEventListener("message", function (event) {
|
|
1080
|
+
if (event.origin !== location.origin) return;
|
|
1081
|
+
var data = event.data || {};
|
|
1082
|
+
if (data.type === "echomem:connected" && data.nonce === nonce) {
|
|
1083
|
+
connected = true;
|
|
1084
|
+
closeAuthWindow();
|
|
1085
|
+
void waitForStats();
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
async function init() {
|
|
1089
|
+
if (!nonce) throw new Error("Missing setup nonce. Re-run echomem-mcp setup.");
|
|
1090
|
+
if (params.get("connected") === "1") {
|
|
1091
|
+
connected = true;
|
|
1092
|
+
if (notifyOpenerConnected()) {
|
|
1093
|
+
// Popup flow: hand the main tab back control, then this popup closes.
|
|
1094
|
+
setHead("EchoMem connected", "Done");
|
|
1095
|
+
app.className = "notice";
|
|
1096
|
+
app.innerHTML = '<h2 style="color:#203820">This device is connected</h2><p style="margin-top:10px">Returning to the local setup page...</p>';
|
|
1097
|
+
} else {
|
|
1098
|
+
// Same-tab fallback (popup was blocked): go straight to the extraction dashboard.
|
|
1099
|
+
void waitForStats();
|
|
1100
|
+
}
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
// Learn the auth URL + connection state, but don't block the local scan on it.
|
|
1104
|
+
getJson("/config").then(function (cfg) {
|
|
1105
|
+
authUrl = cfg.authUrl || authUrl;
|
|
1106
|
+
connected = !!cfg.connected;
|
|
1107
|
+
}).catch(function () {});
|
|
1108
|
+
// Scan-first: show the local forensic report before asking the user to connect.
|
|
1109
|
+
await showReport();
|
|
1110
|
+
}
|
|
1111
|
+
init().catch(function (e) {
|
|
1112
|
+
setHead("Connection issue", "Needs attention");
|
|
1113
|
+
app.className = "error";
|
|
1114
|
+
app.textContent = e && e.message ? e.message : String(e);
|
|
1115
|
+
});
|
|
1116
|
+
})();
|
|
1117
|
+
</script>
|
|
1118
|
+
</body>
|
|
1119
|
+
</html>`;
|
|
1120
|
+
}
|