@gamaze/hicortex 0.20.4 → 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.
package/dist/index.js CHANGED
@@ -1038,7 +1038,7 @@ exports.default = {
1038
1038
  // -----------------------------------------------------------------------
1039
1039
  api.registerTool((_ctx) => ({
1040
1040
  name: "hicortex_search",
1041
- description: "Search long-term memory using semantic similarity. Returns the most relevant memories from past sessions.",
1041
+ description: "Search shared long-term memory (all agents, all sessions). CALL THIS BEFORE assuming, guessing, or asking the user about anything that may have come up before: prior decisions, preferences, project facts, people, hardware, past incidents. If you are about to write 'I don't have information about…', search first.",
1042
1042
  parameters: {
1043
1043
  type: "object",
1044
1044
  properties: {
@@ -1068,7 +1068,7 @@ exports.default = {
1068
1068
  }), { name: "hicortex_search" });
1069
1069
  api.registerTool((_ctx) => ({
1070
1070
  name: "hicortex_get",
1071
- description: "Fetch ONE memory's full content by id — use this to lazy-load entries from the '## Memory recall (auto)' index or from search results whose snippet was not enough. Fetching a memory marks it as used (strengthens it), so fetch entries that could change your action — not every shown one. When the memory shapes your answer, cite it as given in the response — mark a fetched memory `FETCHED` and a one-line entry cited unread `SNIPPET`; don't pass SNIPPET off as established.",
1071
+ description: "Fetch ONE memory's full content by id — use this to lazy-load entries from the '## Memory recall (auto)' index or from search results whose snippet was not enough. Fetching a memory marks it as used (strengthens it), so fetch entries that could change your action — not every shown one. When the memory shapes your answer, cite it to the user (id + date + origin agent) — mark a fetched memory `FETCHED` and a one-line entry cited unread `SNIPPET`; don't pass SNIPPET off as established.",
1072
1072
  parameters: {
1073
1073
  type: "object",
1074
1074
  properties: {
@@ -1101,7 +1101,7 @@ exports.default = {
1101
1101
  }), { name: "hicortex_get" });
1102
1102
  api.registerTool((_ctx) => ({
1103
1103
  name: "hicortex_recent",
1104
- description: "Get recent memories, optionally filtered by project. Queryless recall of the latest memories by project, ranked by importance. Useful to catch up on what happened recently.",
1104
+ description: "Get recent memories, optionally filtered by project. CALL THIS AT THE START of substantive work on a project to catch up on its latest state — cheaper than asking the user what happened.",
1105
1105
  parameters: {
1106
1106
  type: "object",
1107
1107
  properties: {
@@ -1130,7 +1130,7 @@ exports.default = {
1130
1130
  }), { name: "hicortex_recent" });
1131
1131
  api.registerTool((_ctx) => ({
1132
1132
  name: "hicortex_ingest",
1133
- description: "Store a new memory in long-term storage. Use for Knowledge, Decisions, or Learnings.",
1133
+ description: "Store a new memory in long-term storage. Use for Knowledge, Decisions, or Learnings. Capture is automatic (nightly) — use this ONLY for explicitly requested learnings, never routine content.",
1134
1134
  parameters: {
1135
1135
  type: "object",
1136
1136
  properties: {
@@ -1165,7 +1165,7 @@ exports.default = {
1165
1165
  }), { name: "hicortex_ingest" });
1166
1166
  api.registerTool((_ctx) => ({
1167
1167
  name: "hicortex_lessons",
1168
- description: "Get actionable Learnings distilled from past sessions. Auto-generated insights about mistakes to avoid.",
1168
+ description: "Get actionable Learnings — auto-generated insights about mistakes to avoid. CALL THIS before retrying an approach that failed before, or when picking up work where past problems may have been recorded.",
1169
1169
  parameters: {
1170
1170
  type: "object",
1171
1171
  properties: {
@@ -1191,7 +1191,7 @@ exports.default = {
1191
1191
  }), { name: "hicortex_lessons" });
1192
1192
  api.registerTool((_ctx) => ({
1193
1193
  name: "hicortex_index",
1194
- description: "Get the knowledge domain index — shows what topics and projects are stored in memory, grouped by domain.",
1194
+ description: "Get the knowledge domain index — shows what topics and projects are stored in memory, grouped by domain. Call before a broad search to see which knowledge domains exist, or when unsure what the memory covers.",
1195
1195
  parameters: {
1196
1196
  type: "object",
1197
1197
  properties: {},
@@ -1210,7 +1210,7 @@ exports.default = {
1210
1210
  }), { name: "hicortex_index" });
1211
1211
  api.registerTool((_ctx) => ({
1212
1212
  name: "hicortex_graph",
1213
- description: "Query the memory knowledge graph — find connected memories, hub nodes, or paths between memories.",
1213
+ description: "Query the memory knowledge graph — find connected memories, hub nodes, or paths between memories. Use it to explore memories connected to one you just fetched, or to find hub memories in a domain.",
1214
1214
  parameters: {
1215
1215
  type: "object",
1216
1216
  properties: {
package/dist/init.d.ts CHANGED
@@ -7,13 +7,16 @@
7
7
  * 3. OC plugin installed (~/.openclaw/openclaw.json)
8
8
  * 4. CC MCP already registered (~/.claude/settings.json)
9
9
  * 5. Hermes present (~/.hermes) / Pi present (~/.pi/agent) /
10
- * opencode present (~/.config/opencode or ~/.local/share/opencode)
10
+ * opencode present (~/.config/opencode or ~/.local/share/opencode) /
11
+ * Claude Desktop present (macOS ~/Library/Application Support/Claude,
12
+ * Windows %APPDATA%\Claude)
11
13
  * 6. Existing DB (~/.hicortex/ or ~/.openclaw/data/)
12
14
  *
13
15
  * Actions:
14
16
  * - Install persistent daemon (launchd/systemd)
15
17
  * - Register MCP server in CC settings
16
18
  * - Install CC SessionStart hook for query-time lessons
19
+ * - Offer the Claude Desktop stdio MCP entry (opt-in, #381)
17
20
  * - Strip old static CLAUDE.md learnings block if present
18
21
  * - Remove legacy pre-0.10 CC commands (/learn, /hicortex-activate) if present
19
22
  */
package/dist/init.js CHANGED
@@ -8,13 +8,16 @@
8
8
  * 3. OC plugin installed (~/.openclaw/openclaw.json)
9
9
  * 4. CC MCP already registered (~/.claude/settings.json)
10
10
  * 5. Hermes present (~/.hermes) / Pi present (~/.pi/agent) /
11
- * opencode present (~/.config/opencode or ~/.local/share/opencode)
11
+ * opencode present (~/.config/opencode or ~/.local/share/opencode) /
12
+ * Claude Desktop present (macOS ~/Library/Application Support/Claude,
13
+ * Windows %APPDATA%\Claude)
12
14
  * 6. Existing DB (~/.hicortex/ or ~/.openclaw/data/)
13
15
  *
14
16
  * Actions:
15
17
  * - Install persistent daemon (launchd/systemd)
16
18
  * - Register MCP server in CC settings
17
19
  * - Install CC SessionStart hook for query-time lessons
20
+ * - Offer the Claude Desktop stdio MCP entry (opt-in, #381)
18
21
  * - Strip old static CLAUDE.md learnings block if present
19
22
  * - Remove legacy pre-0.10 CC commands (/learn, /hicortex-activate) if present
20
23
  */
@@ -59,6 +62,7 @@ const node_child_process_1 = require("node:child_process");
59
62
  const node_readline_1 = require("node:readline");
60
63
  const node_crypto_1 = require("node:crypto");
61
64
  const claude_md_js_1 = require("./claude-md.js");
65
+ const claude_desktop_js_1 = require("./claude-desktop.js");
62
66
  const config_read_js_1 = require("./config-read.js");
63
67
  const identity_store_js_1 = require("./identity-store.js");
64
68
  const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
@@ -99,6 +103,7 @@ async function detect() {
99
103
  hermesFound: false,
100
104
  piFound: false,
101
105
  opencodeFound: false,
106
+ desktopFound: false,
102
107
  existingDb: false,
103
108
  };
104
109
  // Check local server. /health/detail carries the diagnostics (memories,
@@ -143,6 +148,15 @@ async function detect() {
143
148
  // Check opencode (~/.config/opencode or ~/.local/share/opencode — its
144
149
  // global plugins dir / session store; it auto-loads ~/.config/opencode/plugins/)
145
150
  result.opencodeFound = (0, node_fs_1.existsSync)(OPENCODE_CONFIG_DIR) || (0, node_fs_1.existsSync)(OPENCODE_DATA_DIR);
151
+ // Check Claude Desktop (#381 — macOS ~/Library/Application Support/Claude,
152
+ // Windows %APPDATA%\Claude; Linux has no Desktop build → null → skip
153
+ // silently). Detection is the DIRECTORY: the config file may not exist
154
+ // yet, and creating it fresh is exactly what the Desktop setup step does.
155
+ const desktopDir = (0, claude_desktop_js_1.desktopConfigDir)();
156
+ if (desktopDir && (0, node_fs_1.existsSync)(desktopDir)) {
157
+ result.desktopFound = true;
158
+ result.desktopDir = desktopDir;
159
+ }
146
160
  // Check OC plugin
147
161
  try {
148
162
  const raw = (0, node_fs_1.readFileSync)(OC_CONFIG, "utf-8");
@@ -399,6 +413,84 @@ function setupOpencode() {
399
413
  console.log(" → Restart opencode sessions to load the plugin (recall, identity, lessons, 9 tools)");
400
414
  }
401
415
  // ---------------------------------------------------------------------------
416
+ // Claude Desktop setup
417
+ // ---------------------------------------------------------------------------
418
+ /**
419
+ * Offer to add the Hicortex stdio MCP entry to Claude Desktop's
420
+ * claude_desktop_config.json (#381) — Desktop's only stdio MCP route, and a
421
+ * file owned by ANOTHER app, which is why this step is OPT-IN (default No,
422
+ * its own prompt after the batch actions) while the other harness setups
423
+ * just run. The write itself is merge-safe + atomic + backed up (see
424
+ * claude-desktop.ts); a malformed existing config is refused UNTOUCHED with
425
+ * the hand-fix snippet printed. The entry is the `hicortex mcp` bridge with
426
+ * an ABSOLUTE npx path (GUI apps don't inherit the shell PATH — a bare
427
+ * "npx" is the #1 Desktop failure mode); remote targets carry the server
428
+ * URL + token in env, loopback needs none (the bridge autostarts the daemon
429
+ * and loopback bypasses auth). Never throws — every failure path is a
430
+ * printed warning and init continues. No-ops when Desktop is not installed.
431
+ */
432
+ async function setupClaudeDesktop(serverUrl, authToken) {
433
+ const dir = (0, claude_desktop_js_1.desktopConfigDir)();
434
+ if (!dir || !(0, node_fs_1.existsSync)(dir))
435
+ return; // no Claude Desktop on this machine
436
+ // Same non-interactive guard as persistLlmConfig: a piped/scripted init
437
+ // must not auto-apply an opt-in choice — a readline EOF would read as "".
438
+ if (!process.stdin.isTTY) {
439
+ console.log(" ⚠ Non-interactive stdin — Claude Desktop setup skipped; re-run `hicortex init` interactively to add it.");
440
+ return;
441
+ }
442
+ const answer = (await ask("Add the Hicortex MCP server to Claude Desktop? [y/N] ")).toLowerCase();
443
+ if (answer !== "y" && answer !== "yes") {
444
+ console.log(" → Claude Desktop left unchanged");
445
+ return;
446
+ }
447
+ const configPath = (0, node_path_1.join)(dir, "claude_desktop_config.json");
448
+ // Absolute npx, rejecting npm's ephemeral /_npx/ cache (#176). Null → we
449
+ // cannot write a WORKING entry, so we print it and write nothing.
450
+ const npxPath = (0, claude_desktop_js_1.resolveDesktopNpxPath)();
451
+ if (!npxPath) {
452
+ console.log(" ⚠ Could not locate a durable npx on this machine — nothing written.");
453
+ printDesktopManualEntry(configPath, "<path-to-npx>", serverUrl, authToken);
454
+ return;
455
+ }
456
+ // Remote targets carry the connection in env; loopback needs none.
457
+ const env = (0, claude_desktop_js_1.isLocalServerUrl)(serverUrl)
458
+ ? undefined
459
+ : { HICORTEX_SERVER_URL: serverUrl, ...(authToken ? { HICORTEX_AUTH_TOKEN: authToken } : {}) };
460
+ // getPackageSpec() reads the config.json this run has already written (or
461
+ // an earlier run's), so the Desktop entry honours updateChannel exactly
462
+ // like the daemon/timer ExecStart does.
463
+ const entry = (0, claude_desktop_js_1.buildDesktopServerEntry)(npxPath, getPackageSpec(), env);
464
+ const result = (0, claude_desktop_js_1.writeDesktopServerConfig)(configPath, entry);
465
+ if (result.status === "written") {
466
+ console.log(` ✓ Added the Hicortex MCP server to Claude Desktop (${configPath})`);
467
+ if (result.backupPath) {
468
+ console.log(` Backup of the previous config: ${result.backupPath}`);
469
+ }
470
+ console.log(" → Fully quit and restart Claude Desktop to load it (Cmd+Q on macOS).");
471
+ }
472
+ else if (result.status === "refused") {
473
+ console.log(` ✗ ${result.reason}`);
474
+ printDesktopManualEntry(configPath, npxPath, serverUrl, authToken);
475
+ }
476
+ else {
477
+ console.log(` ⚠ Claude Desktop config write failed: ${result.reason} — nothing was changed.`);
478
+ }
479
+ }
480
+ /**
481
+ * The hand-fix snippet for the two paths where init writes nothing (no
482
+ * durable npx found / existing config refused): the ready-to-paste entry and
483
+ * the file it goes in, so the user can finish by hand in one paste.
484
+ */
485
+ function printDesktopManualEntry(configPath, command, serverUrl, authToken) {
486
+ const env = (0, claude_desktop_js_1.isLocalServerUrl)(serverUrl)
487
+ ? undefined
488
+ : { HICORTEX_SERVER_URL: serverUrl, ...(authToken ? { HICORTEX_AUTH_TOKEN: authToken } : {}) };
489
+ const entry = (0, claude_desktop_js_1.buildDesktopServerEntry)(command, getPackageSpec(), env);
490
+ console.log(` Add it by hand in ${configPath}, under "mcpServers":`);
491
+ console.log(` "hicortex": ${JSON.stringify(entry)}`);
492
+ }
493
+ // ---------------------------------------------------------------------------
402
494
  // Hermes setup
403
495
  // ---------------------------------------------------------------------------
404
496
  /**
@@ -1662,6 +1754,8 @@ async function runInit(options = {}) {
1662
1754
  console.log(` • Pi found at ${PI_AGENT_DIR}`);
1663
1755
  if (d.opencodeFound)
1664
1756
  console.log(` • opencode found at ${OPENCODE_CONFIG_DIR}`);
1757
+ if (d.desktopFound)
1758
+ console.log(` • Claude Desktop found at ${d.desktopDir}`);
1665
1759
  if (d.ccMcpRegistered)
1666
1760
  console.log(" • CC MCP already registered");
1667
1761
  if (d.existingDb)
@@ -1822,6 +1916,10 @@ async function runInit(options = {}) {
1822
1916
  if (d.opencodeFound) {
1823
1917
  setupOpencode();
1824
1918
  }
1919
+ // Offer the Claude Desktop MCP entry (opt-in — Desktop's config file
1920
+ // belongs to another app, so it gets its own prompt after the batch
1921
+ // actions above). No-ops when Desktop is not installed.
1922
+ await setupClaudeDesktop(serverUrl, authToken);
1825
1923
  // Install CC SessionStart hook for query-time lesson injection.
1826
1924
  // Lessons are now fetched live at session start — no static CLAUDE.md block needed.
1827
1925
  installSessionStartHook();
@@ -2014,6 +2112,10 @@ async function runClientInit(serverUrl, agentName) {
2014
2112
  console.log("\nopencode detected — installing plugin...");
2015
2113
  setupOpencode();
2016
2114
  }
2115
+ // Step 8d: Offer the Claude Desktop MCP entry (opt-in; no-ops when Desktop
2116
+ // is not installed). The client config was written in Step 3, so the
2117
+ // entry's package spec already honours updateChannel.
2118
+ await setupClaudeDesktop(serverUrl, authToken);
2017
2119
  console.log("\n✓ Hicortex client setup complete!\n");
2018
2120
  // Telemetry disclosure at install time (informed consent, best practice):
2019
2121
  // opt-out telemetry is only acceptable if the user is TOLD about it.
package/dist/llm.d.ts CHANGED
@@ -23,6 +23,10 @@ export interface LlmConfig {
23
23
  provider: string;
24
24
  /** Max output tokens for all phases (one model). Default 8192. */
25
25
  maxTokens?: number;
26
+ /** Max output tokens for the CLASSIFY tier only — the short JSON-verdict
27
+ * calls (correction/supersession verdicts, rewrite contracts, type + tag
28
+ * classification). Default 1024. See HicortexConfig.classifyMaxTokens (#391). */
29
+ classifyMaxTokens?: number;
26
30
  /** Toggle thinking on the openai-compat path for all phases. Absent = no kwarg sent.
27
31
  * LOCAL-endpoint only (ollama / mlx-lm gateway); see HicortexConfig.enableThinking. */
28
32
  enableThinking?: boolean;
@@ -82,18 +86,19 @@ export declare function resolveExplicitLlmConfig(overrides?: {
82
86
  export declare const resolveLlmConfigForCC: typeof resolveExplicitLlmConfig;
83
87
  /**
84
88
  * Validate + copy the tuning keys (#220: maxTokens + enableThinking + numCtx +
85
- * ollama flush) from the saved disk config onto a runtime LlmConfig. Called by
86
- * BOTH LlmConfig construction sites — the daemon in mcp-server.ts (runs
87
- * distill) AND resolveSavedLlmConfig below (the nightly runs reflect +
88
- * classify) — so every process honors the keys, and a future site calling this
89
- * inherits them by construction.
89
+ * ollama flush; #391: classifyMaxTokens) from the saved disk config onto a
90
+ * runtime LlmConfig. Called by BOTH LlmConfig construction sites — the daemon
91
+ * in mcp-server.ts (runs distill) AND resolveSavedLlmConfig below (the nightly
92
+ * runs reflect + classify) — so every process honors the keys, and a future
93
+ * site calling this inherits them by construction.
90
94
  *
91
- * All keys are optional; absent = call-site defaults (maxTokens 8192, numCtx
92
- * 8192, thinking kwarg omitted, flush off). Wrong-typed values warn and are
93
- * dropped (readPositiveConfig / readStrictBoolean / readNonNegativeConfig) —
94
- * notably a JSON slip `"enableThinking": "false"` (string) is rejected rather
95
- * than coerced to truthy thinking-on, which would silently invert the fix this
96
- * key exists to apply.
95
+ * All keys are optional; absent = call-site defaults (maxTokens 8192,
96
+ * classifyMaxTokens 1024, numCtx 8192, thinking kwarg omitted, flush off).
97
+ * Wrong-typed values warn and are dropped (readPositiveConfig /
98
+ * readStrictBoolean / readNonNegativeConfig) — notably a JSON slip
99
+ * `"enableThinking": "false"` (string) is rejected rather than coerced to
100
+ * truthy thinking-on, which would silently invert the fix this key exists to
101
+ * apply.
97
102
  */
98
103
  export declare function applyTierTuningOverlay(llmConfig: LlmConfig, savedConfig: Record<string, unknown> | null | undefined): void;
99
104
  /**
@@ -213,8 +218,9 @@ export declare class LlmClient {
213
218
  */
214
219
  completeDistill(prompt: string, maxTokens?: number): Promise<LlmResult>;
215
220
  /**
216
- * Classification-tier completion (memory tag classification). One model
217
- * serves all phases (#231) — thin wrapper kept for call-site readability.
221
+ * Classification-tier completion (verdicts + tag/type classification). One
222
+ * model serves all phases (#231) — thin wrapper kept for call-site
223
+ * readability, but the tier keeps its OWN output ceiling (#391).
218
224
  */
219
225
  completeClassify(prompt: string, maxTokens?: number): Promise<LlmResult>;
220
226
  /**
package/dist/llm.js CHANGED
@@ -87,18 +87,19 @@ function resolveExplicitLlmConfig(overrides) {
87
87
  exports.resolveLlmConfigForCC = resolveExplicitLlmConfig;
88
88
  /**
89
89
  * Validate + copy the tuning keys (#220: maxTokens + enableThinking + numCtx +
90
- * ollama flush) from the saved disk config onto a runtime LlmConfig. Called by
91
- * BOTH LlmConfig construction sites — the daemon in mcp-server.ts (runs
92
- * distill) AND resolveSavedLlmConfig below (the nightly runs reflect +
93
- * classify) — so every process honors the keys, and a future site calling this
94
- * inherits them by construction.
90
+ * ollama flush; #391: classifyMaxTokens) from the saved disk config onto a
91
+ * runtime LlmConfig. Called by BOTH LlmConfig construction sites — the daemon
92
+ * in mcp-server.ts (runs distill) AND resolveSavedLlmConfig below (the nightly
93
+ * runs reflect + classify) — so every process honors the keys, and a future
94
+ * site calling this inherits them by construction.
95
95
  *
96
- * All keys are optional; absent = call-site defaults (maxTokens 8192, numCtx
97
- * 8192, thinking kwarg omitted, flush off). Wrong-typed values warn and are
98
- * dropped (readPositiveConfig / readStrictBoolean / readNonNegativeConfig) —
99
- * notably a JSON slip `"enableThinking": "false"` (string) is rejected rather
100
- * than coerced to truthy thinking-on, which would silently invert the fix this
101
- * key exists to apply.
96
+ * All keys are optional; absent = call-site defaults (maxTokens 8192,
97
+ * classifyMaxTokens 1024, numCtx 8192, thinking kwarg omitted, flush off).
98
+ * Wrong-typed values warn and are dropped (readPositiveConfig /
99
+ * readStrictBoolean / readNonNegativeConfig) — notably a JSON slip
100
+ * `"enableThinking": "false"` (string) is rejected rather than coerced to
101
+ * truthy thinking-on, which would silently invert the fix this key exists to
102
+ * apply.
102
103
  */
103
104
  function applyTierTuningOverlay(llmConfig, savedConfig) {
104
105
  if (!savedConfig)
@@ -106,6 +107,9 @@ function applyTierTuningOverlay(llmConfig, savedConfig) {
106
107
  if (savedConfig.maxTokens !== undefined) {
107
108
  llmConfig.maxTokens = (0, config_read_js_1.readPositiveConfig)(savedConfig, "maxTokens", 8192);
108
109
  }
110
+ if (savedConfig.classifyMaxTokens !== undefined) {
111
+ llmConfig.classifyMaxTokens = (0, config_read_js_1.readPositiveConfig)(savedConfig, "classifyMaxTokens", 1024);
112
+ }
109
113
  const thinking = (0, config_read_js_1.readStrictBoolean)(savedConfig, "enableThinking");
110
114
  if (thinking !== undefined) {
111
115
  llmConfig.enableThinking = thinking;
@@ -437,11 +441,18 @@ class LlmClient {
437
441
  return this.complete(this.config.model, prompt, tokens, this.config.timeoutMs ?? 900_000);
438
442
  }
439
443
  /**
440
- * Classification-tier completion (memory tag classification). One model
441
- * serves all phases (#231) — thin wrapper kept for call-site readability.
444
+ * Classification-tier completion (verdicts + tag/type classification). One
445
+ * model serves all phases (#231) — thin wrapper kept for call-site
446
+ * readability, but the tier keeps its OWN output ceiling (#391).
442
447
  */
443
448
  async completeClassify(prompt, maxTokens) {
444
- const tokens = maxTokens ?? this.config.maxTokens ?? 8192;
449
+ // #391: classify-tier ceiling — the call sites' old hardcoded caps
450
+ // (64/32/20, tuned for a local non-reasoning model) starved reasoning
451
+ // models whose internal thinking consumed the whole budget, leaving
452
+ // verdicts empty. Deliberately NOT this.config.maxTokens: that knob
453
+ // governs the heavy phases; this tier has its own (a ceiling, not a
454
+ // target — generation still stops at the model's natural end).
455
+ const tokens = maxTokens ?? this.config.classifyMaxTokens ?? 1024;
445
456
  return this.complete(this.config.model, prompt, tokens, this.config.timeoutMs ?? 900_000);
446
457
  }
447
458
  /**
@@ -11,6 +11,7 @@
11
11
  * POST /messages — message endpoint for MCP clients
12
12
  */
13
13
  import express from "express";
14
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
15
  import type { MemorySearchResult } from "./types.js";
15
16
  /**
16
17
  * Resolve the /distill probe gate (#337): true when the endpoint recently
@@ -28,6 +29,11 @@ export declare function resolveDistillProbeGate(llm: {
28
29
  baseUrl: string;
29
30
  probeTtlMs?: number;
30
31
  }): Promise<boolean>;
32
+ /**
33
+ * Build the McpServer with all Hicortex tools, one per /sse connection.
34
+ * Exported for tests (protocol-level initialize-result checks, #383).
35
+ */
36
+ export declare function createMcpServer(): McpServer;
31
37
  /**
32
38
  * Resolve the request body-size limit in MB (#7, #328 item 2b). Pure —
33
39
  * exported for tests. Precedence: HICORTEX_DISTILL_BODY_LIMIT_MB env >
@@ -49,6 +49,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
49
49
  };
50
50
  Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.resolveDistillProbeGate = resolveDistillProbeGate;
52
+ exports.createMcpServer = createMcpServer;
52
53
  exports.resolveBodyLimitMb = resolveBodyLimitMb;
53
54
  exports.makeBodyLimitErrorHandler = makeBodyLimitErrorHandler;
54
55
  exports.makeContentLengthGate = makeContentLengthGate;
@@ -84,6 +85,7 @@ const seed_lesson_js_1 = require("./seed-lesson.js");
84
85
  const learnings_identity_js_1 = require("./learnings-identity.js");
85
86
  const distiller_js_1 = require("./distiller.js");
86
87
  const dedup_js_1 = require("./dedup.js");
88
+ const reconsolidation_js_1 = require("./reconsolidation.js");
87
89
  const redact_js_1 = require("./redact.js");
88
90
  const init_js_1 = require("./init.js");
89
91
  // ---------------------------------------------------------------------------
@@ -163,11 +165,18 @@ catch { /* fallback */ }
163
165
  // ---------------------------------------------------------------------------
164
166
  // MCP Server setup
165
167
  // ---------------------------------------------------------------------------
168
+ /**
169
+ * Build the McpServer with all Hicortex tools, one per /sse connection.
170
+ * Exported for tests (protocol-level initialize-result checks, #383).
171
+ */
166
172
  function createMcpServer() {
167
- const server = new mcp_js_1.McpServer({
168
- name: "hicortex",
169
- version: VERSION,
170
- });
173
+ // #383: the initialize-result instructions are the only product-owned
174
+ // guidance passive MCP clients (e.g. Claude Desktop — no hooks, no injected
175
+ // sections) ever see: the MCP-native SessionStart. Same off-switch as the
176
+ // identity `memory` section (config `memoryInstructions: false`); the
177
+ // module var is read at call time, so per-connection construction
178
+ // preserves the boot-time gate.
179
+ const server = new mcp_js_1.McpServer({ name: "hicortex", version: VERSION }, { instructions: (0, memory_instructions_js_1.resolveMcpInstructions)(memoryInstructionsEnabled) });
171
180
  // -- hicortex_search --
172
181
  server.tool("hicortex_search", "Search shared long-term memory (all agents, all sessions). CALL THIS BEFORE assuming, guessing, or asking the user about anything that may have come up before: prior decisions, preferences, project facts, people, hardware, past incidents. If you are about to write 'I don't have information about…', search first.", {
173
182
  query: zod_1.z.string().describe("Search query text"),
@@ -218,14 +227,30 @@ function createMcpServer() {
218
227
  }
219
228
  });
220
229
  // -- hicortex_ingest --
221
- server.tool("hicortex_ingest", "Store a new memory in long-term storage. Use for Knowledge, Decisions, or Learnings.", {
230
+ server.tool("hicortex_ingest", "Store a new memory in long-term storage. Use for Knowledge, Decisions, or Learnings. Capture is automatic (nightly) — use this ONLY for explicitly requested learnings, never routine content.", {
222
231
  content: zod_1.z.string().describe("Memory content to store"),
223
232
  project: zod_1.z.string().optional().describe("Project this memory belongs to"),
224
233
  memory_type: zod_1.z.enum(["knowledge", "experience", "decisions", "learnings", "fact", "episode", "decision", "lesson"]).optional().describe("Type of memory (default: Experience). Accepted: Knowledge/Experience/Decisions/Learnings (legacy raw enum also accepted, normalized to the canonical term)."),
225
- }, async ({ content, project, memory_type }) => {
234
+ corrects: zod_1.z.string().optional().describe("ID (8-char prefix or full UUID) of an existing memory this one CORRECTS — records a corrected_by link and retracts the old memory (deterministic, no LLM). Mutually exclusive with supersedes."),
235
+ supersedes: zod_1.z.string().optional().describe("ID (8-char prefix or full UUID) of an existing memory this one SUPERSEDES — records a superseded_by link and marks the old memory superseded (deterministic, no LLM). Mutually exclusive with corrects."),
236
+ }, async ({ content, project, memory_type, corrects, supersedes }) => {
226
237
  if (!db)
227
238
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
228
239
  try {
240
+ // #384 explicit mark — validated BEFORE storing (an unknown/ambiguous
241
+ // target fails the whole call; nothing is written).
242
+ if (corrects !== undefined && supersedes !== undefined) {
243
+ return { content: [{ type: "text", text: "Provide at most one of 'corrects' or 'supersedes'" }], isError: true };
244
+ }
245
+ const rawMark = corrects !== undefined ? corrects : supersedes;
246
+ let explicitMark;
247
+ if (rawMark !== undefined) {
248
+ explicitMark = { kind: corrects !== undefined ? "corrects" : "supersedes", target: rawMark };
249
+ const check = (0, reconsolidation_js_1.checkExplicitMarkTarget)(db, explicitMark);
250
+ if (!check.ok) {
251
+ return { content: [{ type: "text", text: `Ingest failed: ${check.error}` }], isError: true };
252
+ }
253
+ }
229
254
  const embedding = await (0, embedder_js_1.embed)(content);
230
255
  const id = storage.insertMemory(db, content, embedding, {
231
256
  sourceAgent: "claude-code/manual",
@@ -233,7 +258,9 @@ function createMcpServer() {
233
258
  // Normalize legacy raw enum to the canonical term the DB stores.
234
259
  memoryType: memory_type ? (0, type_labels_js_1.normalizeMemoryType)(memory_type) : "experience",
235
260
  });
236
- return { content: [{ type: "text", text: `Memory stored (id: ${id.slice(0, 8)})` }] };
261
+ if (explicitMark)
262
+ (0, reconsolidation_js_1.applyExplicitMark)(db, id, explicitMark);
263
+ return { content: [{ type: "text", text: `Memory stored (id: ${id.slice(0, 8)})${explicitMark ? ` — marked as ${explicitMark.kind === "corrects" ? "correcting" : "superseding"} ${explicitMark.target}` : ""}` }] };
237
264
  }
238
265
  catch (err) {
239
266
  return { content: [{ type: "text", text: `Ingest failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
@@ -253,6 +280,12 @@ function createMcpServer() {
253
280
  const fullId = resolveMemoryId(db, id);
254
281
  if (!fullId)
255
282
  return { content: [{ type: "text", text: `Memory not found: ${id}` }], isError: true };
283
+ // #384: absorbed memories are invisible evidence — an update would
284
+ // re-embed and resurrect them. Refuse; roll back the rewrite first.
285
+ const target = storage.getMemory(db, fullId);
286
+ if (target?.status === "absorbed") {
287
+ return { content: [{ type: "text", text: `Memory ${fullId.slice(0, 8)} is absorbed (folded into a corrected memory) — roll back the absorbing rewrite first (hicortex history --rollback)` }], isError: true };
288
+ }
256
289
  const fields = {};
257
290
  if (content !== undefined)
258
291
  fields.content = content;
@@ -318,8 +351,8 @@ function createMcpServer() {
318
351
  days: zod_1.z.coerce.number().optional().describe("Look back N days (default 7)"),
319
352
  project: zod_1.z.string().optional().describe("Filter by project name"),
320
353
  };
321
- server.tool("hicortex_learnings", "Get actionable Learnings from past sessions. Auto-generated insights about mistakes to avoid.", learningsSchema, learningsHandler);
322
- server.tool("hicortex_lessons", "Get actionable Learnings from past sessions. (Alias for hicortex_learnings.)", learningsSchema, learningsHandler);
354
+ server.tool("hicortex_learnings", "Get actionable Learnings — auto-generated insights about mistakes to avoid. CALL THIS before retrying an approach that failed before, or when picking up work where past problems may have been recorded.", learningsSchema, learningsHandler);
355
+ server.tool("hicortex_lessons", "Get actionable Learnings from past sessions — call it before retrying an approach that failed before. (Alias for hicortex_learnings.)", learningsSchema, learningsHandler);
323
356
  // -- hicortex_identity --
324
357
  // Standing identity layer on-demand (the same data GET /identity returns and
325
358
  // the SessionStart hook injects). Lets an agent re-read its identity after
@@ -351,7 +384,7 @@ function createMcpServer() {
351
384
  }
352
385
  });
353
386
  // -- hicortex_index --
354
- server.tool("hicortex_index", "Get the knowledge domain index — shows what topics and projects are stored in memory, grouped by domain.", {}, async () => {
387
+ server.tool("hicortex_index", "Get the knowledge domain index — shows what topics and projects are stored in memory, grouped by domain. Call before a broad search to see which knowledge domains exist, or when unsure what the memory covers.", {}, async () => {
355
388
  const state = (0, state_js_1.loadState)(stateDir);
356
389
  const moduleIndex = state.moduleIndex;
357
390
  if (moduleIndex && moduleIndex.domains.length > 0) {
@@ -378,7 +411,7 @@ function createMcpServer() {
378
411
  return { content: [{ type: "text", text }] };
379
412
  });
380
413
  // -- hicortex_graph --
381
- server.tool("hicortex_graph", "Query the memory knowledge graph — find connected memories, hub nodes, or paths between memories.", {
414
+ server.tool("hicortex_graph", "Query the memory knowledge graph — find connected memories, hub nodes, or paths between memories. Use it to explore memories connected to one you just fetched, or to find hub memories in a domain.", {
382
415
  operation: zod_1.z.enum(["neighbors", "hubs", "path"]).describe("Graph operation to perform"),
383
416
  id: zod_1.z.string().optional().describe("Memory ID (required for neighbors and path operations)"),
384
417
  target_id: zod_1.z.string().optional().describe("Target memory ID (required for path operation)"),
@@ -885,7 +918,7 @@ async function startServer(options = {}) {
885
918
  res.status(503).json({ error: "Server not initialized" });
886
919
  return;
887
920
  }
888
- const { content, source_agent, source_agent_id, source_domain, project, memory_type, privacy, source_session, session_date } = req.body ?? {};
921
+ const { content, source_agent, source_agent_id, source_domain, project, memory_type, privacy, source_session, session_date, corrects, supersedes } = req.body ?? {};
889
922
  if (!content || typeof content !== "string") {
890
923
  res.status(400).json({ error: "Missing or invalid 'content' field" });
891
924
  return;
@@ -899,6 +932,28 @@ async function startServer(options = {}) {
899
932
  // canonical term the DB stores (knowledge/experience/decisions/learnings).
900
933
  // Canonical values pass through unchanged.
901
934
  const normalizedType = memory_type ? (0, type_labels_js_1.normalizeMemoryType)(memory_type) : memory_type;
935
+ // #384 explicit write-time marking: `corrects` XOR `supersedes`, a single
936
+ // memory id reference. Validated BEFORE anything is written — an unknown
937
+ // or ambiguous target fails the WHOLE request (nothing is stored with a
938
+ // half-applied mark). Deterministic link + status, zero LLM.
939
+ if (corrects !== undefined && supersedes !== undefined) {
940
+ res.status(400).json({ error: "Provide at most one of 'corrects' or 'supersedes'" });
941
+ return;
942
+ }
943
+ let explicitMark;
944
+ const rawMarkValue = corrects !== undefined ? corrects : supersedes;
945
+ if (rawMarkValue !== undefined) {
946
+ if (typeof rawMarkValue !== "string" || !rawMarkValue) {
947
+ res.status(400).json({ error: `'${corrects !== undefined ? "corrects" : "supersedes"}' must be a memory id (8-char prefix or full UUID)` });
948
+ return;
949
+ }
950
+ explicitMark = { kind: corrects !== undefined ? "corrects" : "supersedes", target: rawMarkValue };
951
+ const check = (0, reconsolidation_js_1.checkExplicitMarkTarget)(db, explicitMark);
952
+ if (!check.ok) {
953
+ res.status(check.httpStatus).json({ error: check.error });
954
+ return;
955
+ }
956
+ }
902
957
  // Dedup by source_session (idempotent — skip if already ingested)
903
958
  if (source_session) {
904
959
  const existing = db.prepare("SELECT COUNT(*) as cnt FROM memories WHERE source_session = ?").get(source_session);
@@ -922,7 +977,13 @@ async function startServer(options = {}) {
922
977
  privacy: typeof privacy === "string" ? privacy : null,
923
978
  createdAt: session_date ? new Date(session_date).toISOString() : undefined,
924
979
  });
925
- res.status(201).json({ id, message: "Memory ingested" });
980
+ if (explicitMark)
981
+ (0, reconsolidation_js_1.applyExplicitMark)(db, id, explicitMark);
982
+ res.status(201).json({
983
+ id,
984
+ message: "Memory ingested",
985
+ ...(explicitMark ? { marked: explicitMark.kind } : {}),
986
+ });
926
987
  }
927
988
  catch (err) {
928
989
  res.status(500).json({ error: "Ingestion failed" });
@@ -1337,6 +1398,16 @@ async function startServer(options = {}) {
1337
1398
  res.status(404).json({ error: `Memory not found: ${id}` });
1338
1399
  return;
1339
1400
  }
1401
+ // #384: an absorbed memory is invisible evidence — updating its content
1402
+ // would re-embed and resurrect a row the store deliberately folded into a
1403
+ // corrected target. Roll back the absorbing rewrite first.
1404
+ const target = storage.getMemory(db, fullId);
1405
+ if (target?.status === "absorbed") {
1406
+ res.status(409).json({
1407
+ error: `Memory ${fullId.slice(0, 8)} is absorbed (folded into a corrected memory) — roll back the absorbing rewrite first (hicortex history --rollback)`,
1408
+ });
1409
+ return;
1410
+ }
1340
1411
  const fields = {};
1341
1412
  if (content !== undefined)
1342
1413
  fields.content = content;
package/dist/mcp-stdio.js CHANGED
@@ -261,7 +261,12 @@ async function runMcpStdio(options = {}) {
261
261
  }
262
262
  // Downstream: a low-level Server over stdio advertising exactly what the
263
263
  // daemon offers (tools). Ping is auto-answered by the Protocol base.
264
- const server = new index_js_1.Server({ name: "hicortex", version: VERSION }, { capabilities: { tools: {} } });
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() });
265
270
  // The proxy core — the SDK's documented proxy pattern. Forward the two
266
271
  // tools requests and pass extra.signal through so a downstream
267
272
  // notifications/cancelled aborts the upstream call (which emits the
@@ -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;