@gamaze/hicortex 0.7.1 → 0.10.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/README.md +72 -39
- package/dist/claude-md.d.ts +9 -21
- package/dist/claude-md.js +9 -241
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +29 -11
- package/dist/consolidate.js +0 -7
- package/dist/db.js +24 -0
- package/dist/embedder.d.ts +11 -0
- package/dist/embedder.js +27 -0
- package/dist/extensions.d.ts +41 -88
- package/dist/extensions.js +36 -61
- package/dist/features.d.ts +21 -25
- package/dist/features.js +47 -83
- package/dist/hermes-transcript-reader.d.ts +27 -0
- package/dist/hermes-transcript-reader.js +134 -0
- package/dist/index.d.ts +16 -4
- package/dist/index.js +252 -344
- package/dist/init.d.ts +41 -1
- package/dist/init.js +545 -190
- package/dist/lesson-selection.d.ts +62 -0
- package/dist/lesson-selection.js +159 -0
- package/dist/lessons-context.d.ts +17 -0
- package/dist/lessons-context.js +96 -0
- package/dist/llm.d.ts +42 -29
- package/dist/llm.js +89 -270
- package/dist/mcp-server.d.ts +0 -1
- package/dist/mcp-server.js +404 -86
- package/dist/nightly.d.ts +9 -6
- package/dist/nightly.js +197 -357
- package/dist/oc-transcript-reader.d.ts +20 -0
- package/dist/oc-transcript-reader.js +61 -0
- package/dist/pi-transcript-reader.d.ts +1 -0
- package/dist/status.js +22 -2
- package/dist/storage.d.ts +7 -1
- package/dist/storage.js +28 -7
- package/dist/transcript-reader.d.ts +19 -0
- package/dist/transcript-reader.js +17 -3
- package/dist/types.d.ts +10 -0
- package/dist/uninstall.js +31 -1
- package/hermes-plugin/hicortex/README.md +77 -0
- package/hermes-plugin/hicortex/__init__.py +17 -0
- package/hermes-plugin/hicortex/client.py +162 -0
- package/hermes-plugin/hicortex/config.py +105 -0
- package/hermes-plugin/hicortex/plugin.yaml +12 -0
- package/hermes-plugin/hicortex/provider.py +432 -0
- package/openclaw.plugin.json +17 -44
- package/package.json +7 -5
- package/dist/pro-loader.d.ts +0 -33
- 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
|
-
* -
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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:
|
|
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
|
-
#
|
|
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
|
|
261
|
+
3. Verify the key is recognised:
|
|
240
262
|
\`\`\`bash
|
|
241
|
-
|
|
263
|
+
hicortex status
|
|
242
264
|
\`\`\`
|
|
243
265
|
|
|
244
|
-
4. Tell the user: "
|
|
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: "
|
|
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
|
-
//
|
|
461
|
+
// -------------------------------------------------------------------------
|
|
462
|
+
// Detect candidates from all harnesses on this machine
|
|
463
|
+
// -------------------------------------------------------------------------
|
|
464
|
+
const { findClaudeBinary } = await import("./llm.js");
|
|
320
465
|
const options = [];
|
|
321
|
-
// 1.
|
|
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.
|
|
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:
|
|
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.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
//
|
|
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:
|
|
568
|
+
label: `${candidate.provider}${modelStr} (${sourceStr})`,
|
|
391
569
|
save: () => {
|
|
392
|
-
config.llmApiKey =
|
|
393
|
-
config.llmBaseUrl =
|
|
394
|
-
config.llmProvider =
|
|
395
|
-
if (
|
|
396
|
-
config.llmModel =
|
|
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
|
-
//
|
|
579
|
+
// 4. Manual entry (always available)
|
|
402
580
|
options.push({
|
|
403
|
-
label: "
|
|
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("
|
|
409
|
-
|
|
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("
|
|
414
|
-
|
|
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: "
|
|
604
|
+
label: "Skip — run recall-only for now (configure later with: npx @gamaze/hicortex init)",
|
|
426
605
|
save: () => {
|
|
427
|
-
|
|
428
|
-
process.exit(0);
|
|
606
|
+
// Intentionally leave no LLM config — server starts recall-only.
|
|
429
607
|
},
|
|
430
608
|
});
|
|
431
|
-
//
|
|
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
|
-
|
|
435
|
-
|
|
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
|
|
442
|
-
if (
|
|
443
|
-
console.log(" Invalid choice.");
|
|
444
|
-
|
|
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
|
|
498
|
-
const packageSpec = getPackageSpec();
|
|
724
|
+
const binaryArgs = resolveBinaryArgs();
|
|
499
725
|
if (os === "darwin") {
|
|
500
|
-
return installLaunchd(
|
|
726
|
+
return installLaunchd(binaryArgs);
|
|
501
727
|
}
|
|
502
728
|
else if (os === "linux") {
|
|
503
|
-
return installSystemd(
|
|
729
|
+
return installSystemd(binaryArgs);
|
|
504
730
|
}
|
|
505
731
|
else {
|
|
506
|
-
console.log(` ⚠ Unsupported platform: ${os}. Start the server manually:
|
|
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
|
-
|
|
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
|
-
|
|
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>${
|
|
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(
|
|
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=${
|
|
892
|
+
ExecStart=${execStart}
|
|
578
893
|
Restart=on-failure
|
|
579
894
|
RestartSec=10
|
|
580
895
|
StandardOutput=journal
|
|
581
896
|
StandardError=journal
|
|
582
|
-
Environment=PATH=${
|
|
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("
|
|
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
|
-
//
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
(
|
|
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
|
-
|
|
734
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
|
|
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 —
|
|
765
|
-
|
|
766
|
-
|
|
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
|
|
779
|
-
|
|
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(" ✓
|
|
1122
|
+
console.log(" ✓ Auth token verified");
|
|
800
1123
|
}
|
|
801
1124
|
else {
|
|
802
|
-
console.log(" ✓ Server connected (
|
|
1125
|
+
console.log(" ✓ Server connected (no auth required from localhost)");
|
|
803
1126
|
}
|
|
804
1127
|
}
|
|
805
1128
|
catch {
|
|
806
|
-
// Probe failed but health passed — continue
|
|
1129
|
+
// Probe failed but health passed — continue without token
|
|
807
1130
|
}
|
|
808
|
-
//
|
|
809
|
-
|
|
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
|
-
|
|
816
|
-
|
|
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
|
|
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
|
-
|
|
831
|
-
|
|
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
|
|
1181
|
+
// Step 5: Install CC commands
|
|
849
1182
|
installCcCommands();
|
|
850
|
-
// Step
|
|
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 (
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
|
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
|
|
1207
|
+
console.log("Restart your agents to activate.");
|
|
883
1208
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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
|
-
|
|
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
|
|
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>${
|
|
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
|
|
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=${
|
|
947
|
-
Environment=PATH
|
|
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=*-*-*
|
|
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
|
|
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`);
|