@echomem/mcp 1.4.14 → 1.4.16
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/billing-alert.js +39 -0
- package/dist/hud/diagnostics.js +135 -0
- package/dist/hud/server.js +110 -0
- package/dist/hud/web.js +250 -0
- package/dist/index.js +154 -32
- package/dist/setup-page/client-core.js +2 -0
- package/dist/setup-page/client-extraction.js +101 -4
- package/dist/setup-page/client-lifecycle.js +71 -5
- package/dist/setup-page/document.js +1 -1
- package/dist/setup-page/styles-extraction.js +211 -0
- package/dist/setup.js +112 -40
- package/dist/v1-contract.js +8 -7
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { echoConfigDir } from "./keystore.js";
|
|
4
|
+
const ALERT_FILE = "billing-alert.json";
|
|
5
|
+
const DEFAULT_MAX_AGE_MS = 31 * 24 * 60 * 60 * 1000;
|
|
6
|
+
function alertPath() {
|
|
7
|
+
return path.join(echoConfigDir(), ALERT_FILE);
|
|
8
|
+
}
|
|
9
|
+
export function writeBillingAlert(alert) {
|
|
10
|
+
try {
|
|
11
|
+
fs.mkdirSync(echoConfigDir(), { recursive: true, mode: 0o700 });
|
|
12
|
+
fs.writeFileSync(alertPath(), JSON.stringify({ ...alert, updatedAt: new Date().toISOString() }, null, 2), { mode: 0o600 });
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
/* best-effort: billing UI should never break search */
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function readBillingAlert(maxAgeMs = DEFAULT_MAX_AGE_MS) {
|
|
19
|
+
try {
|
|
20
|
+
const alert = JSON.parse(fs.readFileSync(alertPath(), "utf8"));
|
|
21
|
+
const updatedAt = Date.parse(alert.updatedAt || "");
|
|
22
|
+
if (!updatedAt || Date.now() - updatedAt > maxAgeMs)
|
|
23
|
+
return null;
|
|
24
|
+
if (!alert.message || !alert.pricingUrl)
|
|
25
|
+
return null;
|
|
26
|
+
return alert;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function clearBillingAlert() {
|
|
33
|
+
try {
|
|
34
|
+
fs.unlinkSync(alertPath());
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* absent is fine */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { echoConfigDir, KeyStore } from "../keystore.js";
|
|
5
|
+
const API_BASE = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
6
|
+
const EVENTS_TAIL_LINES = 400;
|
|
7
|
+
function record(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
: null;
|
|
11
|
+
}
|
|
12
|
+
function number(value) {
|
|
13
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
14
|
+
}
|
|
15
|
+
function text(value) {
|
|
16
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
17
|
+
}
|
|
18
|
+
function briefError(error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error || "Connection failed");
|
|
20
|
+
return message
|
|
21
|
+
.replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [redacted]")
|
|
22
|
+
.replace(/ec_[A-Za-z0-9._-]+/g, "ec_[redacted]")
|
|
23
|
+
.replace(/\s+/g, " ")
|
|
24
|
+
.slice(0, 180);
|
|
25
|
+
}
|
|
26
|
+
function normalizePlan(value) {
|
|
27
|
+
const plan = text(value)?.toLowerCase();
|
|
28
|
+
return plan && ["free", "pro", "power", "team", "enterprise"].includes(plan) ? plan : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Parse the account bootstrap contract without importing app-only code into the npm package. */
|
|
31
|
+
export function accountDiagnosticsFromBootstrap(payload) {
|
|
32
|
+
const root = record(payload) ?? {};
|
|
33
|
+
const usage = record(root.usage) ?? {};
|
|
34
|
+
const quota = record(root.memorySearchQuota) ?? {};
|
|
35
|
+
const weeklySearchLimit = number(quota.limit);
|
|
36
|
+
const searchesThisWeek = number(quota.used);
|
|
37
|
+
const searchesRemaining = quota.remaining === null ? null : number(quota.remaining);
|
|
38
|
+
return {
|
|
39
|
+
plan: normalizePlan(root.plan),
|
|
40
|
+
weeklySearchLimit,
|
|
41
|
+
searchesThisWeek,
|
|
42
|
+
searchesRemaining,
|
|
43
|
+
resetAt: text(quota.resetAt),
|
|
44
|
+
memoryCount: number(usage.memories),
|
|
45
|
+
sourceCount: number(usage.sources),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Read only the latest memory-search trace; never return a query, token, or memory content. */
|
|
49
|
+
export function latestSearchTraceFromEvents(lines) {
|
|
50
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
51
|
+
try {
|
|
52
|
+
const event = record(JSON.parse(lines[index] || ""));
|
|
53
|
+
if (!event || event.type !== "tool_call" || event.tool !== "search_memories")
|
|
54
|
+
continue;
|
|
55
|
+
const at = text(event.ts);
|
|
56
|
+
if (!at)
|
|
57
|
+
continue;
|
|
58
|
+
return {
|
|
59
|
+
at,
|
|
60
|
+
ok: typeof event.ok === "boolean" ? event.ok : undefined,
|
|
61
|
+
requestId: text(event.request_id),
|
|
62
|
+
endpoint: text(event.endpoint),
|
|
63
|
+
httpStatus: number(event.http_status),
|
|
64
|
+
errorCode: text(event.error_code),
|
|
65
|
+
errorKind: text(event.error_kind),
|
|
66
|
+
latencyMs: number(event.latency_ms),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// A concurrently appended partial line or old malformed record is safely ignored.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
function loadLatestSearchTrace() {
|
|
76
|
+
try {
|
|
77
|
+
const file = path.join(echoConfigDir(), "events.jsonl");
|
|
78
|
+
return latestSearchTraceFromEvents(fs.readFileSync(file, "utf8").split("\n").slice(-EVENTS_TAIL_LINES));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Read-only, local HUD diagnostics. Credentials are used only in the Authorization header to
|
|
86
|
+
* EchoMem's authenticated account endpoint; this never sends transcripts, queries, or memories.
|
|
87
|
+
*/
|
|
88
|
+
export async function loadHudDiagnostics() {
|
|
89
|
+
const updatedAt = new Date().toISOString();
|
|
90
|
+
const lastSearch = loadLatestSearchTrace();
|
|
91
|
+
const token = new KeyStore().getToken();
|
|
92
|
+
if (!token) {
|
|
93
|
+
return { updatedAt, account: { state: "not_logged_in" }, accountRequest: {}, lastSearch };
|
|
94
|
+
}
|
|
95
|
+
const requestId = `hud-account-${randomUUID()}`;
|
|
96
|
+
const startedAt = Date.now();
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(`${API_BASE}/api/extension/account/bootstrap`, {
|
|
99
|
+
headers: {
|
|
100
|
+
Authorization: `Bearer ${token}`,
|
|
101
|
+
"X-EchoMem-Request-Id": requestId,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const payload = await response.json().catch(() => ({}));
|
|
105
|
+
const telemetry = record(payload)?.telemetry;
|
|
106
|
+
const echoedRequestId = text(record(telemetry)?.requestId) ?? requestId;
|
|
107
|
+
const accountRequest = {
|
|
108
|
+
requestId: echoedRequestId,
|
|
109
|
+
httpStatus: response.status,
|
|
110
|
+
elapsedMs: Date.now() - startedAt,
|
|
111
|
+
};
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
return {
|
|
114
|
+
updatedAt,
|
|
115
|
+
account: { state: "error", error: `Account check returned HTTP ${response.status}` },
|
|
116
|
+
accountRequest,
|
|
117
|
+
lastSearch,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
updatedAt,
|
|
122
|
+
account: { state: "connected", ...accountDiagnosticsFromBootstrap(payload) },
|
|
123
|
+
accountRequest,
|
|
124
|
+
lastSearch,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
updatedAt,
|
|
130
|
+
account: { state: "error", error: briefError(error) },
|
|
131
|
+
accountRequest: { requestId, elapsedMs: Date.now() - startedAt },
|
|
132
|
+
lastSearch,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
package/dist/hud/server.js
CHANGED
|
@@ -6,10 +6,13 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { HudMonitor } from "./monitor.js";
|
|
7
7
|
import { HUD_HTML } from "./web.js";
|
|
8
8
|
import { buildCapsuleText } from "./capsule.js";
|
|
9
|
+
import { loadHudDiagnostics } from "./diagnostics.js";
|
|
9
10
|
import { homePath, newestFile, walkFiles } from "./fs.js";
|
|
10
11
|
import { KeyStore } from "../keystore.js";
|
|
11
12
|
import { assembleCodex, assembleClaude } from "../migrate.js";
|
|
13
|
+
import { readBillingAlert } from "../billing-alert.js";
|
|
12
14
|
const API_BASE = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
15
|
+
const PRICING_URL = (process.env.ECHO_PRICING_URL || "https://echoknows.com/pricing").replace(/\/$/, "");
|
|
13
16
|
export async function createHudServer(opts = {}) {
|
|
14
17
|
const mode = opts.mode || "auto";
|
|
15
18
|
const port = opts.port ?? 17377;
|
|
@@ -42,6 +45,50 @@ export async function createHudServer(opts = {}) {
|
|
|
42
45
|
res.end(JSON.stringify(latest, null, 2));
|
|
43
46
|
return;
|
|
44
47
|
}
|
|
48
|
+
if (url.pathname === "/billing-status") {
|
|
49
|
+
handleBillingStatus(res).catch((error) => {
|
|
50
|
+
if (res.writableEnded)
|
|
51
|
+
return;
|
|
52
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
53
|
+
res.end(JSON.stringify({ ok: false, state: "unknown", reason: error instanceof Error ? error.message : "billing status failed" }));
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (url.pathname === "/open-external") {
|
|
58
|
+
const target = url.searchParams.get("url") || "";
|
|
59
|
+
let ok = false;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = new URL(target);
|
|
62
|
+
if ((parsed.protocol === "https:" || parsed.protocol === "http:") && typeof opts.onOpenExternal === "function") {
|
|
63
|
+
opts.onOpenExternal(parsed.toString());
|
|
64
|
+
ok = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
ok = false;
|
|
69
|
+
}
|
|
70
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
71
|
+
res.end(JSON.stringify({ ok, reason: ok ? undefined : opts.onOpenExternal ? "invalid_url" : "no_desktop" }));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (url.pathname === "/diagnostics") {
|
|
75
|
+
loadHudDiagnostics().then((diagnostics) => {
|
|
76
|
+
if (res.writableEnded)
|
|
77
|
+
return;
|
|
78
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
79
|
+
res.end(JSON.stringify(diagnostics));
|
|
80
|
+
}).catch(() => {
|
|
81
|
+
if (res.writableEnded)
|
|
82
|
+
return;
|
|
83
|
+
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
84
|
+
res.end(JSON.stringify({
|
|
85
|
+
updatedAt: new Date().toISOString(),
|
|
86
|
+
account: { state: "error", error: "Diagnostics unavailable" },
|
|
87
|
+
accountRequest: {},
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
45
92
|
if (url.pathname === "/select") {
|
|
46
93
|
const id = url.searchParams.get("id");
|
|
47
94
|
monitor.pin(id && id !== "auto" ? id : null);
|
|
@@ -245,6 +292,69 @@ export async function createHudServer(opts = {}) {
|
|
|
245
292
|
}),
|
|
246
293
|
};
|
|
247
294
|
}
|
|
295
|
+
async function handleBillingStatus(res) {
|
|
296
|
+
const respond = (obj) => {
|
|
297
|
+
if (res.writableEnded)
|
|
298
|
+
return;
|
|
299
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
300
|
+
res.end(JSON.stringify(obj));
|
|
301
|
+
};
|
|
302
|
+
const pricingUrl = appendSource(PRICING_URL, "hud");
|
|
303
|
+
const alert = readBillingAlert();
|
|
304
|
+
const token = new KeyStore().getToken();
|
|
305
|
+
if (!token) {
|
|
306
|
+
return respond({ ok: false, state: "not_connected", pricingUrl });
|
|
307
|
+
}
|
|
308
|
+
let plan = "unknown";
|
|
309
|
+
let paid = false;
|
|
310
|
+
try {
|
|
311
|
+
const response = await fetch(`${API_BASE}/api/extension/account/bootstrap`, {
|
|
312
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
313
|
+
});
|
|
314
|
+
const data = (await response.json().catch(() => ({})));
|
|
315
|
+
if (response.ok) {
|
|
316
|
+
plan = typeof data.plan === "string" ? data.plan.toLowerCase() : "free";
|
|
317
|
+
paid = ["pro", "power", "team", "enterprise"].includes(plan);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
/* Local alert still gives the HUD something useful. */
|
|
322
|
+
}
|
|
323
|
+
if (alert && (alert.kind === "quota_exceeded" || !paid)) {
|
|
324
|
+
return respond({
|
|
325
|
+
ok: true,
|
|
326
|
+
state: alert.kind,
|
|
327
|
+
plan: alert.plan || plan,
|
|
328
|
+
message: alert.kind === "quota_exceeded" ? "Recall limit reached." : "Recall needs a plan.",
|
|
329
|
+
detail: alert.message,
|
|
330
|
+
code: alert.code,
|
|
331
|
+
pricingUrl: appendSource(alert.pricingUrl || pricingUrl, "hud"),
|
|
332
|
+
updatedAt: alert.updatedAt,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (!paid && plan === "free") {
|
|
336
|
+
return respond({
|
|
337
|
+
ok: true,
|
|
338
|
+
state: "plan_required",
|
|
339
|
+
plan,
|
|
340
|
+
message: "Recall needs a plan.",
|
|
341
|
+
detail: "Search and recall require Echo Pro or Power. Saving conversations keeps working.",
|
|
342
|
+
pricingUrl,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
respond({ ok: true, state: "ok", plan, paid, pricingUrl });
|
|
346
|
+
}
|
|
347
|
+
function appendSource(url, source) {
|
|
348
|
+
try {
|
|
349
|
+
const parsed = new URL(url);
|
|
350
|
+
if (!parsed.searchParams.has("source"))
|
|
351
|
+
parsed.searchParams.set("source", source);
|
|
352
|
+
return parsed.toString();
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return url;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
248
358
|
async function handleCheckpointStatus(latest, res) {
|
|
249
359
|
const respond = (obj) => {
|
|
250
360
|
if (res.writableEnded)
|
package/dist/hud/web.js
CHANGED
|
@@ -16,6 +16,10 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
16
16
|
--brick: #c7372f;
|
|
17
17
|
--green: #43d17a;
|
|
18
18
|
--amber: #e9b949;
|
|
19
|
+
--billing-free-bg: #fff8d9;
|
|
20
|
+
--billing-free-border: rgba(190, 128, 19, 0.36);
|
|
21
|
+
--billing-free: #c08412;
|
|
22
|
+
--billing-free-dark: #87560d;
|
|
19
23
|
--line: rgba(26, 58, 143, 0.22);
|
|
20
24
|
--track: rgba(26, 58, 143, 0.1);
|
|
21
25
|
--shadow: drop-shadow(0 2px 7px rgba(26, 58, 143, 0.12));
|
|
@@ -217,6 +221,32 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
217
221
|
cursor: grab;
|
|
218
222
|
}
|
|
219
223
|
#hud.open .details { display: block; }
|
|
224
|
+
.diagnostics {
|
|
225
|
+
margin: 10px 0 0;
|
|
226
|
+
padding: 10px 11px;
|
|
227
|
+
border: 1px solid rgba(26, 58, 143, 0.18);
|
|
228
|
+
border-radius: 10px;
|
|
229
|
+
background: rgba(255, 255, 255, 0.72);
|
|
230
|
+
}
|
|
231
|
+
.diagnostics-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
232
|
+
.diagnostics-title { font-size: 10px; font-weight: 820; letter-spacing: .02em; color: var(--blue); }
|
|
233
|
+
.diagnostics-refresh {
|
|
234
|
+
border: 0;
|
|
235
|
+
padding: 0;
|
|
236
|
+
background: transparent;
|
|
237
|
+
color: var(--faint);
|
|
238
|
+
font-family: inherit;
|
|
239
|
+
font-size: 10px;
|
|
240
|
+
font-weight: 730;
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
}
|
|
243
|
+
.diagnostics-refresh:hover { color: var(--blue); }
|
|
244
|
+
.diagnostics-body { display: grid; gap: 5px; margin-top: 7px; }
|
|
245
|
+
.diagnostics-line { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 10px; line-height: 1.25; }
|
|
246
|
+
.diagnostics-k { flex: 0 0 auto; color: var(--faint); font-weight: 650; }
|
|
247
|
+
.diagnostics-v { min-width: 0; overflow: hidden; text-align: right; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-family: "JetBrains Mono", ui-monospace, monospace; font-size: 9.5px; font-weight: 650; }
|
|
248
|
+
.diagnostics-v.bad { color: var(--brick); }
|
|
249
|
+
.diagnostics-v.wrap { white-space: normal; overflow-wrap: anywhere; }
|
|
220
250
|
/* The current session is the one object every number and action belongs to — one card. */
|
|
221
251
|
.now-card {
|
|
222
252
|
border: 1px solid rgba(26, 58, 143, 0.24);
|
|
@@ -504,6 +534,75 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
504
534
|
background: rgba(199, 55, 47, 0.07);
|
|
505
535
|
color: #7a241e;
|
|
506
536
|
}
|
|
537
|
+
.billing-banner {
|
|
538
|
+
display: none;
|
|
539
|
+
margin: 0 0 12px;
|
|
540
|
+
border: 1.4px solid var(--billing-free-border);
|
|
541
|
+
border-radius: 12px;
|
|
542
|
+
background: linear-gradient(180deg, var(--billing-free-bg), #fff);
|
|
543
|
+
padding: 10px;
|
|
544
|
+
color: var(--muted);
|
|
545
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82);
|
|
546
|
+
}
|
|
547
|
+
.billing-banner.limit {
|
|
548
|
+
border-color: rgba(255, 95, 87, 0.45);
|
|
549
|
+
background: linear-gradient(180deg, #fff3f2, #fff);
|
|
550
|
+
}
|
|
551
|
+
.billing-top {
|
|
552
|
+
display: flex;
|
|
553
|
+
align-items: center;
|
|
554
|
+
gap: 8px;
|
|
555
|
+
}
|
|
556
|
+
.billing-dot {
|
|
557
|
+
width: 10px;
|
|
558
|
+
height: 10px;
|
|
559
|
+
flex: 0 0 auto;
|
|
560
|
+
border-radius: 999px;
|
|
561
|
+
background: var(--billing-free);
|
|
562
|
+
box-shadow: 0 0 0 4px rgba(190, 128, 19, 0.12);
|
|
563
|
+
}
|
|
564
|
+
.billing-banner.limit .billing-dot {
|
|
565
|
+
background: #ff5f57;
|
|
566
|
+
box-shadow: 0 0 0 4px rgba(255, 95, 87, 0.12);
|
|
567
|
+
}
|
|
568
|
+
.billing-title {
|
|
569
|
+
min-width: 0;
|
|
570
|
+
flex: 1;
|
|
571
|
+
color: var(--billing-free-dark);
|
|
572
|
+
font-size: 13px;
|
|
573
|
+
font-weight: 900;
|
|
574
|
+
line-height: 1.15;
|
|
575
|
+
}
|
|
576
|
+
.billing-banner.limit .billing-title { color: #7a241e; }
|
|
577
|
+
.billing-copy {
|
|
578
|
+
display: block;
|
|
579
|
+
margin: 8px 0 0 18px;
|
|
580
|
+
font-size: 11.5px;
|
|
581
|
+
line-height: 1.35;
|
|
582
|
+
font-weight: 650;
|
|
583
|
+
}
|
|
584
|
+
.billing-link {
|
|
585
|
+
display: inline-flex;
|
|
586
|
+
align-items: center;
|
|
587
|
+
justify-content: center;
|
|
588
|
+
min-height: 28px;
|
|
589
|
+
border: 1px solid var(--billing-free-dark);
|
|
590
|
+
border-radius: 999px;
|
|
591
|
+
background: var(--billing-free);
|
|
592
|
+
color: #fff;
|
|
593
|
+
padding: 0 13px;
|
|
594
|
+
font-family: inherit;
|
|
595
|
+
font-size: 11px;
|
|
596
|
+
font-weight: 900;
|
|
597
|
+
cursor: pointer;
|
|
598
|
+
box-shadow: 0 12px 20px -16px rgba(135, 86, 13, 0.95);
|
|
599
|
+
}
|
|
600
|
+
.billing-banner.limit .billing-link {
|
|
601
|
+
border-color: #7a241e;
|
|
602
|
+
background: #7a241e;
|
|
603
|
+
box-shadow: 0 12px 20px -16px rgba(122, 36, 30, 0.95);
|
|
604
|
+
}
|
|
605
|
+
.billing-link:hover { filter: brightness(0.96); }
|
|
507
606
|
button.renew-btn {
|
|
508
607
|
display: none;
|
|
509
608
|
width: 100%;
|
|
@@ -671,6 +770,14 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
671
770
|
<button class="pin-chip" id="pinchip" type="button" title="Return to auto-follow">pinned x</button>
|
|
672
771
|
<button class="jump-chip" id="jumpchip" type="button" title="Open this conversation">↗</button>
|
|
673
772
|
</div>
|
|
773
|
+
<div class="billing-banner" id="billingbanner">
|
|
774
|
+
<div class="billing-top">
|
|
775
|
+
<span class="billing-dot"></span>
|
|
776
|
+
<strong class="billing-title" id="billingtitle"></strong>
|
|
777
|
+
<button class="billing-link" id="billinglink" type="button">Open pricing</button>
|
|
778
|
+
</div>
|
|
779
|
+
<span class="billing-copy" id="billingcopy"></span>
|
|
780
|
+
</div>
|
|
674
781
|
<div class="state-row">
|
|
675
782
|
<span class="state-dot" id="stateDot"></span>
|
|
676
783
|
<span class="state-word" id="stateWord">--%</span>
|
|
@@ -690,6 +797,10 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
690
797
|
</div>
|
|
691
798
|
</div>
|
|
692
799
|
</div>
|
|
800
|
+
<section class="diagnostics" aria-label="Memory access diagnostics">
|
|
801
|
+
<div class="diagnostics-head"><span class="diagnostics-title">Memory access</span><button class="diagnostics-refresh" id="diagnosticsrefresh" type="button">Refresh</button></div>
|
|
802
|
+
<div class="diagnostics-body" id="diagnosticsbody"><span class="diagnostics-v">Open this panel to check account status.</span></div>
|
|
803
|
+
</section>
|
|
693
804
|
<div class="tabs" id="tabs"></div>
|
|
694
805
|
<div class="meta-row"><span class="meta-line" id="metaline"></span><button class="switchbtn agent-switch" id="switchbtn" title="Switch agent" style="display:none" type="button"></button></div>
|
|
695
806
|
<div class="path" id="path"></div>
|
|
@@ -715,6 +826,10 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
715
826
|
const whyBtn = document.getElementById("whybtn");
|
|
716
827
|
const evidence = document.getElementById("evidence");
|
|
717
828
|
const nudge = document.getElementById("nudge");
|
|
829
|
+
const billingBanner = document.getElementById("billingbanner");
|
|
830
|
+
const billingTitle = document.getElementById("billingtitle");
|
|
831
|
+
const billingCopy = document.getElementById("billingcopy");
|
|
832
|
+
const billingLink = document.getElementById("billinglink");
|
|
718
833
|
const renewBtn = document.getElementById("renewbtn");
|
|
719
834
|
const renewResult = document.getElementById("renewresult");
|
|
720
835
|
const renewMsg = document.getElementById("renewmsg");
|
|
@@ -730,6 +845,8 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
730
845
|
const switchBtn = document.getElementById("switchbtn");
|
|
731
846
|
const revealEl = document.getElementById("reveal");
|
|
732
847
|
const missing = document.getElementById("missing");
|
|
848
|
+
const diagnosticsBody = document.getElementById("diagnosticsbody");
|
|
849
|
+
const diagnosticsRefresh = document.getElementById("diagnosticsrefresh");
|
|
733
850
|
let prevTurnState = null;
|
|
734
851
|
let lastCapsule = "";
|
|
735
852
|
let renewResultSessionKey = "";
|
|
@@ -741,6 +858,9 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
741
858
|
let renewStartMs = 0;
|
|
742
859
|
let renewEta = null;
|
|
743
860
|
let renewEstimateSeq = 0;
|
|
861
|
+
let billingStatus = null;
|
|
862
|
+
let diagnosticsLoaded = false;
|
|
863
|
+
let diagnosticsInFlight = false;
|
|
744
864
|
const CHECKPOINT_CACHE_KEY = "echomemHudCheckpointCacheV2";
|
|
745
865
|
const CHECKPOINT_CACHE_LIMIT = 24;
|
|
746
866
|
const CHECKPOINT_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -753,6 +873,86 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
753
873
|
// viewing", so the user can always point the HUD at the right session. Flip false to show the viewer.
|
|
754
874
|
const DEMO_CLEAN = true;
|
|
755
875
|
|
|
876
|
+
function diagnosticsLine(label, value, bad, wrap) {
|
|
877
|
+
return '<span class="diagnostics-line"><span class="diagnostics-k">' + esc(label) + '</span><span class="diagnostics-v' + (bad ? ' bad' : '') + (wrap ? ' wrap' : '') + '">' + esc(value) + '</span></span>';
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function requestLabel(requestId) {
|
|
881
|
+
return requestId ? requestId : "not available";
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function planLabel(plan) {
|
|
885
|
+
if (!plan) return "connected";
|
|
886
|
+
return plan.slice(0, 1).toUpperCase() + plan.slice(1);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function renderDiagnostics(data) {
|
|
890
|
+
if (!diagnosticsBody) return;
|
|
891
|
+
const account = (data && data.account) || {};
|
|
892
|
+
const accountRequest = (data && data.accountRequest) || {};
|
|
893
|
+
const lines = [];
|
|
894
|
+
if (account.state === "connected") {
|
|
895
|
+
lines.push(diagnosticsLine("account", planLabel(account.plan) + " · authenticated"));
|
|
896
|
+
if (typeof account.searchesThisWeek === "number" && typeof account.weeklySearchLimit === "number") {
|
|
897
|
+
const limit = account.weeklySearchLimit < 0 ? "Unlimited" : String(account.weeklySearchLimit);
|
|
898
|
+
const remaining = account.searchesRemaining === null ? "" : (typeof account.searchesRemaining === "number" ? " · " + account.searchesRemaining + " left" : "");
|
|
899
|
+
lines.push(diagnosticsLine("searches", account.searchesThisWeek + " / " + limit + " UTC week" + remaining));
|
|
900
|
+
if (account.resetAt) lines.push(diagnosticsLine("resets", account.resetAt));
|
|
901
|
+
} else {
|
|
902
|
+
lines.push(diagnosticsLine("searches", "weekly quota not reported"));
|
|
903
|
+
}
|
|
904
|
+
} else if (account.state === "not_logged_in") {
|
|
905
|
+
lines.push(diagnosticsLine("account", "not logged in", true));
|
|
906
|
+
} else {
|
|
907
|
+
lines.push(diagnosticsLine("account", account.error || "unavailable", true));
|
|
908
|
+
}
|
|
909
|
+
const accountStatus = typeof accountRequest.httpStatus === "number"
|
|
910
|
+
? "HTTP " + accountRequest.httpStatus + (typeof accountRequest.elapsedMs === "number" ? " · " + accountRequest.elapsedMs + "ms" : "")
|
|
911
|
+
: "not sent";
|
|
912
|
+
lines.push(diagnosticsLine("account check", accountStatus, account.state === "error"));
|
|
913
|
+
if (accountRequest.requestId) lines.push(diagnosticsLine("account request", requestLabel(accountRequest.requestId), false, true));
|
|
914
|
+
|
|
915
|
+
const last = data && data.lastSearch;
|
|
916
|
+
if (!last) {
|
|
917
|
+
lines.push(diagnosticsLine("last search", "no local trace yet"));
|
|
918
|
+
} else {
|
|
919
|
+
const outcome = last.ok === false ? "failed" : last.ok === true ? "succeeded" : "recorded";
|
|
920
|
+
const status = typeof last.httpStatus === "number" ? "HTTP " + last.httpStatus : outcome;
|
|
921
|
+
const errorLabel = last.errorCode || (last.errorKind && last.errorKind !== "none" ? last.errorKind : "");
|
|
922
|
+
const detail = [status, errorLabel].filter(Boolean).join(" · ");
|
|
923
|
+
lines.push(diagnosticsLine("last search", detail || outcome, last.ok === false));
|
|
924
|
+
if (last.requestId) lines.push(diagnosticsLine("search request", requestLabel(last.requestId), last.ok === false, true));
|
|
925
|
+
}
|
|
926
|
+
diagnosticsBody.innerHTML = lines.join("");
|
|
927
|
+
diagnosticsLoaded = true;
|
|
928
|
+
syncHeight();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function refreshDiagnostics() {
|
|
932
|
+
if (!diagnosticsBody || diagnosticsInFlight) return;
|
|
933
|
+
diagnosticsInFlight = true;
|
|
934
|
+
diagnosticsBody.innerHTML = diagnosticsLine("memory access", "checking…");
|
|
935
|
+
if (diagnosticsRefresh) diagnosticsRefresh.disabled = true;
|
|
936
|
+
try {
|
|
937
|
+
const response = await fetch("/diagnostics");
|
|
938
|
+
renderDiagnostics(await response.json());
|
|
939
|
+
} catch (_) {
|
|
940
|
+
renderDiagnostics({ account: { state: "error", error: "local diagnostic fetch failed" }, accountRequest: {} });
|
|
941
|
+
} finally {
|
|
942
|
+
diagnosticsInFlight = false;
|
|
943
|
+
if (diagnosticsRefresh) diagnosticsRefresh.disabled = false;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function maybeLoadDiagnostics() {
|
|
948
|
+
if (!diagnosticsLoaded && !diagnosticsInFlight) refreshDiagnostics();
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
if (diagnosticsRefresh) diagnosticsRefresh.addEventListener("click", (event) => {
|
|
952
|
+
event.stopPropagation();
|
|
953
|
+
refreshDiagnostics();
|
|
954
|
+
});
|
|
955
|
+
|
|
756
956
|
// Mini mode: resting state is a small pill; opening the panel shows the full layout, closing
|
|
757
957
|
// returns to the pill. Preference persists (localStorage here, window size in main's bounds file).
|
|
758
958
|
const miniBtn = document.getElementById("minibtn");
|
|
@@ -871,6 +1071,19 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
871
1071
|
e.stopPropagation();
|
|
872
1072
|
selectSession("auto");
|
|
873
1073
|
});
|
|
1074
|
+
if (billingLink) billingLink.addEventListener("click", (e) => {
|
|
1075
|
+
e.stopPropagation();
|
|
1076
|
+
const url = billingStatus && billingStatus.pricingUrl;
|
|
1077
|
+
if (!url) return;
|
|
1078
|
+
fetch("/open-external?url=" + encodeURIComponent(url))
|
|
1079
|
+
.then((res) => res.json())
|
|
1080
|
+
.then((data) => {
|
|
1081
|
+
if (!data || !data.ok) window.open(url, "_blank", "noopener");
|
|
1082
|
+
})
|
|
1083
|
+
.catch(() => {
|
|
1084
|
+
try { window.open(url, "_blank", "noopener"); } catch {}
|
|
1085
|
+
});
|
|
1086
|
+
});
|
|
874
1087
|
function selectSession(id) {
|
|
875
1088
|
if (!id) return;
|
|
876
1089
|
fetch("/select?id=" + encodeURIComponent(id))
|
|
@@ -889,6 +1102,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
889
1102
|
}
|
|
890
1103
|
if (event.target instanceof HTMLElement && (event.target.closest(".switchbtn") || event.target.closest(".cornerbtn"))) return;
|
|
891
1104
|
hud.classList.toggle("open");
|
|
1105
|
+
if (hud.classList.contains("open")) maybeLoadDiagnostics();
|
|
892
1106
|
syncHeight();
|
|
893
1107
|
});
|
|
894
1108
|
|
|
@@ -1365,6 +1579,19 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1365
1579
|
const events = new EventSource("/events");
|
|
1366
1580
|
events.onmessage = (event) => render(JSON.parse(event.data));
|
|
1367
1581
|
fetch("/state").then((r) => r.json()).then(render).catch(() => {});
|
|
1582
|
+
refreshBillingStatus();
|
|
1583
|
+
setInterval(refreshBillingStatus, 30000);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
async function refreshBillingStatus() {
|
|
1587
|
+
try {
|
|
1588
|
+
const res = await fetch("/billing-status");
|
|
1589
|
+
billingStatus = await res.json();
|
|
1590
|
+
} catch {
|
|
1591
|
+
billingStatus = null;
|
|
1592
|
+
}
|
|
1593
|
+
renderBillingStatus();
|
|
1594
|
+
syncHeight();
|
|
1368
1595
|
}
|
|
1369
1596
|
|
|
1370
1597
|
function render(state) {
|
|
@@ -1422,6 +1649,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1422
1649
|
jumpChip.style.display = activeId ? "inline-block" : "none";
|
|
1423
1650
|
}
|
|
1424
1651
|
renderWhy(score, oldPct, repeatCount, sat, health, color);
|
|
1652
|
+
renderBillingStatus();
|
|
1425
1653
|
pathEl.textContent = "";
|
|
1426
1654
|
currentSrc = score.sourcePath || null;
|
|
1427
1655
|
if (revealEl) revealEl.style.display = !DEMO_CLEAN && currentSrc ? "inline-block" : "none";
|
|
@@ -1482,6 +1710,7 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1482
1710
|
whyOpen = false;
|
|
1483
1711
|
hud.classList.remove("show-why");
|
|
1484
1712
|
nudge.style.display = "none";
|
|
1713
|
+
renderBillingStatus();
|
|
1485
1714
|
if (renewBtn) renewBtn.style.display = "none";
|
|
1486
1715
|
clearRenewResult();
|
|
1487
1716
|
pathEl.textContent = "";
|
|
@@ -1503,6 +1732,27 @@ export const HUD_HTML = String.raw `<!doctype html>
|
|
|
1503
1732
|
if (mrepoLine) mrepoLine.style.display = repo ? "flex" : "none";
|
|
1504
1733
|
}
|
|
1505
1734
|
|
|
1735
|
+
function renderBillingStatus() {
|
|
1736
|
+
if (!billingBanner) return;
|
|
1737
|
+
const state = billingStatus && billingStatus.state;
|
|
1738
|
+
const blocked = state === "plan_required" || state === "quota_exceeded" || state === "billing_attention";
|
|
1739
|
+
billingBanner.style.display = blocked ? "block" : "none";
|
|
1740
|
+
billingBanner.classList.toggle("limit", state === "quota_exceeded");
|
|
1741
|
+
if (!blocked) return;
|
|
1742
|
+
const title = state === "quota_exceeded"
|
|
1743
|
+
? "Recall limit reached"
|
|
1744
|
+
: state === "plan_required"
|
|
1745
|
+
? "Recall paused · Free plan"
|
|
1746
|
+
: billingStatus.message || "Billing needs attention";
|
|
1747
|
+
const detail = state === "quota_exceeded"
|
|
1748
|
+
? "You used all Pro searches this month. Upgrade or wait for reset."
|
|
1749
|
+
: state === "plan_required"
|
|
1750
|
+
? "Search memory requires Pro or Power. Saving still works."
|
|
1751
|
+
: billingStatus.detail || "Open pricing to restore memory search.";
|
|
1752
|
+
if (billingTitle) billingTitle.textContent = title;
|
|
1753
|
+
if (billingCopy) billingCopy.textContent = detail;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1506
1756
|
function renderWhy(score, oldPct, repeatCount, sat, health, color) {
|
|
1507
1757
|
const rows = [];
|
|
1508
1758
|
if (sat > 0) rows.push(["context", score.modelContextWindow ? sat + "% full · " + fmt(score.ctTokens) : fmt(score.ctTokens) + " tokens"]);
|