@gamaze/hicortex 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-md.d.ts +2 -2
- package/dist/claude-md.js +8 -6
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +23 -9
- package/dist/consolidate.js +9 -22
- package/dist/db.d.ts +2 -0
- package/dist/db.js +88 -9
- package/dist/distiller.d.ts +4 -2
- package/dist/distiller.js +31 -10
- package/dist/extensions.d.ts +126 -0
- package/dist/extensions.js +154 -0
- package/dist/features.d.ts +37 -0
- package/dist/features.js +127 -0
- package/dist/index.js +20 -35
- package/dist/license.d.ts +13 -3
- package/dist/license.js +30 -41
- package/dist/mcp-server.js +50 -18
- package/dist/nightly-status.d.ts +11 -0
- package/dist/nightly-status.js +167 -0
- package/dist/nightly.js +102 -22
- package/dist/state.d.ts +64 -0
- package/dist/state.js +162 -0
- package/package.json +7 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Nightly pipeline status — lightweight check without running the pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Shows:
|
|
6
|
+
* - Last run timestamp + age
|
|
7
|
+
* - Timer/schedule status (systemd/launchd)
|
|
8
|
+
* - DB memory count
|
|
9
|
+
* - Distillation source breakdown
|
|
10
|
+
* - Staleness warnings
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.showNightlyStatus = showNightlyStatus;
|
|
14
|
+
const node_fs_1 = require("node:fs");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const node_os_1 = require("node:os");
|
|
17
|
+
const node_child_process_1 = require("node:child_process");
|
|
18
|
+
const db_js_1 = require("./db.js");
|
|
19
|
+
const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
|
|
20
|
+
const LAST_RUN_PATH = (0, node_path_1.join)(HICORTEX_HOME, "nightly-last-run.txt");
|
|
21
|
+
const CONFIG_PATH = (0, node_path_1.join)(HICORTEX_HOME, "config.json");
|
|
22
|
+
const STALE_THRESHOLD_HOURS = 30;
|
|
23
|
+
async function showNightlyStatus() {
|
|
24
|
+
console.log("Hicortex Nightly Pipeline Status");
|
|
25
|
+
console.log("─".repeat(40));
|
|
26
|
+
// Last run
|
|
27
|
+
let lastRun = null;
|
|
28
|
+
let lastRunStr = "never";
|
|
29
|
+
try {
|
|
30
|
+
const ts = (0, node_fs_1.readFileSync)(LAST_RUN_PATH, "utf-8").trim();
|
|
31
|
+
const d = new Date(ts);
|
|
32
|
+
if (!isNaN(d.getTime())) {
|
|
33
|
+
lastRun = d;
|
|
34
|
+
lastRunStr = ts;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
lastRunStr = `${ts} (invalid)`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// No file
|
|
42
|
+
}
|
|
43
|
+
if (lastRun) {
|
|
44
|
+
const ageMs = Date.now() - lastRun.getTime();
|
|
45
|
+
const ageHours = Math.round(ageMs / (60 * 60 * 1000));
|
|
46
|
+
const ageStr = ageHours < 1 ? "just now" :
|
|
47
|
+
ageHours < 24 ? `${ageHours}h ago` :
|
|
48
|
+
`${Math.round(ageHours / 24)}d ago`;
|
|
49
|
+
const isStale = ageHours > STALE_THRESHOLD_HOURS;
|
|
50
|
+
console.log(`Last run: ${lastRunStr} (${ageStr})${isStale ? " ⚠ STALE" : ""}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log(`Last run: ${lastRunStr}`);
|
|
54
|
+
}
|
|
55
|
+
// LLM config
|
|
56
|
+
try {
|
|
57
|
+
const config = JSON.parse((0, node_fs_1.readFileSync)(CONFIG_PATH, "utf-8"));
|
|
58
|
+
const backend = config.llmBackend ?? "auto-detect";
|
|
59
|
+
const model = config.llmModel ?? "default";
|
|
60
|
+
const mode = config.mode === "client" ? "client → " + (config.serverUrl ?? "?") : "server (local)";
|
|
61
|
+
console.log(`Mode: ${mode}`);
|
|
62
|
+
console.log(`LLM backend: ${backend}${backend !== "auto-detect" ? ` (${model})` : ""}`);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
console.log("Config: not configured (run: hicortex init)");
|
|
66
|
+
}
|
|
67
|
+
// Timer/schedule
|
|
68
|
+
const os = (0, node_os_1.platform)();
|
|
69
|
+
let timerActive = false;
|
|
70
|
+
let timerInfo = "not installed";
|
|
71
|
+
if (os === "darwin") {
|
|
72
|
+
try {
|
|
73
|
+
const out = (0, node_child_process_1.execSync)("launchctl list 2>/dev/null | grep hicortex-nightly", {
|
|
74
|
+
encoding: "utf-8",
|
|
75
|
+
timeout: 3000,
|
|
76
|
+
});
|
|
77
|
+
if (out.trim()) {
|
|
78
|
+
timerActive = true;
|
|
79
|
+
timerInfo = "launchd (loaded)";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { /* not installed */ }
|
|
83
|
+
}
|
|
84
|
+
else if (os === "linux") {
|
|
85
|
+
try {
|
|
86
|
+
const active = (0, node_child_process_1.execSync)("systemctl --user is-active hicortex-nightly.timer 2>/dev/null", {
|
|
87
|
+
encoding: "utf-8",
|
|
88
|
+
timeout: 3000,
|
|
89
|
+
}).trim();
|
|
90
|
+
if (active === "active" || active === "waiting") {
|
|
91
|
+
timerActive = true;
|
|
92
|
+
try {
|
|
93
|
+
const next = (0, node_child_process_1.execSync)("systemctl --user show hicortex-nightly.timer --property=NextElapseUSecRealtime 2>/dev/null", {
|
|
94
|
+
encoding: "utf-8",
|
|
95
|
+
timeout: 3000,
|
|
96
|
+
}).trim();
|
|
97
|
+
const match = next.match(/=(\d+)/);
|
|
98
|
+
if (match) {
|
|
99
|
+
const nextDate = new Date(Number(match[1]) / 1000);
|
|
100
|
+
timerInfo = `systemd (active, next: ${nextDate.toISOString()})`;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
timerInfo = `systemd (${active})`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
timerInfo = `systemd (${active})`;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch { /* not installed */ }
|
|
112
|
+
}
|
|
113
|
+
console.log(`Timer: ${timerInfo}${!timerActive ? " ⚠ Pipeline will NOT run automatically" : ""}`);
|
|
114
|
+
// DB stats
|
|
115
|
+
const dbPath = (0, db_js_1.resolveDbPath)();
|
|
116
|
+
if ((0, node_fs_1.existsSync)(dbPath)) {
|
|
117
|
+
try {
|
|
118
|
+
const { initDb } = await import("./db.js");
|
|
119
|
+
const db = initDb(dbPath);
|
|
120
|
+
const count = db.prepare("SELECT COUNT(*) as c FROM memories").get().c;
|
|
121
|
+
let linkCount = 0;
|
|
122
|
+
try {
|
|
123
|
+
linkCount = db.prepare("SELECT COUNT(*) as c FROM memory_links").get().c;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// memory_links table may not exist in older DBs
|
|
127
|
+
}
|
|
128
|
+
// Source breakdown (top 5)
|
|
129
|
+
const sources = db.prepare("SELECT source_agent, COUNT(*) as cnt FROM memories GROUP BY source_agent ORDER BY cnt DESC LIMIT 5").all();
|
|
130
|
+
console.log(`\nMemories: ${count} (${linkCount} links)`);
|
|
131
|
+
if (sources.length > 0) {
|
|
132
|
+
console.log("Sources:");
|
|
133
|
+
for (const s of sources) {
|
|
134
|
+
console.log(` ${s.source_agent || "unknown"}: ${s.cnt}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
db.close();
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.log(`\nDB: error (${err instanceof Error ? err.message : String(err)})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(`\nDB: not found (run: hicortex init)`);
|
|
145
|
+
}
|
|
146
|
+
// Health assessment
|
|
147
|
+
console.log("\n" + "─".repeat(40));
|
|
148
|
+
const issues = [];
|
|
149
|
+
if (!lastRun)
|
|
150
|
+
issues.push("Pipeline has never run. Run: hicortex nightly");
|
|
151
|
+
else if (lastRun && (Date.now() - lastRun.getTime()) > STALE_THRESHOLD_HOURS * 60 * 60 * 1000) {
|
|
152
|
+
issues.push(`Pipeline hasn't run in ${STALE_THRESHOLD_HOURS}+ hours. Check timer.`);
|
|
153
|
+
}
|
|
154
|
+
if (!timerActive)
|
|
155
|
+
issues.push("No timer installed. Nightly pipeline won't run automatically.");
|
|
156
|
+
if (!(0, node_fs_1.existsSync)(dbPath))
|
|
157
|
+
issues.push("No database found. Run: hicortex init");
|
|
158
|
+
if (issues.length === 0) {
|
|
159
|
+
console.log("✓ Nightly pipeline healthy");
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log("Issues:");
|
|
163
|
+
for (const issue of issues) {
|
|
164
|
+
console.log(` ⚠ ${issue}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
package/dist/nightly.js
CHANGED
|
@@ -55,9 +55,10 @@ const distiller_js_1 = require("./distiller.js");
|
|
|
55
55
|
const consolidate_js_1 = require("./consolidate.js");
|
|
56
56
|
const transcript_reader_js_1 = require("./transcript-reader.js");
|
|
57
57
|
const claude_md_js_1 = require("./claude-md.js");
|
|
58
|
-
const
|
|
58
|
+
const features_js_1 = require("./features.js");
|
|
59
|
+
const extensions_js_1 = require("./extensions.js");
|
|
60
|
+
const state_js_1 = require("./state.js");
|
|
59
61
|
const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
|
|
60
|
-
const LAST_RUN_PATH = (0, node_path_1.join)(HICORTEX_HOME, "nightly-last-run.txt");
|
|
61
62
|
function readNightlyConfig(stateDir) {
|
|
62
63
|
try {
|
|
63
64
|
const configPath = (0, node_path_1.join)(stateDir, "config.json");
|
|
@@ -78,25 +79,24 @@ function readConfigLicenseKey(stateDir) {
|
|
|
78
79
|
return undefined;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
|
-
function readLastRun() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
// No file — first run
|
|
90
|
-
}
|
|
91
|
-
return new Date(0); // Process everything
|
|
82
|
+
function readLastRun(stateDir = HICORTEX_HOME) {
|
|
83
|
+
const ts = (0, state_js_1.loadState)(stateDir).lastNightly;
|
|
84
|
+
if (!ts)
|
|
85
|
+
return new Date(0); // First run — process everything
|
|
86
|
+
const d = new Date(ts);
|
|
87
|
+
return isNaN(d.getTime()) ? new Date(0) : d;
|
|
92
88
|
}
|
|
93
|
-
function writeLastRun() {
|
|
94
|
-
(0,
|
|
95
|
-
|
|
89
|
+
function writeLastRun(stateDir = HICORTEX_HOME) {
|
|
90
|
+
(0, state_js_1.updateState)((s) => {
|
|
91
|
+
s.lastNightly = new Date().toISOString();
|
|
92
|
+
return s;
|
|
93
|
+
}, stateDir);
|
|
96
94
|
}
|
|
97
95
|
async function runNightly(options = {}) {
|
|
98
96
|
const dryRun = options.dryRun ?? false;
|
|
99
97
|
const stateDir = options.stateDir ?? HICORTEX_HOME;
|
|
98
|
+
// One-time migration of legacy state files (no-op if state.json exists)
|
|
99
|
+
(0, state_js_1.migrateLegacyState)(stateDir);
|
|
100
100
|
// Check mode: client or server
|
|
101
101
|
const savedConfig = readNightlyConfig(stateDir);
|
|
102
102
|
if (savedConfig?.mode === "client") {
|
|
@@ -109,9 +109,9 @@ async function runNightly(options = {}) {
|
|
|
109
109
|
// Init DB
|
|
110
110
|
const db = (0, db_js_1.initDb)(dbPath);
|
|
111
111
|
try {
|
|
112
|
-
// License: read from config file or env var
|
|
112
|
+
// License: read from config file or env var, init feature cache
|
|
113
113
|
const licenseKey = readConfigLicenseKey(stateDir) ?? process.env.HICORTEX_LICENSE_KEY;
|
|
114
|
-
await (0,
|
|
114
|
+
await (0, features_js_1.initFeatures)(licenseKey, stateDir);
|
|
115
115
|
// Init LLM: check config.json first, then auto-detect
|
|
116
116
|
let llmConfig;
|
|
117
117
|
const savedConfig = readNightlyConfig(stateDir);
|
|
@@ -182,7 +182,6 @@ async function runNightly(options = {}) {
|
|
|
182
182
|
}
|
|
183
183
|
// Step 2: Distill each session
|
|
184
184
|
let memoriesIngested = 0;
|
|
185
|
-
const features = (0, license_js_1.getFeatures)(stateDir);
|
|
186
185
|
// Detect safe chunk size based on model context window
|
|
187
186
|
const chunkSize = await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.distillModel ?? llmConfig.model, llmConfig.baseUrl);
|
|
188
187
|
for (const batch of batches) {
|
|
@@ -197,8 +196,8 @@ async function runNightly(options = {}) {
|
|
|
197
196
|
continue;
|
|
198
197
|
}
|
|
199
198
|
// Check cap before distilling
|
|
200
|
-
if (
|
|
201
|
-
console.log(`[hicortex] Free tier limit (${
|
|
199
|
+
if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
|
|
200
|
+
console.log(`[hicortex] Free tier limit (${(0, features_js_1.maxMemoriesAllowed)()} memories). Skipping new ingestion. ` +
|
|
202
201
|
`Upgrade: https://hicortex.gamaze.com/`);
|
|
203
202
|
break;
|
|
204
203
|
}
|
|
@@ -238,7 +237,7 @@ async function runNightly(options = {}) {
|
|
|
238
237
|
}
|
|
239
238
|
// Step 4: Inject lessons into CLAUDE.md
|
|
240
239
|
if (!dryRun) {
|
|
241
|
-
const injection = (0, claude_md_js_1.injectLessons)(db, { stateDir });
|
|
240
|
+
const injection = await (0, claude_md_js_1.injectLessons)(db, { stateDir });
|
|
242
241
|
console.log(`[hicortex] CLAUDE.md updated: ${injection.lessonsCount} lessons at ${injection.path}`);
|
|
243
242
|
}
|
|
244
243
|
// Step 5: Update last-run timestamp
|
|
@@ -400,7 +399,88 @@ async function runClientNightly(config, dryRun) {
|
|
|
400
399
|
console.error(`[hicortex] Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
401
400
|
}
|
|
402
401
|
}
|
|
402
|
+
// Inject lessons from server into CLAUDE.md
|
|
403
|
+
if (!dryRun) {
|
|
404
|
+
try {
|
|
405
|
+
await injectLessonsFromServer(serverUrl, authToken);
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
console.error(`[hicortex] CLAUDE.md injection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
403
411
|
if (!dryRun)
|
|
404
412
|
writeLastRun();
|
|
405
413
|
console.log(`[hicortex] Client nightly complete: ${memoriesIngested} memories from ${sessionsSent} sessions → ${serverUrl}`);
|
|
406
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Fetch lessons + memory index from server and inject into CLAUDE.md.
|
|
417
|
+
* Client mode equivalent of the server's injectLessons(db, ...).
|
|
418
|
+
*/
|
|
419
|
+
async function injectLessonsFromServer(serverUrl, authToken) {
|
|
420
|
+
const resp = await fetch(`${serverUrl}/lessons`, {
|
|
421
|
+
headers: authToken ? { "Authorization": `Bearer ${authToken}` } : {},
|
|
422
|
+
signal: AbortSignal.timeout(10_000),
|
|
423
|
+
});
|
|
424
|
+
if (!resp.ok) {
|
|
425
|
+
console.log(`[hicortex] Could not fetch lessons from server (${resp.status})`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const data = await resp.json();
|
|
429
|
+
const maxLessons = (0, features_js_1.lessonsLimit)();
|
|
430
|
+
const selected = await (0, extensions_js_1.getLessonSelector)().select(data.lessons, { maxLessons });
|
|
431
|
+
// Format lessons
|
|
432
|
+
const lessonLines = selected.map((l) => {
|
|
433
|
+
const titleMatch = l.content.match(/## Lesson: (.+)/);
|
|
434
|
+
const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
|
|
435
|
+
const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
|
|
436
|
+
const title = titleMatch ? titleMatch[1] : l.content.slice(0, 150);
|
|
437
|
+
const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
|
|
438
|
+
return `- ${title}${meta ? ` (${meta})` : ""}`;
|
|
439
|
+
});
|
|
440
|
+
// Format project index
|
|
441
|
+
const projectIndex = data.index.projects.map(p => `${p.name}: ${p.count}`);
|
|
442
|
+
// Build block
|
|
443
|
+
const START_MARKER = "<!-- HICORTEX-LEARNINGS:START -->";
|
|
444
|
+
const END_MARKER = "<!-- HICORTEX-LEARNINGS:END -->";
|
|
445
|
+
const blockParts = [START_MARKER, "## Hicortex Memory"];
|
|
446
|
+
blockParts.push("", "You have access to shared long-term memory across all agents and sessions.", "BEFORE making decisions, search memory: `hicortex_search` for prior decisions on the same topic.", "Use `hicortex_context` at session start for recent project state.");
|
|
447
|
+
if (lessonLines.length > 0) {
|
|
448
|
+
blockParts.push("", "### Lessons (updated nightly)");
|
|
449
|
+
blockParts.push(...lessonLines);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
blockParts.push("", "### Getting Started");
|
|
453
|
+
blockParts.push("- Search past decisions with `hicortex_search` before starting work");
|
|
454
|
+
blockParts.push("- Save important decisions with `hicortex_ingest`");
|
|
455
|
+
blockParts.push("- Lessons will appear here after the first nightly run");
|
|
456
|
+
}
|
|
457
|
+
if (projectIndex.length > 0) {
|
|
458
|
+
blockParts.push("", "### Memory Index");
|
|
459
|
+
blockParts.push(projectIndex.join(" | "));
|
|
460
|
+
blockParts.push(`${data.index.total} memories, ${data.index.lessonCount} lessons, ${data.index.sourceCount} agents. Search with \`hicortex_search\`.`);
|
|
461
|
+
}
|
|
462
|
+
blockParts.push(END_MARKER);
|
|
463
|
+
const block = blockParts.join("\n");
|
|
464
|
+
// Write to CLAUDE.md (uses fs/path/os already imported at top of file)
|
|
465
|
+
const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
|
|
466
|
+
let content = "";
|
|
467
|
+
try {
|
|
468
|
+
content = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
|
|
469
|
+
}
|
|
470
|
+
catch { }
|
|
471
|
+
const startIdx = content.indexOf(START_MARKER);
|
|
472
|
+
const endIdx = content.indexOf(END_MARKER);
|
|
473
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
474
|
+
content = content.slice(0, startIdx) + block + content.slice(endIdx + END_MARKER.length);
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
if (content.length > 0 && !content.endsWith("\n"))
|
|
478
|
+
content += "\n";
|
|
479
|
+
if (content.length > 0)
|
|
480
|
+
content += "\n";
|
|
481
|
+
content += block + "\n";
|
|
482
|
+
}
|
|
483
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(claudeMdPath), { recursive: true });
|
|
484
|
+
(0, node_fs_1.writeFileSync)(claudeMdPath, content);
|
|
485
|
+
console.log(`[hicortex] CLAUDE.md updated: ${lessonLines.length} lessons, ${data.index.total} memories indexed`);
|
|
486
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized state management — single ~/.hicortex/state.json file.
|
|
3
|
+
*
|
|
4
|
+
* Replaces 4 separate state files used by previous versions:
|
|
5
|
+
* - nightly-last-run.txt → state.lastNightly
|
|
6
|
+
* - last-consolidated.txt → state.lastConsolidated
|
|
7
|
+
* - tier.json → state.tier
|
|
8
|
+
* - license-validated.txt → state.tier.validatedAt (subsumed)
|
|
9
|
+
*
|
|
10
|
+
* Why one file:
|
|
11
|
+
* - Atomic writes (write to temp + rename)
|
|
12
|
+
* - Easier debugging (one file to inspect)
|
|
13
|
+
* - No filesystem chatter from multiple separate writes
|
|
14
|
+
* - Single migration path going forward
|
|
15
|
+
*
|
|
16
|
+
* Note: ~/.hicortex/config.json is intentionally NOT merged here. Config is
|
|
17
|
+
* user-edited and tracked separately from machine state.
|
|
18
|
+
*/
|
|
19
|
+
import type { LicenseInfo } from "./types.js";
|
|
20
|
+
/** Persisted tier information — reflects the last successful validation. */
|
|
21
|
+
export interface PersistedTier {
|
|
22
|
+
/** Tier name from the validation API response. */
|
|
23
|
+
tier: LicenseInfo["tier"];
|
|
24
|
+
/** ISO timestamp of when this tier was validated against the API. */
|
|
25
|
+
validatedAt: string;
|
|
26
|
+
/** Cached features object — used by features.ts and offline fallback. */
|
|
27
|
+
features: LicenseInfo["features"];
|
|
28
|
+
}
|
|
29
|
+
export interface HicortexState {
|
|
30
|
+
/** ISO timestamp of the last nightly transcript scan watermark. */
|
|
31
|
+
lastNightly?: string;
|
|
32
|
+
/** ISO timestamp of the last consolidation pipeline run. */
|
|
33
|
+
lastConsolidated?: string;
|
|
34
|
+
/** Last-known license tier (replaces tier.json + license-validated.txt). */
|
|
35
|
+
tier?: PersistedTier;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load the state file. Returns an empty state if the file is missing
|
|
39
|
+
* or corrupted (callers should handle missing fields with defaults).
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadState(stateDir?: string): HicortexState;
|
|
42
|
+
/**
|
|
43
|
+
* Atomically write the state file. Uses write-to-temp + rename so a crash
|
|
44
|
+
* during write cannot leave a half-written state.json on disk.
|
|
45
|
+
*/
|
|
46
|
+
export declare function saveState(state: HicortexState, stateDir?: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Read-modify-write helper. The updater receives the current state and
|
|
49
|
+
* returns the next state (or void if it mutates in place).
|
|
50
|
+
*/
|
|
51
|
+
export declare function updateState(updater: (state: HicortexState) => HicortexState | void, stateDir?: string): HicortexState;
|
|
52
|
+
/**
|
|
53
|
+
* One-time migration from the 4 legacy state files to state.json.
|
|
54
|
+
*
|
|
55
|
+
* Behaviour:
|
|
56
|
+
* 1. If state.json already exists, do nothing (and clean up any leftover
|
|
57
|
+
* legacy files from a previously interrupted migration).
|
|
58
|
+
* 2. Otherwise, read whichever legacy files exist, build a HicortexState,
|
|
59
|
+
* write state.json, and delete the legacy files.
|
|
60
|
+
*
|
|
61
|
+
* Idempotent: safe to call on every boot.
|
|
62
|
+
* Returns true if migration ran, false if state.json already existed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function migrateLegacyState(stateDir?: string): boolean;
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Centralized state management — single ~/.hicortex/state.json file.
|
|
4
|
+
*
|
|
5
|
+
* Replaces 4 separate state files used by previous versions:
|
|
6
|
+
* - nightly-last-run.txt → state.lastNightly
|
|
7
|
+
* - last-consolidated.txt → state.lastConsolidated
|
|
8
|
+
* - tier.json → state.tier
|
|
9
|
+
* - license-validated.txt → state.tier.validatedAt (subsumed)
|
|
10
|
+
*
|
|
11
|
+
* Why one file:
|
|
12
|
+
* - Atomic writes (write to temp + rename)
|
|
13
|
+
* - Easier debugging (one file to inspect)
|
|
14
|
+
* - No filesystem chatter from multiple separate writes
|
|
15
|
+
* - Single migration path going forward
|
|
16
|
+
*
|
|
17
|
+
* Note: ~/.hicortex/config.json is intentionally NOT merged here. Config is
|
|
18
|
+
* user-edited and tracked separately from machine state.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.loadState = loadState;
|
|
22
|
+
exports.saveState = saveState;
|
|
23
|
+
exports.updateState = updateState;
|
|
24
|
+
exports.migrateLegacyState = migrateLegacyState;
|
|
25
|
+
const node_fs_1 = require("node:fs");
|
|
26
|
+
const node_path_1 = require("node:path");
|
|
27
|
+
const node_os_1 = require("node:os");
|
|
28
|
+
const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
|
|
29
|
+
const STATE_FILE = "state.json";
|
|
30
|
+
/**
|
|
31
|
+
* Load the state file. Returns an empty state if the file is missing
|
|
32
|
+
* or corrupted (callers should handle missing fields with defaults).
|
|
33
|
+
*/
|
|
34
|
+
function loadState(stateDir = HICORTEX_HOME) {
|
|
35
|
+
const path = (0, node_path_1.join)(stateDir, STATE_FILE);
|
|
36
|
+
try {
|
|
37
|
+
const raw = (0, node_fs_1.readFileSync)(path, "utf-8");
|
|
38
|
+
const parsed = JSON.parse(raw);
|
|
39
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Atomically write the state file. Uses write-to-temp + rename so a crash
|
|
47
|
+
* during write cannot leave a half-written state.json on disk.
|
|
48
|
+
*/
|
|
49
|
+
function saveState(state, stateDir = HICORTEX_HOME) {
|
|
50
|
+
try {
|
|
51
|
+
(0, node_fs_1.mkdirSync)(stateDir, { recursive: true });
|
|
52
|
+
const path = (0, node_path_1.join)(stateDir, STATE_FILE);
|
|
53
|
+
const tmp = `${path}.tmp`;
|
|
54
|
+
(0, node_fs_1.writeFileSync)(tmp, JSON.stringify(state, null, 2));
|
|
55
|
+
(0, node_fs_1.renameSync)(tmp, path);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.warn(`[hicortex] Failed to save state: ${err instanceof Error ? err.message : String(err)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read-modify-write helper. The updater receives the current state and
|
|
63
|
+
* returns the next state (or void if it mutates in place).
|
|
64
|
+
*/
|
|
65
|
+
function updateState(updater, stateDir = HICORTEX_HOME) {
|
|
66
|
+
const current = loadState(stateDir);
|
|
67
|
+
const result = updater(current);
|
|
68
|
+
const next = result ?? current;
|
|
69
|
+
saveState(next, stateDir);
|
|
70
|
+
return next;
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// One-time legacy migration
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
const LEGACY_FILES = [
|
|
76
|
+
"nightly-last-run.txt",
|
|
77
|
+
"last-consolidated.txt",
|
|
78
|
+
"license-validated.txt",
|
|
79
|
+
"tier.json",
|
|
80
|
+
];
|
|
81
|
+
/**
|
|
82
|
+
* One-time migration from the 4 legacy state files to state.json.
|
|
83
|
+
*
|
|
84
|
+
* Behaviour:
|
|
85
|
+
* 1. If state.json already exists, do nothing (and clean up any leftover
|
|
86
|
+
* legacy files from a previously interrupted migration).
|
|
87
|
+
* 2. Otherwise, read whichever legacy files exist, build a HicortexState,
|
|
88
|
+
* write state.json, and delete the legacy files.
|
|
89
|
+
*
|
|
90
|
+
* Idempotent: safe to call on every boot.
|
|
91
|
+
* Returns true if migration ran, false if state.json already existed.
|
|
92
|
+
*/
|
|
93
|
+
function migrateLegacyState(stateDir = HICORTEX_HOME) {
|
|
94
|
+
const statePath = (0, node_path_1.join)(stateDir, STATE_FILE);
|
|
95
|
+
if ((0, node_fs_1.existsSync)(statePath)) {
|
|
96
|
+
cleanupLegacyFiles(stateDir);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const state = {};
|
|
100
|
+
let foundAny = false;
|
|
101
|
+
// 1. nightly-last-run.txt → state.lastNightly
|
|
102
|
+
const ln = readLegacyText(stateDir, "nightly-last-run.txt");
|
|
103
|
+
if (ln) {
|
|
104
|
+
state.lastNightly = ln;
|
|
105
|
+
foundAny = true;
|
|
106
|
+
}
|
|
107
|
+
// 2. last-consolidated.txt → state.lastConsolidated
|
|
108
|
+
const lc = readLegacyText(stateDir, "last-consolidated.txt");
|
|
109
|
+
if (lc) {
|
|
110
|
+
state.lastConsolidated = lc;
|
|
111
|
+
foundAny = true;
|
|
112
|
+
}
|
|
113
|
+
// 3. tier.json → state.tier (full object)
|
|
114
|
+
const tierRaw = readLegacyText(stateDir, "tier.json");
|
|
115
|
+
if (tierRaw) {
|
|
116
|
+
try {
|
|
117
|
+
state.tier = JSON.parse(tierRaw);
|
|
118
|
+
foundAny = true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Corrupted tier.json — ignore
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// 4. license-validated.txt → state.tier.validatedAt (only if no tier yet)
|
|
125
|
+
// (subsumed by tier.json when it exists)
|
|
126
|
+
if (!state.tier) {
|
|
127
|
+
const lv = readLegacyText(stateDir, "license-validated.txt");
|
|
128
|
+
if (lv) {
|
|
129
|
+
// We have a validation timestamp but no tier object — preserve as
|
|
130
|
+
// a placeholder so offline grace can still work. The features module
|
|
131
|
+
// will re-validate on next boot to get the full features back.
|
|
132
|
+
// We deliberately don't fabricate features here.
|
|
133
|
+
foundAny = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (foundAny) {
|
|
137
|
+
saveState(state, stateDir);
|
|
138
|
+
cleanupLegacyFiles(stateDir);
|
|
139
|
+
console.log("[hicortex] Migrated legacy state files to ~/.hicortex/state.json");
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
function readLegacyText(stateDir, name) {
|
|
145
|
+
try {
|
|
146
|
+
const raw = (0, node_fs_1.readFileSync)((0, node_path_1.join)(stateDir, name), "utf-8").trim();
|
|
147
|
+
return raw || null;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function cleanupLegacyFiles(stateDir) {
|
|
154
|
+
for (const name of LEGACY_FILES) {
|
|
155
|
+
try {
|
|
156
|
+
(0, node_fs_1.unlinkSync)((0, node_path_1.join)(stateDir, name));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// File didn't exist — fine
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Human-like memory for self-improving AI agents. Automatic capturing, nightly reflection, and cross-agent learning. Works with Claude Code and OpenClaw.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -45,13 +45,17 @@
|
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=18"
|
|
47
47
|
},
|
|
48
|
-
"license": "
|
|
48
|
+
"license": "MIT",
|
|
49
49
|
"homepage": "https://hicortex.gamaze.com",
|
|
50
50
|
"repository": {
|
|
51
51
|
"type": "git",
|
|
52
|
-
"url": "https://github.com/
|
|
52
|
+
"url": "https://github.com/gamaze-labs/hicortex.git",
|
|
53
53
|
"directory": "packages/openclaw-plugin"
|
|
54
54
|
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/gamaze-labs/hicortex/issues"
|
|
57
|
+
},
|
|
58
|
+
"author": "Mattias Hansson",
|
|
55
59
|
"dependencies": {
|
|
56
60
|
"@huggingface/transformers": "^3.0.0",
|
|
57
61
|
"@modelcontextprotocol/sdk": "^1.28.0",
|