@echomem/mcp 1.4.35 → 1.4.37
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 +7 -11
- package/dist/codex-sync.js +1 -1
- package/dist/headless-runtime.js +254 -0
- package/dist/index.js +106 -12
- package/dist/package-metadata.js +4 -4
- package/dist/setup.js +43 -47
- package/dist/update-check.js +18 -4
- package/dist/v1-contract.js +100 -86
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ For headless systems and development, the granular CLI commands remain available
|
|
|
55
55
|
| `npx -y @echomem/mcp@latest setup` | One-off setup without keeping a global CLI command |
|
|
56
56
|
| `echomem-mcp setup [--client cursor\|windsurf\|claude-desktop\|claude-code\|codex]` | Write client config + log in |
|
|
57
57
|
| `echomem-mcp setup --skip-login [--client cursor\|windsurf\|claude-desktop\|claude-code\|codex]` | Write client config without opening the browser or changing credentials |
|
|
58
|
-
| `npx -y @echomem/mcp@latest update --all` |
|
|
58
|
+
| `npx -y @echomem/mcp@latest update --all` | Bootstrap or repair the durable per-user runtime and repoint detected client configs, with no browser login |
|
|
59
59
|
| `npx -y @echomem/mcp@latest update --client codex` | Update one client only |
|
|
60
60
|
| `echomem-mcp login` | Approve device in browser (or use `--token` / `--passphrase`) |
|
|
61
61
|
| `echomem-mcp unlock` | Privately unlock the vault on this trusted device |
|
|
@@ -65,10 +65,11 @@ For headless systems and development, the granular CLI commands remain available
|
|
|
65
65
|
| `echomem-mcp logout` | Remove stored credentials |
|
|
66
66
|
|
|
67
67
|
The bridge reports its package version in MCP server instructions and in tool descriptions. It also
|
|
68
|
-
checks npm for a newer published bridge using a cached, non-blocking check.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
checks npm for a newer published bridge using a cached, non-blocking check. Standalone installations
|
|
69
|
+
stage compatible updates in the background under `~/.echomem/mcp-runtime` and atomically activate
|
|
70
|
+
them for the next MCP session; the current handshake never waits for npm. Agents can call
|
|
71
|
+
`echomem_update_status` to inspect progress. `ECHO_DISABLE_AUTO_UPDATE=1` disables automatic
|
|
72
|
+
installation, and `npx -y @echomem/mcp@latest update --all` remains the bootstrap and repair command.
|
|
72
73
|
|
|
73
74
|
Agents can still call `echo_context_health` for an on-demand local context-health report. It reads
|
|
74
75
|
the local Codex/Claude logs and does not require a separate process or desktop overlay.
|
|
@@ -189,14 +190,9 @@ ECHO_API_TOKEN="your_token" ECHO_API_BASE_URL="http://localhost:3000" npm run st
|
|
|
189
190
|
* **`search_memories_by_keywords`**: Retrieve memories by matching the `keys` field.
|
|
190
191
|
* **`search_others_memories`**: Search other users' public memories through MemoryFeed public search.
|
|
191
192
|
* **`delete_memory`**: Delete a single personal memory through a two-step confirmation flow. First call with `memoryId` only to preview the target and receive `confirmationToken`; after the user explicitly confirms, call again with `confirmed: true` and that exact token. This deletes the memory row only and preserves raw `source_of_truth` conversation records.
|
|
192
|
-
* **`echomem_update_status`**: Check the installed bridge against the latest published npm version. Works without login, uses cached background checks in normal operation, and
|
|
193
|
+
* **`echomem_update_status`**: Check the installed bridge against the latest published npm version. Works without login, uses cached background checks in normal operation, and reports automatic installation state plus a fallback repair command.
|
|
193
194
|
* **`echo_context_health`**: Return the local Codex/Claude context-health score as markdown. Works without login and uploads no transcript content.
|
|
194
195
|
|
|
195
|
-
Legacy aliases are preserved for compatibility:
|
|
196
|
-
|
|
197
|
-
* `search_memories_by_description_semantic` -> `search_memories`
|
|
198
|
-
* `search_memories_by_time_range` -> `get_memories_by_time_range`
|
|
199
|
-
|
|
200
196
|
Contract reference:
|
|
201
197
|
|
|
202
198
|
* `docs/PUBLIC_API_CONTRACT_V1.md`
|
package/dist/codex-sync.js
CHANGED
|
@@ -132,7 +132,7 @@ function summarizeToolArgs(toolName, args) {
|
|
|
132
132
|
tag_count: Array.isArray(args.tags) ? args.tags.length : undefined,
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
|
-
if (toolName === "get_memories_by_time_range"
|
|
135
|
+
if (toolName === "get_memories_by_time_range") {
|
|
136
136
|
return {
|
|
137
137
|
has_start_date: !!asString(args.startDate),
|
|
138
138
|
has_end_date: !!asString(args.endDate),
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { echoConfigDir } from "./keystore.js";
|
|
6
|
+
import { MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION } from "./package-metadata.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const RUNTIME_LAYOUT = 1;
|
|
9
|
+
const UPDATE_LOCK_STALE_MS = 15 * 60 * 1000;
|
|
10
|
+
export function headlessRuntimeRoot() {
|
|
11
|
+
return path.join(echoConfigDir(), "mcp-runtime");
|
|
12
|
+
}
|
|
13
|
+
function versionsRoot() {
|
|
14
|
+
return path.join(headlessRuntimeRoot(), "versions");
|
|
15
|
+
}
|
|
16
|
+
function activeRuntimePath() {
|
|
17
|
+
return path.join(headlessRuntimeRoot(), "active.json");
|
|
18
|
+
}
|
|
19
|
+
export function headlessLauncherPath() {
|
|
20
|
+
return path.join(headlessRuntimeRoot(), "launcher.mjs");
|
|
21
|
+
}
|
|
22
|
+
function packageEntryForPrefix(prefix) {
|
|
23
|
+
return path.join(prefix, "node_modules", ...MCP_PACKAGE_NAME.split("/"), "dist", "index.js");
|
|
24
|
+
}
|
|
25
|
+
function packageJsonForPrefix(prefix) {
|
|
26
|
+
return path.join(prefix, "node_modules", ...MCP_PACKAGE_NAME.split("/"), "package.json");
|
|
27
|
+
}
|
|
28
|
+
function safeVersion(value) {
|
|
29
|
+
const version = value.trim();
|
|
30
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
31
|
+
throw new Error(`Invalid EchoMem MCP version: ${value}`);
|
|
32
|
+
}
|
|
33
|
+
return version;
|
|
34
|
+
}
|
|
35
|
+
function relativeEntryForVersion(version) {
|
|
36
|
+
return path.join("versions", version, "node_modules", ...MCP_PACKAGE_NAME.split("/"), "dist", "index.js");
|
|
37
|
+
}
|
|
38
|
+
function atomicWriteJson(file, value) {
|
|
39
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
40
|
+
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
41
|
+
fs.writeFileSync(temporary, JSON.stringify(value, null, 2), { mode: 0o600 });
|
|
42
|
+
fs.renameSync(temporary, file);
|
|
43
|
+
try {
|
|
44
|
+
fs.chmodSync(file, 0o600);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* Best effort on platforms without POSIX permissions. */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function ensureLauncher() {
|
|
51
|
+
const root = headlessRuntimeRoot();
|
|
52
|
+
const launcher = headlessLauncherPath();
|
|
53
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
54
|
+
const source = [
|
|
55
|
+
"#!/usr/bin/env node",
|
|
56
|
+
'import fs from "node:fs";',
|
|
57
|
+
'import path from "node:path";',
|
|
58
|
+
'import { fileURLToPath, pathToFileURL } from "node:url";',
|
|
59
|
+
"const root = path.dirname(fileURLToPath(import.meta.url));",
|
|
60
|
+
'const active = JSON.parse(fs.readFileSync(path.join(root, "active.json"), "utf8"));',
|
|
61
|
+
'if (active.layout !== 1 || typeof active.entry !== "string" || path.isAbsolute(active.entry) || active.entry.split(/[\\\\/]/).includes("..")) {',
|
|
62
|
+
' throw new Error("EchoMem MCP runtime pointer is invalid; run `npx -y @echomem/mcp@latest update --all`.");',
|
|
63
|
+
"}",
|
|
64
|
+
"const entry = path.resolve(root, active.entry);",
|
|
65
|
+
'if (!fs.existsSync(entry)) throw new Error("EchoMem MCP runtime is missing; run `npx -y @echomem/mcp@latest update --all`.");',
|
|
66
|
+
"await import(pathToFileURL(entry).href);",
|
|
67
|
+
"",
|
|
68
|
+
].join("\n");
|
|
69
|
+
let current = "";
|
|
70
|
+
try {
|
|
71
|
+
current = fs.readFileSync(launcher, "utf8");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* Create it below. */
|
|
75
|
+
}
|
|
76
|
+
if (current !== source)
|
|
77
|
+
fs.writeFileSync(launcher, source, { mode: 0o700 });
|
|
78
|
+
try {
|
|
79
|
+
fs.chmodSync(launcher, 0o700);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* Best effort on platforms without POSIX permissions. */
|
|
83
|
+
}
|
|
84
|
+
return launcher;
|
|
85
|
+
}
|
|
86
|
+
function readPackageVersion(prefix) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(fs.readFileSync(packageJsonForPrefix(prefix), "utf8"));
|
|
89
|
+
return typeof parsed.version === "string" ? parsed.version : undefined;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function readHeadlessRuntimeInstallation() {
|
|
96
|
+
try {
|
|
97
|
+
const active = JSON.parse(fs.readFileSync(activeRuntimePath(), "utf8"));
|
|
98
|
+
if (active.layout !== RUNTIME_LAYOUT || typeof active.version !== "string" || typeof active.entry !== "string") {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (path.isAbsolute(active.entry) || active.entry.split(/[\\/]/).includes(".."))
|
|
102
|
+
return undefined;
|
|
103
|
+
const entry = path.resolve(headlessRuntimeRoot(), active.entry);
|
|
104
|
+
const expectedRoot = `${path.resolve(headlessRuntimeRoot())}${path.sep}`;
|
|
105
|
+
if (!entry.startsWith(expectedRoot) || !fs.existsSync(entry))
|
|
106
|
+
return undefined;
|
|
107
|
+
const prefix = path.join(versionsRoot(), active.version);
|
|
108
|
+
if (readPackageVersion(prefix) !== active.version)
|
|
109
|
+
return undefined;
|
|
110
|
+
return { version: active.version, launcher: ensureLauncher(), entry };
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function activateRuntime(prefix, version) {
|
|
117
|
+
const entry = packageEntryForPrefix(prefix);
|
|
118
|
+
if (!fs.existsSync(entry) || readPackageVersion(prefix) !== version) {
|
|
119
|
+
throw new Error(`${MCP_PACKAGE_NAME}@${version} did not install a valid MCP entry point`);
|
|
120
|
+
}
|
|
121
|
+
const launcher = ensureLauncher();
|
|
122
|
+
atomicWriteJson(activeRuntimePath(), {
|
|
123
|
+
layout: RUNTIME_LAYOUT,
|
|
124
|
+
version,
|
|
125
|
+
entry: relativeEntryForVersion(version),
|
|
126
|
+
activatedAt: new Date().toISOString(),
|
|
127
|
+
});
|
|
128
|
+
return { version, launcher, entry };
|
|
129
|
+
}
|
|
130
|
+
function resolveNpmInvocation() {
|
|
131
|
+
const candidates = [
|
|
132
|
+
process.env.npm_execpath,
|
|
133
|
+
path.resolve(path.dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
134
|
+
path.resolve(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
|
|
135
|
+
].filter((candidate) => Boolean(candidate));
|
|
136
|
+
for (const npmCli of candidates) {
|
|
137
|
+
try {
|
|
138
|
+
if (fs.existsSync(npmCli))
|
|
139
|
+
return { command: process.execPath, prefixArgs: [npmCli] };
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* Keep looking. */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { command: process.platform === "win32" ? "npm.cmd" : "npm", prefixArgs: [] };
|
|
146
|
+
}
|
|
147
|
+
function npmInstallArguments(prefix, version) {
|
|
148
|
+
return [
|
|
149
|
+
"install",
|
|
150
|
+
"--ignore-scripts",
|
|
151
|
+
"--omit=dev",
|
|
152
|
+
"--no-audit",
|
|
153
|
+
"--no-fund",
|
|
154
|
+
"--no-package-lock",
|
|
155
|
+
"--prefix",
|
|
156
|
+
prefix,
|
|
157
|
+
`${MCP_PACKAGE_NAME}@${version}`,
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
function prepareStaging(version) {
|
|
161
|
+
const destination = path.join(versionsRoot(), version);
|
|
162
|
+
fs.mkdirSync(versionsRoot(), { recursive: true, mode: 0o700 });
|
|
163
|
+
const staging = path.join(versionsRoot(), `.${version}-${process.pid}-${Date.now()}`);
|
|
164
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
165
|
+
return { destination, staging };
|
|
166
|
+
}
|
|
167
|
+
function finishStaging(staging, destination, version) {
|
|
168
|
+
if (readPackageVersion(staging) !== version || !fs.existsSync(packageEntryForPrefix(staging))) {
|
|
169
|
+
throw new Error(`npm installed an invalid ${MCP_PACKAGE_NAME}@${version} runtime`);
|
|
170
|
+
}
|
|
171
|
+
if (fs.existsSync(destination))
|
|
172
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
173
|
+
fs.renameSync(staging, destination);
|
|
174
|
+
return activateRuntime(destination, version);
|
|
175
|
+
}
|
|
176
|
+
export function installHeadlessRuntimeSync(targetVersion = MCP_PACKAGE_VERSION) {
|
|
177
|
+
const version = safeVersion(targetVersion);
|
|
178
|
+
const existing = readHeadlessRuntimeInstallation();
|
|
179
|
+
if (existing?.version === version)
|
|
180
|
+
return existing;
|
|
181
|
+
const destination = path.join(versionsRoot(), version);
|
|
182
|
+
if (readPackageVersion(destination) === version && fs.existsSync(packageEntryForPrefix(destination))) {
|
|
183
|
+
return activateRuntime(destination, version);
|
|
184
|
+
}
|
|
185
|
+
const { staging } = prepareStaging(version);
|
|
186
|
+
const npm = resolveNpmInvocation();
|
|
187
|
+
try {
|
|
188
|
+
execFileSync(npm.command, [...npm.prefixArgs, ...npmInstallArguments(staging, version)], {
|
|
189
|
+
stdio: "inherit",
|
|
190
|
+
windowsHide: true,
|
|
191
|
+
});
|
|
192
|
+
return finishStaging(staging, destination, version);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
196
|
+
throw new Error(`Could not stage ${MCP_PACKAGE_NAME}@${version}: ${error instanceof Error ? error.message : String(error)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function acquireUpdateLock() {
|
|
200
|
+
const lock = path.join(headlessRuntimeRoot(), "update.lock");
|
|
201
|
+
fs.mkdirSync(headlessRuntimeRoot(), { recursive: true, mode: 0o700 });
|
|
202
|
+
try {
|
|
203
|
+
const descriptor = fs.openSync(lock, "wx", 0o600);
|
|
204
|
+
fs.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
|
|
205
|
+
fs.closeSync(descriptor);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
try {
|
|
209
|
+
const age = Date.now() - fs.statSync(lock).mtimeMs;
|
|
210
|
+
if (age <= UPDATE_LOCK_STALE_MS)
|
|
211
|
+
return undefined;
|
|
212
|
+
fs.rmSync(lock, { force: true });
|
|
213
|
+
return acquireUpdateLock();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return () => fs.rmSync(lock, { force: true });
|
|
220
|
+
}
|
|
221
|
+
export async function autoUpdateHeadlessRuntime(targetVersion) {
|
|
222
|
+
if (process.env.ECHO_DISABLE_AUTO_UPDATE === "1")
|
|
223
|
+
return { state: "disabled" };
|
|
224
|
+
let version;
|
|
225
|
+
try {
|
|
226
|
+
version = safeVersion(targetVersion);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
return { state: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
230
|
+
}
|
|
231
|
+
if (readHeadlessRuntimeInstallation()?.version === version)
|
|
232
|
+
return { state: "already-installed" };
|
|
233
|
+
const releaseLock = acquireUpdateLock();
|
|
234
|
+
if (!releaseLock)
|
|
235
|
+
return { state: "busy" };
|
|
236
|
+
const { destination, staging } = prepareStaging(version);
|
|
237
|
+
const npm = resolveNpmInvocation();
|
|
238
|
+
try {
|
|
239
|
+
await execFileAsync(npm.command, [...npm.prefixArgs, ...npmInstallArguments(staging, version)], {
|
|
240
|
+
timeout: 120_000,
|
|
241
|
+
windowsHide: true,
|
|
242
|
+
maxBuffer: 1024 * 1024,
|
|
243
|
+
});
|
|
244
|
+
const installation = finishStaging(staging, destination, version);
|
|
245
|
+
return { state: "installed", installation };
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
249
|
+
return { state: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
releaseLock();
|
|
253
|
+
}
|
|
254
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { runCli } from "./setup.js";
|
|
|
15
15
|
import { MCP_PACKAGE_VERSION, MCP_SERVER_INSTRUCTIONS, MEMORY_CITATION_INSTRUCTION, SAVED_MEMORY_RECEIPT_INSTRUCTION, } from "./package-metadata.js";
|
|
16
16
|
import { clearBillingAlert, writeBillingAlert } from "./billing-alert.js";
|
|
17
17
|
import { checkLatestUpdateStatus, formatUpdateNotice, formatUpdateStatusText, startBackgroundUpdateCheck, } from "./update-check.js";
|
|
18
|
+
import { autoUpdateHeadlessRuntime } from "./headless-runtime.js";
|
|
18
19
|
const ECHO_API_BASE_URL = process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app";
|
|
19
20
|
const ECHO_PRICING_URL = process.env.ECHO_PRICING_URL || "https://echoknows.com/account";
|
|
20
21
|
const ECHO_MEMORY_WEB_URL = (process.env.ECHO_MEMORY_WEB_URL || "https://echoknows.com/memory").replace(/\/$/, "");
|
|
@@ -192,6 +193,56 @@ function readNumber(record, key) {
|
|
|
192
193
|
const value = record[key];
|
|
193
194
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
194
195
|
}
|
|
196
|
+
function normalizeQueryAlias(args) {
|
|
197
|
+
if (!isRecord(args) || !("conversation" in args))
|
|
198
|
+
return args;
|
|
199
|
+
const query = readString(args, "query") ?? readString(args, "conversation");
|
|
200
|
+
const { conversation: _ignored, ...rest } = args;
|
|
201
|
+
return query ? { ...rest, query } : rest;
|
|
202
|
+
}
|
|
203
|
+
const PERSONAL_RECALL_TOOL_NAMES = new Set([
|
|
204
|
+
canonicalToolNames.search,
|
|
205
|
+
canonicalToolNames.timeRange,
|
|
206
|
+
canonicalToolNames.keywords,
|
|
207
|
+
]);
|
|
208
|
+
function hasKeywordInput(args) {
|
|
209
|
+
const value = args.keywords;
|
|
210
|
+
if (Array.isArray(value)) {
|
|
211
|
+
return value.some((item) => typeof item === "string" && item.trim().length > 0);
|
|
212
|
+
}
|
|
213
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
214
|
+
}
|
|
215
|
+
function routePersonalRecallInvocation(canonicalName, args) {
|
|
216
|
+
if (!PERSONAL_RECALL_TOOL_NAMES.has(canonicalName)) {
|
|
217
|
+
return { canonicalName, args };
|
|
218
|
+
}
|
|
219
|
+
const normalizedArgs = normalizeQueryAlias(args);
|
|
220
|
+
if (!isRecord(normalizedArgs)) {
|
|
221
|
+
return { canonicalName, args: normalizedArgs };
|
|
222
|
+
}
|
|
223
|
+
const hasSemanticInput = Boolean(readString(normalizedArgs, "query")
|
|
224
|
+
|| readNumber(normalizedArgs, "timeFrameDays") !== undefined);
|
|
225
|
+
const hasKeywords = hasKeywordInput(normalizedArgs);
|
|
226
|
+
const hasDateRangeInput = Boolean(readString(normalizedArgs, "startDate") || readString(normalizedArgs, "endDate"));
|
|
227
|
+
const intentCount = Number(hasSemanticInput) + Number(hasKeywords) + Number(hasDateRangeInput);
|
|
228
|
+
if (intentCount > 1) {
|
|
229
|
+
return {
|
|
230
|
+
canonicalName,
|
|
231
|
+
args: normalizedArgs,
|
|
232
|
+
error: "Ambiguous memory search arguments. Use exactly one input shape: {query, optional timeFrameDays} for topic search, {keywords} for exact-key search, or {startDate, endDate} for a date range.",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (hasSemanticInput) {
|
|
236
|
+
return { canonicalName: canonicalToolNames.search, args: normalizedArgs };
|
|
237
|
+
}
|
|
238
|
+
if (hasKeywords) {
|
|
239
|
+
return { canonicalName: canonicalToolNames.keywords, args: normalizedArgs };
|
|
240
|
+
}
|
|
241
|
+
if (hasDateRangeInput) {
|
|
242
|
+
return { canonicalName: canonicalToolNames.timeRange, args: normalizedArgs };
|
|
243
|
+
}
|
|
244
|
+
return { canonicalName, args: normalizedArgs };
|
|
245
|
+
}
|
|
195
246
|
function errorCodeFrom(value) {
|
|
196
247
|
if (!isRecord(value))
|
|
197
248
|
return undefined;
|
|
@@ -783,12 +834,12 @@ class EchoMemApiClient {
|
|
|
783
834
|
};
|
|
784
835
|
}
|
|
785
836
|
async searchMemories(args, trace) {
|
|
786
|
-
const parsed = searchMemoriesSchema.parse(args ?? {});
|
|
837
|
+
const parsed = searchMemoriesSchema.parse(normalizeQueryAlias(args) ?? {});
|
|
787
838
|
const query = parsed.query?.trim();
|
|
788
839
|
const limit = parsed.limit ?? parsed.k ?? 10;
|
|
789
840
|
const threshold = parsed.threshold ?? 0.1;
|
|
790
841
|
if (!query && !parsed.timeFrameDays) {
|
|
791
|
-
throw new McpError(ErrorCode.InvalidParams, "
|
|
842
|
+
throw new McpError(ErrorCode.InvalidParams, "search_memories requires a non-empty query. For example: {\"query\":\"research article hero image\",\"timeFrameDays\":14}. For an explicit date range, call get_memories_by_time_range with both startDate and endDate.");
|
|
792
843
|
}
|
|
793
844
|
const enc = await this.encState(); // throws LockedError for an encrypted account without a key
|
|
794
845
|
if (!query) {
|
|
@@ -1166,7 +1217,7 @@ class EchoMemMCPServer {
|
|
|
1166
1217
|
this.events = new EventLogger({ session_id: this.client.getSessionId(), app_version: SERVER_VERSION });
|
|
1167
1218
|
if (!DESKTOP_MANAGED) {
|
|
1168
1219
|
startBackgroundUpdateCheck((status) => {
|
|
1169
|
-
this.
|
|
1220
|
+
this.handleUpdateStatus(status);
|
|
1170
1221
|
});
|
|
1171
1222
|
}
|
|
1172
1223
|
this.setupToolHandlers();
|
|
@@ -1176,6 +1227,33 @@ class EchoMemMCPServer {
|
|
|
1176
1227
|
process.exit(0);
|
|
1177
1228
|
});
|
|
1178
1229
|
}
|
|
1230
|
+
handleUpdateStatus(status) {
|
|
1231
|
+
this.updateStatus = status;
|
|
1232
|
+
if (!status.updateAvailable || !status.latestVersion)
|
|
1233
|
+
return;
|
|
1234
|
+
this.updateStatus = { ...status, autoUpdateState: "installing" };
|
|
1235
|
+
void autoUpdateHeadlessRuntime(status.latestVersion).then((result) => {
|
|
1236
|
+
if (result.state === "installed" || result.state === "already-installed") {
|
|
1237
|
+
this.updateStatus = { ...status, autoUpdateState: "ready" };
|
|
1238
|
+
}
|
|
1239
|
+
else if (result.state === "busy") {
|
|
1240
|
+
this.updateStatus = { ...status, autoUpdateState: "installing" };
|
|
1241
|
+
}
|
|
1242
|
+
else {
|
|
1243
|
+
this.updateStatus = {
|
|
1244
|
+
...status,
|
|
1245
|
+
autoUpdateState: "failed",
|
|
1246
|
+
error: result.state === "failed" ? result.error : "automatic updates are disabled",
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
}).catch((error) => {
|
|
1250
|
+
this.updateStatus = {
|
|
1251
|
+
...status,
|
|
1252
|
+
autoUpdateState: "failed",
|
|
1253
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1254
|
+
};
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1179
1257
|
getMcpClientAnalytics() {
|
|
1180
1258
|
const hostPlatform = normalizeMcpHostPlatform(this.mcpClientName)
|
|
1181
1259
|
?? detectMcpHostFromEnv()
|
|
@@ -1220,7 +1298,12 @@ class EchoMemMCPServer {
|
|
|
1220
1298
|
return { tools: listToolSpecs({ map, groupMap, updateNotice }) };
|
|
1221
1299
|
});
|
|
1222
1300
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1223
|
-
const
|
|
1301
|
+
const resolvedCanonicalName = resolveCanonicalToolName(request.params.name);
|
|
1302
|
+
const recallRoute = routePersonalRecallInvocation(resolvedCanonicalName, request.params.arguments);
|
|
1303
|
+
const canonicalName = recallRoute.canonicalName;
|
|
1304
|
+
const toolArgs = canonicalName === canonicalToolNames.others
|
|
1305
|
+
? normalizeQueryAlias(recallRoute.args)
|
|
1306
|
+
: recallRoute.args;
|
|
1224
1307
|
const clientVersion = this.server.getClientVersion();
|
|
1225
1308
|
this.mcpClientName = clientVersion?.name ?? this.mcpClientName;
|
|
1226
1309
|
this.mcpClientVersion = clientVersion?.version ?? this.mcpClientVersion;
|
|
@@ -1235,8 +1318,8 @@ class EchoMemMCPServer {
|
|
|
1235
1318
|
conversation_id: this.client.getSessionId(),
|
|
1236
1319
|
tool_name: request.params.name,
|
|
1237
1320
|
canonical_tool_name: canonicalName,
|
|
1238
|
-
...triggerAnalyticsForTool(canonicalName,
|
|
1239
|
-
...inputAnalyticsForTool(canonicalName,
|
|
1321
|
+
...triggerAnalyticsForTool(canonicalName, toolArgs),
|
|
1322
|
+
...inputAnalyticsForTool(canonicalName, toolArgs),
|
|
1240
1323
|
};
|
|
1241
1324
|
const analyticsCallId = randomUUID();
|
|
1242
1325
|
// One event per call. Handlers enrich `rec` with tool-specific detail; we finalize + log in `finally`.
|
|
@@ -1247,6 +1330,9 @@ class EchoMemMCPServer {
|
|
|
1247
1330
|
group_map_injected: this.groupMapInjected,
|
|
1248
1331
|
};
|
|
1249
1332
|
try {
|
|
1333
|
+
if (recallRoute.error) {
|
|
1334
|
+
throw new McpError(ErrorCode.InvalidParams, recallRoute.error);
|
|
1335
|
+
}
|
|
1250
1336
|
// The usage report is a local, $0 audit — works with no login (value before signup).
|
|
1251
1337
|
if (canonicalName === canonicalToolNames.report) {
|
|
1252
1338
|
return { content: [{ type: "text", text: await buildReportText(false) }] };
|
|
@@ -1262,8 +1348,8 @@ class EchoMemMCPServer {
|
|
|
1262
1348
|
}
|
|
1263
1349
|
const force = isRecord(request.params.arguments) && request.params.arguments.force === true;
|
|
1264
1350
|
const status = await checkLatestUpdateStatus({ force });
|
|
1265
|
-
this.
|
|
1266
|
-
return { content: [{ type: "text", text: formatUpdateStatusText(status) }] };
|
|
1351
|
+
this.handleUpdateStatus(status);
|
|
1352
|
+
return { content: [{ type: "text", text: formatUpdateStatusText(this.updateStatus ?? status) }] };
|
|
1267
1353
|
}
|
|
1268
1354
|
if (canonicalName === canonicalToolNames.contextHealth) {
|
|
1269
1355
|
const client = isRecord(request.params.arguments) && typeof request.params.arguments.client === "string"
|
|
@@ -1315,17 +1401,17 @@ class EchoMemMCPServer {
|
|
|
1315
1401
|
}
|
|
1316
1402
|
switch (canonicalName) {
|
|
1317
1403
|
case canonicalToolNames.search:
|
|
1318
|
-
return await this.handleSearch(
|
|
1404
|
+
return await this.handleSearch(toolArgs, rec);
|
|
1319
1405
|
case canonicalToolNames.save:
|
|
1320
1406
|
return await this.handleSave(request.params.arguments, rec);
|
|
1321
1407
|
case canonicalToolNames.timeRange:
|
|
1322
|
-
return await this.handleTimeRange(
|
|
1408
|
+
return await this.handleTimeRange(toolArgs);
|
|
1323
1409
|
case canonicalToolNames.getByContext:
|
|
1324
1410
|
return await this.handleGetByContext(request.params.arguments);
|
|
1325
1411
|
case canonicalToolNames.checkpointByContext:
|
|
1326
1412
|
return await this.handleGetCheckpointByContext(request.params.arguments);
|
|
1327
1413
|
case canonicalToolNames.keywords:
|
|
1328
|
-
return await this.handleKeywords(
|
|
1414
|
+
return await this.handleKeywords(toolArgs);
|
|
1329
1415
|
case canonicalToolNames.friends:
|
|
1330
1416
|
return await this.handleFriends(request.params.arguments);
|
|
1331
1417
|
case canonicalToolNames.searchUsers:
|
|
@@ -1333,7 +1419,7 @@ class EchoMemMCPServer {
|
|
|
1333
1419
|
case canonicalToolNames.sendFriendRequest:
|
|
1334
1420
|
return await this.handleSendFriendRequest(request.params.arguments);
|
|
1335
1421
|
case canonicalToolNames.others:
|
|
1336
|
-
return await this.handleOthers(
|
|
1422
|
+
return await this.handleOthers(toolArgs);
|
|
1337
1423
|
case canonicalToolNames.publicMemory:
|
|
1338
1424
|
return await this.handlePublicMemory(request.params.arguments);
|
|
1339
1425
|
case canonicalToolNames.recordCitations:
|
|
@@ -2257,6 +2343,14 @@ Details: ${m.details || "N/A"}`;
|
|
|
2257
2343
|
content: [{ type: "text", text: this.sharingFallbackText(scopeText, groupName) }],
|
|
2258
2344
|
};
|
|
2259
2345
|
}
|
|
2346
|
+
if (elicitation.action === "cancel") {
|
|
2347
|
+
return {
|
|
2348
|
+
content: [{
|
|
2349
|
+
type: "text",
|
|
2350
|
+
text: this.sharingFallbackText(scopeText, groupName),
|
|
2351
|
+
}],
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2260
2354
|
if (elicitation.action !== "accept") {
|
|
2261
2355
|
return {
|
|
2262
2356
|
content: [{
|
package/dist/package-metadata.js
CHANGED
|
@@ -28,13 +28,13 @@ export const MCP_VAULT_UNLOCK_INSTRUCTION = MCP_DESKTOP_MANAGED
|
|
|
28
28
|
? "open Echo Desktop and unlock the vault there"
|
|
29
29
|
: "run `echomem-mcp unlock` locally";
|
|
30
30
|
const MCP_UPDATE_INSTRUCTION = MCP_DESKTOP_MANAGED
|
|
31
|
-
? "Echo Desktop
|
|
32
|
-
: `
|
|
31
|
+
? "Echo Desktop auto-installs compatible MCP updates"
|
|
32
|
+
: `updates auto-install in the background; on failure run \`${MCP_UPDATE_ALL_COMMAND}\``;
|
|
33
33
|
export const MEMORY_CITATION_INSTRUCTION = 'If the user-facing answer materially relies on one or more EchoMem memories, end it with a compact "EchoMem sources:" list containing only the memories actually used. For memories owned by teammates or accepted friends, call record_memory_citations immediately before the final answer with those exact Memory IDs. Do not cite memories that were merely retrieved. Use each memory key as the Markdown label and its canonical https://echoknows.com/memory/<memory-id> URL. Omit the sources section and citation receipt when no memory informed the answer.';
|
|
34
34
|
export const SAVED_MEMORY_RECEIPT_INSTRUCTION = 'After save_conversation succeeds and returns one or more saved memory IDs, make the save visible in the final user-facing answer with a compact "EchoMem saved:" list containing every memory created by that call. Use each memory key as the Markdown label and its canonical https://echoknows.com/memory/<memory-id> URL. This save receipt is separate from "EchoMem sources:" and does not imply that the newly saved memories informed the answer.';
|
|
35
35
|
export const MCP_SERVER_INSTRUCTIONS = [
|
|
36
36
|
`${MCP_PACKAGE_DESCRIPTION} (${MCP_PACKAGE_LABEL}).`,
|
|
37
|
-
`If
|
|
37
|
+
`If stale, ${MCP_UPDATE_INSTRUCTION}; restart the MCP session afterward. Updates never delay startup.`,
|
|
38
38
|
"Before re-deriving prior decisions or preferences, use search_memories.",
|
|
39
39
|
`Before the final response for a durable decision, implementation, fix, commit, passing verification, release, or milestone, call save_conversation. Skip secrets and trivial work. If the encrypted vault is locked, tell the user to ${MCP_VAULT_UNLOCK_INSTRUCTION}.`,
|
|
40
40
|
"After a successful save, show every memory created by that call in a compact EchoMem saved: list with canonical links; this is separate from \"EchoMem sources:\".",
|
|
@@ -45,5 +45,5 @@ export const MCP_SERVER_INSTRUCTIONS = [
|
|
|
45
45
|
"Group profiles, conversation sharing, publication, sensitive-memory flags, and deletion require explicit user confirmation. Never store or log an echo_grp_ invite code.",
|
|
46
46
|
].join(" ");
|
|
47
47
|
export function withMcpVersion(description) {
|
|
48
|
-
return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, ${MCP_UPDATE_INSTRUCTION}, then start a new MCP session. Do not run updates repeatedly
|
|
48
|
+
return `${description}\n\nEchoMem MCP bridge: ${MCP_PACKAGE_LABEL}. If this version is stale, ${MCP_UPDATE_INSTRUCTION}, then start a new MCP session. Do not run manual updates repeatedly.`;
|
|
49
49
|
}
|
package/dist/setup.js
CHANGED
|
@@ -34,6 +34,7 @@ import { repoLabel, validateForensicReportForSetup } from "./forensics.js";
|
|
|
34
34
|
import { installSaveCheckpointHooks } from "./hud/hooks.js";
|
|
35
35
|
import { MCP_PACKAGE_LABEL, MCP_PACKAGE_NAME, MCP_PACKAGE_VERSION, MCP_UPDATE_ALL_COMMAND, MCP_UPDATE_COMMAND } from "./package-metadata.js";
|
|
36
36
|
import { checkLatestUpdateStatus, compareSemver, readCachedUpdateStatus } from "./update-check.js";
|
|
37
|
+
import { installHeadlessRuntimeSync, readHeadlessRuntimeInstallation, } from "./headless-runtime.js";
|
|
37
38
|
// The setup dashboard, account login, and encryption passphrase entry are all served by this
|
|
38
39
|
// localhost bridge. The hosted API only sends OTP email, verifies the code, and mints a device token.
|
|
39
40
|
const API_BASE_URL = (process.env.ECHO_API_BASE_URL || "https://echo-mem-chrome.vercel.app").replace(/\/$/, "");
|
|
@@ -239,6 +240,10 @@ export function buildServerEntry(opts = {}) {
|
|
|
239
240
|
if (opts.devEntryPath) {
|
|
240
241
|
return { command: "node", args: [opts.devEntryPath] };
|
|
241
242
|
}
|
|
243
|
+
const managed = readHeadlessRuntimeInstallation();
|
|
244
|
+
if (managed) {
|
|
245
|
+
return { command: process.execPath, args: [managed.launcher] };
|
|
246
|
+
}
|
|
242
247
|
// Spawn the ALREADY-INSTALLED bridge directly (this node + this script's real path) instead of
|
|
243
248
|
// `npx -y @echomem/mcp`. `npx -y` re-resolves and, on a cache miss, NETWORK-fetches from the npm
|
|
244
249
|
// registry on EVERY client start — on a slow/flaky network that delays the MCP handshake past the
|
|
@@ -291,51 +296,12 @@ function resolveGlobalEntry() {
|
|
|
291
296
|
}
|
|
292
297
|
return null;
|
|
293
298
|
}
|
|
294
|
-
|
|
295
|
-
if (
|
|
296
|
-
return false;
|
|
297
|
-
const installedVersion = durableEntry
|
|
298
|
-
? packageVersionFromPath(durableEntry)?.version
|
|
299
|
-
: undefined;
|
|
300
|
-
return installedVersion !== targetVersion;
|
|
301
|
-
}
|
|
302
|
-
function installDurableGlobalUpdate() {
|
|
303
|
-
const runningEntry = (() => {
|
|
304
|
-
try {
|
|
305
|
-
return fs.realpathSync(process.argv[1] || "");
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
return process.argv[1] || "";
|
|
309
|
-
}
|
|
310
|
-
})();
|
|
311
|
-
const currentGlobalEntry = resolveGlobalEntry();
|
|
312
|
-
if (!needsDurableGlobalUpdate(runningEntry, currentGlobalEntry))
|
|
299
|
+
function installDurableHeadlessRuntime() {
|
|
300
|
+
if (process.env.ECHO_DISABLE_RUNTIME_BOOTSTRAP === "1")
|
|
313
301
|
return;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
if (npmExecPath && fs.existsSync(npmExecPath)) {
|
|
319
|
-
execFileSync(process.execPath, [npmExecPath, "install", "-g", packageSpec], {
|
|
320
|
-
stdio: "inherit",
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
execFileSync(process.platform === "win32" ? "npm.cmd" : "npm", ["install", "-g", packageSpec], {
|
|
325
|
-
stdio: "inherit",
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
catch (error) {
|
|
330
|
-
throw new Error(`Could not install ${packageSpec} globally: ${error instanceof Error ? error.message : String(error)}`);
|
|
331
|
-
}
|
|
332
|
-
const installedEntry = resolveGlobalEntry();
|
|
333
|
-
const installedVersion = installedEntry
|
|
334
|
-
? packageVersionFromPath(installedEntry)?.version
|
|
335
|
-
: undefined;
|
|
336
|
-
if (installedVersion !== MCP_PACKAGE_VERSION) {
|
|
337
|
-
throw new Error(`Global EchoMem bridge is ${installedVersion || "missing"} after update; expected ${MCP_PACKAGE_VERSION}.`);
|
|
338
|
-
}
|
|
302
|
+
console.log(`Staging durable per-user ${MCP_PACKAGE_NAME}@${MCP_PACKAGE_VERSION} runtime…`);
|
|
303
|
+
const installation = installHeadlessRuntimeSync();
|
|
304
|
+
console.log(`✅ Activated ${MCP_PACKAGE_NAME}@${installation.version} for new MCP sessions.`);
|
|
339
305
|
}
|
|
340
306
|
/** The TOML block EchoMem adds to ~/.codex/config.toml. No secret — the bridge reads the keystore. */
|
|
341
307
|
export function codexTomlBlock(entry) {
|
|
@@ -450,8 +416,30 @@ export function writeJsonClientConfig(configPath, entry) {
|
|
|
450
416
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
451
417
|
}
|
|
452
418
|
export function writeClaudeCodeConfig(entry) {
|
|
419
|
+
const addArguments = ["mcp", "add-json", "-s", "local", "echomem", JSON.stringify(entry)];
|
|
453
420
|
try {
|
|
454
|
-
execFileSync("claude",
|
|
421
|
+
execFileSync("claude", addArguments, {
|
|
422
|
+
encoding: "utf8",
|
|
423
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
424
|
+
timeout: 10000,
|
|
425
|
+
});
|
|
426
|
+
return "wrote";
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
const stderr = error.stderr;
|
|
430
|
+
const detail = Buffer.isBuffer(stderr) ? stderr.toString("utf8") : String(stderr ?? "");
|
|
431
|
+
if (!detail.includes("already exists"))
|
|
432
|
+
return "unavailable";
|
|
433
|
+
}
|
|
434
|
+
// Claude Code's CLI will not replace a same-name local server. Once the replacement entry is
|
|
435
|
+
// fully constructed, remove only EchoMem and immediately re-add it; sibling MCP servers remain.
|
|
436
|
+
try {
|
|
437
|
+
execFileSync("claude", ["mcp", "remove", "echomem", "-s", "local"], {
|
|
438
|
+
encoding: "utf8",
|
|
439
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
440
|
+
timeout: 10000,
|
|
441
|
+
});
|
|
442
|
+
execFileSync("claude", addArguments, {
|
|
455
443
|
encoding: "utf8",
|
|
456
444
|
stdio: ["ignore", "pipe", "pipe"],
|
|
457
445
|
timeout: 10000,
|
|
@@ -2652,6 +2640,14 @@ function parseFlags(argv) {
|
|
|
2652
2640
|
return flags;
|
|
2653
2641
|
}
|
|
2654
2642
|
async function cmdSetup(flags) {
|
|
2643
|
+
if (!flags.dev && !flags["skip-runtime-install"]) {
|
|
2644
|
+
try {
|
|
2645
|
+
installDurableHeadlessRuntime();
|
|
2646
|
+
}
|
|
2647
|
+
catch (error) {
|
|
2648
|
+
console.log(`ℹ️ Automatic runtime staging was unavailable; keeping the existing bridge (${error instanceof Error ? error.message : String(error)}).`);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2655
2651
|
const entry = buildServerEntry({ devEntryPath: typeof flags.dev === "string" ? flags.dev : undefined });
|
|
2656
2652
|
const requested = typeof flags.client === "string" ? flags.client : undefined;
|
|
2657
2653
|
const targets = selectSetupTargets(requested, Boolean(flags.all));
|
|
@@ -2800,8 +2796,8 @@ function writeSaveCheckpointHooksForTargets(targets) {
|
|
|
2800
2796
|
}
|
|
2801
2797
|
async function cmdUpdate(flags) {
|
|
2802
2798
|
if (!flags.dev)
|
|
2803
|
-
|
|
2804
|
-
await cmdSetup({ ...flags, "skip-login": true });
|
|
2799
|
+
installDurableHeadlessRuntime();
|
|
2800
|
+
await cmdSetup({ ...flags, "skip-login": true, "skip-runtime-install": true });
|
|
2805
2801
|
console.log(`Update config complete. Start a new MCP session to load ${MCP_PACKAGE_LABEL}.`);
|
|
2806
2802
|
}
|
|
2807
2803
|
function selectSetupTargets(requested, all) {
|
package/dist/update-check.js
CHANGED
|
@@ -107,10 +107,18 @@ export function startBackgroundUpdateCheck(onStatus) {
|
|
|
107
107
|
export function formatUpdateNotice(status) {
|
|
108
108
|
if (!status?.updateAvailable || !status.latestVersion)
|
|
109
109
|
return undefined;
|
|
110
|
+
if (status.autoUpdateState === "ready") {
|
|
111
|
+
return `EchoMem ${status.latestVersion} was installed automatically. Start a new agent/MCP session to use it.`;
|
|
112
|
+
}
|
|
113
|
+
if (status.autoUpdateState === "installing") {
|
|
114
|
+
return `EchoMem ${status.latestVersion} is downloading automatically in the background. The next agent/MCP session will use it.`;
|
|
115
|
+
}
|
|
116
|
+
if (status.autoUpdateState === "failed") {
|
|
117
|
+
return `EchoMem ${status.latestVersion} could not be installed automatically. Run \`${status.command}\`, then start a new agent/MCP session.`;
|
|
118
|
+
}
|
|
110
119
|
return [
|
|
111
120
|
`EchoMem update available: installed ${status.currentVersion}, latest ${status.latestVersion}.`,
|
|
112
|
-
|
|
113
|
-
"After updating, start a new agent/MCP session.",
|
|
121
|
+
"It will install automatically in the background for the next agent/MCP session.",
|
|
114
122
|
].join(" ");
|
|
115
123
|
}
|
|
116
124
|
export function formatUpdateStatusText(status) {
|
|
@@ -120,8 +128,14 @@ export function formatUpdateStatusText(status) {
|
|
|
120
128
|
`Update available: ${status.updateAvailable ? "yes" : "no"}`,
|
|
121
129
|
];
|
|
122
130
|
if (status.updateAvailable) {
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
if (status.autoUpdateState === "ready")
|
|
132
|
+
lines.push("Automatic update: installed for the next agent/MCP session");
|
|
133
|
+
else if (status.autoUpdateState === "installing")
|
|
134
|
+
lines.push("Automatic update: installing in the background");
|
|
135
|
+
else if (status.autoUpdateState === "failed")
|
|
136
|
+
lines.push(`Automatic update: failed; fallback command: ${status.command}`);
|
|
137
|
+
else
|
|
138
|
+
lines.push("Automatic update: scheduled in the background");
|
|
125
139
|
}
|
|
126
140
|
if (status.checkedAt)
|
|
127
141
|
lines.push(`Checked at: ${status.checkedAt} (${status.source})`);
|
package/dist/v1-contract.js
CHANGED
|
@@ -34,7 +34,6 @@ export const canonicalToolNames = {
|
|
|
34
34
|
};
|
|
35
35
|
export const legacyAliasToCanonical = {
|
|
36
36
|
search_memories_by_description_semantic: canonicalToolNames.search,
|
|
37
|
-
search_memories_by_time_range: canonicalToolNames.timeRange,
|
|
38
37
|
};
|
|
39
38
|
export function resolveCanonicalToolName(toolName) {
|
|
40
39
|
return legacyAliasToCanonical[toolName] ?? toolName;
|
|
@@ -44,7 +43,6 @@ const READ_ONLY_TOOL_NAMES = new Set([
|
|
|
44
43
|
canonicalToolNames.search,
|
|
45
44
|
"search_memories_by_description_semantic",
|
|
46
45
|
canonicalToolNames.timeRange,
|
|
47
|
-
"search_memories_by_time_range",
|
|
48
46
|
canonicalToolNames.keywords,
|
|
49
47
|
canonicalToolNames.friends,
|
|
50
48
|
canonicalToolNames.searchUsers,
|
|
@@ -166,11 +164,11 @@ const triggerMetadataSchema = {
|
|
|
166
164
|
};
|
|
167
165
|
export const searchMemoriesSchema = z.object({
|
|
168
166
|
...triggerMetadataSchema,
|
|
169
|
-
query: z.string().optional(),
|
|
170
|
-
k: z.number().optional(),
|
|
171
|
-
limit: z.number().optional(),
|
|
172
|
-
threshold: z.number().optional().default(0.1),
|
|
173
|
-
timeFrameDays: z.number().optional(),
|
|
167
|
+
query: z.string().trim().min(1).optional(),
|
|
168
|
+
k: z.number().int().min(1).max(50).optional(),
|
|
169
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
170
|
+
threshold: z.number().min(0).max(1).optional().default(0.1),
|
|
171
|
+
timeFrameDays: z.number().int().min(1).max(3650).optional(),
|
|
174
172
|
includeAnswer: z.boolean().optional().default(false),
|
|
175
173
|
});
|
|
176
174
|
export const saveConversationSchema = z.object({
|
|
@@ -189,11 +187,20 @@ export const saveConversationSchema = z.object({
|
|
|
189
187
|
}))
|
|
190
188
|
.optional(),
|
|
191
189
|
});
|
|
190
|
+
const dateBoundarySchema = z.string().trim().min(1).refine((value) => Number.isFinite(Date.parse(value)), "Expected an ISO-8601 date or date-time string");
|
|
192
191
|
export const timeRangeSchema = z.object({
|
|
193
192
|
...triggerMetadataSchema,
|
|
194
|
-
startDate:
|
|
195
|
-
endDate:
|
|
196
|
-
limit: z.number().optional().default(50),
|
|
193
|
+
startDate: dateBoundarySchema,
|
|
194
|
+
endDate: dateBoundarySchema,
|
|
195
|
+
limit: z.number().int().min(1).max(100).optional().default(50),
|
|
196
|
+
}).superRefine((value, ctx) => {
|
|
197
|
+
if (Date.parse(value.startDate) > Date.parse(value.endDate)) {
|
|
198
|
+
ctx.addIssue({
|
|
199
|
+
code: z.ZodIssueCode.custom,
|
|
200
|
+
path: ["startDate"],
|
|
201
|
+
message: "startDate must be earlier than or equal to endDate",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
197
204
|
});
|
|
198
205
|
const keywordListSchema = z.preprocess((value) => {
|
|
199
206
|
if (typeof value !== "string")
|
|
@@ -206,15 +213,15 @@ const keywordListSchema = z.preprocess((value) => {
|
|
|
206
213
|
export const keywordsSchema = z.object({
|
|
207
214
|
...triggerMetadataSchema,
|
|
208
215
|
keywords: keywordListSchema,
|
|
209
|
-
limit: z.number().optional().default(10),
|
|
216
|
+
limit: z.number().int().min(1).max(50).optional().default(10),
|
|
210
217
|
});
|
|
211
218
|
export const listFriendsSchema = z.object({
|
|
212
219
|
...triggerMetadataSchema,
|
|
213
220
|
});
|
|
214
221
|
export const searchUsersSchema = z.object({
|
|
215
222
|
...triggerMetadataSchema,
|
|
216
|
-
query: z.string().min(1),
|
|
217
|
-
limit: z.number().optional().default(10),
|
|
223
|
+
query: z.string().trim().min(1),
|
|
224
|
+
limit: z.number().int().min(1).max(50).optional().default(10),
|
|
218
225
|
});
|
|
219
226
|
export const sendFriendRequestSchema = z.object({
|
|
220
227
|
...triggerMetadataSchema,
|
|
@@ -222,17 +229,17 @@ export const sendFriendRequestSchema = z.object({
|
|
|
222
229
|
});
|
|
223
230
|
export const othersSchema = z.object({
|
|
224
231
|
...triggerMetadataSchema,
|
|
225
|
-
query: z.string().optional().default(""),
|
|
226
|
-
limit: z.number().optional().default(10),
|
|
232
|
+
query: z.string().trim().optional().default(""),
|
|
233
|
+
limit: z.number().int().min(1).max(50).optional().default(10),
|
|
227
234
|
target: z.string().optional(),
|
|
228
235
|
ownerUserId: z.string().optional(),
|
|
229
236
|
ownerName: z.string().optional(),
|
|
230
237
|
targetFriendIds: z.array(z.string()).optional(),
|
|
231
238
|
targetFriendNames: z.array(z.string()).optional(),
|
|
232
239
|
recordAccess: z.boolean().optional(),
|
|
233
|
-
kPerUser: z.number().optional(),
|
|
234
|
-
similarityThreshold: z.number().optional(),
|
|
235
|
-
timeFrameDays: z.number().optional(),
|
|
240
|
+
kPerUser: z.number().int().min(1).max(50).optional(),
|
|
241
|
+
similarityThreshold: z.number().min(0).max(1).optional(),
|
|
242
|
+
timeFrameDays: z.number().int().min(1).max(3650).optional(),
|
|
236
243
|
});
|
|
237
244
|
export const publicMemorySchema = z.object({
|
|
238
245
|
...triggerMetadataSchema,
|
|
@@ -329,8 +336,8 @@ export const deleteMemorySchema = z.object({
|
|
|
329
336
|
});
|
|
330
337
|
export const getByContextSchema = z.object({
|
|
331
338
|
...triggerMetadataSchema,
|
|
332
|
-
contextId: z.string().min(1),
|
|
333
|
-
limit: z.number().optional().default(50),
|
|
339
|
+
contextId: z.string().trim().min(1),
|
|
340
|
+
limit: z.number().int().min(1).max(100).optional().default(50),
|
|
334
341
|
});
|
|
335
342
|
export function listToolSpecs(opts = {}) {
|
|
336
343
|
const currentTime = new Date().toISOString();
|
|
@@ -349,42 +356,64 @@ export function listToolSpecs(opts = {}) {
|
|
|
349
356
|
const groupMapSection = groupMap
|
|
350
357
|
? `\n\nThis user's company group currently shares work in these areas (a relevance guide — search the group when the task relates to one of these people or topics):\n${compactGroupRoster(groupMap)}\n${groupMap}\n`
|
|
351
358
|
: "";
|
|
359
|
+
const semanticSearchInputSchema = {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
query: {
|
|
363
|
+
type: "string",
|
|
364
|
+
minLength: 1,
|
|
365
|
+
description: "Required topic or question to search for. Never omit this field or send it as conversation.",
|
|
366
|
+
},
|
|
367
|
+
limit: { type: "integer", minimum: 1, maximum: 50, default: 10 },
|
|
368
|
+
threshold: { type: "number", minimum: 0, maximum: 1, default: 0.1 },
|
|
369
|
+
timeFrameDays: {
|
|
370
|
+
type: "integer",
|
|
371
|
+
minimum: 1,
|
|
372
|
+
maximum: 3650,
|
|
373
|
+
description: "Optional recency filter for this query, such as 14 for the last two weeks.",
|
|
374
|
+
},
|
|
375
|
+
triggerMessage: {
|
|
376
|
+
type: "string",
|
|
377
|
+
description: "Optional: the user's message that caused this recall. EchoMem stores only a redacted analytics preview and hash.",
|
|
378
|
+
},
|
|
379
|
+
triggerMessageRole: { type: "string", default: "user" },
|
|
380
|
+
},
|
|
381
|
+
required: ["query"],
|
|
382
|
+
};
|
|
383
|
+
const timeRangeInputSchema = {
|
|
384
|
+
type: "object",
|
|
385
|
+
properties: {
|
|
386
|
+
startDate: {
|
|
387
|
+
type: "string",
|
|
388
|
+
minLength: 1,
|
|
389
|
+
description: "Required ISO-8601 start date or date-time. Must not be later than endDate.",
|
|
390
|
+
},
|
|
391
|
+
endDate: {
|
|
392
|
+
type: "string",
|
|
393
|
+
minLength: 1,
|
|
394
|
+
description: "Required ISO-8601 end date or date-time. Must not be earlier than startDate.",
|
|
395
|
+
},
|
|
396
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 50 },
|
|
397
|
+
triggerMessage: {
|
|
398
|
+
type: "string",
|
|
399
|
+
description: "Optional: the user's message that caused this lookup. EchoMem stores only a redacted analytics preview and hash.",
|
|
400
|
+
},
|
|
401
|
+
triggerMessageRole: { type: "string", default: "user" },
|
|
402
|
+
},
|
|
403
|
+
required: ["startDate", "endDate"],
|
|
404
|
+
};
|
|
352
405
|
const tools = [
|
|
353
406
|
{
|
|
354
407
|
name: canonicalToolNames.search,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
properties: {
|
|
359
|
-
query: { type: "string" },
|
|
360
|
-
limit: { type: "number", default: 10 },
|
|
361
|
-
threshold: { type: "number", default: 0.1 },
|
|
362
|
-
timeFrameDays: { type: "number" },
|
|
363
|
-
triggerMessage: {
|
|
364
|
-
type: "string",
|
|
365
|
-
description: "Optional: the user's message that caused this recall. EchoMem stores only a redacted analytics preview and hash.",
|
|
366
|
-
},
|
|
367
|
-
triggerMessageRole: { type: "string", default: "user" },
|
|
368
|
-
},
|
|
369
|
-
},
|
|
408
|
+
title: "Search your memories by topic",
|
|
409
|
+
description: withMcpVersion(`TOPIC SEARCH for the user's own EchoMem memories across all AI tools. Always pass a non-empty query; optionally add timeFrameDays to filter that topic to recent memories. Do not pass keywords, startDate, or endDate. For exact memory-key terms use search_memories_by_keywords. For an explicit calendar range use get_memories_by_time_range. Use this tool instead of re-deriving or re-asking what the user already settled. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}${mapSection}\nReturns ranked memories only; the MCP host model writes the final answer. Current time: ${currentTime}.${updateSection}`),
|
|
410
|
+
inputSchema: semanticSearchInputSchema,
|
|
370
411
|
},
|
|
371
412
|
{
|
|
372
413
|
name: "search_memories_by_description_semantic",
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
properties: {
|
|
377
|
-
query: { type: "string" },
|
|
378
|
-
limit: { type: "number", default: 10 },
|
|
379
|
-
threshold: { type: "number", default: 0.1 },
|
|
380
|
-
timeFrameDays: { type: "number" },
|
|
381
|
-
triggerMessage: {
|
|
382
|
-
type: "string",
|
|
383
|
-
description: "Optional: the user's message that caused this recall. EchoMem stores only a redacted analytics preview and hash.",
|
|
384
|
-
},
|
|
385
|
-
triggerMessageRole: { type: "string", default: "user" },
|
|
386
|
-
},
|
|
387
|
-
},
|
|
414
|
+
title: "Legacy topic search (compatibility)",
|
|
415
|
+
description: `LEGACY TOPIC SEARCH alias for search_memories. Prefer search_memories. Always pass a non-empty query; timeFrameDays is only an optional recency filter. Do not pass keywords, startDate, or endDate. ${recallPlanNote} ${searchBillingReplyInstruction} ${memoryCitationInstruction}`,
|
|
416
|
+
inputSchema: semanticSearchInputSchema,
|
|
388
417
|
},
|
|
389
418
|
{
|
|
390
419
|
name: canonicalToolNames.save,
|
|
@@ -426,25 +455,14 @@ export function listToolSpecs(opts = {}) {
|
|
|
426
455
|
},
|
|
427
456
|
{
|
|
428
457
|
name: canonicalToolNames.timeRange,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
properties: {
|
|
433
|
-
startDate: { type: "string" },
|
|
434
|
-
endDate: { type: "string" },
|
|
435
|
-
limit: { type: "number", default: 50 },
|
|
436
|
-
triggerMessage: {
|
|
437
|
-
type: "string",
|
|
438
|
-
description: "Optional: the user's message that caused this lookup. EchoMem stores only a redacted analytics preview and hash.",
|
|
439
|
-
},
|
|
440
|
-
triggerMessageRole: { type: "string", default: "user" },
|
|
441
|
-
},
|
|
442
|
-
required: ["startDate", "endDate"],
|
|
443
|
-
},
|
|
458
|
+
title: "Get your memories by date range",
|
|
459
|
+
description: `DATE-RANGE FETCH for the user's own memories. Always pass both startDate and endDate as ISO-8601 strings. Do not pass query, keywords, or timeFrameDays. For a topic with a recency filter use search_memories instead. ${recallPlanNote} ${memoryCitationInstruction} Current time: ${currentTime}.`,
|
|
460
|
+
inputSchema: timeRangeInputSchema,
|
|
444
461
|
},
|
|
445
462
|
{
|
|
446
463
|
name: canonicalToolNames.keywords,
|
|
447
|
-
|
|
464
|
+
title: "Search your memory keys exactly",
|
|
465
|
+
description: `EXACT-KEYWORD SEARCH over the keys field of the user's own memories. Pass keywords as valid JSON: preferably an array of quoted strings, for example {"keywords":["flow-lab","flow.html","Rive"],"limit":8}. Do not pass query, timeFrameDays, startDate, or endDate. For natural-language topic search use search_memories. A comma-separated JSON string is accepted only as a compatibility fallback. Never emit bare comma-separated tokens. ${recallPlanNote} ${memoryCitationInstruction}`,
|
|
448
466
|
inputSchema: {
|
|
449
467
|
type: "object",
|
|
450
468
|
properties: {
|
|
@@ -477,6 +495,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
477
495
|
},
|
|
478
496
|
{
|
|
479
497
|
name: canonicalToolNames.friends,
|
|
498
|
+
title: "List accepted EchoMem friends",
|
|
480
499
|
description: "Friends: list accepted EchoMem friends with each friend's public memory count. Use this before asking a specific friend by name.",
|
|
481
500
|
inputSchema: {
|
|
482
501
|
type: "object",
|
|
@@ -491,7 +510,8 @@ export function listToolSpecs(opts = {}) {
|
|
|
491
510
|
},
|
|
492
511
|
{
|
|
493
512
|
name: canonicalToolNames.searchUsers,
|
|
494
|
-
|
|
513
|
+
title: "Search the EchoMem user directory",
|
|
514
|
+
description: "USER-DIRECTORY SEARCH by display name or username before sending a friend request. This searches people/accounts, not memory content. Results include total memories, public memories, and current relationship state.",
|
|
495
515
|
inputSchema: {
|
|
496
516
|
type: "object",
|
|
497
517
|
properties: {
|
|
@@ -530,12 +550,16 @@ export function listToolSpecs(opts = {}) {
|
|
|
530
550
|
},
|
|
531
551
|
{
|
|
532
552
|
name: canonicalToolNames.others,
|
|
533
|
-
|
|
553
|
+
title: "Search teammates' and friends' memories",
|
|
554
|
+
description: `PEER-MEMORY SEARCH for public memories owned by accepted friends or company-group members—not the user's own memories. Pass query for a topic; omit it only when intentionally browsing peer memories, and optionally use target to scope a person. Do not use this tool for the user's private memories or the EchoMem user directory. EchoMem identifies the caller from the EchoMem credential and has already excluded only that authenticated user's own memories. Present every returned owner; never filter again using a Claude account, host profile, git identity, or inference. For onboarding and division-of-work questions, call get_group_context first. Returned memories are recorded in memory_views for the owners. ${memoryCitationInstruction}${groupMapSection}`,
|
|
534
555
|
inputSchema: {
|
|
535
556
|
type: "object",
|
|
536
557
|
properties: {
|
|
537
|
-
query: {
|
|
538
|
-
|
|
558
|
+
query: {
|
|
559
|
+
type: "string",
|
|
560
|
+
description: "Optional peer-memory topic. Omit only for an intentional broad browse; never send this field as conversation.",
|
|
561
|
+
},
|
|
562
|
+
limit: { type: "integer", minimum: 1, maximum: 50, default: 10 },
|
|
539
563
|
target: {
|
|
540
564
|
type: "string",
|
|
541
565
|
description: "Accessible friend or group-member user id or exact display name. Prefer this for @Name asks.",
|
|
@@ -562,9 +586,9 @@ export function listToolSpecs(opts = {}) {
|
|
|
562
586
|
type: "boolean",
|
|
563
587
|
description: "Defaults true. When true, returned public memories are recorded in memory_views.",
|
|
564
588
|
},
|
|
565
|
-
kPerUser: { type: "
|
|
566
|
-
similarityThreshold: { type: "number", default: 0.1 },
|
|
567
|
-
timeFrameDays: { type: "
|
|
589
|
+
kPerUser: { type: "integer", minimum: 1, maximum: 50, default: 5 },
|
|
590
|
+
similarityThreshold: { type: "number", minimum: 0, maximum: 1, default: 0.1 },
|
|
591
|
+
timeFrameDays: { type: "integer", minimum: 1, maximum: 3650 },
|
|
568
592
|
triggerMessage: {
|
|
569
593
|
type: "string",
|
|
570
594
|
description: "Optional: the user's message that caused this public-memory search. EchoMem stores only a redacted analytics preview and hash.",
|
|
@@ -575,6 +599,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
575
599
|
},
|
|
576
600
|
{
|
|
577
601
|
name: canonicalToolNames.publicMemory,
|
|
602
|
+
title: "Get one teammate or friend memory",
|
|
578
603
|
description: `Fetch one public memory by id when its owner is an accepted friend or shares your company group. If the caller is not the owner, EchoMem records the access in memory_views. ${memoryCitationInstruction}`,
|
|
579
604
|
inputSchema: {
|
|
580
605
|
type: "object",
|
|
@@ -892,6 +917,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
892
917
|
},
|
|
893
918
|
{
|
|
894
919
|
name: canonicalToolNames.getByContext,
|
|
920
|
+
title: "Get the exact memories from a saved context",
|
|
895
921
|
description: withMcpVersion(`Deterministically re-fetch the exact batch of memories saved under one contextId — no semantic search, no ranking, just that session's saved capsule. ${recallPlanNote} save_conversation returns a contextId; pass it here to pull back precisely those memories, e.g. to warm up a fresh session with what a prior session saved, or to verify the saved facts are still present. ${memoryCitationInstruction} Current time: ${currentTime}.`),
|
|
896
922
|
inputSchema: {
|
|
897
923
|
type: "object",
|
|
@@ -909,6 +935,7 @@ export function listToolSpecs(opts = {}) {
|
|
|
909
935
|
},
|
|
910
936
|
{
|
|
911
937
|
name: canonicalToolNames.checkpointByContext,
|
|
938
|
+
title: "Rebuild a checkpoint from a saved context",
|
|
912
939
|
description: withMcpVersion(`Rebuild a clean-context checkpoint / decision log from one EchoMem contextId. ${recallPlanNote} Use this when the user or EchoMem returns a contextId for a renewed coding session and you need the session handoff, not a raw memory dump. It deterministically fetches that context and formats it as orientation state: decisions, carryover, constraints, and checkpoints. ${memoryCitationInstruction} Current time: ${currentTime}.`),
|
|
913
940
|
inputSchema: {
|
|
914
941
|
type: "object",
|
|
@@ -971,19 +998,6 @@ export function listToolSpecs(opts = {}) {
|
|
|
971
998
|
},
|
|
972
999
|
},
|
|
973
1000
|
},
|
|
974
|
-
{
|
|
975
|
-
name: "search_memories_by_time_range",
|
|
976
|
-
description: "Legacy alias for get_memories_by_time_range.",
|
|
977
|
-
inputSchema: {
|
|
978
|
-
type: "object",
|
|
979
|
-
properties: {
|
|
980
|
-
startDate: { type: "string" },
|
|
981
|
-
endDate: { type: "string" },
|
|
982
|
-
limit: { type: "number", default: 50 },
|
|
983
|
-
},
|
|
984
|
-
required: ["startDate", "endDate"],
|
|
985
|
-
},
|
|
986
|
-
},
|
|
987
1001
|
];
|
|
988
1002
|
return tools.map(decorateLocalToolSpec);
|
|
989
1003
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echomem/mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.37",
|
|
4
4
|
"description": "EchoMem MCP bridge: cloud-first memory tools and the Agent Doctor workspace forensics report (cost ledger + 3D repo city)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"test:ui": "npm run build && node test/setup-ui.test.mjs",
|
|
34
34
|
"test:onboarding-resilience": "npm run build && node test/onboarding-resilience.test.mjs",
|
|
35
35
|
"test:billing-ui": "npm run build && node test/setup-ui.test.mjs billing",
|
|
36
|
-
"test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs",
|
|
36
|
+
"test": "npm run build && node test/local-data-paths.test.mjs && node test/crypto.test.mjs && node test/integration.test.mjs && node test/onboarding-resilience.test.mjs && node test/local-auth.test.mjs && node test/retrieval-only.test.mjs && node test/no-restart.test.mjs && node test/report.test.mjs && node test/forensics.test.mjs && node test/canonical-golden.test.mjs && node test/tools.test.mjs && node test/group-map.test.mjs && node test/update-check.test.mjs && node test/headless-runtime.test.mjs && node test/claude-code-config.test.mjs && node test/delete.test.mjs && node test/low-touch-tools.test.mjs && node test/migrate.test.mjs && node test/restart-recovery.test.mjs && node test/save-checkpoint-hook.test.mjs",
|
|
37
37
|
"prepack": "npm run build && node scripts/bundle-city.mjs"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|