@gamaze/hicortex 0.20.3 → 0.20.5

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.
Files changed (44) hide show
  1. package/README.md +20 -1
  2. package/assets/dashboard.html +318 -8
  3. package/assets/identity.html +349 -8
  4. package/assets/viz.html +352 -8
  5. package/dist/backup.d.ts +12 -8
  6. package/dist/backup.js +13 -9
  7. package/dist/claude-desktop.d.ts +138 -0
  8. package/dist/claude-desktop.js +251 -0
  9. package/dist/cli.d.ts +7 -2
  10. package/dist/cli.js +84 -3
  11. package/dist/consolidate.d.ts +8 -1
  12. package/dist/consolidate.js +82 -2
  13. package/dist/dashboard.d.ts +17 -0
  14. package/dist/dashboard.js +32 -0
  15. package/dist/db.js +36 -0
  16. package/dist/dedup.d.ts +157 -25
  17. package/dist/dedup.js +376 -83
  18. package/dist/domain-classify.js +4 -2
  19. package/dist/index.js +7 -7
  20. package/dist/init.d.ts +4 -1
  21. package/dist/init.js +103 -1
  22. package/dist/llm.d.ts +19 -13
  23. package/dist/llm.js +25 -14
  24. package/dist/mcp-server.d.ts +6 -0
  25. package/dist/mcp-server.js +94 -13
  26. package/dist/mcp-stdio.d.ts +138 -0
  27. package/dist/mcp-stdio.js +313 -0
  28. package/dist/memory-instructions.d.ts +18 -0
  29. package/dist/memory-instructions.js +39 -2
  30. package/dist/nightly.js +14 -1
  31. package/dist/reconsolidation.d.ts +323 -0
  32. package/dist/reconsolidation.js +1226 -0
  33. package/dist/retrieval.d.ts +14 -0
  34. package/dist/retrieval.js +41 -3
  35. package/dist/state.d.ts +23 -1
  36. package/dist/storage.d.ts +25 -0
  37. package/dist/storage.js +49 -7
  38. package/dist/type-classify.js +4 -2
  39. package/dist/types.d.ts +188 -0
  40. package/hermes-plugin/hicortex/provider.py +29 -17
  41. package/opencode-plugin/hicortex/index.ts +7 -7
  42. package/package.json +4 -2
  43. package/pi-extension/hicortex/index.ts +7 -7
  44. package/server.json +44 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Hicortex stdio MCP bridge (#375) — the `hicortex mcp` subcommand.
3
+ *
4
+ * Registry/stdio MCP clients (Claude Desktop, Cursor, the MCP Registry's own
5
+ * install flow) launch a stdio command and speak MCP over stdin/stdout. The
6
+ * daemon's MCP surface is HTTP/SSE on :8787, so this module bridges the two:
7
+ * a low-level SDK `Server` over `StdioServerTransport` downstream (the client
8
+ * side) and an SDK `Client` over `SSEClientTransport` upstream (the daemon
9
+ * side), forwarding tools/list + tools/call. The daemon's MCP surface is
10
+ * tools-only (no resources/prompts registered in mcp-server.ts) and ping is
11
+ * auto-answered by the SDK Protocol base, so tools-forwarding loses nothing.
12
+ *
13
+ * WHY proxy instead of in-process stdio with direct DB access (design note,
14
+ * issue #375):
15
+ * 1. db.ts enables WAL but sets no busy_timeout — a second writer process
16
+ * (the common case: the daemon already running on server-mode installs)
17
+ * takes immediate SQLITE_BUSY during nightly consolidation's long
18
+ * transactions → tool-call failures.
19
+ * 2. createMcpServer()'s nine tool handlers close over ~10 module-level
20
+ * vars initialized by the ~250-line boot inside startServer() —
21
+ * in-process would mean refactoring the production boot path or
22
+ * duplicating it (drift).
23
+ * 3. warmEmbedder loads a 150-300 MB ONNX model per process — one per MCP
24
+ * client (Claude Desktop + CC + Cursor = 3x), vs zero for the bridge.
25
+ * 4. mcp-server.ts's own header states the model: "One process, one DB
26
+ * connection, one embedder".
27
+ *
28
+ * Target resolution (precedence): HICORTEX_SERVER_URL env → remote bridge,
29
+ * NEVER spawns anything; else config via the SAME semantics as
30
+ * learnings-identity.resolveConfig() (client-mode serverUrl → remote;
31
+ * server-mode → http://127.0.0.1:<port ?? 8787>); no usable config →
32
+ * http://127.0.0.1:8787. Token: HICORTEX_AUTH_TOKEN env → config.authToken.
33
+ * The token rides SSEClientTransport's requestInit headers (verified in the
34
+ * installed SDK 1.28: merged into BOTH the GET /sse and POST /messages).
35
+ *
36
+ * Local autostart: when the target is loopback and /health is
37
+ * connection-refused, spawn a DETACHED `cli.js server --port <n>` (unref,
38
+ * stdio ignored) and poll /health (~250 ms interval, 30 s cap). Concurrent
39
+ * bridges racing EADDRINUSE self-heal — the loser's child dies, the winner's
40
+ * daemon answers the poll, so the loop keeps polling regardless of child
41
+ * state. /health answering but not ok = a foreign or broken service on the
42
+ * port: explicit error, never a spawn. A remote target that is down is
43
+ * likewise an explicit error — we never spawn for remote URLs.
44
+ *
45
+ * STDIO DISCIPLINE: stdout carries ONLY the MCP protocol. Every diagnostic
46
+ * goes to stderr; fatal errors are a one-liner on stderr + non-zero exit
47
+ * (thrown to cli.ts's catch). Cancellation downstream→upstream rides the
48
+ * SDK-native path: the Protocol base aborts the handler's extra.signal on
49
+ * notifications/cancelled, and passing that signal into client.callTool makes
50
+ * the upstream Client emit its own notifications/cancelled with the CORRECT
51
+ * upstream request id (a verbatim forward would carry the downstream id,
52
+ * which means nothing to the daemon) — and reject the in-flight bridge call.
53
+ */
54
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
55
+ /** Where the bridge target came from — surfaces in the startup diagnostic. */
56
+ export type BridgeTargetSource = "option" | "env" | "config" | "default";
57
+ export interface BridgeTarget {
58
+ /** Base URL, trailing slashes stripped (endpoints append /sse, /health). */
59
+ url: string;
60
+ /** Loopback target → eligible for local autostart. */
61
+ local: boolean;
62
+ /** Port of the resolved URL — what an autospawned daemon must listen on. */
63
+ port: number;
64
+ source: BridgeTargetSource;
65
+ }
66
+ /** Loopback check for URL hostnames (Node's URL keeps the brackets on [::1]). */
67
+ export declare function isLoopbackHost(hostname: string): boolean;
68
+ /**
69
+ * Resolve the daemon/server the bridge should talk to. Precedence: explicit
70
+ * option (the runMcpStdio test seam) → HICORTEX_SERVER_URL env → config file
71
+ * (client-mode serverUrl = remote; server-mode = localhost, the
72
+ * resolveConfig() semantics already shared by both CC hooks) → default local
73
+ * 8787. Blank/whitespace env values are ignored, not mistaken for targets.
74
+ */
75
+ export declare function resolveBridgeTarget(explicitUrl?: string): BridgeTarget;
76
+ /**
77
+ * Resolve the bearer token for the upstream connection: explicit option →
78
+ * HICORTEX_AUTH_TOKEN env → config.authToken. Undefined = no token (a local
79
+ * daemon needs none — loopback bypasses auth).
80
+ */
81
+ export declare function resolveBridgeToken(explicitToken?: string): string | undefined;
82
+ export interface HealthProbe {
83
+ /** Any HTTP response arrived (even a non-200 one). */
84
+ reachable: boolean;
85
+ /** The endpoint answered ok — a healthy Hicortex /health. */
86
+ ok: boolean;
87
+ }
88
+ /**
89
+ * One GET /health probe. Connection refused / timeout / DNS failure →
90
+ * { reachable: false }; a response that is not ok → { reachable: true, ok:
91
+ * false } — the two carry different autostart decisions, so a boolean alone
92
+ * cannot express them.
93
+ */
94
+ export declare function probeHealthOnce(url: string, timeoutMs?: number): Promise<HealthProbe>;
95
+ export type AutostartDecision = {
96
+ action: "bridge";
97
+ } | {
98
+ action: "spawn";
99
+ } | {
100
+ action: "fail";
101
+ reason: string;
102
+ };
103
+ /**
104
+ * Pure decision from one health probe: healthy → bridge; refused + loopback
105
+ * → spawn a local daemon; refused + remote → fail with an actionable message
106
+ * (never spawn for remote URLs); answering-but-not-ok → fail explicitly (a
107
+ * foreign or broken service owns the port — spawning next to it cannot help).
108
+ */
109
+ export declare function decideAutostart(probe: HealthProbe, target: BridgeTarget): AutostartDecision;
110
+ export interface EnsureDaemonOptions {
111
+ /** Local autostart toggle (default true). Remote targets never spawn. */
112
+ autostart?: boolean;
113
+ probeHealth?: (url: string) => Promise<HealthProbe>;
114
+ spawnDaemon?: (port: number) => Promise<void> | void;
115
+ /** Poll pacing overrides — small values keep the timeout test fast. */
116
+ pollIntervalMs?: number;
117
+ pollTotalMs?: number;
118
+ }
119
+ /**
120
+ * Ensure something healthy answers at the target before bridging: probe once,
121
+ * decide, and if spawning — poll until healthy (or the deadline). Throws on
122
+ * every fail-path (explicit error, never silent degradation).
123
+ */
124
+ export declare function ensureDaemonReady(target: BridgeTarget, options?: EnsureDaemonOptions): Promise<void>;
125
+ export interface McpStdioOptions extends EnsureDaemonOptions {
126
+ /** Explicit target URL (test seam; normally resolved from env/config). */
127
+ serverUrl?: string;
128
+ /** Explicit bearer token (test seam; normally env → config). */
129
+ authToken?: string;
130
+ /** Injectable downstream transport (test seam; default: real stdio). */
131
+ downstream?: Transport;
132
+ }
133
+ /**
134
+ * Run the stdio MCP bridge. Resolves only after the downstream transport
135
+ * closes (the lifecycle handlers then exit the process); every setup failure
136
+ * throws for cli.ts to report on stderr and exit 1.
137
+ */
138
+ export declare function runMcpStdio(options?: McpStdioOptions): Promise<void>;
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ /**
3
+ * Hicortex stdio MCP bridge (#375) — the `hicortex mcp` subcommand.
4
+ *
5
+ * Registry/stdio MCP clients (Claude Desktop, Cursor, the MCP Registry's own
6
+ * install flow) launch a stdio command and speak MCP over stdin/stdout. The
7
+ * daemon's MCP surface is HTTP/SSE on :8787, so this module bridges the two:
8
+ * a low-level SDK `Server` over `StdioServerTransport` downstream (the client
9
+ * side) and an SDK `Client` over `SSEClientTransport` upstream (the daemon
10
+ * side), forwarding tools/list + tools/call. The daemon's MCP surface is
11
+ * tools-only (no resources/prompts registered in mcp-server.ts) and ping is
12
+ * auto-answered by the SDK Protocol base, so tools-forwarding loses nothing.
13
+ *
14
+ * WHY proxy instead of in-process stdio with direct DB access (design note,
15
+ * issue #375):
16
+ * 1. db.ts enables WAL but sets no busy_timeout — a second writer process
17
+ * (the common case: the daemon already running on server-mode installs)
18
+ * takes immediate SQLITE_BUSY during nightly consolidation's long
19
+ * transactions → tool-call failures.
20
+ * 2. createMcpServer()'s nine tool handlers close over ~10 module-level
21
+ * vars initialized by the ~250-line boot inside startServer() —
22
+ * in-process would mean refactoring the production boot path or
23
+ * duplicating it (drift).
24
+ * 3. warmEmbedder loads a 150-300 MB ONNX model per process — one per MCP
25
+ * client (Claude Desktop + CC + Cursor = 3x), vs zero for the bridge.
26
+ * 4. mcp-server.ts's own header states the model: "One process, one DB
27
+ * connection, one embedder".
28
+ *
29
+ * Target resolution (precedence): HICORTEX_SERVER_URL env → remote bridge,
30
+ * NEVER spawns anything; else config via the SAME semantics as
31
+ * learnings-identity.resolveConfig() (client-mode serverUrl → remote;
32
+ * server-mode → http://127.0.0.1:<port ?? 8787>); no usable config →
33
+ * http://127.0.0.1:8787. Token: HICORTEX_AUTH_TOKEN env → config.authToken.
34
+ * The token rides SSEClientTransport's requestInit headers (verified in the
35
+ * installed SDK 1.28: merged into BOTH the GET /sse and POST /messages).
36
+ *
37
+ * Local autostart: when the target is loopback and /health is
38
+ * connection-refused, spawn a DETACHED `cli.js server --port <n>` (unref,
39
+ * stdio ignored) and poll /health (~250 ms interval, 30 s cap). Concurrent
40
+ * bridges racing EADDRINUSE self-heal — the loser's child dies, the winner's
41
+ * daemon answers the poll, so the loop keeps polling regardless of child
42
+ * state. /health answering but not ok = a foreign or broken service on the
43
+ * port: explicit error, never a spawn. A remote target that is down is
44
+ * likewise an explicit error — we never spawn for remote URLs.
45
+ *
46
+ * STDIO DISCIPLINE: stdout carries ONLY the MCP protocol. Every diagnostic
47
+ * goes to stderr; fatal errors are a one-liner on stderr + non-zero exit
48
+ * (thrown to cli.ts's catch). Cancellation downstream→upstream rides the
49
+ * SDK-native path: the Protocol base aborts the handler's extra.signal on
50
+ * notifications/cancelled, and passing that signal into client.callTool makes
51
+ * the upstream Client emit its own notifications/cancelled with the CORRECT
52
+ * upstream request id (a verbatim forward would carry the downstream id,
53
+ * which means nothing to the daemon) — and reject the in-flight bridge call.
54
+ */
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.isLoopbackHost = isLoopbackHost;
57
+ exports.resolveBridgeTarget = resolveBridgeTarget;
58
+ exports.resolveBridgeToken = resolveBridgeToken;
59
+ exports.probeHealthOnce = probeHealthOnce;
60
+ exports.decideAutostart = decideAutostart;
61
+ exports.ensureDaemonReady = ensureDaemonReady;
62
+ exports.runMcpStdio = runMcpStdio;
63
+ const node_child_process_1 = require("node:child_process");
64
+ const node_fs_1 = require("node:fs");
65
+ const node_path_1 = require("node:path");
66
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
67
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
68
+ const index_js_2 = require("@modelcontextprotocol/sdk/client/index.js");
69
+ const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
70
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
71
+ const learnings_identity_js_1 = require("./learnings-identity.js");
72
+ // Version for the downstream Server declaration — read from package.json
73
+ // relative to __dirname exactly like mcp-server.ts does (both compile into
74
+ // dist/, so ".." lands on the package root). The bridge reports the SAME
75
+ // identity as the daemon's own McpServer so every surface agrees.
76
+ let VERSION = "0.0.0";
77
+ try {
78
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf-8"));
79
+ VERSION = pkg.version;
80
+ }
81
+ catch { /* fallback — matches mcp-server.ts */ }
82
+ const DEFAULT_BRIDGE_PORT = 8787;
83
+ const HEALTH_PROBE_TIMEOUT_MS = 2000;
84
+ const AUTOSTART_POLL_INTERVAL_MS = 250;
85
+ const AUTOSTART_POLL_TOTAL_MS = 30_000;
86
+ /** Loopback check for URL hostnames (Node's URL keeps the brackets on [::1]). */
87
+ function isLoopbackHost(hostname) {
88
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
89
+ }
90
+ function parseTargetUrl(raw, source) {
91
+ const url = raw.trim().replace(/\/+$/, "");
92
+ let parsed;
93
+ try {
94
+ parsed = new URL(url);
95
+ }
96
+ catch {
97
+ // A user-editable env value / config value that is not a URL must fail
98
+ // with a message naming the input — not a bare "Invalid URL".
99
+ const origin = source === "option" ? "the given server URL"
100
+ : source === "env" ? "HICORTEX_SERVER_URL"
101
+ : source === "config" ? "the config serverUrl"
102
+ : "the default server URL";
103
+ throw new Error(`Cannot bridge to ${origin}: "${url}" is not a valid URL.`);
104
+ }
105
+ return {
106
+ url,
107
+ local: isLoopbackHost(parsed.hostname),
108
+ port: parseInt(parsed.port, 10) || DEFAULT_BRIDGE_PORT,
109
+ source,
110
+ };
111
+ }
112
+ /**
113
+ * Resolve the daemon/server the bridge should talk to. Precedence: explicit
114
+ * option (the runMcpStdio test seam) → HICORTEX_SERVER_URL env → config file
115
+ * (client-mode serverUrl = remote; server-mode = localhost, the
116
+ * resolveConfig() semantics already shared by both CC hooks) → default local
117
+ * 8787. Blank/whitespace env values are ignored, not mistaken for targets.
118
+ */
119
+ function resolveBridgeTarget(explicitUrl) {
120
+ if (typeof explicitUrl === "string" && explicitUrl.trim() !== "") {
121
+ return parseTargetUrl(explicitUrl, "option");
122
+ }
123
+ const envUrl = process.env.HICORTEX_SERVER_URL;
124
+ if (typeof envUrl === "string" && envUrl.trim() !== "") {
125
+ return parseTargetUrl(envUrl, "env");
126
+ }
127
+ // resolveConfig() already encodes both config shapes (client → remote
128
+ // serverUrl; server → http://127.0.0.1:<port ?? 8787>) and returns null
129
+ // when there is no usable config.
130
+ const config = (0, learnings_identity_js_1.resolveConfig)();
131
+ if (config)
132
+ return parseTargetUrl(config.serverUrl, "config");
133
+ return parseTargetUrl(`http://127.0.0.1:${DEFAULT_BRIDGE_PORT}`, "default");
134
+ }
135
+ /**
136
+ * Resolve the bearer token for the upstream connection: explicit option →
137
+ * HICORTEX_AUTH_TOKEN env → config.authToken. Undefined = no token (a local
138
+ * daemon needs none — loopback bypasses auth).
139
+ */
140
+ function resolveBridgeToken(explicitToken) {
141
+ if (typeof explicitToken === "string" && explicitToken.trim() !== "")
142
+ return explicitToken.trim();
143
+ const envToken = process.env.HICORTEX_AUTH_TOKEN;
144
+ if (typeof envToken === "string" && envToken.trim() !== "")
145
+ return envToken.trim();
146
+ return (0, learnings_identity_js_1.resolveConfig)()?.authToken;
147
+ }
148
+ /**
149
+ * One GET /health probe. Connection refused / timeout / DNS failure →
150
+ * { reachable: false }; a response that is not ok → { reachable: true, ok:
151
+ * false } — the two carry different autostart decisions, so a boolean alone
152
+ * cannot express them.
153
+ */
154
+ async function probeHealthOnce(url, timeoutMs = HEALTH_PROBE_TIMEOUT_MS) {
155
+ try {
156
+ const resp = await fetch(`${url}/health`, { signal: AbortSignal.timeout(timeoutMs) });
157
+ return { reachable: true, ok: resp.ok };
158
+ }
159
+ catch {
160
+ return { reachable: false, ok: false };
161
+ }
162
+ }
163
+ /**
164
+ * Pure decision from one health probe: healthy → bridge; refused + loopback
165
+ * → spawn a local daemon; refused + remote → fail with an actionable message
166
+ * (never spawn for remote URLs); answering-but-not-ok → fail explicitly (a
167
+ * foreign or broken service owns the port — spawning next to it cannot help).
168
+ */
169
+ function decideAutostart(probe, target) {
170
+ if (probe.ok)
171
+ return { action: "bridge" };
172
+ if (probe.reachable) {
173
+ return {
174
+ action: "fail",
175
+ reason: `Something is answering at ${target.url}/health but it is not a healthy Hicortex server. ` +
176
+ `A foreign or broken service owns that port — inspect it (e.g. lsof -i :${target.port}), ` +
177
+ `then either free the port or point HICORTEX_SERVER_URL at the real Hicortex server.`,
178
+ };
179
+ }
180
+ if (!target.local) {
181
+ return {
182
+ action: "fail",
183
+ reason: `Cannot reach the Hicortex server at ${target.url}. Start it on the server machine ` +
184
+ `(check with \`hicortex status\`, start with \`npx @gamaze/hicortex server\`) or fix HICORTEX_SERVER_URL. ` +
185
+ `If it answers 401 once up, set HICORTEX_AUTH_TOKEN to the server's auth token.`,
186
+ };
187
+ }
188
+ return { action: "spawn" };
189
+ }
190
+ /**
191
+ * Spawn a detached daemon: `node cli.js server --port <n>`. Detached + unref
192
+ * + ignored stdio — the daemon OUTLIVES this bridge (the product model; init
193
+ * installs it as a persistent daemon for exactly this) and never touches the
194
+ * bridge's stdio MCP wire. The spawned child's exit is not monitored on
195
+ * purpose: in the EADDRINUSE race (two bridges started the same missing
196
+ * daemon), the loser's child dies and the winner's daemon answers the
197
+ * /health poll — monitoring would teach us nothing actionable.
198
+ */
199
+ function defaultSpawnDaemon(port) {
200
+ const child = (0, node_child_process_1.spawn)(process.execPath, [(0, node_path_1.join)(__dirname, "cli.js"), "server", "--port", String(port)], { detached: true, stdio: "ignore" });
201
+ child.unref();
202
+ }
203
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
204
+ /**
205
+ * Ensure something healthy answers at the target before bridging: probe once,
206
+ * decide, and if spawning — poll until healthy (or the deadline). Throws on
207
+ * every fail-path (explicit error, never silent degradation).
208
+ */
209
+ async function ensureDaemonReady(target, options = {}) {
210
+ const probe = options.probeHealth ?? ((url) => probeHealthOnce(url));
211
+ const autostart = options.autostart ?? true;
212
+ const intervalMs = options.pollIntervalMs ?? AUTOSTART_POLL_INTERVAL_MS;
213
+ const totalMs = options.pollTotalMs ?? AUTOSTART_POLL_TOTAL_MS;
214
+ const initial = await probe(target.url);
215
+ const decision = decideAutostart(initial, target);
216
+ if (decision.action === "bridge")
217
+ return;
218
+ if (decision.action === "fail")
219
+ throw new Error(decision.reason);
220
+ if (!autostart) {
221
+ throw new Error(`No Hicortex server is running at ${target.url} (autostart disabled).`);
222
+ }
223
+ await (options.spawnDaemon ?? defaultSpawnDaemon)(target.port);
224
+ const deadline = Date.now() + totalMs;
225
+ while (Date.now() < deadline) {
226
+ await sleep(intervalMs);
227
+ // A mid-boot non-ok answer (daemon warming up) is NOT a foreign service —
228
+ // only the INITIAL probe's reachable-non-ok fails. Keep polling.
229
+ const current = await probe(target.url);
230
+ if (current.ok)
231
+ return;
232
+ }
233
+ throw new Error(`The Hicortex server did not become healthy at ${target.url}/health within ` +
234
+ `${Math.round(totalMs / 1000)}s of autostart. Try \`npx @gamaze/hicortex server\` in a terminal ` +
235
+ `to see the daemon's startup error, then re-run this command.`);
236
+ }
237
+ /**
238
+ * Run the stdio MCP bridge. Resolves only after the downstream transport
239
+ * closes (the lifecycle handlers then exit the process); every setup failure
240
+ * throws for cli.ts to report on stderr and exit 1.
241
+ */
242
+ async function runMcpStdio(options = {}) {
243
+ const target = resolveBridgeTarget(options.serverUrl);
244
+ const token = resolveBridgeToken(options.authToken);
245
+ await ensureDaemonReady(target, options);
246
+ // Upstream: the daemon's SSE MCP endpoint. requestInit headers ride BOTH
247
+ // the GET /sse and the POST /messages (SDK 1.28 _commonHeaders/send).
248
+ const upstream = new sse_js_1.SSEClientTransport(new URL(`${target.url}/sse`), token !== undefined ? { requestInit: { headers: { Authorization: `Bearer ${token}` } } } : {});
249
+ const client = new index_js_2.Client({ name: "hicortex-mcp-bridge", version: VERSION });
250
+ try {
251
+ await client.connect(upstream);
252
+ }
253
+ catch (err) {
254
+ // 401 from the daemon's auth middleware (remote connections; loopback is
255
+ // exempt). SseError carries the HTTP status as .code.
256
+ if (err.code === 401) {
257
+ throw new Error(`The Hicortex server at ${target.url} rejected the connection (401). ` +
258
+ `Set HICORTEX_AUTH_TOKEN to the server's auth token — it is printed by \`hicortex status\` on the server box.`);
259
+ }
260
+ throw err instanceof Error ? err : new Error(String(err));
261
+ }
262
+ // Downstream: a low-level Server over stdio advertising exactly what the
263
+ // daemon offers (tools). Ping is auto-answered by the Protocol base.
264
+ // #383: forward the DAEMON's initialize-result instructions verbatim — the
265
+ // daemon owns the text and the memoryInstructions gate, so the two surfaces
266
+ // cannot diverge and no config read is duplicated in the bridge (a
267
+ // pre-#383 remote daemon simply has none to forward; undefined omits the
268
+ // field from the bridge's own initialize result).
269
+ const server = new index_js_1.Server({ name: "hicortex", version: VERSION }, { capabilities: { tools: {} }, instructions: client.getInstructions() });
270
+ // The proxy core — the SDK's documented proxy pattern. Forward the two
271
+ // tools requests and pass extra.signal through so a downstream
272
+ // notifications/cancelled aborts the upstream call (which emits the
273
+ // correctly-id'd cancellation to the daemon). Nothing else is forwarded
274
+ // request-wise: the daemon is tools-only and the base class answers ping.
275
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async (_request, extra) => (await client.listTools(undefined, { signal: extra.signal })));
276
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request, extra) => (await client.callTool(request.params, undefined, { signal: extra.signal })));
277
+ // Upstream → downstream notifications, best-effort: the daemon's tool-list
278
+ // changes or log messages reach the client; a closed far end must not kill
279
+ // the bridge from inside a notification handler.
280
+ client.fallbackNotificationHandler = async (notification) => {
281
+ try {
282
+ await server.notification(notification);
283
+ }
284
+ catch {
285
+ // Best-effort by design.
286
+ }
287
+ };
288
+ // Lifecycle: whichever side ends first tears down the other. The `exiting`
289
+ // guard keeps our OWN client.close() (graceful path) from being read as an
290
+ // upstream loss.
291
+ let exiting = false;
292
+ const shutdown = (code) => {
293
+ if (exiting)
294
+ return;
295
+ exiting = true;
296
+ void Promise.allSettled([server.close(), client.close()]).then(() => process.exit(code));
297
+ };
298
+ // Downstream closed (the MCP client went away) → close upstream → exit 0.
299
+ server.onclose = () => shutdown(0);
300
+ // Upstream transport died → the bridge cannot serve anything → exit 1.
301
+ client.onclose = () => {
302
+ if (exiting)
303
+ return;
304
+ console.error("[hicortex] mcp: lost the connection to the Hicortex server");
305
+ shutdown(1);
306
+ };
307
+ process.once("SIGINT", () => shutdown(0));
308
+ process.once("SIGTERM", () => shutdown(0));
309
+ const downstream = options.downstream ?? new stdio_js_1.StdioServerTransport();
310
+ await server.connect(downstream);
311
+ // Diagnostics NEVER touch stdout (the MCP wire) — stderr only.
312
+ console.error(`[hicortex] mcp: bridging stdio <-> ${target.url}/sse (target: ${target.source})`);
313
+ }
@@ -18,11 +18,29 @@
18
18
  * The section name is RESERVED: PUT /identity rejects it, and the synthetic
