@gamaze/hicortex 0.11.1 → 0.12.1

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/db.js CHANGED
@@ -11,13 +11,14 @@ exports.resolveDbPath = resolveDbPath;
11
11
  exports.initDb = initDb;
12
12
  exports.getSchemaVersion = getSchemaVersion;
13
13
  exports.getStats = getStats;
14
+ const paths_js_1 = require("./paths.js");
14
15
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
15
16
  const node_fs_1 = require("node:fs");
16
17
  const node_path_1 = require("node:path");
17
18
  const node_os_1 = require("node:os");
18
19
  const EMBEDDING_DIMENSIONS = 384;
19
20
  /** Canonical Hicortex home directory. */
20
- const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
21
+ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
21
22
  /** Legacy OC plugin DB path (pre-v0.3 installations). */
22
23
  const LEGACY_OC_DB = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "data", "hicortex.db");
23
24
  /**
package/dist/features.js CHANGED
@@ -21,11 +21,10 @@ exports.memoryCapReached = memoryCapReached;
21
21
  exports.lessonsLimit = lessonsLimit;
22
22
  exports.remoteIngestAllowed = remoteIngestAllowed;
23
23
  exports.getCurrentFeatures = getCurrentFeatures;
24
- const node_path_1 = require("node:path");
25
- const node_os_1 = require("node:os");
24
+ const paths_js_1 = require("./paths.js");
26
25
  const license_js_1 = require("./license.js");
27
26
  const state_js_1 = require("./state.js");
28
- const DEFAULT_STATE_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
27
+ const DEFAULT_STATE_DIR = (0, paths_js_1.hicortexHome)();
29
28
  // A single canonical "full" feature set — no tiers.
30
29
  const FULL_FEATURES = {
31
30
  reflection: true,
package/dist/index.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Responsibilities (recall-only adapter, like the Hermes plugin):
12
12
  * - before_agent_start → GET /lessons (fail-soft, 3s timeout) → inject context
13
- * - Tools → HTTP proxies to /search, /context, /ingest, /lessons
13
+ * - Tools → HTTP proxies to /search, /recent, /ingest, /lessons
14
14
  *
15
15
  * CAPTURE IS NOT THIS PLUGIN'S JOB. OpenClaw persists sessions at
16
16
  * ~/.openclaw/agents/<agentId>/sessions/*.jsonl in the Pi v3 format; the
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Responsibilities (recall-only adapter, like the Hermes plugin):
13
13
  * - before_agent_start → GET /lessons (fail-soft, 3s timeout) → inject context
14
- * - Tools → HTTP proxies to /search, /context, /ingest, /lessons
14
+ * - Tools → HTTP proxies to /search, /recent, /ingest, /lessons
15
15
  *
16
16
  * CAPTURE IS NOT THIS PLUGIN'S JOB. OpenClaw persists sessions at
17
17
  * ~/.openclaw/agents/<agentId>/sessions/*.jsonl in the Pi v3 format; the
@@ -19,6 +19,7 @@
19
19
  * canonical nightly-from-logs, same as CC JSONL and Hermes state.db.
20
20
  */
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
+ const paths_js_1 = require("./paths.js");
22
23
  const features_js_1 = require("./features.js");
23
24
  const extensions_js_1 = require("./extensions.js");
24
25
  const state_js_1 = require("./state.js");
@@ -30,7 +31,7 @@ const node_os_1 = require("node:os");
30
31
  // ---------------------------------------------------------------------------
31
32
  const DEFAULT_SERVER_URL = "http://127.0.0.1:8787";
32
33
  const LESSONS_TIMEOUT_MS = 3000;
33
- const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
34
+ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
34
35
  // ---------------------------------------------------------------------------
35
36
  // Module state — initialized in registerService.start()
36
37
  // ---------------------------------------------------------------------------
@@ -50,13 +51,30 @@ async function serverGet(path, timeoutMs) {
50
51
  signal: AbortSignal.timeout(timeoutMs),
51
52
  });
52
53
  if (!resp.ok)
53
- return null;
54
- return await resp.json();
54
+ return { data: null, status: resp.status };
55
+ return { data: await resp.json(), status: resp.status };
55
56
  }
