@gamaze/hicortex 0.7.1 → 0.10.0

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 (49) hide show
  1. package/README.md +57 -39
  2. package/dist/claude-md.d.ts +9 -21
  3. package/dist/claude-md.js +9 -241
  4. package/dist/cli.d.ts +3 -2
  5. package/dist/cli.js +29 -11
  6. package/dist/consolidate.js +0 -7
  7. package/dist/db.js +24 -0
  8. package/dist/embedder.d.ts +11 -0
  9. package/dist/embedder.js +27 -0
  10. package/dist/extensions.d.ts +41 -88
  11. package/dist/extensions.js +36 -61
  12. package/dist/features.d.ts +21 -25
  13. package/dist/features.js +47 -83
  14. package/dist/hermes-transcript-reader.d.ts +27 -0
  15. package/dist/hermes-transcript-reader.js +134 -0
  16. package/dist/index.d.ts +16 -4
  17. package/dist/index.js +252 -344
  18. package/dist/init.d.ts +41 -1
  19. package/dist/init.js +545 -190
  20. package/dist/lesson-selection.d.ts +62 -0
  21. package/dist/lesson-selection.js +159 -0
  22. package/dist/lessons-context.d.ts +17 -0
  23. package/dist/lessons-context.js +96 -0
  24. package/dist/llm.d.ts +42 -29
  25. package/dist/llm.js +89 -270
  26. package/dist/mcp-server.d.ts +0 -1
  27. package/dist/mcp-server.js +404 -86
  28. package/dist/nightly.d.ts +9 -6
  29. package/dist/nightly.js +197 -357
  30. package/dist/oc-transcript-reader.d.ts +20 -0
  31. package/dist/oc-transcript-reader.js +61 -0
  32. package/dist/pi-transcript-reader.d.ts +1 -0
  33. package/dist/status.js +22 -2
  34. package/dist/storage.d.ts +7 -1
  35. package/dist/storage.js +28 -7
  36. package/dist/transcript-reader.d.ts +19 -0
  37. package/dist/transcript-reader.js +17 -3
  38. package/dist/types.d.ts +10 -0
  39. package/dist/uninstall.js +31 -1
  40. package/hermes-plugin/hicortex/README.md +77 -0
  41. package/hermes-plugin/hicortex/__init__.py +17 -0
  42. package/hermes-plugin/hicortex/client.py +162 -0
  43. package/hermes-plugin/hicortex/config.py +105 -0
  44. package/hermes-plugin/hicortex/plugin.yaml +12 -0
  45. package/hermes-plugin/hicortex/provider.py +432 -0
  46. package/openclaw.plugin.json +17 -44
  47. package/package.json +7 -5
  48. package/dist/pro-loader.d.ts +0 -33
  49. package/dist/pro-loader.js +0 -187
package/dist/init.js CHANGED
@@ -12,20 +12,29 @@
12
12
  * Actions:
13
13
  * - Install persistent daemon (launchd/systemd)
14
14
  * - Register MCP server in CC settings
15
- * - Inject CLAUDE.md learnings block
15
+ * - Install CC SessionStart hook for query-time lessons
16
+ * - Strip old static CLAUDE.md learnings block if present
16
17
  * - Install CC custom commands (/learn, /hicortex-activate)
17
18
  */
18
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.parseEnvFile = parseEnvFile;
21
+ exports.generateAuthToken = generateAuthToken;
22
+ exports.persistAuthToken = persistAuthToken;
23
+ exports.installSessionStartHook = installSessionStartHook;
19
24
  exports.runInit = runInit;
25
+ exports.resolveNightlyHour = resolveNightlyHour;
20
26
  const node_fs_1 = require("node:fs");
21
27
  const node_path_1 = require("node:path");
22
28
  const node_os_1 = require("node:os");
23
29
  const node_child_process_1 = require("node:child_process");
24
30
  const node_readline_1 = require("node:readline");
31
+ const node_crypto_1 = require("node:crypto");
32
+ const claude_md_js_1 = require("./claude-md.js");
25
33
  const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
26
34
  const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
27
35
  const CC_COMMANDS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "commands");
28
36
  const OC_CONFIG = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
37
+ const HERMES_HOME = process.env.HERMES_HOME || (0, node_path_1.join)((0, node_os_1.homedir)(), ".hermes");
29
38
  const DEFAULT_PORT = 8787;