19
19
  * text overrides any user file of the same name (enforced means enforced).
20
20
  * Off-switch: config `memoryInstructions: false`.
21
+ *
22
+ * Since #383 the file is also the source for the MCP standing instructions
23
+ * (the initialize-result `instructions` field) — the same policy, shaped for
24
+ * passive MCP clients, behind the same off-switch.
21
25
  */
22
26
  export declare const MEMORY_SECTION_NAME = "memory";
23
27
  /** The product-authored instruction text. Keep compact (~120 tokens): it is
24
28
  * injected once per session into every agent on the fleet. */
25
29
  export declare function renderMemoryInstructions(): string;
30
+ /** The MCP-client-shaped sibling (#383): standing instructions for the MCP
31
+ * `instructions` field in the initialize result — the MCP-native
32
+ * SessionStart. Passive MCP clients (Claude Desktop etc.) run no hooks and
33
+ * see no injected sections, so this field is the ONLY product-owned
34
+ * guidance their model ever receives; the 2026-09-10 field test showed the
35
+ * memory going entirely unused without it. Compact by construction (same
36
+ * ~120-token budget as the identity sibling) and shares the two policy
37
+ * sentences verbatim — one source, two surfaces. Drops the hook-only
38
+ * surfaces (the `## Memory recall (auto)` index) those clients never see. */
39
+ export declare function renderMcpInstructions(): string;
40
+ /** The config gate as a pure function (#383): enabled → the rendered text,
41
+ * disabled → undefined (the SDK omits the field from the initialize result,
42
+ * so `memoryInstructions: false` silences BOTH surfaces with one switch). */
43
+ export declare function resolveMcpInstructions(enabled: boolean): string | undefined;
26
44
  /** True for the reserved product section name (case-insensitive guard —
27
45
  * section names are lowercase by allowlist, but be safe). */
