@gamaze/hicortex 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Your agents learn from every session — successes and mistakes. Hicortex captures experiences, distills lessons, and applies them automatically. Connect multiple agents to shared memory and they improve together, overnight.
4
4
 
5
- Works with **Claude Code** (primary), **OpenClaw** (in-process plugin), and any MCP-compatible agent.
5
+ Works with **Claude Code**, **Pi**, **OpenClaw**, and any MCP-compatible agent.
6
6
 
7
7
  **Website:** [hicortex.gamaze.com](https://hicortex.gamaze.com) · **Docs:** [hicortex.gamaze.com/docs](https://hicortex.gamaze.com/docs/)
8
8
 
@@ -39,8 +39,8 @@ openclaw gateway restart
39
39
 
40
40
  | When | What | How |
41
41
  |------|------|-----|
42
- | Agent start | Recent lessons injected into context | CLAUDE.md block / OC hook |
43
- | Agent end | Conversation captured | CC: nightly transcript scan / OC: hook |
42
+ | Agent start | Recent lessons injected into context | CLAUDE.md / EXPERIENCE.md / OC hook |
43
+ | Agent end | Conversation captured | CC + Pi: nightly transcript scan / OC: hook |
44
44
  | Nightly | Distill → score → reflect → link → inject | Automatic pipeline |
45
45
 
46
46
  ## Agent Tools (MCP)
@@ -159,7 +159,7 @@ Canonical location: `~/.hicortex/hicortex.db`. Existing OC installations at `~/.
159
159
  ## Development
160
160
 
161
161
  ```bash
162
- cd packages/openclaw-plugin
162
+ cd packages/hicortex
163
163
  npm install
164
164
  npm run build
165
165
  npm test
package/dist/distiller.js CHANGED
@@ -175,16 +175,25 @@ function extractConversationText(messages) {
175
175
  continue;
176
176
  if (m.isSidechain)
177
177
  continue;
178
- // Extract content OpenClaw messages have content at top level;
179
- // Python distiller format has message.content
180
- const content = m.content ?? m.message?.content;
178
+ // Extract the message role from whichever format we're dealing with:
179
+ // OC hook: m.role = "user" | "assistant"
180
+ // CC JSONL: m.type = "user" | "assistant"
181
+ // Pi JSONL: m.message.role = "user" | "assistant" | "toolResult"
182
+ const nestedMsg = m.message;
183
+ const msgRole = String(m.role ?? nestedMsg?.role ?? m.type ?? "");
184
+ // Skip tool results — they're noisy (file contents, command output) and
185
+ // add bulk without much extractable knowledge for distillation.
186
+ if (msgRole === "toolResult" || msgRole === "tool_result")
187
+ continue;
188
+ // Extract content — OC has content at top level; CC/Pi have message.content
189
+ const content = m.content ?? nestedMsg?.content;
181
190
  if (content === undefined || content === null)
182
191
  continue;
183
192
  let text = extractTextFromContent(content);
184
193
  text = cleanMessageContent(text);
185
194
  if (text.length < 20)
186
195
  continue;
187
- const role = m.role === "user" || m.type === "user" ? "USER" : "ASSISTANT";
196
+ const role = msgRole === "user" ? "USER" : "ASSISTANT";
188
197
  parts.push(`${role}: ${text}`);
189
198
  }
190
199
  return parts.join("\n\n");
@@ -123,4 +123,51 @@ export declare function setExtensions(ext: Partial<typeof activeExtensions>): vo
123
123
  export declare function getLessonSelector(): LessonSelector;
124
124
  /** Get the active PromptStrategy (default unless Pro is loaded). */
125
125
  export declare function getPromptStrategy(): PromptStrategy;
126
+ /**
127
+ * The object passed to a Pro package's `activate()` function at boot.
128
+ * Pro packages receive this to register their extensions and access OSS
129
+ * runtime APIs they need.
130
+ *
131
+ * Design rationale: Pro code never STATICALLY imports from the OSS client.
132
+ * All runtime access is through this context object. This has two benefits:
133
+ * 1. Pro bundles are self-contained — they don't have `require("../...")`
134
+ * calls that would break when the tarball is installed to
135
+ * ~/.hicortex/pro/ at runtime.
136
+ * 2. Pro code can only access what the OSS client exposes here, so the
137
+ * blast radius of a malicious/buggy Pro release is contained.
138
+ *
139
+ * Type-only imports of `LessonSelector`, `PromptStrategy` etc. in Pro code
140
+ * are fine — they're erased at compile time and produce no runtime imports.
141
+ */
142
+ export interface ProActivationContext {
143
+ /** Register a lesson selector implementation. */
144
+ setSelector(selector: LessonSelector): void;
145
+ /** Register a prompt strategy implementation. */
146
+ setPrompts(prompts: PromptStrategy): void;
147
+ /** The version of the OSS host (from package.json). Pro can use this
148
+ * to gate features against host compatibility. */
149
+ hostVersion: string;
150
+ /** Log through the OSS logging surface so Pro logs get the [hicortex]
151
+ * prefix and unified formatting. */
152
+ log(message: string): void;
153
+ }
154
+ /**
155
+ * The shape Pro packages must export as their default export.
156
+ * See `packages/hicortex/src/pro/index.ts` for the reference impl.
157
+ */
158
+ export interface ProPackage {
159
+ /** Called once at OSS boot if a Pro license is valid and the Pro
160
+ * tarball has been downloaded + extracted. Should register extensions
161
+ * via the context and return. Errors abort Pro activation but do not
162
+ * abort the OSS host. */
163
+ activate(ctx: ProActivationContext): void | Promise<void>;
164
+ }
165
+ /**
166
+ * Build an activation context for a Pro package. Called from the Pro
167
+ * loader in features.ts / pro-loader.ts.
168
+ *
169
+ * Keep this function small — it's the ONLY surface a Pro package gets.
170
+ * Expanding it expands the attack surface, so add fields deliberately.
171
+ */
172
+ export declare function createProActivationContext(hostVersion: string): ProActivationContext;
126
173
  export {};
@@ -34,6 +34,7 @@ exports.defaultPromptStrategy = exports.defaultLessonSelector = void 0;
34
34
  exports.setExtensions = setExtensions;
35
35
  exports.getLessonSelector = getLessonSelector;
36
36
  exports.getPromptStrategy = getPromptStrategy;
37
+ exports.createProActivationContext = createProActivationContext;
37
38
  /**
38
39
  * Default LessonSelector — preserves current OSS behaviour exactly.
39
40
  * `slice(0, maxLessons)` over the candidate pool, no re-ranking.
@@ -152,3 +153,24 @@ function getLessonSelector() {
152
153
  function getPromptStrategy() {
153
154
  return activeExtensions.prompts;
154
155
  }
156
+ /**
157
+ * Build an activation context for a Pro package. Called from the Pro
158
+ * loader in features.ts / pro-loader.ts.
159
+ *
160
+ * Keep this function small — it's the ONLY surface a Pro package gets.
161
+ * Expanding it expands the attack surface, so add fields deliberately.
162
+ */
163
+ function createProActivationContext(hostVersion) {
164
+ return {
165
+ setSelector(selector) {
166
+ activeExtensions = { ...activeExtensions, selector };
167
+ },
168
+ setPrompts(prompts) {
169
+ activeExtensions = { ...activeExtensions, prompts };
170
+ },
171
+ hostVersion,
172
+ log(message) {
173
+ console.log(`[hicortex][pro] ${message}`);
174
+ },
175
+ };
176
+ }
@@ -22,7 +22,7 @@ import type { LicenseInfo } from "./types.js";
22
22
  * After this returns, sync getters (isPro, lessonsLimit, etc.) are deterministic
23
23
  * and reflect the user's actual tier — no more "free during validation window".
24
24
  */
25
- export declare function initFeatures(licenseKey: string | undefined, stateDir?: string): Promise<void>;
25
+ export declare function initFeatures(licenseKey: string | undefined, stateDir?: string, hostVersion?: string): Promise<void>;
26
26
  /** Are we on a paid tier (Pro, Team, Lifetime)? */
27
27
  export declare function isPro(): boolean;
28
28
  /** Memory count cap. -1 = unlimited (paid). */
package/dist/features.js CHANGED
@@ -54,7 +54,7 @@ function persistTier(stateDir, info) {
54
54
  * After this returns, sync getters (isPro, lessonsLimit, etc.) are deterministic
55
55
  * and reflect the user's actual tier — no more "free during validation window".
56
56
  */
57
- async function initFeatures(licenseKey, stateDir = DEFAULT_STATE_DIR) {
57
+ async function initFeatures(licenseKey, stateDir = DEFAULT_STATE_DIR, hostVersion = "0.0.0") {
58
58
  if (initialized)
59
59
  return;
60
60
  initialized = true;
@@ -66,7 +66,7 @@ async function initFeatures(licenseKey, stateDir = DEFAULT_STATE_DIR) {
66
66
  else {
67
67
  currentFeatures = FREE_FEATURES;
68
68
  }
69
- // Step 2: No key → free tier, done
69
+ // Step 2: No key → free tier, done. Pro loader is only run for paid tiers.
70
70
  if (!licenseKey)
71
71
  return;
72
72
  // Step 3: Validate
@@ -96,6 +96,20 @@ async function initFeatures(licenseKey, stateDir = DEFAULT_STATE_DIR) {
96
96
  // Keep persisted features
97
97
  });
98
98
  }
99
+ // Step 4: If the current tier is paid, try to load the Pro extension bundle.
100
+ // This is best-effort — if loading fails (network, missing tarball, bad
101
+ // extraction, Pro package throws on activate), OSS defaults remain in effect
102
+ // and the host keeps running. No user-visible crash.
103
+ if (isPro()) {
104
+ try {
105
+ const { loadPro } = await import("./pro-loader.js");
106
+ await loadPro(licenseKey, stateDir, hostVersion);
107
+ }
108
+ catch (err) {
109
+ const msg = err instanceof Error ? err.message : String(err);
110
+ console.warn(`[hicortex][pro] Pro loader failed to import: ${msg}`);
111
+ }
112
+ }
99
113
  }
100
114
  // ---------------------------------------------------------------------------
101
115
  // Public API — sync getters used everywhere in the codebase
package/dist/nightly.js CHANGED
@@ -54,6 +54,7 @@ const storage = __importStar(require("./storage.js"));
54
54
  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
+ const pi_transcript_reader_js_1 = require("./pi-transcript-reader.js");
57
58
  const claude_md_js_1 = require("./claude-md.js");
58
59
  const features_js_1 = require("./features.js");
59
60
  const extensions_js_1 = require("./extensions.js");
@@ -171,14 +172,20 @@ async function runNightly(options = {}) {
171
172
  ? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
172
173
  : llmConfig.distillModel ?? "";
173
174
  console.log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}`);
174
- // Step 1: Read new CC transcripts
175
+ // Step 1: Read new transcripts (CC + Pi)
175
176
  const since = readLastRun();
176
- console.log(`[hicortex] Reading CC transcripts since ${since.toISOString()}`);
177
- const batches = (0, transcript_reader_js_1.readCcTranscripts)(since);
178
- console.log(`[hicortex] Found ${batches.length} new session(s)`);
177
+ console.log(`[hicortex] Reading transcripts since ${since.toISOString()}`);
178
+ const ccBatches = (0, transcript_reader_js_1.readCcTranscripts)(since);
179
+ const piBatches = (0, pi_transcript_reader_js_1.readPiTranscripts)(since);
180
+ const batches = [...ccBatches, ...piBatches];
181
+ if (ccBatches.length > 0)
182
+ console.log(`[hicortex] Found ${ccBatches.length} CC session(s)`);
183
+ if (piBatches.length > 0)
184
+ console.log(`[hicortex] Found ${piBatches.length} Pi session(s)`);
185
+ console.log(`[hicortex] Total: ${batches.length} new session(s)`);
179
186
  if (batches.length === 0 && !dryRun) {
180
187
  // Still run consolidation — there may be unscored memories from OC
181
- console.log(`[hicortex] No new CC transcripts. Running consolidation only.`);
188
+ console.log(`[hicortex] No new transcripts. Running consolidation only.`);
182
189
  }
183
190
  // Step 2: Distill each session
184
191
  let memoriesIngested = 0;
@@ -268,10 +275,15 @@ async function runNightly(options = {}) {
268
275
  console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
269
276
  (report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
270
277
  }
271
- // Step 4: Inject lessons into CLAUDE.md
278
+ // Step 4: Inject lessons into the target file (CLAUDE.md or EXPERIENCE.md
279
+ // or custom path — configurable via lessonTarget in config.json)
272
280
  if (!dryRun) {
273
- const injection = await (0, claude_md_js_1.injectLessons)(db, { stateDir });
274
- console.log(`[hicortex] CLAUDE.md updated: ${injection.lessonsCount} lessons at ${injection.path}`);
281
+ const lessonTarget = savedConfig?.lessonTarget;
282
+ const injection = await (0, claude_md_js_1.injectLessons)(db, {
283
+ claudeMdPath: lessonTarget,
284
+ stateDir,
285
+ });
286
+ console.log(`[hicortex] Lessons updated: ${injection.lessonsCount} lessons at ${injection.path}`);
275
287
  }
276
288
  // Step 5: Update last-run timestamp
277
289
  // CRITICAL: only advance lastRun if every session was processed without
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pi agent transcript reader.
3
+ *
4
+ * Reads session .jsonl files from ~/.pi/agent/sessions/ in the Pi coding
5
+ * agent's format. Returns the same TranscriptBatch shape as the CC reader
6
+ * (transcript-reader.ts) so the downstream distillation pipeline is
7
+ * format-agnostic.
8
+ *
9
+ * Pi JSONL format (version 3):
10
+ * session — session header: {id, cwd, timestamp, version}
11
+ * model_change — provider + model switch (skip for distillation)
12
+ * thinking_level_change — thinking mode (skip)
13
+ * message — user/assistant/toolResult conversation entries
14
+ * custom — extension events (skip)
15
+ * custom_message — extension messages (skip)
16
+ *
17
+ * Directory layout:
18
+ * ~/.pi/agent/sessions/
19
+ * --home-agents-Agents-raider--/
20
+ * 2026-04-10T18-37-44-615Z_<uuid>.jsonl
21
+ * 2026-04-11T07-51-28-282Z_<uuid>.jsonl
22
+ * --home-agents-Development-MAIC--/
23
+ * ...
24
+ *
25
+ * The encoded-cwd uses double-dash separators: /home/agents/Agents/raider
26
+ * becomes --home-agents-Agents-raider--. The session header's `cwd` field
27
+ * is the canonical path; the directory name is a filesystem-safe encoding.
28
+ */
29
+ export interface TranscriptBatch {
30
+ sessionId: string;
31
+ projectName: string;
32
+ date: string;
33
+ entries: unknown[];
34
+ }
35
+ /**
36
+ * Read Pi session transcripts modified after `since`.
37
+ *
38
+ * Scans the Pi sessions directory for .jsonl files, filters by mtime,
39
+ * parses each into a TranscriptBatch.
40
+ *
41
+ * @param since Only return sessions with mtime > this date
42
+ * @param sessionsDir Override the session directory (default: ~/.pi/agent/sessions/)
43
+ */
44
+ export declare function readPiTranscripts(since: Date, sessionsDir?: string): TranscriptBatch[];
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ /**
3
+ * Pi agent transcript reader.
4
+ *
5
+ * Reads session .jsonl files from ~/.pi/agent/sessions/ in the Pi coding
6
+ * agent's format. Returns the same TranscriptBatch shape as the CC reader
7
+ * (transcript-reader.ts) so the downstream distillation pipeline is
8
+ * format-agnostic.
9
+ *
10
+ * Pi JSONL format (version 3):
11
+ * session — session header: {id, cwd, timestamp, version}
12
+ * model_change — provider + model switch (skip for distillation)
13
+ * thinking_level_change — thinking mode (skip)
14
+ * message — user/assistant/toolResult conversation entries
15
+ * custom — extension events (skip)
16
+ * custom_message — extension messages (skip)
17
+ *
18
+ * Directory layout:
19
+ * ~/.pi/agent/sessions/
20
+ * --home-agents-Agents-raider--/
21
+ * 2026-04-10T18-37-44-615Z_<uuid>.jsonl
22
+ * 2026-04-11T07-51-28-282Z_<uuid>.jsonl
23
+ * --home-agents-Development-MAIC--/
24
+ * ...
25
+ *
26
+ * The encoded-cwd uses double-dash separators: /home/agents/Agents/raider
27
+ * becomes --home-agents-Agents-raider--. The session header's `cwd` field
28
+ * is the canonical path; the directory name is a filesystem-safe encoding.
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.readPiTranscripts = readPiTranscripts;
32
+ const node_fs_1 = require("node:fs");
33
+ const node_path_1 = require("node:path");
34
+ const node_os_1 = require("node:os");
35
+ const DEFAULT_PI_SESSIONS_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".pi", "agent", "sessions");
36
+ /**
37
+ * Read Pi session transcripts modified after `since`.
38
+ *
39
+ * Scans the Pi sessions directory for .jsonl files, filters by mtime,
40
+ * parses each into a TranscriptBatch.
41
+ *
42
+ * @param since Only return sessions with mtime > this date
43
+ * @param sessionsDir Override the session directory (default: ~/.pi/agent/sessions/)
44
+ */
45
+ function readPiTranscripts(since, sessionsDir = DEFAULT_PI_SESSIONS_DIR) {
46
+ const batches = [];
47
+ let projectDirs;
48
+ try {
49
+ projectDirs = (0, node_fs_1.readdirSync)(sessionsDir);
50
+ }
51
+ catch {
52
+ // Directory doesn't exist — no Pi sessions. Not an error.
53
+ return [];
54
+ }
55
+ for (const projectDir of projectDirs) {
56
+ const projectPath = (0, node_path_1.join)(sessionsDir, projectDir);
57
+ let files;
58
+ try {
59
+ const stat = (0, node_fs_1.statSync)(projectPath);
60
+ if (!stat.isDirectory())
61
+ continue;
62
+ files = (0, node_fs_1.readdirSync)(projectPath).filter((f) => f.endsWith(".jsonl"));
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ for (const file of files) {
68
+ const filePath = (0, node_path_1.join)(projectPath, file);
69
+ // Filter by modification time
70
+ try {
71
+ const stat = (0, node_fs_1.statSync)(filePath);
72
+ if (stat.mtime <= since)
73
+ continue;
74
+ }
75
+ catch {
76
+ continue;
77
+ }
78
+ // Parse the JSONL file
79
+ try {
80
+ const raw = (0, node_fs_1.readFileSync)(filePath, "utf-8");
81
+ const lines = raw.split("\n").filter((l) => l.trim());
82
+ const entries = [];
83
+ let sessionId = "";
84
+ let sessionCwd = "";
85
+ let sessionDate = "";
86
+ for (const line of lines) {
87
+ try {
88
+ const entry = JSON.parse(line);
89
+ entries.push(entry);
90
+ // Extract metadata from the session header
91
+ if (entry.type === "session") {
92
+ sessionId = entry.id ?? "";
93
+ sessionCwd = entry.cwd ?? "";
94
+ // Date from the session timestamp or filename
95
+ sessionDate =
96
+ entry.timestamp?.slice(0, 10) ??
97
+ extractDateFromFilename(file) ??
98
+ "";
99
+ }
100
+ }
101
+ catch {
102
+ // Skip malformed lines
103
+ }
104
+ }
105
+ // Derive project name from the cwd or directory name
106
+ const projectName = deriveProjectName(sessionCwd, projectDir);
107
+ // Use filename UUID as fallback session ID
108
+ if (!sessionId) {
109
+ sessionId = extractUuidFromFilename(file) ?? file;
110
+ }
111
+ if (!sessionDate) {
112
+ sessionDate = extractDateFromFilename(file) ?? "";
113
+ }
114
+ if (entries.length > 0) {
115
+ batches.push({
116
+ sessionId,
117
+ projectName,
118
+ date: sessionDate,
119
+ entries,
120
+ });
121
+ }
122
+ }
123
+ catch {
124
+ // File read or parse failed — skip
125
+ }
126
+ }
127
+ }
128
+ return batches;
129
+ }
130
+ /**
131
+ * Extract the last path segment from a cwd as the project name.
132
+ * /home/agents/Agents/raider → "raider"
133
+ * Falls back to decoding the directory name if cwd is empty.
134
+ */
135
+ function deriveProjectName(cwd, encodedDir) {
136
+ if (cwd) {
137
+ const segments = cwd.split("/").filter(Boolean);
138
+ return segments[segments.length - 1] ?? "unknown";
139
+ }
140
+ // Decode the Pi directory encoding: --home-agents-Agents-raider-- → raider
141
+ const decoded = encodedDir.replace(/^--/, "").replace(/--$/, "").split("-");
142
+ return decoded[decoded.length - 1] ?? "unknown";
143
+ }
144
+ /**
145
+ * Extract the date (YYYY-MM-DD) from a Pi session filename.
146
+ * Format: 2026-04-10T18-37-44-615Z_<uuid>.jsonl → "2026-04-10"
147
+ */
148
+ function extractDateFromFilename(filename) {
149
+ const match = filename.match(/^(\d{4}-\d{2}-\d{2})T/);
150
+ return match ? match[1] : null;
151
+ }
152
+ /**
153
+ * Extract the UUID from a Pi session filename.
154
+ * Format: 2026-04-10T18-37-44-615Z_f4227d47-e54f-4977-a50c-4de7f6d1fa21.jsonl
155
+ */
156
+ function extractUuidFromFilename(filename) {
157
+ const match = filename.match(/_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/);
158
+ return match ? match[1] : null;
159
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pro extension loader.
3
+ *
4
+ * Called from features.ts at boot when a valid paid license is detected.
5
+ * Responsibilities:
6
+ * 1. Check ~/.hicortex/pro/installed.json for the currently-installed
7
+ * Pro version (if any).
8
+ * 2. Fetch GET /api/pro/meta from hicortex.gamaze.com to discover the
9
+ * latest available version for the caller's license tier.
10
+ * 3. If the installed version is older (or missing), download the
11
+ * tarball, verify the sha256 sidecar, and extract to ~/.hicortex/pro/.
12
+ * 4. Dynamic-import ~/.hicortex/pro/package/index.js and call activate()
13
+ * on its default export with a ProActivationContext.
14
+ *
15
+ * Failure modes (all soft — OSS host keeps running with defaults):
16
+ * - Network to /api/pro/meta fails → use whatever is already installed
17
+ * - No cached Pro and network fails → Pro not activated, OSS defaults apply
18
+ * - Downloaded tarball fails sha256 → abort download, keep old version
19
+ * - import() of activated module throws → log warning, keep defaults
20
+ *
21
+ * The loader is strictly best-effort. It must NEVER crash the OSS host.
22
+ */
23
+ /**
24
+ * Entry point called from features.ts at boot.
25
+ *
26
+ * @param licenseKey The Pro license key (hctx-...) from config
27
+ * @param stateDir Usually ~/.hicortex/
28
+ * @param hostVersion The version of the OSS host (from package.json) — passed
29
+ * to the Pro activate() for compatibility gating
30
+ * @param serverUrl Override for the Pro meta/download endpoint (defaults
31
+ * to https://hicortex.gamaze.com). Useful for testing.
32
+ */
33
+ export declare function loadPro(licenseKey: string, stateDir: string, hostVersion: string, serverUrl?: string): Promise<void>;
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ /**
3
+ * Pro extension loader.
4
+ *
5
+ * Called from features.ts at boot when a valid paid license is detected.
6
+ * Responsibilities:
7
+ * 1. Check ~/.hicortex/pro/installed.json for the currently-installed
8
+ * Pro version (if any).
9
+ * 2. Fetch GET /api/pro/meta from hicortex.gamaze.com to discover the
10
+ * latest available version for the caller's license tier.
11
+ * 3. If the installed version is older (or missing), download the
12
+ * tarball, verify the sha256 sidecar, and extract to ~/.hicortex/pro/.
13
+ * 4. Dynamic-import ~/.hicortex/pro/package/index.js and call activate()
14
+ * on its default export with a ProActivationContext.
15
+ *
16
+ * Failure modes (all soft — OSS host keeps running with defaults):
17
+ * - Network to /api/pro/meta fails → use whatever is already installed
18
+ * - No cached Pro and network fails → Pro not activated, OSS defaults apply
19
+ * - Downloaded tarball fails sha256 → abort download, keep old version
20
+ * - import() of activated module throws → log warning, keep defaults
21
+ *
22
+ * The loader is strictly best-effort. It must NEVER crash the OSS host.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.loadPro = loadPro;
26
+ const node_crypto_1 = require("node:crypto");
27
+ const node_fs_1 = require("node:fs");
28
+ const node_path_1 = require("node:path");
29
+ const node_url_1 = require("node:url");
30
+ const node_child_process_1 = require("node:child_process");
31
+ const extensions_js_1 = require("./extensions.js");
32
+ const VALIDATE_URL = "https://hicortex.gamaze.com";
33
+ /**
34
+ * Entry point called from features.ts at boot.
35
+ *
36
+ * @param licenseKey The Pro license key (hctx-...) from config
37
+ * @param stateDir Usually ~/.hicortex/
38
+ * @param hostVersion The version of the OSS host (from package.json) — passed
39
+ * to the Pro activate() for compatibility gating
40
+ * @param serverUrl Override for the Pro meta/download endpoint (defaults
41
+ * to https://hicortex.gamaze.com). Useful for testing.
42
+ */
43
+ async function loadPro(licenseKey, stateDir, hostVersion, serverUrl = VALIDATE_URL) {
44
+ const proRoot = (0, node_path_1.join)(stateDir, "pro");
45
+ try {
46
+ (0, node_fs_1.mkdirSync)(proRoot, { recursive: true });
47
+ }
48
+ catch {
49
+ // If we can't even create the dir, there's nothing to load
50
+ return;
51
+ }
52
+ // Step 1: read installed state (if any)
53
+ const installedStatePath = (0, node_path_1.join)(proRoot, "installed.json");
54
+ const installed = readInstalledState(installedStatePath);
55
+ // Step 2: try to fetch the latest meta. Network failure is soft.
56
+ let meta = null;
57
+ try {
58
+ meta = await fetchProMeta(serverUrl, licenseKey);
59
+ }
60
+ catch (err) {
61
+ const msg = err instanceof Error ? err.message : String(err);
62
+ console.warn(`[hicortex][pro] Could not fetch Pro metadata (${msg}) — using cached version if present`);
63
+ }
64
+ // Step 3: if meta is newer than installed, download and extract
65
+ if (meta && (!installed || installed.version !== meta.version)) {
66
+ try {
67
+ await downloadAndExtract(serverUrl, licenseKey, meta, proRoot);
68
+ writeInstalledState(installedStatePath, {
69
+ version: meta.version,
70
+ installedAt: new Date().toISOString(),
71
+ sha256: meta.sha256,
72
+ });
73
+ console.log(`[hicortex][pro] Installed Pro v${meta.version}`);
74
+ }
75
+ catch (err) {
76
+ const msg = err instanceof Error ? err.message : String(err);
77
+ console.warn(`[hicortex][pro] Pro download failed: ${msg}`);
78
+ }
79
+ }
80
+ // Step 4: activate whatever is installed (if anything)
81
+ const indexPath = (0, node_path_1.join)(proRoot, "package", "index.js");
82
+ if (!(0, node_fs_1.existsSync)(indexPath)) {
83
+ // No Pro installed and meta fetch couldn't install one
84
+ return;
85
+ }
86
+ try {
87
+ const mod = await import((0, node_url_1.pathToFileURL)(indexPath).href);
88
+ const pkg = (mod.default ?? mod);
89
+ if (typeof pkg?.activate !== "function") {
90
+ console.warn(`[hicortex][pro] Pro package has no activate() function`);
91
+ return;
92
+ }
93
+ const ctx = (0, extensions_js_1.createProActivationContext)(hostVersion);
94
+ await pkg.activate(ctx);
95
+ }
96
+ catch (err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ console.warn(`[hicortex][pro] Pro activation failed: ${msg}`);
99
+ }
100
+ }
101
+ // ---------------------------------------------------------------------------
102
+ // Implementation helpers
103
+ // ---------------------------------------------------------------------------
104
+ function readInstalledState(path) {
105
+ try {
106
+ const raw = (0, node_fs_1.readFileSync)(path, "utf-8");
107
+ return JSON.parse(raw);
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ function writeInstalledState(path, state) {
114
+ try {
115
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
116
+ (0, node_fs_1.writeFileSync)(path, JSON.stringify(state, null, 2));
117
+ }
118
+ catch {
119
+ // Non-fatal — we'll just re-download next boot
120
+ }
121
+ }
122
+ async function fetchProMeta(serverUrl, licenseKey) {
123
+ const url = `${serverUrl.replace(/\/$/, "")}/api/pro/meta`;
124
+ const resp = await fetch(url, {
125
+ headers: { Authorization: `Bearer ${licenseKey}` },
126
+ signal: AbortSignal.timeout(10_000),
127
+ });
128
+ if (resp.status === 401 || resp.status === 403) {
129
+ throw new Error(`Pro access denied (HTTP ${resp.status}) — check license key`);
130
+ }
131
+ if (resp.status === 404) {
132
+ throw new Error("No Pro release available yet");
133
+ }
134
+ if (!resp.ok) {
135
+ throw new Error(`HTTP ${resp.status}`);
136
+ }
137
+ const data = (await resp.json());
138
+ if (!data.version || !data.url) {
139
+ throw new Error("Malformed /api/pro/meta response");
140
+ }
141
+ return data;
142
+ }
143
+ async function downloadAndExtract(serverUrl, licenseKey, meta, proRoot) {
144
+ const downloadUrl = `${serverUrl.replace(/\/$/, "")}${meta.url.startsWith("/") ? "" : "/"}${meta.url}`;
145
+ const resp = await fetch(downloadUrl, {
146
+ headers: { Authorization: `Bearer ${licenseKey}` },
147
+ signal: AbortSignal.timeout(60_000),
148
+ });
149
+ if (!resp.ok) {
150
+ throw new Error(`Download failed with HTTP ${resp.status}`);
151
+ }
152
+ // Stream to a temp file so we can verify the hash before extracting
153
+ const tmpPath = (0, node_path_1.join)(proRoot, `.pro-download-${Date.now()}.tgz`);
154
+ const buf = Buffer.from(await resp.arrayBuffer());
155
+ (0, node_fs_1.writeFileSync)(tmpPath, buf);
156
+ // Verify sha256 if the server provided one
157
+ if (meta.sha256) {
158
+ const hash = (0, node_crypto_1.createHash)("sha256").update(buf).digest("hex");
159
+ if (hash !== meta.sha256) {
160
+ try {
161
+ (0, node_fs_1.rmSync)(tmpPath);
162
+ }
163
+ catch { /* non-fatal */ }
164
+ throw new Error(`sha256 mismatch: expected ${meta.sha256}, got ${hash}`);
165
+ }
166
+ }
167
+ // Extract to proRoot (overwrites existing package/ directory)
168
+ const existingPackageDir = (0, node_path_1.join)(proRoot, "package");
169
+ if ((0, node_fs_1.existsSync)(existingPackageDir)) {
170
+ try {
171
+ (0, node_fs_1.rmSync)(existingPackageDir, { recursive: true, force: true });
172
+ }
173
+ catch { /* non-fatal */ }
174
+ }
175
+ // Use the system tar to extract (cross-platform, no new dependency)
176
+ const result = (0, node_child_process_1.spawnSync)("tar", ["-xzf", tmpPath, "-C", proRoot], {
177
+ encoding: "utf-8",
178
+ });
179
+ if (result.status !== 0) {
180
+ throw new Error(`tar extraction failed: ${result.stderr || "unknown error"}`);
181
+ }
182
+ // Clean up the temp tarball
183
+ try {
184
+ (0, node_fs_1.rmSync)(tmpPath);
185
+ }
186
+ catch { /* non-fatal */ }
187
+ }
@@ -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.4.6",
5
+ "version": "0.5.0",
6
6
  "kind": "lifecycle",
7
7
  "skills": ["./skills/hicortex-memory", "./skills/hicortex-learn", "./skills/hicortex-activate"],
8
8
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gamaze/hicortex",
3
- "version": "0.4.6",
3
+ "version": "0.5.0",
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": {
@@ -50,12 +50,12 @@
50
50
  "repository": {
51
51
  "type": "git",
52
52
  "url": "https://github.com/gamaze-labs/hicortex.git",
53
- "directory": "packages/openclaw-plugin"
53
+ "directory": "packages/hicortex"
54
54
  },
55
55
  "bugs": {
56
56
  "url": "https://github.com/gamaze-labs/hicortex/issues"
57
57
  },
58
- "author": "Mattias Hansson",
58
+ "author": "Aironic Ventures Ltd.",
59
59
  "dependencies": {
60
60
  "@huggingface/transformers": "^3.0.0",
61
61
  "@modelcontextprotocol/sdk": "^1.28.0",