@gamaze/hicortex 0.4.3 → 0.4.5

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/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 license_js_1 = require("./license.js");
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
- try {
83
- const ts = (0, node_fs_1.readFileSync)(LAST_RUN_PATH, "utf-8").trim();
84
- const d = new Date(ts);
85
- if (!isNaN(d.getTime()))
86
- return d;
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, node_fs_1.mkdirSync)(HICORTEX_HOME, { recursive: true });
95
- (0, node_fs_1.writeFileSync)(LAST_RUN_PATH, new Date().toISOString());
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, license_js_1.validateLicense)(licenseKey, stateDir);
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 (features.maxMemories > 0 && storage.countMemories(db) >= features.maxMemories) {
201
- console.log(`[hicortex] Free tier limit (${features.maxMemories} memories). Skipping new ingestion. ` +
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
@@ -427,8 +426,8 @@ async function injectLessonsFromServer(serverUrl, authToken) {
427
426
  return;
428
427
  }
429
428
  const data = await resp.json();
430
- const maxLessons = 10;
431
- const selected = data.lessons.slice(0, maxLessons);
429
+ const maxLessons = (0, features_js_1.lessonsLimit)();
430
+ const selected = await (0, extensions_js_1.getLessonSelector)().select(data.lessons, { maxLessons });
432
431
  // Format lessons
433
432
  const lessonLines = selected.map((l) => {
434
433
  const titleMatch = l.content.match(/## Lesson: (.+)/);
@@ -462,14 +461,11 @@ async function injectLessonsFromServer(serverUrl, authToken) {
462
461
  }
463
462
  blockParts.push(END_MARKER);
464
463
  const block = blockParts.join("\n");
465
- // Write to CLAUDE.md
466
- const { readFileSync, writeFileSync, mkdirSync } = await import("node:fs");
467
- const { join, dirname } = await import("node:path");
468
- const { homedir } = await import("node:os");
469
- const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
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");
470
466
  let content = "";
471
467
  try {
472
- content = readFileSync(claudeMdPath, "utf-8");
468
+ content = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
473
469
  }
474
470
  catch { }
475
471
  const startIdx = content.indexOf(START_MARKER);
@@ -484,7 +480,7 @@ async function injectLessonsFromServer(serverUrl, authToken) {
484
480
  content += "\n";
485
481
  content += block + "\n";
486
482
  }
487
- mkdirSync(dirname(claudeMdPath), { recursive: true });
488
- writeFileSync(claudeMdPath, content);
483
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(claudeMdPath), { recursive: true });
484
+ (0, node_fs_1.writeFileSync)(claudeMdPath, content);
489
485
  console.log(`[hicortex] CLAUDE.md updated: ${lessonLines.length} lessons, ${data.index.total} memories indexed`);
490
486
  }
@@ -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
+ }
@@ -2,7 +2,7 @@
2
2
  "id": "hicortex",
3
3
  "name": "Hicortex — Long-term Memory That Learns",
4
4
  "description": "Your agents remember past decisions, avoid repeated mistakes, and get smarter every day. Nightly reflection generates actionable lessons that automatically update agent behavior.",
5
- "version": "0.3.11",
5
+ "version": "0.4.5",
6
6
  "kind": "lifecycle",
7
7
  "skills": ["./skills/hicortex-memory", "./skills/hicortex-learn", "./skills/hicortex-activate"],
8
8
  "configSchema": {
@@ -10,7 +10,7 @@
10
10
  "properties": {
11
11
  "licenseKey": {
12
12
  "type": "string",
13
- "description": "Hicortex license key (hctx-...). Leave empty for free tier (100 memory cap)."
13
+ "description": "Hicortex license key (hctx-...). Leave empty for free tier (250 memory cap)."
14
14
  },
15
15
  "llmBaseUrl": {
16
16
  "type": "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
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": "UNLICENSED",
48
+ "license": "MIT",
49
49
  "homepage": "https://hicortex.gamaze.com",
50
50
  "repository": {
51
51
  "type": "git",
52
- "url": "https://github.com/mha33/hicortex.git",
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",
@@ -23,12 +23,12 @@ When invoked with `/learn <text>`, store the learning in long-term memory via hi
23
23
  ## Example
24
24
 
25
25
  ```
26
- /learn z.ai API uses Bearer auth on all endpoints, not x-api-key
26
+ /learn always check provider docs before assuming an API uses the same auth scheme as OpenAI
27
27
  ```
28
28
 
29
29
  Becomes:
30
30
  ```
31
- hicortex_ingest(content="LEARNING: z.ai API uses Bearer auth on all three endpoints (paas, coding, anthropic). Not x-api-key. (2026-03-24)", project="global", memory_type="lesson")
31
+ hicortex_ingest(content="LEARNING: always check provider docs before assuming an API uses the same auth scheme as OpenAI header names and token formats vary widely (Bearer vs x-api-key vs custom). (2026-04-07)", project="global", memory_type="lesson")
32
32
  ```
33
33
 
34
34
  ## Rules