28
46
  export declare function isReservedSectionName(name: unknown): boolean;
@@ -19,13 +19,25 @@
19
19
  * The section name is RESERVED: PUT /identity rejects it, and the synthetic
20
20
  * text overrides any user file of the same name (enforced means enforced).
21
21
  * Off-switch: config `memoryInstructions: false`.
22
+ *
23
+ * Since #383 the file is also the source for the MCP standing instructions
24
+ * (the initialize-result `instructions` field) — the same policy, shaped for
25
+ * passive MCP clients, behind the same off-switch.
22
26
  */
23
27
  Object.defineProperty(exports, "__esModule", { value: true });
24
28
  exports.MEMORY_SECTION_NAME = void 0;
25
29
  exports.renderMemoryInstructions = renderMemoryInstructions;
30
+ exports.renderMcpInstructions = renderMcpInstructions;
31
+ exports.resolveMcpInstructions = resolveMcpInstructions;
26
32
  exports.isReservedSectionName = isReservedSectionName;
27
33
  exports.injectMemorySection = injectMemorySection;
28
34
  exports.MEMORY_SECTION_NAME = "memory";
35
+ /** Capture policy, shared by BOTH instruction surfaces (identity section +
36
+ * MCP initialize result) so they can never disagree. Sentence only — each
37
+ * renderer prefixes its own bullet marker. */
38
+ const CAPTURE_POLICY = "Capture is automatic (nightly). Do not manually ingest routine content — `hicortex_ingest` is for explicitly requested learnings only.";
39
+ /** Infrastructure policy, shared by BOTH instruction surfaces (same rule). */
40
+ const INFRA_POLICY = "Never inspect, test, or modify memory/plugin/gateway infrastructure (configs, services, tokens). If a memory tool seems missing or broken, say so and stop.";
29
41
  /** The product-authored instruction text. Keep compact (~120 tokens): it is
30
42
  * injected once per session into every agent on the fleet. */
