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