56
57
  catch {
57
- return null;
58
+ return { data: null, status: null };
58
59
  }
59
60
  }
61
+ /**
62
+ * Human-readable GET failure. Distinguishes a down server from an HTTP error —
63
+ * in particular a 404, so plugin/server version skew reads as a version
64
+ * problem, not a network one. The /context→/recent rename hint (0.12) is
65
+ * added only for /recent, where it is the overwhelmingly likely cause.
66
+ */
67
+ function describeGetFailure(status, endpoint) {
68
+ if (status === null)
69
+ return "server unreachable";
70
+ if (status === 404) {
71
+ const renameHint = endpoint.startsWith("/recent")
72
+ ? " (0.12 renamed /context to /recent)"
73
+ : "";
74
+ return `HTTP 404 — ${endpoint} not found on the server; likely plugin/server version skew${renameHint}. Upgrade the server first.`;
75
+ }
76
+ return `server returned HTTP ${status}`;
77
+ }
60
78
  async function serverPost(path, body, timeoutMs) {
61
79
  try {
62
80
  const resp = await fetch(`${serverUrl}${path}`, {
@@ -145,7 +163,7 @@ exports.default = {
145
163
  api.on("before_agent_start", async (_event, ctx) => {
146
164
  try {
147
165
  // One fetch — build context and check cap from the same response.
148
- const data = await serverGet("/lessons", LESSONS_TIMEOUT_MS);
166
+ const { data } = await serverGet("/lessons", LESSONS_TIMEOUT_MS);
149
167
  if (!data || !data.lessons || data.lessons.length === 0)
150
168
  return {};
151
169
  const maxLessons = (0, features_js_1.lessonsLimit)();
@@ -199,9 +217,9 @@ exports.default = {
199
217
  params.set("limit", String(args.limit));
200
218
  if (args.project)
201
219
  params.set("project", args.project);
202
- const data = await serverGet(`/search?${params}`, 10000);
220
+ const { data, status } = await serverGet(`/search?${params}`, 10000);
203
221
  if (!data)
204
- return { error: "Search failed: server unreachable" };
222
+ return { error: `Search failed: ${describeGetFailure(status, "/search")}` };
205
223
  return formatToolResults(data.results ?? []);
206
224
  }
207
225
  catch (err) {
@@ -210,8 +228,8 @@ exports.default = {
210
228
  },
211
229
  }), { name: "hicortex_search" });
212
230
  api.registerTool((_ctx) => ({
213
- name: "hicortex_context",
214
- description: "Get recent context memories, optionally filtered by project. Useful to recall what happened recently.",
231
+ name: "hicortex_recent",
232
+ 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.",
215
233
  parameters: {
216
234
  type: "object",
217
235
  properties: {
@@ -227,16 +245,16 @@ exports.default = {
227
245
  if (args?.limit)
228
246
  params.set("limit", String(args.limit));
229
247
  const qs = params.toString();
230
- const data = await serverGet(`/context${qs ? `?${qs}` : ""}`, 10000);
248
+ const { data, status } = await serverGet(`/recent${qs ? `?${qs}` : ""}`, 10000);
231
249
  if (!data)
232
- return { error: "Context search failed: server unreachable" };
250
+ return { error: `Recent recall failed: ${describeGetFailure(status, "/recent")}` };
233
251
  return formatToolResults(data.results ?? []);
234
252
  }
235
253
  catch (err) {
236
- return { error: `Context search failed: ${err instanceof Error ? err.message : String(err)}` };
254
+ return { error: `Recent recall failed: ${err instanceof Error ? err.message : String(err)}` };
237
255
  }
238
256
  },
239
- }), { name: "hicortex_context" });
257
+ }), { name: "hicortex_recent" });
240
258
  api.registerTool((_ctx) => ({
241
259
  name: "hicortex_ingest",
242
260
  description: "Store a new memory in long-term storage. Use for important facts, decisions, or lessons.",
@@ -284,9 +302,9 @@ exports.default = {
284
302
  },
285
303
  async execute(_callId, args, _ctx) {
286
304
  try {
287
- const data = await serverGet("/lessons", LESSONS_TIMEOUT_MS);
305
+ const { data, status } = await serverGet("/lessons", LESSONS_TIMEOUT_MS);
288
306
  if (!data)
289
- return { error: "Lessons fetch failed: server unreachable" };
307
+ return { error: `Lessons fetch failed: ${describeGetFailure(status, "/lessons")}` };
290
308
  const lessons = data.lessons ?? [];
291
309
  if (lessons.length === 0) {
292
310
  return { content: [{ type: "text", text: "No lessons found." }] };
@@ -308,9 +326,9 @@ exports.default = {
308
326
  },
309
327
  async execute(_callId, _args, _ctx) {
310
328
  try {
311
- const data = await serverGet("/index", 10000);
329
+ const { data, status } = await serverGet("/index", 10000);
312
330
  if (!data)
313
- return { error: "Index fetch failed: server unreachable" };
331
+ return { error: `Index fetch failed: ${describeGetFailure(status, "/index")}` };
314
332
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
315
333
  }
316
334
  catch (err) {
@@ -350,9 +368,9 @@ exports.default = {
350
368
  params.set("domain", args.domain);
351
369
  if (args.relationship)
352
370
  params.set("relationship", args.relationship);
353
- const data = await serverGet(`/graph?${params}`, 10000);
371
+ const { data, status } = await serverGet(`/graph?${params}`, 10000);
354
372
  if (!data)
355
- return { error: "Graph query failed: server unreachable" };
373
+ return { error: `Graph query failed: ${describeGetFailure(status, "/graph")}` };
356
374
  return { content: [{ type: "text", text: JSON.stringify(data) }] };
357
375
  }
358
376
  catch (err) {
@@ -432,7 +450,7 @@ exports.default = {
432
450
  // ---------------------------------------------------------------------------
433
451
  const HICORTEX_TOOLS = [
434
452
  "hicortex_search",
435
- "hicortex_context",
453
+ "hicortex_recent",
436
454
  "hicortex_ingest",
437
455
  "hicortex_lessons",
438
456
  "hicortex_index",
package/dist/init.d.ts CHANGED
@@ -61,6 +61,15 @@ export declare const GENERIC_DEFAULT_DOMAINS: DomainDef[];
61
61
  export declare function scaffoldDefaultDomains(configPath: string): {
62
62
  scaffolded: boolean;
63
63
  };
64
+ /**
65
+ * True if a resolved binary path lives in npm's ephemeral npx cache
66
+ * (`~/.npm/_npx/<hash>/node_modules/.bin/…`). When `hicortex init` is itself
67
+ * run via `npx -y @gamaze/hicortex init`, npx prepends that cache dir to PATH,
68
+ * so `which hicortex` resolves there. npm garbage-collects `_npx`, so any
69
+ * SessionStart hook or nightly timer wired to such a path breaks silently
70
+ * later — the "looks configured but isn't" trap (#176). Never persist it.
71
+ */
72
+ export declare function isEphemeralNpxPath(binPath: string): boolean;
64
73
  /**
65
74
  * Install (or verify) the CC SessionStart hook that runs `hicortex lessons-context`.
66
75
  * The hook fetches lessons from the configured server at session start and injects
package/dist/init.js CHANGED
@@ -22,9 +22,11 @@ exports.parseEnvFile = parseEnvFile;
22
22
  exports.generateAuthToken = generateAuthToken;
23
23
  exports.persistAuthToken = persistAuthToken;
24
24
  exports.scaffoldDefaultDomains = scaffoldDefaultDomains;
25
+ exports.isEphemeralNpxPath = isEphemeralNpxPath;
25
26
  exports.installSessionStartHook = installSessionStartHook;
26
27
  exports.runInit = runInit;
27
28
  exports.resolveNightlyHour = resolveNightlyHour;
29
+ const paths_js_1 = require("./paths.js");
28
30
  const node_fs_1 = require("node:fs");
29
31
  const node_path_1 = require("node:path");
30
32
  const node_os_1 = require("node:os");
@@ -32,7 +34,7 @@ const node_child_process_1 = require("node:child_process");
32
34
  const node_readline_1 = require("node:readline");
33
35
  const node_crypto_1 = require("node:crypto");
34
36
  const claude_md_js_1 = require("./claude-md.js");
35
- const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
37
+ const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
36
38
  const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
37
39
  const CC_COMMANDS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "commands");
38
40
  const OC_CONFIG = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
@@ -188,7 +190,7 @@ function installCcCommands() {
188
190
  name: learn
189
191
  description: Save an explicit learning/insight to Hicortex long-term memory. Immediate storage, no nightly wait. Use when you discover something worth remembering across sessions.
190
192
  argument-hint: <learning to save>
191
- allowed-tools: mcp__hicortex__hicortex_ingest, mcp__hicortex__hicortex_search, mcp__hicortex__hicortex_context, mcp__hicortex__hicortex_lessons
193
+ allowed-tools: mcp__hicortex__hicortex_ingest, mcp__hicortex__hicortex_search, mcp__hicortex__hicortex_recent, mcp__hicortex__hicortex_lessons
192
194
  ---
193
195
 
194
196
  # Save Learning to Hicortex
@@ -234,7 +236,7 @@ Becomes a call to hicortex_ingest with:
234
236
  name: hicortex-activate
235
237
  description: Register a Hicortex commercial license key. Personal and noncommercial use is free; commercial use requires a per-seat license from hicortex.gamaze.com.
236
238
  argument-hint: <license-key>
237
- allowed-tools: Bash(mkdir:*), Bash(echo:*), Bash(launchctl:*), Bash(systemctl:*), Bash(curl:*), mcp__hicortex__hicortex_ingest, mcp__hicortex__hicortex_search, mcp__hicortex__hicortex_context, mcp__hicortex__hicortex_lessons
239
+ allowed-tools: Bash(mkdir:*), Bash(echo:*), Bash(launchctl:*), Bash(systemctl:*), Bash(curl:*), mcp__hicortex__hicortex_ingest, mcp__hicortex__hicortex_search, mcp__hicortex__hicortex_recent, mcp__hicortex__hicortex_lessons
238
240
  ---
239
241
 
240
242
  # Register Hicortex Commercial License
@@ -640,7 +642,7 @@ async function persistLlmConfig() {
640
642
  await options[selectedIdx].save();
641
643
  const selectedLabel = options[selectedIdx].label;
642
644
  if (selectedLabel.startsWith("Skip")) {
643
- console.log(" No LLM configured — server will run recall-only (search/lessons/context work).\n" +
645
+ console.log(" No LLM configured — server will run recall-only (search/lessons/recent work).\n" +
644
646
  " To enable capture and consolidation later, run: npx @gamaze/hicortex init");
645
647
  }
646
648
  else {
@@ -789,16 +791,31 @@ function findNpxPath() {
789
791
  return "/usr/local/bin/npx";
790
792
  }
791
793
  }
794
+ /**
795
+ * True if a resolved binary path lives in npm's ephemeral npx cache
796
+ * (`~/.npm/_npx/<hash>/node_modules/.bin/…`). When `hicortex init` is itself
797
+ * run via `npx -y @gamaze/hicortex init`, npx prepends that cache dir to PATH,
798
+ * so `which hicortex` resolves there. npm garbage-collects `_npx`, so any
799
+ * SessionStart hook or nightly timer wired to such a path breaks silently
800
+ * later — the "looks configured but isn't" trap (#176). Never persist it.
801
+ */
802
+ function isEphemeralNpxPath(binPath) {
803
+ return binPath.includes("/_npx/");
804
+ }
792
805
  /**
793
806
  * Resolve the absolute path of the hicortex binary.
794
807
  * For global npm installs (e.g. /usr/bin/hicortex) this is the binary itself.
795
808
  * For dev/npx installs, falls back to `npx <packageSpec> <command>` form.
796
809
  * Returns an array: [binaryPath] for global, or [npxPath, "-y", packageSpec] for npx.
810
+ *
811
+ * A `which hicortex` hit inside the npx cache (#176) is REJECTED — it is
812
+ * ephemeral, so we emit the durable `npx -y <spec>` form instead. This is the
813
+ * standard client path (`npx … init`), where the fix matters most.
797
814
  */
798
815
  function resolveBinaryArgs() {
799
816
  try {
800
817
  const bin = (0, node_child_process_1.execSync)("which hicortex", { encoding: "utf-8" }).trim();
801
- if (bin)
818
+ if (bin && !isEphemeralNpxPath(bin))
802
819
  return [bin];
803
820
  }
804
821
  catch { /* not in PATH as a global binary */ }
@@ -1282,10 +1299,12 @@ function installNightlyCron(hour) {
1282
1299
  const hh = String(hour).padStart(2, "0");
1283
1300
  // PATH must start with the binary's own directory (see installLaunchd for rationale).
1284
1301
  const binDir = (0, node_path_1.dirname)(binaryArgs[0]);
1302
+ // One canonical nightly log path across platforms — status output, docs,
1303
+ // and support instructions all reference this single location.
1304
+ const logPath = (0, node_path_1.join)(HICORTEX_HOME, "nightly.log");
1285
1305
  if (os === "darwin") {
1286
1306
  const plistDir = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents");
1287
1307
  const plistPath = (0, node_path_1.join)(plistDir, "com.gamaze.hicortex-nightly.plist");
1288
- const logPath = (0, node_path_1.join)(HICORTEX_HOME, "nightly.log");
1289
1308
  // Never overwrite an existing schedule — users tune these (multi-slot
1290
1309
  // capture windows, quiet hours). Fresh installs only.
1291
1310
  if ((0, node_fs_1.existsSync)(plistPath)) {
@@ -1341,18 +1360,18 @@ ${programArgs}
1341
1360
  const configDir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "systemd", "user");
1342
1361
  const servicePath = (0, node_path_1.join)(configDir, "hicortex-nightly.service");
1343
1362
  const timerPath = (0, node_path_1.join)(configDir, "hicortex-nightly.timer");
1344
- // Never overwrite an existing schedule — users tune these. Fresh installs only.
1345
- if ((0, node_fs_1.existsSync)(timerPath)) {
1346
- console.log(` ✓ Nightly timer already installed — leaving existing schedule as-is`);
1347
- return;
1348
- }
1349
1363
  const execStart = [...binaryArgs, "nightly"].join(" ");
1364
+ // File logging, not journal: oneshot runs on machines with a volatile
1365
+ // journal (e.g. Raspberry Pi defaults) otherwise fail without a trace.
1366
+ // Same log path as the macOS plist. append: needs systemd ≥ 240 (2018).
1350
1367
  const service = `[Unit]
1351
1368
  Description=Hicortex Nightly (distill + POST)
1352
1369
 
1353
1370
  [Service]
1354
1371
  Type=oneshot
1355
1372
  ExecStart=${execStart}
1373
+ StandardOutput=append:${logPath}
1374
+ StandardError=append:${logPath}
1356
1375
  Environment=PATH=${binDir}:/usr/local/bin:/usr/bin:/bin
1357
1376
  Environment=HOME=${(0, node_os_1.homedir)()}
1358
1377
  WorkingDirectory=${(0, node_os_1.homedir)()}`;
@@ -1366,7 +1385,18 @@ Persistent=true
1366
1385
  [Install]
1367
1386
  WantedBy=timers.target`;
1368
1387
  (0, node_fs_1.mkdirSync)(configDir, { recursive: true });
1388
+ // The .service file is ours — always refresh it so fixes (like file
1389
+ // logging) reach existing installs. The .timer holds the user-tuned
1390
+ // schedule and is never overwritten.
1369
1391
  (0, node_fs_1.writeFileSync)(servicePath, service);
1392
+ if ((0, node_fs_1.existsSync)(timerPath)) {
1393
+ try {
1394
+ (0, node_child_process_1.execSync)("systemctl --user daemon-reload", { stdio: "pipe" });
1395
+ }
1396
+ catch { /* fine */ }
1397
+ console.log(` ✓ Nightly service refreshed — existing timer schedule kept as-is`);
1398
+ return;
1399
+ }
1370
1400
  (0, node_fs_1.writeFileSync)(timerPath, timer);
1371
1401
  try {
1372
1402
  (0, node_child_process_1.execSync)("systemctl --user daemon-reload", { stdio: "pipe" });
@@ -1,17 +1,30 @@
1
1
  /**
2
- * lessons-context — query-time lessons fetch for the CC SessionStart hook.
2
+ * lessons-context — query-time context injection for the CC SessionStart hook.
3
3
  *
4
4
  * Replaces file-based injection (injectLessons / injectLessonsFromServer).
5
- * Reads ~/.hicortex/config.json to find the server URL, GETs /lessons,
6
- * and prints a compact Markdown block to stdout so CC picks it up as
7
- * session context.
5
+ * Reads ~/.hicortex/config.json to find the server URL, then fetches TWO
6
+ * endpoints concurrently and prints a compact Markdown block to stdout so CC
7
+ * picks it up as session context:
8
+ *
9
+ * GET /context → the standing context layer (user info + rules; 0.12).
10
+ * Injected as a `## Context` block ONLY when this harness
11
+ * ("cc") is in the server-resolved `clients` list (self-gate).
12
+ * GET /lessons → episodic memory lessons + memory index, rendered as the
13
+ * existing `## Hicortex Memory` block.
14
+ *
15
+ * The two fetches run in Promise.all, each with its OWN 3 s timeout and
16
+ * INDEPENDENT fail-soft: a /context failure must never cost the lessons block,
17
+ * and vice versa. Sequential fetches would double worst-case SessionStart
18
+ * latency (~6 s) — see spec §7.
8
19
  *
9
20
  * Fail-soft by design: ANY failure (missing config, network error, non-2xx,
10
21
  * parse error) results in silent exit-0. A broken hook must never block a
11
- * CC session.
22
+ * CC session, and a broken /context fetch must never blank the whole output.
12
23
  */
13
24
  /**
14
- * Fetch lessons from the configured server and return a formatted Markdown
15
- * block, or null on any failure (caller should print nothing and exit 0).
25
+ * Fetch context + lessons concurrently and return the combined Markdown block,
26
+ * or null when neither yields anything (nothing to inject; caller prints
27
+ * nothing and exits 0). The `## Context` block is prepended before the existing
28
+ * `## Hicortex Memory` block.
16
29
  */
17
30
  export declare function fetchLessonsContext(): Promise<string | null>;
@@ -1,59 +1,74 @@
1
1
  "use strict";
2
2
  /**
3
- * lessons-context — query-time lessons fetch for the CC SessionStart hook.
3
+ * lessons-context — query-time context injection for the CC SessionStart hook.
4
4
  *
5
5
  * Replaces file-based injection (injectLessons / injectLessonsFromServer).
6
- * Reads ~/.hicortex/config.json to find the server URL, GETs /lessons,
7
- * and prints a compact Markdown block to stdout so CC picks it up as
8
- * session context.
6
+ * Reads ~/.hicortex/config.json to find the server URL, then fetches TWO
7
+ * endpoints concurrently and prints a compact Markdown block to stdout so CC
8
+ * picks it up as session context:
9
+ *
10
+ * GET /context → the standing context layer (user info + rules; 0.12).
11
+ * Injected as a `## Context` block ONLY when this harness
12
+ * ("cc") is in the server-resolved `clients` list (self-gate).
13
+ * GET /lessons → episodic memory lessons + memory index, rendered as the
14
+ * existing `## Hicortex Memory` block.
15
+ *
16
+ * The two fetches run in Promise.all, each with its OWN 3 s timeout and
17
+ * INDEPENDENT fail-soft: a /context failure must never cost the lessons block,
18
+ * and vice versa. Sequential fetches would double worst-case SessionStart
19
+ * latency (~6 s) — see spec §7.
9
20
  *
10
21
  * Fail-soft by design: ANY failure (missing config, network error, non-2xx,
11
22
  * parse error) results in silent exit-0. A broken hook must never block a
12
- * CC session.
23
+ * CC session, and a broken /context fetch must never blank the whole output.
13
24
  */
14
25
  Object.defineProperty(exports, "__esModule", { value: true });
15
26
  exports.fetchLessonsContext = fetchLessonsContext;
16
27
  const node_fs_1 = require("node:fs");
17
28
  const node_path_1 = require("node:path");
18
- const node_os_1 = require("node:os");
19
29
  const features_js_1 = require("./features.js");
20
30
  const extensions_js_1 = require("./extensions.js");
21
31
  const state_js_1 = require("./state.js");
22
- const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
32
+ const paths_js_1 = require("./paths.js");
23
33
  const DEFAULT_PORT = 8787;
34
+ /** Harness name this hook injects for — used to self-gate on GET /context `clients`. */
35
+ const THIS_HARNESS = "cc";
24
36
  /**
25
- * Fetch lessons from the configured server and return a formatted Markdown
26
- * block, or null on any failure (caller should print nothing and exit 0).
37
+ * Read ~/.hicortex/config.json and resolve the server URL + auth token, or
38
+ * null when there is no usable config (server not set up yet fail soft).
27
39
  */
28
- async function fetchLessonsContext() {
29
- let config = {};
40
+ function resolveConfig() {
41
+ const home = (0, paths_js_1.hicortexHome)();
42
+ let config;
30
43
  try {
31
- config = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(HICORTEX_HOME, "config.json"), "utf-8"));
44
+ config = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(home, "config.json"), "utf-8"));
32
45
  }
33
46
  catch {
34
- // No config file — server not set up yet. Fail soft.
35
47
  return null;
36
48
  }
37
- // Determine server URL: client mode uses serverUrl; server mode uses localhost.
49
+ // Client mode uses serverUrl; server mode uses localhost.
38
50
  const serverUrl = config.mode === "client" && typeof config.serverUrl === "string"
39
51
  ? config.serverUrl.replace(/\/+$/, "")
40
52
  : `http://127.0.0.1:${config.port ?? DEFAULT_PORT}`;
41
- const authToken = config.authToken;
42
- let data;
43
- try {
44
- const resp = await fetch(`${serverUrl}/lessons`, {
45
- headers: authToken ? { "Authorization": `Bearer ${authToken}` } : {},
46
- signal: AbortSignal.timeout(3000),
47
- });
48
- if (!resp.ok)
49
- return null;
50
- data = await resp.json();
51
- }
52
- catch {
53
+ return { serverUrl, authToken: config.authToken, home };
54
+ }
55
+ function authHeaders(authToken) {
56
+ return authToken ? { "Authorization": `Bearer ${authToken}` } : {};
57
+ }
58
+ /**
59
+ * Fetch /lessons and build the `## Hicortex Memory` block, or null on any
60
+ * failure (missing/non-2xx/parse). Preserves the pre-0.12 behavior exactly.
61
+ */
62
+ async function fetchLessonsBlock(cfg) {
63
+ const resp = await fetch(`${cfg.serverUrl}/lessons`, {
64
+ headers: authHeaders(cfg.authToken),
65
+ signal: AbortSignal.timeout(3000),
66
+ });
67
+ if (!resp.ok)
53
68
  return null;
54
- }
69
+ const data = await resp.json();
55
70
  const maxLessons = (0, features_js_1.lessonsLimit)();
56
- const moduleIndex = data.moduleIndex ?? (0, state_js_1.loadState)(HICORTEX_HOME).moduleIndex;
71
+ const moduleIndex = data.moduleIndex ?? (0, state_js_1.loadState)(cfg.home).moduleIndex;
57
72
  // The SessionStart hook runs in the session's working directory, whose last
58
73
  // path component matches the capture-side CC project name convention
59
74
  // (transcript-reader's decodeProjectDirName also takes the last component).
@@ -71,12 +86,11 @@ async function fetchLessonsContext() {
71
86
  const parts = ["## Hicortex Memory", ""];
72
87
  parts.push("You have access to shared long-term memory across all agents and sessions.");
73
88
  parts.push("BEFORE making decisions, search memory: `hicortex_search` for prior decisions on the same topic.");
74
- parts.push("Use `hicortex_context` at session start for recent project state.");
89
+ parts.push("Use `hicortex_recent` at session start for recent project state.");
75
90
  if (lessonLines.length > 0) {
76
91
  parts.push("", "### Lessons (updated nightly)");
77
92
  parts.push(...lessonLines);
78
93
  }
79
- // Memory index
80
94
  const { index } = data;
81
95
  if (moduleIndex && moduleIndex.domains.length > 0) {
82
96
  parts.push("", "### Memory Index");
@@ -95,3 +109,79 @@ async function fetchLessonsContext() {
95
109
  }
96
110
  return parts.join("\n");
97
111
  }
112
+ /**
113
+ * Title-case a section name for its heading: split on `-`/`_`, capitalize each
114
+ * word ("user" → "User", "my_notes" → "My Notes").
115
+ */
116
+ function titleCaseSection(name) {
117
+ return name
118
+ .split(/[-_]+/)
119
+ .filter(Boolean)
120
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
121
+ .join(" ");
122
+ }
123
+ /**
124
+ * Stable section ordering: `user` first, then `rules` (the seeded primary
125
+ * sections, spec §8), then every other section alphabetically. Server-side
126
+ * enumeration order (readdirSync) is FS-dependent, so we sort here for a
127
+ * deterministic injection block.
128
+ */
129
+ function orderSectionNames(names) {
130
+ const primaries = ["user", "rules"].filter((p) => names.includes(p));
131
+ const rest = names.filter((n) => n !== "user" && n !== "rules").sort();
132
+ return [...primaries, ...rest];
133
+ }
134
+ /**
135
+ * Fetch /context and build the `## Context` block, or null when nothing should
136
+ * be injected: non-2xx, this harness not in `clients`, no sections, or all
137
+ * sections empty. Throws propagate to the caller's fail-soft catch.
138
+ */
139
+ async function fetchContextBlock(cfg) {
140
+ const resp = await fetch(`${cfg.serverUrl}/context`, {
141
+ headers: authHeaders(cfg.authToken),
142
+ signal: AbortSignal.timeout(3000),
143
+ });
144
+ if (!resp.ok)
145
+ return null;
146
+ const data = await resp.json();
147
+ // Self-gate: only inject when this harness is in the server-resolved list.
148
+ const clients = Array.isArray(data.clients) ? data.clients : [];
149
+ if (!clients.includes(THIS_HARNESS))
150
+ return null;
151
+ const sections = data.sections;
152
+ if (!sections || typeof sections !== "object" || Array.isArray(sections))
153
+ return null;
154
+ const names = orderSectionNames(Object.keys(sections));
155
+ const bodyParts = [];
156
+ for (const name of names) {
157
+ const body = sections[name];
158
+ if (typeof body !== "string" || body.trim() === "")
159
+ continue;
160
+ bodyParts.push(`### ${titleCaseSection(name)}`, "", body.trim());
161
+ }
162
+ if (bodyParts.length === 0)
163
+ return null;
164
+ return ["## Context", "", ...bodyParts].join("\n");
165
+ }
166
+ /**
167
+ * Fetch context + lessons concurrently and return the combined Markdown block,
168
+ * or null when neither yields anything (nothing to inject; caller prints
169
+ * nothing and exits 0). The `## Context` block is prepended before the existing
170
+ * `## Hicortex Memory` block.
171
+ */
172
+ async function fetchLessonsContext() {
173
+ const cfg = resolveConfig();
174
+ if (!cfg)
175
+ return null;
176
+ // Independent fail-soft: each branch degrades to null without affecting the
177
+ // other. Promise.all runs them concurrently — each carries its own 3 s
178
+ // timeout, so worst-case latency stays ~3 s, not ~6 s (spec §7).
179
+ const [contextBlock, lessonsBlock] = await Promise.all([
180
+ fetchContextBlock(cfg).catch(() => null),
181
+ fetchLessonsBlock(cfg).catch(() => null),
182
+ ]);
183
+ const blocks = [contextBlock, lessonsBlock].filter((b) => b !== null && b !== "");
184
+ if (blocks.length === 0)
185
+ return null;
186
+ return blocks.join("\n\n");
187
+ }