31
43
  function renderMemoryInstructions() {
@@ -34,10 +46,35 @@ function renderMemoryInstructions() {
34
46
  "- A `## Memory recall (auto)` index may arrive with prompts: it is a MENU, not content. Fetch a full memory with `hicortex_get(id)` when the entry could change how you handle the current task.",
35
47
  "- Recall before assuming: `hicortex_search` for prior decisions/facts/preferences, `hicortex_recent` to catch up on a project.",
36
48
  "- Cite any memory you rely on by id + date, and mark it `FETCHED` (you read the full memory via `hicortex_get`) or `SNIPPET` (the one-line entry only). Don't present a SNIPPET citation as established. On conflicts, newer memories supersede older.",
37
- "- Capture is automatic (nightly). Do not manually ingest routine content — `hicortex_ingest` is for explicitly requested learnings only.",
38
- "- Never inspect, test, or modify memory/plugin/gateway infrastructure (configs, services, tokens). If a memory tool seems missing or broken, say so and stop.",
49
+ `- ${CAPTURE_POLICY}`,
50
+ `- ${INFRA_POLICY}`,
39
51
  ].join("\n");
40
52
  }
53
+ /** The MCP-client-shaped sibling (#383): standing instructions for the MCP
54
+ * `instructions` field in the initialize result — the MCP-native
55
+ * SessionStart. Passive MCP clients (Claude Desktop etc.) run no hooks and
56
+ * see no injected sections, so this field is the ONLY product-owned
57
+ * guidance their model ever receives; the 2026-09-10 field test showed the
58
+ * memory going entirely unused without it. Compact by construction (same
59
+ * ~120-token budget as the identity sibling) and shares the two policy
60
+ * sentences verbatim — one source, two surfaces. Drops the hook-only
61
+ * surfaces (the `## Memory recall (auto)` index) those clients never see. */
62
+ function renderMcpInstructions() {
63
+ return [
64
+ "This server is the user's persistent long-term memory, shared by all their agents and sessions: what was learned, decided, or corrected survives.",
65
+ "CALL `hicortex_search` BEFORE answering questions about the user's history, projects, or preferences — before assuming, guessing, or asking the user something that may already be known.",
66
+ "Call `hicortex_recent` at the start of substantive work on a project to catch up on its latest state.",
67
+ "Search results are one-line summaries — fetch the full memory with `hicortex_get` when it could change how you handle the task, and cite what you rely on (id + date). On conflicts, newer memories supersede older.",
68
+ `- ${CAPTURE_POLICY}`,
69
+ `- ${INFRA_POLICY}`,
70
+ ].join("\n");
71
+ }
72
+ /** The config gate as a pure function (#383): enabled → the rendered text,
73
+ * disabled → undefined (the SDK omits the field from the initialize result,
74
+ * so `memoryInstructions: false` silences BOTH surfaces with one switch). */
75
+ function resolveMcpInstructions(enabled) {
76
+ return enabled ? renderMcpInstructions() : undefined;
77
+ }
41
78
  /** True for the reserved product section name (case-insensitive guard —
42
79
  * section names are lowercase by allowlist, but be safe). */