30
39
  async function detect() {
31
40
  const result = {
@@ -33,6 +42,7 @@ async function detect() {
33
42
  remoteServer: false,
34
43
  ocPlugin: false,
35
44
  ccMcpRegistered: false,
45
+ hermesFound: false,
36
46
  existingDb: false,
37
47
  };
38
48
  // Check local server
@@ -64,6 +74,8 @@ async function detect() {
64
74
  }
65
75
  catch { /* not reachable */ }
66
76
  }
77
+ // Check Hermes
78
+ result.hermesFound = (0, node_fs_1.existsSync)(HERMES_HOME);
67
79
  // Check OC plugin
68
80
  try {
69
81
  const raw = (0, node_fs_1.readFileSync)(OC_CONFIG, "utf-8");
@@ -121,10 +133,15 @@ function registerCcMcp(serverUrl) {
121
133
  console.log(` ⚠ claude CLI registration failed (${msg}), writing ~/.claude.json directly`);
122
134
  const claudeJsonPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude.json");
123
135
  let config = {};
124
- try {
125
- config = JSON.parse((0, node_fs_1.readFileSync)(claudeJsonPath, "utf-8"));
136
+ if ((0, node_fs_1.existsSync)(claudeJsonPath)) {
137
+ try {
138
+ config = JSON.parse((0, node_fs_1.readFileSync)(claudeJsonPath, "utf-8"));
139
+ }
140
+ catch {
141
+ console.log(` ⚠ ${claudeJsonPath} exists but is not valid JSON — skipping MCP registration. Fix the file, then re-run init or run: claude mcp add hicortex --transport sse ${serverUrl}/sse`);
142
+ return;
143
+ }
126
144
  }
127
- catch { /* create new */ }
128
145
  if (!config.mcpServers)
129
146
  config.mcpServers = {};
130
147
  config.mcpServers.hicortex = {
@@ -139,10 +156,15 @@ function registerCcMcp(serverUrl) {
139
156
  }
140
157
  function allowHicortexTools() {
141
158
  let settings = {};
142
- try {
143
- settings = JSON.parse((0, node_fs_1.readFileSync)(CC_SETTINGS, "utf-8"));
159
+ if ((0, node_fs_1.existsSync)(CC_SETTINGS)) {
160
+ try {
161
+ settings = JSON.parse((0, node_fs_1.readFileSync)(CC_SETTINGS, "utf-8"));
162
+ }
163
+ catch {
164
+ console.log(` ⚠ ${CC_SETTINGS} exists but is not valid JSON — skipping tool permissions. Fix the file, then re-run init or add "mcp__hicortex__*" to permissions.allow manually.`);
165
+ return;
166
+ }
144
167
  }
145
- catch { /* create new */ }
146
168
  if (!settings.permissions)
147
169
  settings.permissions = {};
148
170
  const perms = settings.permissions;
@@ -205,15 +227,15 @@ Becomes a call to hicortex_ingest with:
205
227
  else {
206
228
  (0, node_fs_1.writeFileSync)(learnPath, learnContent);
207
229
  }
208
- // /hicortex-activate command
230
+ // /hicortex-activate command — registers a commercial license key for display in status
209
231
  const activateContent = `---
210
232
  name: hicortex-activate
211
- description: Activate a Hicortex license key for unlimited memory. Use after purchasing at hicortex.gamaze.com.
233
+ description: Register a Hicortex commercial license key. Personal and noncommercial use is free; commercial use requires a per-seat license from hicortex.gamaze.com.
212
234
  argument-hint: <license-key>
213
235
  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
214
236
  ---
215
237
 
216
- # Activate Hicortex License
238
+ # Register Hicortex Commercial License
217
239
 
218
240
  ## If key provided (e.g. /hicortex-activate hctx-abc123)
219
241
 
@@ -236,23 +258,122 @@ On Linux:
236
258
  systemctl --user restart hicortex
237
259
  \`\`\`
238
260
 
239
- 3. Verify the server is back:
261
+ 3. Verify the key is recognised:
240
262
  \`\`\`bash
241
- curl -s http://127.0.0.1:8787/health
263
+ hicortex status
242
264
  \`\`\`
243
265
 
244
- 4. Tell the user: "License activated! Hicortex now has unlimited memory."
266
+ 4. Tell the user: "Commercial license registered. The license tier will appear in \`hicortex status\`."
245
267
 
246
268
  ## If no key provided
247
269
 
248
- Tell them: "Get a license key at https://hicortex.gamaze.com/ after purchase, you'll receive your key by email. Then tell me the key and I'll activate it."
270
+ Tell them: "Hicortex is free for personal and noncommercial use. Commercial use requires a per-seat license see https://hicortex.gamaze.com/. After purchase you will receive a key; pass it here and I'll register it."
249
271
  `;
250
272
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(CC_COMMANDS_DIR, "hicortex-activate.md"), activateContent);
251
273
  console.log(` ✓ Installed /learn and /hicortex-activate commands in ${CC_COMMANDS_DIR}`);
252
274
  }
275
+ // ---------------------------------------------------------------------------
276
+ // Hermes setup
277
+ // ---------------------------------------------------------------------------
278
+ function setupHermes(serverUrl, authToken) {
279
+ const pluginSource = (0, node_path_1.join)(__dirname, "..", "hermes-plugin", "hicortex");
280
+ if (!(0, node_fs_1.existsSync)(pluginSource)) {
281
+ console.log(" ⚠ Hermes plugin not found in package — skipping Hermes setup");
282
+ return;
283
+ }
284
+ const pluginsDir = (0, node_path_1.join)(HERMES_HOME, "plugins", "hicortex");
285
+ (0, node_fs_1.mkdirSync)(pluginsDir, { recursive: true });
286
+ // Copy plugin files
287
+ const pluginFiles = (0, node_fs_1.readdirSync)(pluginSource);
288
+ for (const f of pluginFiles) {
289
+ const src = (0, node_path_1.join)(pluginSource, f);
290
+ if ((0, node_fs_1.statSync)(src).isFile()) {
291
+ (0, node_fs_1.copyFileSync)(src, (0, node_path_1.join)(pluginsDir, f));
292
+ }
293
+ }
294
+ console.log(` ✓ Copied Hermes plugin to ${pluginsDir}`);
295
+ // Write plugin config.json (server URL only). The auth token is a SECRET and
296
+ // is deliberately NOT written here — `hermes memory setup` routes it to
297
+ // $HERMES_HOME/.env, and localhost bypasses auth entirely. Capture threshold
298
+ // omitted → the plugin's own default applies.
299
+ const config = { hicortex_url: serverUrl };
300
+ const configPath = (0, node_path_1.join)(pluginsDir, "config.json");
301
+ (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2));
302
+ console.log(` ✓ Plugin config → ${serverUrl}`);
303
+ // For profile-based setups, symlink the shared plugin into each profile's
304
+ // plugin dir (non-destructive). Discovery scans $HERMES_HOME/plugins/, so
305
+ // this is belt-and-suspenders for profile-scoped installs.
306
+ const profilesDir = (0, node_path_1.join)(HERMES_HOME, "profiles");
307
+ if ((0, node_fs_1.existsSync)(profilesDir)) {
308
+ let profiles = [];
309
+ try {
310
+ profiles = (0, node_fs_1.readdirSync)(profilesDir).filter((d) => {
311
+ try {
312
+ return (0, node_fs_1.statSync)((0, node_path_1.join)(profilesDir, d)).isDirectory() && !d.startsWith("_") && !d.startsWith(".");
313
+ }
314
+ catch {
315
+ return false;
316
+ }
317
+ });
318
+ }
319
+ catch { /* no profiles dir readable */ }
320
+ for (const profile of profiles) {
321
+ const profPluginsDir = (0, node_path_1.join)(profilesDir, profile, "plugins");
322
+ const symlinkPath = (0, node_path_1.join)(profPluginsDir, "hicortex");
323
+ (0, node_fs_1.mkdirSync)(profPluginsDir, { recursive: true });
324
+ try {
325
+ (0, node_fs_1.rmSync)(symlinkPath, { recursive: true, force: true });
326
+ }
327
+ catch { /* not present */ }
328
+ try {
329
+ (0, node_fs_1.symlinkSync)(pluginsDir, symlinkPath);
330
+ }
331
+ catch (e) {
332
+ console.log(` ⚠ Symlink failed for ${profile} (${e instanceof Error ? e.message : e}) — copy manually if needed`);
333
+ }
334
+ }
335
+ }
336
+ // Activation is left to Hermes' own tooling. We NEVER edit config.yaml —
337
+ // Hermes' `memory setup` discovers this plugin automatically and writes the
338
+ // config with its own YAML-aware writer (routing the token to .env).
339
+ const isRemote = !(serverUrl.includes("127.0.0.1") || serverUrl.includes("localhost"));
340
+ console.log(" → Activate with: hermes memory setup (select 'hicortex')");
341
+ if ((0, node_fs_1.existsSync)(profilesDir)) {
342
+ console.log(" Run once per profile if you use Hermes profiles.");
343
+ }
344
+ if (isRemote) {
345
+ console.log(" Remote server: enter the auth token when prompted (stored in $HERMES_HOME/.env).");
346
+ }
347
+ }
348
+ /**
349
+ * Parse a KEY=VALUE env file (e.g. ~/.hermes/.env or ~/.claude/settings.json env block).
350
+ * Handles: comments (#), quoted values, empty lines.
351
+ * Exported for testability.
352
+ */
353
+ function parseEnvFile(content) {
354
+ const result = {};
355
+ for (const raw of content.split("\n")) {
356
+ const line = raw.trim();
357
+ if (!line || line.startsWith("#"))
358
+ continue;
359
+ const eqIdx = line.indexOf("=");
360
+ if (eqIdx === -1)
361
+ continue;
362
+ const key = line.slice(0, eqIdx).trim();
363
+ if (!key)
364
+ continue;
365
+ let value = line.slice(eqIdx + 1).trim();
366
+ // Strip matching surrounding quotes (single or double)
367
+ if ((value.startsWith('"') && value.endsWith('"')) ||
368
+ (value.startsWith("'") && value.endsWith("'"))) {
369
+ value = value.slice(1, -1);
370
+ }
371
+ result[key] = value;
372
+ }
373
+ return result;
374
+ }
253
375
  /**
254
376
  * Read LLM config from OC's openclaw.json + auth-profiles.json.
255
- * Used as fallback when no env vars are set (e.g. Claude Max subscription users).
256
377
  */
257
378
  function readOcLlmConfig() {
258
379
  try {
@@ -299,9 +420,30 @@ function readOcLlmConfig() {
299
420
  return null;
300
421
  }
301
422
  }
423
+ /** Merge candidates by (key, provider) — concatenate source labels. */
424
+ function mergeByKey(candidates) {
425
+ const seen = new Map();
426
+ for (const c of candidates) {
427
+ const dedupeKey = `${c.provider}::${c.key}`;
428
+ const existing = seen.get(dedupeKey);
429
+ if (existing) {
430
+ for (const s of c.sources) {
431
+ if (!existing.sources.includes(s))
432
+ existing.sources.push(s);
433
+ }
434
+ }
435
+ else {
436
+ seen.set(dedupeKey, { ...c, sources: [...c.sources] });
437
+ }
438
+ }
439
+ return [...seen.values()];
440
+ }
302
441
  /**
303
442
  * Detect or ask for LLM config and persist to ~/.hicortex/config.json.
304
443
  * The daemon can't inherit shell env vars, so we persist here.
444
+ * LLM choice is always user-controlled: candidates are detected and presented
445
+ * as a numbered list; the user picks one. Nothing is auto-applied.
446
+ * If the user cancels, the server runs recall-only (no LLM).
305
447
  */
306
448
  async function persistLlmConfig() {
307
449
  const configPath = (0, node_path_1.join)(HICORTEX_HOME, "config.json");
@@ -316,16 +458,18 @@ async function persistLlmConfig() {
316
458
  console.log(` ✓ LLM config already configured`);
317
459
  return;
318
460
  }
319
- // Auto-detect all available LLM options
461
+ // -------------------------------------------------------------------------
462
+ // Detect candidates from all harnesses on this machine
463
+ // -------------------------------------------------------------------------
464
+ const { findClaudeBinary } = await import("./llm.js");
320
465
  const options = [];
321
- // 1. Check Ollama (local models — best for privacy and quality)
466
+ // 1. Ollama local models (LOCAL)
322
467
  const ollamaModels = detectOllamaModels();
323
468
  if (ollamaModels.length > 0) {
324
- // Pick the largest model — only recommend if >= 7GB (~7b+ parameter models)
325
469
  const best = ollamaModels[0]; // already sorted by size desc
326
470
  const goodEnough = best.sizeGb >= 7;
327
471
  options.push({
328
- label: `Ollama ${best.name} (local${best.sizeGb ? `, ${best.sizeGb}GB` : ""}${goodEnough ? "" : ", small model"})`,
472
+ label: `Ollama ${best.name} (local${best.sizeGb ? `, ${best.sizeGb}GB` : ""}${goodEnough ? "" : ", small model"}) [LOCAL]`,
329
473
  recommended: goodEnough,
330
474
  save: () => {
331
475
  config.llmBackend = "ollama";
@@ -334,10 +478,9 @@ async function persistLlmConfig() {
334
478
  saveConfig(configPath, config);
335
479
  },
336
480
  });
337
- // Add other models if available
338
481
  for (const m of ollamaModels.slice(1, 3)) {
339
482
  options.push({
340
- label: `Ollama ${m.name} (local${m.sizeGb ? `, ${m.sizeGb}GB` : ""})`,
483
+ label: `Ollama ${m.name} (local${m.sizeGb ? `, ${m.sizeGb}GB` : ""}) [LOCAL]`,
341
484
  save: () => {
342
485
  config.llmBackend = "ollama";
343
486
  config.llmBaseUrl = "http://localhost:11434";
@@ -347,12 +490,11 @@ async function persistLlmConfig() {
347
490
  });
348
491
  }
349
492
  }
350
- // 2. Check Claude CLI
351
- const { findClaudeBinary } = await import("./llm.js");
493
+ // 2. Claude CLI binary (CLOUD — subscription)
352
494
  const claudePath = findClaudeBinary();
353
495
  if (claudePath) {
354
496
  options.push({
355
- label: "Claude CLI (subscription, Haiku model)",
497
+ label: `Claude CLI — ${claudePath} (subscription, cloud)`,
356
498
  recommended: ollamaModels.length === 0,
357
499
  save: () => {
358
500
  config.llmBackend = "claude-cli";
@@ -360,58 +502,94 @@ async function persistLlmConfig() {
360
502
  },
361
503
  });
362
504
  }
363
- // 3. Check env vars
364
- if (process.env.ANTHROPIC_API_KEY) {
365
- options.push({
366
- label: "Anthropic API (from ANTHROPIC_API_KEY)",
367
- save: () => {
368
- config.llmApiKey = process.env.ANTHROPIC_API_KEY;
369
- config.llmBaseUrl = process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com";
370
- config.llmProvider = "anthropic";
371
- saveConfig(configPath, config);
372
- },
373
- });
505
+ // 3. API key candidates: collect from all harness sources, then dedup.
506
+ const apiKeyCandidates = [];
507
+ // Helper: note a detected key from a given source
508
+ function noteKey(key, provider, baseUrl, sourceLabel, model) {
509
+ if (!key)
510
+ return;
511
+ apiKeyCandidates.push({ key, provider, baseUrl, sources: [sourceLabel], model });
374
512
  }
375
- if (process.env.OPENAI_API_KEY) {
376
- options.push({
377
- label: "OpenAI API (from OPENAI_API_KEY)",
378
- save: () => {
379
- config.llmApiKey = process.env.OPENAI_API_KEY;
380
- config.llmBaseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com";
381
- config.llmProvider = "openai";
382
- saveConfig(configPath, config);
383
- },
384
- });
513
+ // Process env vars (detection only — not silent defaults)
514
+ const envProviders = [
515
+ { envKey: "ANTHROPIC_API_KEY", provider: "anthropic", baseUrlEnv: "ANTHROPIC_BASE_URL", defaultBaseUrl: "https://api.anthropic.com" },
516
+ { envKey: "OPENAI_API_KEY", provider: "openai", baseUrlEnv: "OPENAI_BASE_URL", defaultBaseUrl: "https://api.openai.com" },
517
+ { envKey: "GOOGLE_API_KEY", provider: "google", defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta" },
518
+ ];
519
+ for (const ep of envProviders) {
520
+ const val = process.env[ep.envKey];
521
+ if (val) {
522
+ const baseUrl = (ep.baseUrlEnv ? process.env[ep.baseUrlEnv] : undefined) ?? ep.defaultBaseUrl;
523
+ noteKey(val, ep.provider, baseUrl, "from environment, cloud");
524
+ }
385
525
  }
386
- // 4. Check OC auth-profiles
526
+ // Hermes .env file (read-only parsing, no editing)
527
+ try {
528
+ const hermesEnvPath = (0, node_path_1.join)(HERMES_HOME, ".env");
529
+ const hermesEnvContent = (0, node_fs_1.readFileSync)(hermesEnvPath, "utf-8");
530
+ const hermesEnv = parseEnvFile(hermesEnvContent);
531
+ const openaiBase = hermesEnv["OPENAI_BASE_URL"];
532
+ for (const ep of envProviders) {
533
+ const val = hermesEnv[ep.envKey];
534
+ if (val) {
535
+ const baseUrl = (ep.baseUrlEnv && hermesEnv[ep.baseUrlEnv])
536
+ ? hermesEnv[ep.baseUrlEnv]
537
+ : (openaiBase && ep.provider === "openai" ? openaiBase : ep.defaultBaseUrl);
538
+ noteKey(val, ep.provider, baseUrl, "detected in Hermes .env, cloud");
539
+ }
540
+ }
541
+ }
542
+ catch { /* no Hermes .env or unreadable — skip */ }
543
+ // Claude Code settings.json env block (read-only parsing)
544
+ try {
545
+ const ccSettings = JSON.parse((0, node_fs_1.readFileSync)(CC_SETTINGS, "utf-8"));
546
+ const ccEnv = ccSettings?.env ?? {};
547
+ for (const ep of envProviders) {
548
+ const val = ccEnv[ep.envKey];
549
+ if (typeof val === "string" && val) {
550
+ const baseUrl = (ep.baseUrlEnv && typeof ccEnv[ep.baseUrlEnv] === "string")
551
+ ? ccEnv[ep.baseUrlEnv]
552
+ : ep.defaultBaseUrl;
553
+ noteKey(val, ep.provider, baseUrl, "detected in Claude Code settings, cloud");
554
+ }
555
+ }
556
+ }
557
+ catch { /* no CC settings or unreadable — skip */ }
558
+ // OpenClaw auth-profiles (existing helper)
387
559
  const ocLlm = readOcLlmConfig();
388
560
  if (ocLlm) {
561
+ noteKey(ocLlm.apiKey, ocLlm.provider, ocLlm.baseUrl, "detected in OpenClaw config, cloud", ocLlm.model);
562
+ }
563
+ // Deduplicate by (provider, key) and build options
564
+ for (const candidate of mergeByKey(apiKeyCandidates)) {
565
+ const sourceStr = candidate.sources.join(", ");
566
+ const modelStr = candidate.model ? ` — ${candidate.model}` : "";
389
567
  options.push({
390
- label: `OpenClaw (${ocLlm.provider}/${ocLlm.model ?? "default"})`,
568
+ label: `${candidate.provider}${modelStr} (${sourceStr})`,
391
569
  save: () => {
392
- config.llmApiKey = ocLlm.apiKey;
393
- config.llmBaseUrl = ocLlm.baseUrl;
394
- config.llmProvider = ocLlm.provider;
395
- if (ocLlm.model)
396
- config.llmModel = ocLlm.model;
570
+ config.llmApiKey = candidate.key;
571
+ config.llmBaseUrl = candidate.baseUrl;
572
+ config.llmProvider = candidate.provider;
573
+ if (candidate.model)
574
+ config.llmModel = candidate.model;
397
575
  saveConfig(configPath, config);
398
576
  },
399
577
  });
400
578
  }
401
- // 5. Always offer manual entry and cancel
579
+ // 4. Manual entry (always available)
402
580
  options.push({
403
- label: "Other provider (requires API key)",
581
+ label: "Enter provider manually (API key required)",
404
582
  save: async () => {
405
583
  console.log("\n Providers: Anthropic, OpenAI, Google, OpenRouter, or any OpenAI-compatible endpoint");
406
584
  const baseUrl = await ask(" Provider base URL: ");
407
585
  if (!baseUrl) {
408
- console.log(" Cancelled.");
409
- process.exit(0);
586
+ console.log(" Cancelled — server will run recall-only until you re-run init.");
587
+ return;
410
588
  }
411
589
  const apiKey = await ask(" API key: ");
412
590
  if (!apiKey) {
413
- console.log(" Cancelled.");
414
- process.exit(0);
591
+ console.log(" Cancelled — server will run recall-only until you re-run init.");
592
+ return;
415
593
  }
416
594
  const model = await ask(" Model name (optional): ");
417
595
  config.llmApiKey = apiKey;
@@ -421,30 +599,51 @@ async function persistLlmConfig() {
421
599
  saveConfig(configPath, config);
422
600
  },
423
601
  });
602
+ // 5. Skip / recall-only (always available)
424
603
  options.push({
425
- label: "Cancel installation",
604
+ label: "Skip — run recall-only for now (configure later with: npx @gamaze/hicortex init)",
426
605
  save: () => {
427
- console.log("\n Hicortex requires an LLM to function. Installation cancelled.");
428
- process.exit(0);
606
+ // Intentionally leave no LLM config server starts recall-only.
429
607
  },
430
608
  });
431
- // Find recommended index
609
+ // -------------------------------------------------------------------------
610
+ // Always prompt — never auto-apply even if exactly one candidate detected
611
+ // -------------------------------------------------------------------------
612
+ // Non-interactive stdin (piped/scripted init): a readline EOF resolves as ""
613
+ // and would silently select the recommended default — an auto-apply. LLM
614
+ // choice is user-controlled by design: skip instead.
615
+ if (!process.stdin.isTTY) {
616
+ console.log(" ⚠ Non-interactive stdin — skipping LLM selection (user-controlled by design).\n" +
617
+ " The server runs recall-only until you configure an LLM: re-run `hicortex init`\n" +
618
+ " interactively, or set llmBackend/llmBaseUrl/llmApiKey in ~/.hicortex/config.json.");
619
+ return;
620
+ }
432
621
  const recommendedIdx = options.findIndex(o => o.recommended);
433
622
  const defaultIdx = recommendedIdx >= 0 ? recommendedIdx : 0;
434
- // Display
435
- console.log("\n LLM for nightly distillation:\n");
623
+ console.log("\n LLM for nightly distillation and consolidation:\n");
624
+ if (options.length === 2) {
625
+ // Only "manual" and "skip" — nothing detected
626
+ console.log(" No LLM detected automatically on this machine.");
627
+ }
436
628
  for (let i = 0; i < options.length; i++) {
437
629
  const marker = i === defaultIdx ? " (recommended)" : "";
438
630
  console.log(` ${i + 1}. ${options[i].label}${marker}`);
439
631
  }
440
632
  const choice = await ask(`\n Choice [${defaultIdx + 1}]: `);
441
- const selected = choice ? parseInt(choice, 10) - 1 : defaultIdx;
442
- if (selected < 0 || selected >= options.length) {
443
- console.log(" Invalid choice.");
444
- process.exit(1);
633
+ const selectedIdx = choice.trim() ? parseInt(choice.trim(), 10) - 1 : defaultIdx;
634
+ if (isNaN(selectedIdx) || selectedIdx < 0 || selectedIdx >= options.length) {
635
+ console.log(" Invalid choice — server will run recall-only until you re-run init.");
636
+ return;
637
+ }
638
+ await options[selectedIdx].save();
639
+ const selectedLabel = options[selectedIdx].label;
640
+ if (selectedLabel.startsWith("Skip")) {
641
+ console.log(" No LLM configured — server will run recall-only (search/lessons/context work).\n" +
642
+ " To enable capture and consolidation later, run: npx @gamaze/hicortex init");
643
+ }
644
+ else {
645
+ console.log(` ✓ LLM configured: ${selectedLabel}`);
445
646
  }
446
- await options[selected].save();
447
- console.log(` ✓ LLM configured: ${options[selected].label}`);
448
647
  }
449
648
  function detectOllamaModels() {
450
649
  try {
@@ -470,6 +669,34 @@ function saveConfig(configPath, config) {
470
669
  (0, node_fs_1.mkdirSync)(HICORTEX_HOME, { recursive: true });
471
670
  (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2));
472
671
  }
672
+ /**
673
+ * Generate a random auth token in the format hctx-<32 hex chars>.
674
+ * Exported for testability.
675
+ */
676
+ function generateAuthToken() {
677
+ return `hctx-${(0, node_crypto_1.randomBytes)(16).toString("hex")}`;
678
+ }
679
+ /**
680
+ * Ensure a server-mode auth token exists in config.json.
681
+ * Generates and saves one if absent. Never overwrites an existing token.
682
+ * Returns the token (existing or newly generated).
683
+ * Exported for testability.
684
+ */
685
+ function persistAuthToken(configPath) {
686
+ let config = {};
687
+ try {
688
+ config = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
689
+ }
690
+ catch { /* new file */ }
691
+ if (config.authToken && typeof config.authToken === "string") {
692
+ return { token: config.authToken, generated: false };
693
+ }
694
+ const token = generateAuthToken();
695
+ config.authToken = token;
696
+ (0, node_fs_1.mkdirSync)(HICORTEX_HOME, { recursive: true });
697
+ (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2));
698
+ return { token, generated: true };
699
+ }
473
700
  /**
474
701
  * Determine the npm package specifier for the daemon.
475
702
  * Uses tag-based resolution so restarts pick up new versions automatically.
@@ -494,16 +721,15 @@ function getPackageSpec() {
494
721
  }
495
722
  function installDaemon() {
496
723
  const os = (0, node_os_1.platform)();
497
- const npxPath = findNpxPath();
498
- const packageSpec = getPackageSpec();
724
+ const binaryArgs = resolveBinaryArgs();
499
725
  if (os === "darwin") {
500
- return installLaunchd(npxPath, packageSpec);
726
+ return installLaunchd(binaryArgs);
501
727
  }
502
728
  else if (os === "linux") {
503
- return installSystemd(npxPath, packageSpec);
729
+ return installSystemd(binaryArgs);
504
730
  }
505
731
  else {
506
- console.log(` ⚠ Unsupported platform: ${os}. Start the server manually: npx ${packageSpec} server`);
732
+ console.log(` ⚠ Unsupported platform: ${os}. Start the server manually: ${[...binaryArgs, "server"].join(" ")}`);
507
733
  return false;
508
734
  }
509
735
  }
@@ -515,11 +741,100 @@ function findNpxPath() {
515
741
  return "/usr/local/bin/npx";
516
742
  }
517
743
  }
518
- function installLaunchd(npxPath, packageSpec) {
744
+ /**
745
+ * Resolve the absolute path of the hicortex binary.
746
+ * For global npm installs (e.g. /usr/bin/hicortex) this is the binary itself.
747
+ * For dev/npx installs, falls back to `npx <packageSpec> <command>` form.
748
+ * Returns an array: [binaryPath] for global, or [npxPath, "-y", packageSpec] for npx.
749
+ */
750
+ function resolveBinaryArgs() {
751
+ try {
752
+ const bin = (0, node_child_process_1.execSync)("which hicortex", { encoding: "utf-8" }).trim();
753
+ if (bin)
754
+ return [bin];
755
+ }
756
+ catch { /* not in PATH as a global binary */ }
757
+ const npxPath = findNpxPath();
758
+ const packageSpec = getPackageSpec();
759
+ return [npxPath, "-y", packageSpec];
760
+ }
761
+ /**
762
+ * Install (or verify) the CC SessionStart hook that runs `hicortex lessons-context`.
763
+ * The hook fetches lessons from the configured server at session start and injects
764
+ * them as context — replacing the old static CLAUDE.md block.
765
+ *
766
+ * Idempotent: skips if a SessionStart hook containing "lessons-context" already exists.
767
+ * Uses JSON.parse/JSON.stringify to safely merge into ~/.claude/settings.json.
768
+ *
769
+ * @param settingsPath Override for the settings.json path (used in tests; defaults to CC_SETTINGS).
770
+ */
771
+ function installSessionStartHook(settingsPath) {
772
+ const targetPath = settingsPath ?? CC_SETTINGS;
773
+ const binaryArgs = resolveBinaryArgs();
774
+ // Build the command string: "/path/to/hicortex lessons-context" or "npx -y @gamaze/hicortex lessons-context"
775
+ const command = [...binaryArgs, "lessons-context"].join(" ");
776
+ let settings = {};
777
+ if ((0, node_fs_1.existsSync)(targetPath)) {
778
+ try {
779
+ settings = JSON.parse((0, node_fs_1.readFileSync)(targetPath, "utf-8"));
780
+ }
781
+ catch {
782
+ // File exists but is malformed — do NOT overwrite (would destroy the user's entire CC config).
783
+ console.log(` ⚠ ${targetPath} exists but is not valid JSON — skipping SessionStart hook.`);
784
+ console.log(` Fix the file, then re-run init, or add the hook manually:`);
785
+ console.log(` command: "${command}"`);
786
+ return;
787
+ }
788
+ }
789
+ // Ensure hooks object and SessionStart array exist
790
+ if (!settings.hooks || typeof settings.hooks !== "object") {
791
+ settings.hooks = {};
792
+ }
793
+ const hooks = settings.hooks;
794
+ if (!Array.isArray(hooks.SessionStart)) {
795
+ hooks.SessionStart = [];
796
+ }
797
+ const sessionStart = hooks.SessionStart;
798
+ // Idempotent: skip if any existing entry's command contains "lessons-context"
799
+ const alreadyInstalled = sessionStart.some((entry) => {
800
+ if (typeof entry !== "object" || entry === null)
801
+ return false;
802
+ const e = entry;
803
+ // CC hook format: { hooks: [{ type: "command", command: "..." }] }
804
+ if (Array.isArray(e.hooks)) {
805
+ return e.hooks.some((h) => {
806
+ if (typeof h !== "object" || h === null)
807
+ return false;
808
+ const hook = h;
809
+ return typeof hook.command === "string" && hook.command.includes("lessons-context");
810
+ });
811
+ }
812
+ return false;
813
+ });
814
+ if (alreadyInstalled) {
815
+ console.log(` ✓ SessionStart hook already installed`);
816
+ return;
817
+ }
818
+ sessionStart.push({
819
+ hooks: [{ type: "command", command, timeout: 10 }],
820
+ });
821
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(targetPath), { recursive: true });
822
+ (0, node_fs_1.writeFileSync)(targetPath, JSON.stringify(settings, null, 2));
823
+ console.log(` ✓ Installed SessionStart hook: ${command}`);
824
+ }
825
+ function installLaunchd(binaryArgs) {
519
826
  const plistDir = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents");
520
827
  const plistPath = (0, node_path_1.join)(plistDir, "com.gamaze.hicortex.plist");
521
828
  const logPath = (0, node_path_1.join)(HICORTEX_HOME, "server.log");
522
829
  const errLogPath = (0, node_path_1.join)(HICORTEX_HOME, "server-err.log");
830
+ // Build ProgramArguments as individual <string> elements.
831
+ const programArgs = [...binaryArgs, "server"]
832
+ .map((a) => ` <string>${a}</string>`)
833
+ .join("\n");
834
+ // PATH must start with the binary's own directory so the sibling node
835
+ // binary (correct version for nvm installs) is found first.
836
+ // launchd has no PATH by default; without this, node itself won't be found.
837
+ const binDir = (0, node_path_1.dirname)(binaryArgs[0]);
523
838
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
524
839
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
525
840
  <plist version="1.0">
@@ -528,10 +843,7 @@ function installLaunchd(npxPath, packageSpec) {
528
843
  <string>com.gamaze.hicortex</string>
529
844
  <key>ProgramArguments</key>
530
845
  <array>
531
- <string>${npxPath}</string>
532
- <string>-y</string>
533
- <string>${packageSpec}</string>
534
- <string>server</string>
846
+ ${programArgs}
535
847
  </array>
536
848
  <key>KeepAlive</key>
537
849
  <true/>
@@ -544,7 +856,7 @@ function installLaunchd(npxPath, packageSpec) {
544
856
  <key>EnvironmentVariables</key>
545
857
  <dict>
546
858
  <key>PATH</key>
547
- <string>${(0, node_path_1.dirname)(npxPath)}:/usr/local/bin:/usr/bin:/bin</string>
859
+ <string>${binDir}:/usr/local/bin:/usr/bin:/bin</string>
548
860
  </dict>
549
861
  </dict>
550
862
  </plist>`;
@@ -566,20 +878,23 @@ function installLaunchd(npxPath, packageSpec) {
566
878
  return false;
567
879
  }
568
880
  }
569
- function installSystemd(npxPath, packageSpec) {
881
+ function installSystemd(binaryArgs) {
570
882
  const unitDir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "systemd", "user");
571
883
  const servicePath = (0, node_path_1.join)(unitDir, "hicortex.service");
884
+ const execStart = [...binaryArgs, "server"].join(" ");
885
+ // PATH must start with the binary's own directory (see installLaunchd for rationale).
886
+ const binDir = (0, node_path_1.dirname)(binaryArgs[0]);
572
887
  const service = `[Unit]
573
888
  Description=Hicortex MCP server — long-term memory for AI agents
574
889
 
575
890
  [Service]
576
891
  Type=simple
577
- ExecStart=${npxPath} -y ${packageSpec} server
892
+ ExecStart=${execStart}
578
893
  Restart=on-failure
579
894
  RestartSec=10
580
895
  StandardOutput=journal
581
896
  StandardError=journal
582
- Environment=PATH=${(0, node_path_1.dirname)(npxPath)}:/usr/local/bin:/usr/bin:/bin
897
+ Environment=PATH=${binDir}:/usr/local/bin:/usr/bin:/bin
583
898
 
584
899
  [Install]
585
900
  WantedBy=default.target
@@ -629,6 +944,8 @@ async function runInit(options = {}) {
629
944
  console.log(` • Remote server at ${d.remoteServerUrl} (${d.memoryCount ?? "?"} memories)`);
630
945
  if (d.ocPlugin)
631
946
  console.log(" • OpenClaw plugin installed");
947
+ if (d.hermesFound)
948
+ console.log(` • Hermes found at ${HERMES_HOME}`);
632
949
  if (d.ccMcpRegistered)
633
950
  console.log(" • CC MCP already registered");
634
951
  if (d.existingDb)
@@ -658,8 +975,10 @@ async function runInit(options = {}) {
658
975
  actions.push("Install Hicortex server daemon");
659
976
  if (!d.ccMcpRegistered)
660
977
  actions.push("Register MCP server in CC settings");
978
+ if (d.hermesFound)
979
+ actions.push("Install Hermes plugin + configure");
661
980
  actions.push("Install /learn and /hicortex-activate commands");
662
- actions.push("Add Hicortex Learnings block to CLAUDE.md");
981
+ actions.push("Install SessionStart hook (query-time lessons)");
663
982
  if (actions.length === 0) {
664
983
  console.log("Everything is already configured. Nothing to do.");
665
984
  return;
@@ -676,6 +995,20 @@ async function runInit(options = {}) {
676
995
  // Phase 3: Execute
677
996
  // Persist LLM config for the daemon
678
997
  await persistLlmConfig();
998
+ // Generate and persist auth token (upgrade-safe: never overwrites existing token).
999
+ const configPath = (0, node_path_1.join)(HICORTEX_HOME, "config.json");
1000
+ const { token: authToken, generated: tokenGenerated } = persistAuthToken(configPath);
1001
+ if (tokenGenerated) {
1002
+ console.log(`\n Auth token for clients: ${authToken}`);
1003
+ console.log(` (stored in ~/.hicortex/config.json — also shown by \`hicortex status\`)\n`);
1004
+ }
1005
+ else {
1006
+ console.log(` ✓ Auth token already configured`);
1007
+ }
1008
+ // Install the nightly job (capture via localhost /distill + consolidation).
1009
+ // Without it a server-mode install never captures or consolidates — the
1010
+ // daemon only serves recall + /distill. Skips if a schedule already exists.
1011
+ installNightlyCron(resolveNightlyHour("server"));
679
1012
  // Install daemon if needed
680
1013
  if (!d.localServer && !d.remoteServer) {
681
1014
  installDaemon();
@@ -704,41 +1037,31 @@ async function runInit(options = {}) {
704
1037
  allowHicortexTools();
705
1038
  // Install CC commands
706
1039
  installCcCommands();
707
- // Inject CLAUDE.md (only if server has a DB we can read)
708
- // For now, just create the block with agent guidance and no lessons
709
- // Lessons will populate on first nightly run
710
- const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
711
- if (!(0, node_fs_1.existsSync)(claudeMdPath) || !(0, node_fs_1.readFileSync)(claudeMdPath, "utf-8").includes("HICORTEX-LEARNINGS")) {
712
- (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(claudeMdPath), { recursive: true });
713
- let content = "";
714
- try {
715
- content = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
716
- }
717
- catch { /* new file */ }
718
- const block = [
719
- "<!-- HICORTEX-LEARNINGS:START -->",
720
- "## Hicortex Memory",
721
- "",
722
- "You have access to long-term memory via Hicortex MCP tools. Use `hicortex_search` when you need context from past sessions, decisions, or prior work. Use `hicortex_context` at session start to recall recent project state. Use `hicortex_ingest` to save important decisions or learnings. Sessions are auto-captured nightly.",
723
- "<!-- HICORTEX-LEARNINGS:END -->",
724
- ].join("\n");
725
- if (content.length > 0 && !content.endsWith("\n"))
726
- content += "\n";
727
- if (content.length > 0)
728
- content += "\n";
729
- content += block + "\n";
730
- (0, node_fs_1.writeFileSync)(claudeMdPath, content);
731
- console.log(` ✓ Added Hicortex Learnings block to ${claudeMdPath}`);
1040
+ // Setup Hermes if detected
1041
+ if (d.hermesFound) {
1042
+ // localhost bypass makes the token optional for co-located installs;
1043
+ // pass it for remote setups so setupHermes can include it in its instructions.
1044
+ const isLocal = serverUrl.includes("127.0.0.1") || serverUrl.includes("localhost");
1045
+ setupHermes(serverUrl, isLocal ? "" : authToken);
732
1046
  }
733
- else {
734
- console.log(` ✓ CLAUDE.md already has Hicortex Learnings block`);
1047
+ // Install CC SessionStart hook for query-time lesson injection.
1048
+ // Lessons are now fetched live at session start — no static CLAUDE.md block needed.
1049
+ installSessionStartHook();
1050
+ // Strip the old static lessons block from CLAUDE.md (0.9.0 migration).
1051
+ // Lessons are now delivered via the SessionStart hook instead.
1052
+ const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
1053
+ if ((0, claude_md_js_1.removeLessonsBlock)(claudeMdPath)) {
1054
+ console.log(` ✓ Removed old static lessons block from ${claudeMdPath} — lessons now injected at session start`);
735
1055
  }
736
1056
  console.log("\n✓ Hicortex setup complete!\n");
737
1057
  console.log("Next steps:");
738
- console.log(" 1. Restart Claude Code to pick up the new MCP server");
739
- console.log(" 2. Ask your agent: 'What Hicortex tools do you have?'");
740
- console.log(" 3. Try /learn to save something to long-term memory");
741
- console.log(` 4. Check server: curl ${serverUrl}/health`);
1058
+ console.log(" 1. Restart Claude Code to pick up the new MCP server and SessionStart hook");
1059
+ if (d.hermesFound) {
1060
+ console.log(" 2. Activate the Hermes plugin: run `hermes memory setup`, select 'hicortex', then restart the gateway(s)");
1061
+ }
1062
+ console.log(" 3. Ask your agent: 'What Hicortex tools do you have?'");
1063
+ console.log(" 4. Try /learn to save something to long-term memory");
1064
+ console.log(` 5. Check server: curl ${serverUrl}/health`);
742
1065
  }
743
1066
  // ---------------------------------------------------------------------------
744
1067
  // Client Mode Init
@@ -761,22 +1084,22 @@ async function runClientInit(serverUrl) {
761
1084
  console.error(`\n Make sure the Hicortex server is running and accessible.`);
762
1085
  process.exit(1);
763
1086
  }
764
- // Step 2: Auth — try default token first, prompt only if rejected
765
- const DEFAULT_AUTH_TOKEN = "hctx-default-token";
766
- let authToken = DEFAULT_AUTH_TOKEN;
1087
+ // Step 2: Auth — probe with no token; if 401, ask the user.
1088
+ // The server's token is shown by `hicortex status` on the server box,
1089
+ // or readable from ~/.hicortex/config.json on that machine.
1090
+ let authToken = "";
767
1091
  try {
768
1092
  const probe = await fetch(`${serverUrl}/ingest`, {
769
1093
  method: "POST",
770
- headers: {
771
- "Content-Type": "application/json",
772
- "Authorization": `Bearer ${DEFAULT_AUTH_TOKEN}`,
773
- },
1094
+ headers: { "Content-Type": "application/json" },
774
1095
  body: JSON.stringify({ content: "" }),
775
1096
  signal: AbortSignal.timeout(5000),
776
1097
  });
777
1098
  if (probe.status === 401) {
778
- // Server uses a custom token — ask the user
779
- const tokenAnswer = await ask("\nServer uses a custom auth token. Enter token: ");
1099
+ // Server requires a token — ask the user where to get it
1100
+ console.log("\n Server requires an auth token.");
1101
+ console.log(" Find it on the server: run `hicortex status`, or read ~/.hicortex/config.json");
1102
+ const tokenAnswer = await ask(" Enter token: ");
780
1103
  authToken = tokenAnswer.trim();
781
1104
  if (!authToken) {
782
1105
  console.error(" ✗ Auth token required but not provided.");
@@ -796,99 +1119,128 @@ async function runClientInit(serverUrl) {
796
1119
  console.error(" ✗ Auth token rejected by server.");
797
1120
  process.exit(1);
798
1121
  }
799
- console.log(" ✓ Custom auth token verified");
1122
+ console.log(" ✓ Auth token verified");
800
1123
  }
801
1124
  else {
802
- console.log(" ✓ Server connected (default auth)");
1125
+ console.log(" ✓ Server connected (no auth required from localhost)");
803
1126
  }
804
1127
  }
805
1128
  catch {
806
- // Probe failed but health passed — continue with default token
1129
+ // Probe failed but health passed — continue without token
807
1130
  }
808
- // Step 3: Configure LLM for local distillation
809
- console.log("\nConfigure LLM for local session distillation:");
810
- await persistLlmConfig();
811
- // Step 4: Save client config
1131
+ // Client mode needs no LLM capture is denoise-only; the server distills.
1132
+ // Step 3: Save client config
812
1133
  (0, node_fs_1.mkdirSync)(HICORTEX_HOME, { recursive: true });
813
1134
  const configPath = (0, node_path_1.join)(HICORTEX_HOME, "config.json");
814
1135
  let config = {};
815
- try {
816
- config = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
1136
+ if ((0, node_fs_1.existsSync)(configPath)) {
1137
+ try {
1138
+ config = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
1139
+ }
1140
+ catch {
1141
+ console.log(` ⚠ ${configPath} exists but is not valid JSON — starting with empty config (licenseKey and LLM settings may need to be re-entered).`);
1142
+ }
817
1143
  }
818
- catch { }
819
1144
  config.mode = "client";
820
1145
  config.serverUrl = serverUrl;
821
1146
  if (authToken)
822
1147
  config.authToken = authToken;
823
1148
  saveConfig(configPath, config);
824
1149
  console.log(` ✓ Client config saved to ${configPath}`);
825
- // Step 5: Register CC MCP pointing to remote server
1150
+ // Step 4: Register CC MCP pointing to remote server
826
1151
  if (authToken) {
827
1152
  // Write directly with auth header
828
1153
  const claudeJsonPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude.json");
829
1154
  let claudeConfig = {};
830
- try {
831
- claudeConfig = JSON.parse((0, node_fs_1.readFileSync)(claudeJsonPath, "utf-8"));
1155
+ let claudeJsonOk = true;
1156
+ if ((0, node_fs_1.existsSync)(claudeJsonPath)) {
1157
+ try {
1158
+ claudeConfig = JSON.parse((0, node_fs_1.readFileSync)(claudeJsonPath, "utf-8"));
1159
+ }
1160
+ catch {
1161
+ console.log(` ⚠ ${claudeJsonPath} exists but is not valid JSON — skipping MCP registration. Fix the file, then re-run init or run: claude mcp add hicortex --transport sse ${serverUrl}/sse`);
1162
+ claudeJsonOk = false;
1163
+ }
1164
+ }
1165
+ if (claudeJsonOk) {
1166
+ if (!claudeConfig.mcpServers)
1167
+ claudeConfig.mcpServers = {};
1168
+ claudeConfig.mcpServers.hicortex = {
1169
+ type: "sse",
1170
+ url: `${serverUrl}/sse`,
1171
+ headers: { "Authorization": `Bearer ${authToken}` },
1172
+ };
1173
+ (0, node_fs_1.writeFileSync)(claudeJsonPath, JSON.stringify(claudeConfig, null, 2));
1174
+ console.log(` ✓ Registered MCP server with auth`);
832
1175
  }
833
- catch { }
834
- if (!claudeConfig.mcpServers)
835
- claudeConfig.mcpServers = {};
836
- claudeConfig.mcpServers.hicortex = {
837
- type: "sse",
838
- url: `${serverUrl}/sse`,
839
- headers: { "Authorization": `Bearer ${authToken}` },
840
- };
841
- (0, node_fs_1.writeFileSync)(claudeJsonPath, JSON.stringify(claudeConfig, null, 2));
842
- console.log(` ✓ Registered MCP server with auth`);
843
1176
  }
844
1177
  else {
845
1178
  registerCcMcp(serverUrl);
846
1179
  }
847
1180
  allowHicortexTools();
848
- // Step 6: Install CC commands
1181
+ // Step 5: Install CC commands
849
1182
  installCcCommands();
850
- // Step 7: Inject CLAUDE.md learnings block
1183
+ // Step 6: Install SessionStart hook for query-time lessons.
1184
+ installSessionStartHook();
1185
+ // Strip the old static CLAUDE.md lessons block if present (0.9.0 migration).
851
1186
  const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
852
- if (!(0, node_fs_1.existsSync)(claudeMdPath) || !(0, node_fs_1.readFileSync)(claudeMdPath, "utf-8").includes("HICORTEX-LEARNINGS")) {
853
- (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(claudeMdPath), { recursive: true });
854
- let content = "";
855
- try {
856
- content = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
857
- }
858
- catch { }
859
- const block = [
860
- "<!-- HICORTEX-LEARNINGS:START -->",
861
- "## Hicortex Memory",
862
- "",
863
- "You have access to long-term memory via Hicortex MCP tools. Use `hicortex_search` when you need context from past sessions, decisions, or prior work. Use `hicortex_context` at session start to recall recent project state. Use `hicortex_ingest` to save important decisions or learnings. Sessions are auto-captured nightly.",
864
- "<!-- HICORTEX-LEARNINGS:END -->",
865
- ].join("\n");
866
- if (content.length > 0 && !content.endsWith("\n"))
867
- content += "\n";
868
- if (content.length > 0)
869
- content += "\n";
870
- content += block + "\n";
871
- (0, node_fs_1.writeFileSync)(claudeMdPath, content);
872
- console.log(` ✓ Added Hicortex Learnings block`);
873
- }
874
- // Step 8: Install nightly cron (distill locally, POST to server)
875
- installNightlyCron();
1187
+ if ((0, claude_md_js_1.removeLessonsBlock)(claudeMdPath)) {
1188
+ console.log(` ✓ Removed old static lessons block from CLAUDE.md lessons now injected at session start`);
1189
+ }
1190
+ // Step 7: Install nightly cron (denoise locally, POST to server /distill)
1191
+ installNightlyCron(resolveNightlyHour("client"));
1192
+ // Step 8: Setup Hermes if detected
1193
+ if ((0, node_fs_1.existsSync)(HERMES_HOME)) {
1194
+ console.log("\nHermes detected installing plugin...");
1195
+ setupHermes(serverUrl, authToken);
1196
+ }
876
1197
  console.log("\n✓ Hicortex client setup complete!\n");
877
1198
  console.log("How it works:");
878
1199
  console.log(" • MCP tools (search, context, ingest) talk to the remote server");
879
- console.log(" • Nightly pipeline distills CC transcripts locally, POSTs memories to server");
1200
+ console.log(" • Nightly pipeline denoises CC transcripts, POSTs to server for distillation");
1201
+ console.log(" • Lessons fetched live at each CC session start (SessionStart hook)");
880
1202
  console.log(" • No local database — all memories stored on the server");
1203
+ if ((0, node_fs_1.existsSync)(HERMES_HOME)) {
1204
+ console.log(" • Hermes plugin installed — run `hermes memory setup` (select 'hicortex') + restart gateway(s) to activate");
1205
+ }
881
1206
  console.log(`\nServer: ${serverUrl}`);
882
- console.log("Restart Claude Code to activate.");
1207
+ console.log("Restart your agents to activate.");
883
1208
  }
884
- function installNightlyCron() {
885
- const npxPath = findNpxPath();
886
- const packageSpec = getPackageSpec();
1209
+ /**
1210
+ * Resolve the nightly hour (0–23, local time) for the generated schedule.
1211
+ * Priority: `nightlyHour` in ~/.hicortex/config.json → mode default.
1212
+ * Defaults: client 02:00, server 03:00 — staggered so that in mixed fleets
1213
+ * clients push their sessions before the server's capture + consolidation run.
1214
+ */
1215
+ function resolveNightlyHour(mode, configDir = HICORTEX_HOME) {
1216
+ try {
1217
+ const config = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(configDir, "config.json"), "utf-8"));
1218
+ const h = config.nightlyHour;
1219
+ if (typeof h === "number" && Number.isInteger(h) && h >= 0 && h <= 23)
1220
+ return h;
1221
+ }
1222
+ catch { /* no config yet — use the default */ }
1223
+ return mode === "server" ? 3 : 2;
1224
+ }
1225
+ function installNightlyCron(hour) {
1226
+ const binaryArgs = resolveBinaryArgs();
887
1227
  const os = (0, node_os_1.platform)();
1228
+ const hh = String(hour).padStart(2, "0");
1229
+ // PATH must start with the binary's own directory (see installLaunchd for rationale).
1230
+ const binDir = (0, node_path_1.dirname)(binaryArgs[0]);
888
1231
  if (os === "darwin") {
889
1232
  const plistDir = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents");
890
1233
  const plistPath = (0, node_path_1.join)(plistDir, "com.gamaze.hicortex-nightly.plist");
891
1234
  const logPath = (0, node_path_1.join)(HICORTEX_HOME, "nightly.log");
1235
+ // Never overwrite an existing schedule — users tune these (multi-slot
1236
+ // capture windows, quiet hours). Fresh installs only.
1237
+ if ((0, node_fs_1.existsSync)(plistPath)) {
1238
+ console.log(` ✓ Nightly cron already installed — leaving existing schedule as-is`);
1239
+ return;
1240
+ }
1241
+ const programArgs = [...binaryArgs, "nightly"]
1242
+ .map((a) => ` <string>${a}</string>`)
1243
+ .join("\n");
892
1244
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
893
1245
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
894
1246
  <plist version="1.0">
@@ -897,15 +1249,12 @@ function installNightlyCron() {
897
1249
  <string>com.gamaze.hicortex-nightly</string>
898
1250
  <key>ProgramArguments</key>
899
1251
  <array>
900
- <string>${npxPath}</string>
901
- <string>-y</string>
902
- <string>${packageSpec}</string>
903
- <string>nightly</string>
1252
+ ${programArgs}
904
1253
  </array>
905
1254
  <key>StartCalendarInterval</key>
906
1255
  <dict>
907
1256
  <key>Hour</key>
908
- <integer>2</integer>
1257
+ <integer>${hour}</integer>
909
1258
  <key>Minute</key>
910
1259
  <integer>0</integer>
911
1260
  </dict>
@@ -916,7 +1265,7 @@ function installNightlyCron() {
916
1265
  <key>EnvironmentVariables</key>
917
1266
  <dict>
918
1267
  <key>PATH</key>
919
- <string>${(0, node_path_1.dirname)(npxPath)}:/usr/local/bin:/usr/bin:/bin</string>
1268
+ <string>${binDir}:/usr/local/bin:/usr/bin:/bin</string>
920
1269
  </dict>
921
1270
  </dict>
922
1271
  </plist>`;
@@ -928,7 +1277,7 @@ function installNightlyCron() {
928
1277
  }
929
1278
  catch { }
930
1279
  (0, node_child_process_1.execSync)(`launchctl load ${plistPath}`, { stdio: "pipe" });
931
- console.log(` ✓ Installed nightly cron (runs daily at 02:00)`);
1280
+ console.log(` ✓ Installed nightly cron (runs daily at ${hh}:00)`);
932
1281
  }
933
1282
  catch {
934
1283
  console.log(` ⚠ Could not load nightly plist. Load manually: launchctl load ${plistPath}`);
@@ -938,20 +1287,26 @@ function installNightlyCron() {
938
1287
  const configDir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "systemd", "user");
939
1288
  const servicePath = (0, node_path_1.join)(configDir, "hicortex-nightly.service");
940
1289
  const timerPath = (0, node_path_1.join)(configDir, "hicortex-nightly.timer");
1290
+ // Never overwrite an existing schedule — users tune these. Fresh installs only.
1291
+ if ((0, node_fs_1.existsSync)(timerPath)) {
1292
+ console.log(` ✓ Nightly timer already installed — leaving existing schedule as-is`);
1293
+ return;
1294
+ }
1295
+ const execStart = [...binaryArgs, "nightly"].join(" ");
941
1296
  const service = `[Unit]
942
1297
  Description=Hicortex Nightly (distill + POST)
943
1298
 
944
1299
  [Service]
945
1300
  Type=oneshot
946
- ExecStart=${npxPath} -y ${packageSpec} nightly
947
- Environment=PATH=/usr/local/bin:/usr/bin:/bin
1301
+ ExecStart=${execStart}
1302
+ Environment=PATH=${binDir}:/usr/local/bin:/usr/bin:/bin
948
1303
  Environment=HOME=${(0, node_os_1.homedir)()}
949
1304
  WorkingDirectory=${(0, node_os_1.homedir)()}`;
950
1305
  const timer = `[Unit]
951
1306
  Description=Hicortex Nightly Timer
952
1307
 
953
1308
  [Timer]
954
- OnCalendar=*-*-* 02:00:00
1309
+ OnCalendar=*-*-* ${hh}:00:00
955
1310
  Persistent=true
956
1311
 
957
1312
  [Install]
@@ -962,7 +1317,7 @@ WantedBy=timers.target`;
962
1317
  try {
963
1318
  (0, node_child_process_1.execSync)("systemctl --user daemon-reload", { stdio: "pipe" });
964
1319
  (0, node_child_process_1.execSync)("systemctl --user enable --now hicortex-nightly.timer", { stdio: "pipe" });
965
- console.log(` ✓ Installed nightly timer (runs daily at 02:00)`);
1320
+ console.log(` ✓ Installed nightly timer (runs daily at ${hh}:00)`);
966
1321
  }
967
1322
  catch {
968
1323
  console.log(` ⚠ Could not enable nightly timer. Enable manually: systemctl --user enable --now hicortex-nightly.timer`);