@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
package/dist/index.js
CHANGED
|
@@ -13,8 +13,10 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
13
13
|
import { fetchEncryptionConfig, decryptMemoryFields } from "./encryption.js";
|
|
14
14
|
import { runCli } from "./setup.js";
|
|
15
15
|
import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS } from "./package-metadata.js";
|
|
16
|
+
import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
|
|
16
17
|
import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
|
|
17
18
|
const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
|
|
19
|
+
const ECHO_PRICING_URL = process.env.ECHO_PRICING_URL || "https://echoknows.com/pricing";
|
|
18
20
|
/** Thrown when no API token is present yet — the model gets a "run login" nudge, not a hard error. */
|
|
19
21
|
class NoTokenError extends Error {
|
|
20
22
|
}
|
|
@@ -80,6 +82,56 @@ function describeError(error) {
|
|
|
80
82
|
const fallback = stringifyErrorValue(error);
|
|
81
83
|
return fallback || "Unknown error";
|
|
82
84
|
}
|
|
85
|
+
function appendPricingSource(url, source) {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = new URL(url);
|
|
88
|
+
if (!parsed.searchParams.has("source")) {
|
|
89
|
+
parsed.searchParams.set("source", source);
|
|
90
|
+
}
|
|
91
|
+
return parsed.toString();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return url;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function formatUpgradeRequiredResult(error) {
|
|
98
|
+
if (!axios.isAxiosError(error))
|
|
99
|
+
return null;
|
|
100
|
+
const status = error.response?.status;
|
|
101
|
+
if (status !== 402 && status !== 403 && status !== 429)
|
|
102
|
+
return null;
|
|
103
|
+
const data = error.response?.data;
|
|
104
|
+
if (!isRecord(data))
|
|
105
|
+
return null;
|
|
106
|
+
const upgradeUrl = readString(data, "upgradeUrl")
|
|
107
|
+
?? readString(data, "pricingUrl")
|
|
108
|
+
?? readString(data, "url")
|
|
109
|
+
?? ECHO_PRICING_URL;
|
|
110
|
+
const message = readString(data, "message")
|
|
111
|
+
?? "Memory search and recall require an Echo Pro or Power plan.";
|
|
112
|
+
const code = readString(data, "error") ?? readString(data, "code");
|
|
113
|
+
const plan = readString(data, "plan");
|
|
114
|
+
const pricingUrl = appendPricingSource(upgradeUrl, "mcp_search");
|
|
115
|
+
const normalizedCode = String(code || "").toUpperCase();
|
|
116
|
+
const kind = status === 429 || normalizedCode.includes("QUOTA") || normalizedCode.includes("LIMIT")
|
|
117
|
+
? "quota_exceeded"
|
|
118
|
+
: status === 402 || normalizedCode.includes("REQUIRES_PLAN")
|
|
119
|
+
? "plan_required"
|
|
120
|
+
: "billing_attention";
|
|
121
|
+
writeBillingAlert({
|
|
122
|
+
kind,
|
|
123
|
+
code,
|
|
124
|
+
message,
|
|
125
|
+
plan,
|
|
126
|
+
pricingUrl,
|
|
127
|
+
});
|
|
128
|
+
return [
|
|
129
|
+
message,
|
|
130
|
+
"",
|
|
131
|
+
code ? `Reason: ${code}.` : "",
|
|
132
|
+
`Open pricing: ${pricingUrl}`,
|
|
133
|
+
].filter(Boolean).join("\n");
|
|
134
|
+
}
|
|
83
135
|
/** Map a thrown error to the telemetry error_kind taxonomy. */
|
|
84
136
|
function classifyError(error) {
|
|
85
137
|
if (error instanceof NoTokenError)
|
|
@@ -105,6 +157,11 @@ function readNumber(record, key) {
|
|
|
105
157
|
const value = record[key];
|
|
106
158
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
107
159
|
}
|
|
160
|
+
function errorCodeFrom(value) {
|
|
161
|
+
if (!isRecord(value))
|
|
162
|
+
return undefined;
|
|
163
|
+
return readString(value, "error") || readString(value, "code") || readString(value, "errorCode");
|
|
164
|
+
}
|
|
108
165
|
function compactOneLine(value, maxLength) {
|
|
109
166
|
if (!value)
|
|
110
167
|
return undefined;
|
|
@@ -569,30 +626,59 @@ class EchoMemApiClient {
|
|
|
569
626
|
throw error;
|
|
570
627
|
}
|
|
571
628
|
}
|
|
572
|
-
async searchMemoriesRaw(query, opts, enc, retrievalOnly = false) {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
629
|
+
async searchMemoriesRaw(query, opts, enc, retrievalOnly = false, trace) {
|
|
630
|
+
const endpoint = "/api/extension/memories/search";
|
|
631
|
+
let response;
|
|
632
|
+
try {
|
|
633
|
+
response = await this.axios.post(endpoint, {
|
|
634
|
+
query,
|
|
635
|
+
k: opts.limit,
|
|
636
|
+
similarityThreshold: opts.threshold,
|
|
637
|
+
timeFrameDays: opts.timeFrameDays,
|
|
638
|
+
requestId: opts.requestId,
|
|
639
|
+
sessionId: this.sessionId,
|
|
640
|
+
});
|
|
641
|
+
trace?.({ requestId: opts.requestId, endpoint, httpStatus: response.status });
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
trace?.({
|
|
645
|
+
requestId: opts.requestId,
|
|
646
|
+
endpoint,
|
|
647
|
+
httpStatus: axios.isAxiosError(error) ? error.response?.status : undefined,
|
|
648
|
+
errorCode: axios.isAxiosError(error) ? errorCodeFrom(error.response?.data) : undefined,
|
|
649
|
+
});
|
|
650
|
+
throw error;
|
|
651
|
+
}
|
|
581
652
|
const data = enc.enabled ? await this.decryptResult(response.data, enc.key) : response.data;
|
|
582
653
|
return retrievalOnly && isRecord(data)
|
|
583
654
|
? { ...data, retrievalOnly: true, tuned: false }
|
|
584
655
|
: data;
|
|
585
656
|
}
|
|
586
|
-
async searchMemoriesRetrieved(query, opts, enc) {
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
657
|
+
async searchMemoriesRetrieved(query, opts, enc, trace) {
|
|
658
|
+
const endpoint = "/api/extension/memories/deep-search/candidates";
|
|
659
|
+
let response;
|
|
660
|
+
try {
|
|
661
|
+
response = await this.axios.post(endpoint, {
|
|
662
|
+
query,
|
|
663
|
+
requestId: opts.requestId,
|
|
664
|
+
logicalRequestId: opts.requestId,
|
|
665
|
+
sessionId: this.sessionId,
|
|
666
|
+
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
|
667
|
+
});
|
|
668
|
+
trace?.({ requestId: opts.requestId, endpoint, httpStatus: response.status });
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
trace?.({
|
|
672
|
+
requestId: opts.requestId,
|
|
673
|
+
endpoint,
|
|
674
|
+
httpStatus: axios.isAxiosError(error) ? error.response?.status : undefined,
|
|
675
|
+
errorCode: axios.isAxiosError(error) ? errorCodeFrom(error.response?.data) : undefined,
|
|
676
|
+
});
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
594
679
|
const data = response.data ?? {};
|
|
595
680
|
if (data.success === false) {
|
|
681
|
+
trace?.({ requestId: opts.requestId, endpoint, httpStatus: response.status, errorCode: errorCodeFrom(data) });
|
|
596
682
|
throw new Error(`deep-search candidates proxy error: ${data.error || "unknown"}`);
|
|
597
683
|
}
|
|
598
684
|
const rawCandidates = Array.isArray(data.personalCandidates) ? data.personalCandidates : [];
|
|
@@ -648,16 +734,31 @@ class EchoMemApiClient {
|
|
|
648
734
|
// never hits dg-web directly, so no new auth is needed and no other caller is
|
|
649
735
|
// disrupted. NOTE: encrypted users still get server-side plaintext only; the
|
|
650
736
|
// phase-1 + bridge-local-decrypt + phase-2 flow (spec §11) is the remaining work.
|
|
651
|
-
async searchMemoriesTuned(query, requestId) {
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
737
|
+
async searchMemoriesTuned(query, requestId, trace) {
|
|
738
|
+
const endpoint = "/api/extension/memories/deep-search";
|
|
739
|
+
let response;
|
|
740
|
+
try {
|
|
741
|
+
response = await this.axios.post(endpoint, {
|
|
742
|
+
query,
|
|
743
|
+
requestId,
|
|
744
|
+
logicalRequestId: requestId,
|
|
745
|
+
sessionId: this.sessionId,
|
|
746
|
+
userTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
|
|
747
|
+
});
|
|
748
|
+
trace?.({ requestId, endpoint, httpStatus: response.status });
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
trace?.({
|
|
752
|
+
requestId,
|
|
753
|
+
endpoint,
|
|
754
|
+
httpStatus: axios.isAxiosError(error) ? error.response?.status : undefined,
|
|
755
|
+
errorCode: axios.isAxiosError(error) ? errorCodeFrom(error.response?.data) : undefined,
|
|
756
|
+
});
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
659
759
|
const data = response.data ?? {};
|
|
660
760
|
if (data.success === false) {
|
|
761
|
+
trace?.({ requestId, endpoint, httpStatus: response.status, errorCode: errorCodeFrom(data) });
|
|
661
762
|
throw new Error(`deep-search proxy error: ${data.error || "unknown"}`);
|
|
662
763
|
}
|
|
663
764
|
const primary = (data.retrievedMemorySnapshot?.primary ?? []);
|
|
@@ -669,7 +770,7 @@ class EchoMemApiClient {
|
|
|
669
770
|
}));
|
|
670
771
|
return { success: true, tuned: true, answer: String(data.answer || data.response || "").trim(), memories };
|
|
671
772
|
}
|
|
672
|
-
async searchMemories(args) {
|
|
773
|
+
async searchMemories(args, trace) {
|
|
673
774
|
const parsed = searchMemoriesSchema.parse(args ?? {});
|
|
674
775
|
const query = parsed.query?.trim();
|
|
675
776
|
const limit = parsed.limit ?? parsed.k ?? 10;
|
|
@@ -695,17 +796,17 @@ class EchoMemApiClient {
|
|
|
695
796
|
requestId: randomUUID(),
|
|
696
797
|
};
|
|
697
798
|
if (!parsed.includeAnswer) {
|
|
698
|
-
return this.searchMemoriesRetrieved(query, searchOpts, enc);
|
|
799
|
+
return this.searchMemoriesRetrieved(query, searchOpts, enc, trace);
|
|
699
800
|
}
|
|
700
801
|
// Encrypted account: the server can't synthesize over plaintext it doesn't hold, so use the raw
|
|
701
802
|
// retriever (returns ciphertext) and decrypt locally — zero-knowledge preserved end-to-end.
|
|
702
803
|
if (enc.enabled) {
|
|
703
|
-
return await this.searchMemoriesRaw(query, searchOpts, enc);
|
|
804
|
+
return await this.searchMemoriesRaw(query, searchOpts, enc, false, trace);
|
|
704
805
|
}
|
|
705
806
|
// A synthesized answer is metered through the legacy route. Do not fall
|
|
706
807
|
// back to raw retrieval after a ledger outage: that would return a result
|
|
707
808
|
// without a durable usage fact.
|
|
708
|
-
return this.searchMemoriesTuned(query, searchOpts.requestId);
|
|
809
|
+
return this.searchMemoriesTuned(query, searchOpts.requestId, trace);
|
|
709
810
|
}
|
|
710
811
|
/** Decrypt the model-visible fields on a `{ memories: [...] }` response locally. */
|
|
711
812
|
async decryptResult(data, key) {
|
|
@@ -1101,6 +1202,12 @@ class EchoMemMCPServer {
|
|
|
1101
1202
|
if (error instanceof McpError) {
|
|
1102
1203
|
throw error;
|
|
1103
1204
|
}
|
|
1205
|
+
const upgradeRequired = formatUpgradeRequiredResult(error);
|
|
1206
|
+
if (upgradeRequired) {
|
|
1207
|
+
return {
|
|
1208
|
+
content: [{ type: "text", text: upgradeRequired }],
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1104
1211
|
return {
|
|
1105
1212
|
content: [{ type: "text", text: `Error: ${describeError(error)}` }],
|
|
1106
1213
|
isError: true,
|
|
@@ -1129,6 +1236,13 @@ class EchoMemMCPServer {
|
|
|
1129
1236
|
map_injected: rec.map_injected,
|
|
1130
1237
|
encrypted_user: rec.encrypted,
|
|
1131
1238
|
extracted_memory_count: rec.memories_extracted,
|
|
1239
|
+
// Preserve only the safe correlation fields needed to join a
|
|
1240
|
+
// bridge-local failure to the central operational trace. Query
|
|
1241
|
+
// text and upstream bodies never leave the local event stream.
|
|
1242
|
+
request_id: rec.request_id,
|
|
1243
|
+
endpoint: rec.endpoint,
|
|
1244
|
+
http_status: rec.http_status,
|
|
1245
|
+
error_code: rec.error_code ?? (rec.error_kind === "none" ? undefined : rec.error_kind),
|
|
1132
1246
|
};
|
|
1133
1247
|
void this.client.trackMcpAnalyticsEvent(rec.ok ? "[MCP] EchoMem Tool Succeeded" : "[MCP] EchoMem Tool Failed", finalAnalytics, `${analyticsCallId}:generic-completed`);
|
|
1134
1248
|
void this.client.trackMcpAnalyticsEvent(toolEventName(canonicalName, rec.ok ? "Succeeded" : "Failed"), finalAnalytics, `${analyticsCallId}:completed`);
|
|
@@ -1142,7 +1256,15 @@ class EchoMemMCPServer {
|
|
|
1142
1256
|
rec.query_len = query.length;
|
|
1143
1257
|
rec.query_hash = hashText(query);
|
|
1144
1258
|
}
|
|
1145
|
-
const result = await this.client.searchMemories(args)
|
|
1259
|
+
const result = await this.client.searchMemories(args, (trace) => {
|
|
1260
|
+
if (!rec)
|
|
1261
|
+
return;
|
|
1262
|
+
rec.request_id = trace.requestId;
|
|
1263
|
+
rec.endpoint = trace.endpoint;
|
|
1264
|
+
rec.http_status = trace.httpStatus;
|
|
1265
|
+
rec.error_code = trace.errorCode;
|
|
1266
|
+
});
|
|
1267
|
+
clearBillingAlert();
|
|
1146
1268
|
if (rec) {
|
|
1147
1269
|
// "Which memories were used": topic keys + delivered context size, for the comparison dataset.
|
|
1148
1270
|
const mems = Array.isArray(result?.memories) ? result.memories : [];
|
|
@@ -1249,7 +1371,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1249
1371
|
typeof capsuleId === "string" && capsuleId ? `Capsule ID: ${capsuleId}` : "",
|
|
1250
1372
|
typeof contextId === "string" && contextId ? `Context: ${contextId}` : "",
|
|
1251
1373
|
"",
|
|
1252
|
-
`To reload this capsule in a fresh session: get_memories_by_context({ contextId: "${contextId}" })`,
|
|
1374
|
+
`To reload this capsule in a fresh session with Pro/Power recall: get_memories_by_context({ contextId: "${contextId}" })`,
|
|
1253
1375
|
].filter(Boolean).join("\n");
|
|
1254
1376
|
return { content: [{ type: "text", text }] };
|
|
1255
1377
|
}
|
|
@@ -1273,7 +1395,7 @@ Details: ${m.details || "N/A"}`)
|
|
|
1273
1395
|
`Successfully ingested conversation. Extracted ${memoriesExtracted} memory distinct events.`,
|
|
1274
1396
|
list,
|
|
1275
1397
|
saved.length
|
|
1276
|
-
? `Verify these captured the key facts.
|
|
1398
|
+
? `Verify these captured the key facts. With Pro/Power recall, re-fetch this exact batch later by searching the ids above${typeof contextId === "string" && contextId ? ` (context ${contextId})` : ""}.`
|
|
1277
1399
|
: "",
|
|
1278
1400
|
].filter(Boolean).join("\n\n");
|
|
1279
1401
|
return { content: [{ type: "text", text }] };
|
|
@@ -21,6 +21,8 @@ export const SETUP_PAGE_CLIENT_CORE = String.raw ` var params = new URLSear
|
|
|
21
21
|
var reportMounted = false; // report shell (city iframe) mounted once; loading is an overlay on it, not a separate page
|
|
22
22
|
var authUrl = "";
|
|
23
23
|
var workspacePath = "";
|
|
24
|
+
var billingStatus = null;
|
|
25
|
+
var recallGateSkipped = false;
|
|
24
26
|
var authWindow = null;
|
|
25
27
|
var connectionPollStarted = false;
|
|
26
28
|
var statsPollStarted = false;
|
|
@@ -342,7 +342,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
342
342
|
'<div class="resultNumber"><strong>' + esc(number(extracted)) + '</strong><span>' + esc(memoryNoun) + '</span></div>') +
|
|
343
343
|
'</div>' +
|
|
344
344
|
'</div>' +
|
|
345
|
-
(done ?
|
|
345
|
+
(done ? recallGateBlock() : "") +
|
|
346
346
|
(done ? "" :
|
|
347
347
|
'<div class="memoryReceipt">' +
|
|
348
348
|
'<p class="receiptK">What just happened</p>' +
|
|
@@ -353,7 +353,7 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
353
353
|
'</div>') +
|
|
354
354
|
'</div>' +
|
|
355
355
|
(done ? hudTip() + doneCloseNote() : "");
|
|
356
|
-
|
|
356
|
+
wireRecallGate();
|
|
357
357
|
return;
|
|
358
358
|
}
|
|
359
359
|
setHead(preparing ? "Preparing" : "Extracting", preparing ? "Preparing" : "Extracting");
|
|
@@ -447,6 +447,103 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
447
447
|
setLaunchStatus(msg);
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
|
+
function paidRecallPlan(plan) {
|
|
451
|
+
return ["pro", "power", "team", "enterprise"].indexOf(String(plan || "").toLowerCase()) >= 0;
|
|
452
|
+
}
|
|
453
|
+
function recallPricingUrl() {
|
|
454
|
+
return (billingStatus && billingStatus.pricingUrl)
|
|
455
|
+
|| "https://echoknows.com/pricing?source=mcp_onboarding";
|
|
456
|
+
}
|
|
457
|
+
function recallGateUnlocked() {
|
|
458
|
+
return recallGateSkipped || paidRecallPlan(billingStatus && billingStatus.plan);
|
|
459
|
+
}
|
|
460
|
+
function recallTrialAvailable() {
|
|
461
|
+
return !billingStatus || billingStatus.trialAvailable !== false;
|
|
462
|
+
}
|
|
463
|
+
function recallGateBlock() {
|
|
464
|
+
var unlocked = recallGateUnlocked();
|
|
465
|
+
var trialAvailable = recallTrialAvailable();
|
|
466
|
+
return '<div class="recallGateBlock">' +
|
|
467
|
+
'<section class="recallGate' + (unlocked ? " is-hidden" : "") + '" id="recallGate">' +
|
|
468
|
+
'<p class="recallEyebrow"><i></i>Premium recall</p>' +
|
|
469
|
+
'<h3 id="recallGateTitle">' + (trialAvailable ? "Unlock memory search for your agents." : "Ready to turn recall back on?") + '</h3>' +
|
|
470
|
+
'<p id="recallGateBody">' + (trialAvailable ? "Start a 14-day trial and let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way." : "Choose Pro or Power to let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way.") + '</p>' +
|
|
471
|
+
'<div class="recallTrial' + (trialAvailable ? "" : " is-hidden") + '" id="recallTrialBadge" aria-label="14 days free"><strong>14</strong><span>days free<br>then choose what fits</span></div>' +
|
|
472
|
+
'<p class="recallUsedNote' + (trialAvailable ? " is-hidden" : "") + '" id="recallUsedNote">Your trial was already used. You can still unlock recall by choosing Pro or Power.</p>' +
|
|
473
|
+
'<div class="recallGateActions">' +
|
|
474
|
+
'<button type="button" class="primary" id="recallStartTrial">' + (trialAvailable ? "Compare Pro and Power" : "See pricing") + '</button>' +
|
|
475
|
+
'<button type="button" class="textButton" id="recallSkip">Skip recall for now</button>' +
|
|
476
|
+
'</div>' +
|
|
477
|
+
'<p class="recallFine" id="recallFine">' + (trialAvailable ? "Card required for trial. Cancel anytime." : "Cancel anytime.") + '</p>' +
|
|
478
|
+
'<p class="launchStatus" id="recallGateStatus"></p>' +
|
|
479
|
+
'</section>' +
|
|
480
|
+
'<div id="recallTryWrap" class="recallTryWrap' + (unlocked ? "" : " is-hidden") + '">' +
|
|
481
|
+
'<p id="recallSkipNote" class="recallSkipNote' + (recallGateSkipped ? "" : " is-hidden") + '">Saving will keep working. Search and recall require Pro or Power when this prompt calls <code>search_memories</code>.</p>' +
|
|
482
|
+
tryItCard() +
|
|
483
|
+
'</div>' +
|
|
484
|
+
'</div>';
|
|
485
|
+
}
|
|
486
|
+
function setRecallGateStatus(message) {
|
|
487
|
+
var node = document.getElementById("recallGateStatus");
|
|
488
|
+
if (node) node.textContent = message || "";
|
|
489
|
+
}
|
|
490
|
+
function applyRecallGate() {
|
|
491
|
+
var gate = document.getElementById("recallGate");
|
|
492
|
+
var wrap = document.getElementById("recallTryWrap");
|
|
493
|
+
var skipNote = document.getElementById("recallSkipNote");
|
|
494
|
+
var unlocked = recallGateUnlocked();
|
|
495
|
+
if (gate) gate.classList.toggle("is-hidden", unlocked);
|
|
496
|
+
if (wrap) wrap.classList.toggle("is-hidden", !unlocked);
|
|
497
|
+
if (skipNote) skipNote.classList.toggle("is-hidden", !recallGateSkipped);
|
|
498
|
+
var trialAvailable = recallTrialAvailable();
|
|
499
|
+
var badge = document.getElementById("recallTrialBadge");
|
|
500
|
+
if (badge) badge.classList.toggle("is-hidden", !trialAvailable);
|
|
501
|
+
var usedNote = document.getElementById("recallUsedNote");
|
|
502
|
+
if (usedNote) usedNote.classList.toggle("is-hidden", trialAvailable);
|
|
503
|
+
var title = document.getElementById("recallGateTitle");
|
|
504
|
+
if (title) title.textContent = trialAvailable ? "Unlock memory search for your agents." : "Ready to turn recall back on?";
|
|
505
|
+
var body = document.getElementById("recallGateBody");
|
|
506
|
+
if (body) body.textContent = trialAvailable
|
|
507
|
+
? "Start a 14-day trial and let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way."
|
|
508
|
+
: "Choose Pro or Power to let Codex, Claude, and MCP clients search what Echo saved. Saving conversations keeps working either way.";
|
|
509
|
+
var cta = document.getElementById("recallStartTrial");
|
|
510
|
+
if (cta) cta.textContent = trialAvailable ? "Compare Pro and Power" : "See pricing";
|
|
511
|
+
var fine = document.getElementById("recallFine");
|
|
512
|
+
if (fine) fine.textContent = trialAvailable ? "Card required for trial. Cancel anytime." : "Cancel anytime.";
|
|
513
|
+
}
|
|
514
|
+
async function refreshBillingStatus() {
|
|
515
|
+
try {
|
|
516
|
+
billingStatus = await getJson("/billing-status");
|
|
517
|
+
applyRecallGate();
|
|
518
|
+
if (!recallGateUnlocked() && billingStatus && billingStatus.plan === "unknown") {
|
|
519
|
+
setRecallGateStatus("Could not check your plan. You can start the trial or skip and keep setup going.");
|
|
520
|
+
}
|
|
521
|
+
} catch (_) {
|
|
522
|
+
setRecallGateStatus("Could not check your plan. You can start the trial or skip and keep setup going.");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function openRecallPricing() {
|
|
526
|
+
var url = recallPricingUrl();
|
|
527
|
+
setRecallGateStatus("Opening pricing in a new tab...");
|
|
528
|
+
try {
|
|
529
|
+
var opened = window.open(url, "_blank", "noopener,noreferrer");
|
|
530
|
+
if (!opened) setRecallGateStatus("Your browser blocked the pricing tab. Allow pop-ups for this page, then click Compare plans again.");
|
|
531
|
+
} catch (_) {
|
|
532
|
+
setRecallGateStatus("Could not open a new tab. Allow pop-ups for this page, then click Compare plans again.");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function skipRecallGate() {
|
|
536
|
+
recallGateSkipped = true;
|
|
537
|
+
applyRecallGate();
|
|
538
|
+
}
|
|
539
|
+
function wireRecallGate() {
|
|
540
|
+
var trialBtn = document.getElementById("recallStartTrial");
|
|
541
|
+
if (trialBtn) trialBtn.addEventListener("click", openRecallPricing);
|
|
542
|
+
var skipBtn = document.getElementById("recallSkip");
|
|
543
|
+
if (skipBtn) skipBtn.addEventListener("click", skipRecallGate);
|
|
544
|
+
wireTryButtons();
|
|
545
|
+
void refreshBillingStatus();
|
|
546
|
+
}
|
|
450
547
|
function tryItCard() {
|
|
451
548
|
var prompt = recallPrompt();
|
|
452
549
|
return '<div class="tryCard">' +
|
|
@@ -500,10 +597,10 @@ export const SETUP_PAGE_CLIENT_EXTRACTION = String.raw ` /* ---------- dash
|
|
|
500
597
|
'<span class="seal">Skipped for now</span>' +
|
|
501
598
|
'<h2>You can come back anytime.</h2>' +
|
|
502
599
|
'<p>Echo saved what already finished. When you are ready, run <code>echomem-mcp init</code> again — it picks up right where it left off.</p>' +
|
|
503
|
-
|
|
600
|
+
recallGateBlock() +
|
|
504
601
|
hudTip() +
|
|
505
602
|
'</div>';
|
|
506
|
-
|
|
603
|
+
wireRecallGate();
|
|
507
604
|
}
|
|
508
605
|
async function endExtraction() {
|
|
509
606
|
if (extractionEnded) return;
|
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
// Generated by splitting the previous inline setup page. Keep this source readable: it is emitted into one browser IIFE.
|
|
2
2
|
export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-first poll for the forensic report ---------- */
|
|
3
|
+
function renderLocalScanConsent() {
|
|
4
|
+
setScanMode(false);
|
|
5
|
+
setCityMode(false);
|
|
6
|
+
setExtractMode(false);
|
|
7
|
+
setReadyMode(false);
|
|
8
|
+
setHead("Local file access", "Consent");
|
|
9
|
+
app.className = "consentStage";
|
|
10
|
+
app.innerHTML =
|
|
11
|
+
'<section class="localConsent">' +
|
|
12
|
+
'<p class="consentEyebrow">Local visualization</p>' +
|
|
13
|
+
'<h2>Let Echo scan locally to build your Agent Doctor view?</h2>' +
|
|
14
|
+
'<p>Echo reads local Codex and Claude Code history on this machine only, then turns it into a private usage visualization. Nothing from this scan leaves your device.</p>' +
|
|
15
|
+
'<div class="consentActions">' +
|
|
16
|
+
'<button type="button" class="primary" id="allowLocalScan">Continue with local scan</button>' +
|
|
17
|
+
'<button type="button" class="textButton" id="declineLocalScan">Skip the insight for now</button>' +
|
|
18
|
+
'</div>' +
|
|
19
|
+
'<p class="consentFine" id="consentStatus"></p>' +
|
|
20
|
+
'</section>';
|
|
21
|
+
var allow = document.getElementById("allowLocalScan");
|
|
22
|
+
if (allow) allow.onclick = allowLocalScan;
|
|
23
|
+
var decline = document.getElementById("declineLocalScan");
|
|
24
|
+
if (decline) decline.onclick = declineLocalScan;
|
|
25
|
+
}
|
|
26
|
+
function setConsentStatus(message) {
|
|
27
|
+
var node = document.getElementById("consentStatus");
|
|
28
|
+
if (node) node.textContent = message || "";
|
|
29
|
+
}
|
|
30
|
+
async function allowLocalScan() {
|
|
31
|
+
setConsentStatus("Starting local scan...");
|
|
32
|
+
try {
|
|
33
|
+
await postJson("/report-consent", { allowed: true });
|
|
34
|
+
await showReport();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
renderReportIssue("CONSENT_SAVE_FAILED", error && error.message ? error.message : "Could not start the local scan.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function declineLocalScan() {
|
|
40
|
+
setConsentStatus("Skipping Agent Doctor...");
|
|
41
|
+
try {
|
|
42
|
+
await postJson("/report-consent", { allowed: false });
|
|
43
|
+
} catch (_) {}
|
|
44
|
+
renderLocalScanSkipped();
|
|
45
|
+
}
|
|
46
|
+
function renderLocalScanSkipped() {
|
|
47
|
+
setScanMode(false);
|
|
48
|
+
setCityMode(false);
|
|
49
|
+
setExtractMode(false);
|
|
50
|
+
setReadyMode(true);
|
|
51
|
+
setHead("Agent Doctor skipped", connected ? "Signed in" : "Optional");
|
|
52
|
+
app.className = "consentStage";
|
|
53
|
+
app.innerHTML =
|
|
54
|
+
'<section class="localConsent">' +
|
|
55
|
+
'<p class="consentEyebrow">Visualization skipped</p>' +
|
|
56
|
+
'<h2>No problem. Echo can still set up memory.</h2>' +
|
|
57
|
+
'<p>We will skip Agent Doctor and continue with EchoMem setup.</p>' +
|
|
58
|
+
'<div class="consentActions">' +
|
|
59
|
+
'<button type="button" class="primary" data-connect-echo>' + (connected ? "Continue setup" : "Connect EchoMem") + '</button>' +
|
|
60
|
+
'<button type="button" class="textButton" id="changeConsent">Go back</button>' +
|
|
61
|
+
'</div>' +
|
|
62
|
+
'<p class="consentFine" data-signin-help></p>' +
|
|
63
|
+
'<a class="button secondary hidden" data-signin-fallback target="_blank" rel="noopener noreferrer">Open sign-in</a>' +
|
|
64
|
+
'</section>';
|
|
65
|
+
bindConnect();
|
|
66
|
+
var change = document.getElementById("changeConsent");
|
|
67
|
+
if (change) change.onclick = renderLocalScanConsent;
|
|
68
|
+
}
|
|
3
69
|
async function showReport() {
|
|
4
70
|
if (reportPollStarted) return;
|
|
5
71
|
reportPollStarted = true;
|
|
@@ -103,14 +169,14 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
|
|
|
103
169
|
}
|
|
104
170
|
return;
|
|
105
171
|
}
|
|
106
|
-
// Learn the auth URL + connection state
|
|
107
|
-
|
|
172
|
+
// Learn the auth URL + connection state before asking for optional local file access.
|
|
173
|
+
try {
|
|
174
|
+
var cfg = await getJson("/config");
|
|
108
175
|
authUrl = cfg.authUrl || authUrl;
|
|
109
176
|
connected = !!cfg.connected;
|
|
110
177
|
workspacePath = cfg.workspacePath || workspacePath;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
await showReport();
|
|
178
|
+
} catch (_) {}
|
|
179
|
+
renderLocalScanConsent();
|
|
114
180
|
}
|
|
115
181
|
__ECHO_SETUP_STARTUP__;
|
|
116
182
|
`;
|
|
@@ -11,7 +11,7 @@ const SETUP_PAGE_BODY = String.raw `
|
|
|
11
11
|
</div>
|
|
12
12
|
<div class="pill" id="status">Local</div>
|
|
13
13
|
</header>
|
|
14
|
-
<section id="app" class="state">
|
|
14
|
+
<section id="app" class="state">Preparing setup...</section>
|
|
15
15
|
</main>
|
|
16
16
|
<dialog id="echo-note" class="echo-note-dlg">
|
|
17
17
|
<button class="nclose" aria-label="Close">×</button>
|