43
80
  function isReservedSectionName(name) {
package/dist/nightly.js CHANGED
@@ -735,7 +735,20 @@ async function runNightly(options = {}) {
735
735
  // #241: config-driven total LLM-call ceiling (default 5000, was 200).
736
736
  (0, config_read_js_1.readPositiveConfig)(savedConfig ?? {}, "consolidateMaxLlmCalls", consolidate_js_1.CONSOLIDATE_MAX_LLM_CALLS),
737
737
  // #245: soft cap on the corpus (default 10000; 0 disables eviction).
738
- memorySoftCapResolved);
738
+ memorySoftCapResolved, {
739
+ // #384 reconsolidation knobs — threaded exactly like the
740
+ // supersession pair above; the stage validates and falls back
741
+ // to its defaults (0.75 / 0.80) on invalid/absent values.
742
+ minSimilarity: savedConfig?.correctionMinSimilarity,
743
+ rewriteMinConfidence: savedConfig?.correctionRewriteMinConfidence,
744
+ // #392 unified-resolution knobs: the deterministic-merge
745
+ // ceiling (legacy dedupMergeThreshold honored when the new
746
+ // key is absent) and the pacing cap. Same validation posture
747
+ // — the stage defaults to 0.92 / 250.
748
+ autoMergeThreshold: (savedConfig?.dedupAutoMergeThreshold ??
749
+ savedConfig?.dedupMergeThreshold),
750
+ maxMerges: savedConfig?.dedupNightlyMaxMerges,
751
+ });
739
752
  console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
740
753
  (report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
741
754
  consolidationStatus = report.status;