@gamaze/hicortex 0.4.5 → 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");
@@ -205,15 +214,24 @@ async function distillSession(llm, conversation, projectName, date, chunkSizeCha
205
214
  }
206
215
  // Use provided chunk size or default to no chunking
207
216
  const chunkSize = chunkSizeChars ?? MAX_TRANSCRIPT_CHARS;
208
- // If transcript fits in one chunk, distill directly
217
+ // If transcript fits in one chunk, distill directly (errors propagate)
209
218
  if (transcript.length <= chunkSize) {
210
219
  return distillChunk(llm, transcript, projectName, date);
211
220
  }
212
- // Chunk large transcripts and distill each segment
221
+ // Chunk large transcripts and distill each segment.
222
+ //
223
+ // Partial success policy:
224
+ // - If SOME chunks succeed and SOME fail, return the partial results and
225
+ // log a warning. The caller gets *something* and can decide whether
226
+ // to count this as success.
227
+ // - If ALL chunks fail, throw — no useful output, and the caller needs
228
+ // to know this session hit a transient error.
213
229
  const chunks = splitIntoChunks(transcript, chunkSize);
214
230
  console.log(`[hicortex] Chunking ${transcript.length} chars into ${chunks.length} segments`);
215
231
  const allEntries = [];
216
232
  const seen = new Set();
233
+ let chunkFailures = 0;
234
+ let lastError = null;
217
235
  for (let i = 0; i < chunks.length; i++) {
218
236
  console.log(`[hicortex] Chunk ${i + 1}/${chunks.length} (${chunks[i].length} chars)`);
219
237
  try {
@@ -230,30 +248,45 @@ async function distillSession(llm, conversation, projectName, date, chunkSizeCha
230
248
  catch (err) {
231
249
  const msg = err instanceof Error ? err.message : String(err);
232
250
  console.error(`[hicortex] Chunk ${i + 1} failed: ${msg}`);
233
- // Continue with remaining chunks — partial extraction is better than none
251
+ chunkFailures++;
252
+ lastError = err instanceof Error ? err : new Error(msg);
234
253
  }
235
254
  }
255
+ // If every chunk failed, the session wasn't actually processed. Throw so
256
+ // the nightly pipeline knows to retry this session next run.
257
+ if (chunkFailures === chunks.length) {
258
+ throw lastError ?? new Error("All distillation chunks failed");
259
+ }
260
+ if (chunkFailures > 0) {
261
+ console.warn(`[hicortex] Partial distillation: ${chunks.length - chunkFailures}/${chunks.length} chunks succeeded`);
262
+ }
236
263
  return allEntries;
237
264
  }
238
265
  /**
239
266
  * Distill a single chunk of conversation text.
267
+ *
268
+ * Behaviour contract:
269
+ * - Returns `[]` for legitimate empty results (NO_EXTRACT, empty LLM response,
270
+ * transcript produced no entries). These are terminal states — the chunk was
271
+ * processed successfully, there's just nothing worth keeping.
272
+ * - Throws for transient errors (LLM unreachable, HTTP 4xx/5xx, timeout, model
273
+ * not found, rate limit). These MUST propagate so the nightly pipeline can
274
+ * distinguish "nothing to extract" from "try again later" and avoid
275
+ * advancing the last-run watermark past sessions it never actually processed.
240
276
  */
241
277
  async function distillChunk(llm, transcript, projectName, date) {
242
278
  const prompt = (0, prompts_js_1.distillation)(projectName, date, transcript);
243
- try {
244
- const result = await llm.completeDistill(prompt);
245
- if (!result)
246
- return [];
247
- if (result === "NO_EXTRACT" || result.slice(0, 20).includes("NO_EXTRACT")) {
248
- return [];
249
- }
250
- return parseDistilledEntries(result);
251
- }
252
- catch (err) {
253
- const msg = err instanceof Error ? err.message : String(err);
254
- console.error(`[hicortex] Distillation LLM error: ${msg}`);
279
+ // NOTE: Intentionally no try/catch here. Transient LLM errors (network
280
+ // failures, 4xx/5xx, model-not-found, timeouts) propagate up to the caller
281
+ // so the nightly pipeline can treat them as "retry later" instead of
282
+ // "processed successfully with zero extractions".
283
+ const result = await llm.completeDistill(prompt);
284
+ if (!result)
285
+ return [];
286
+ if (result === "NO_EXTRACT" || result.slice(0, 20).includes("NO_EXTRACT")) {
255
287
  return [];
256
288
  }
289
+ return parseDistilledEntries(result);
257
290
  }
258
291
  /**
259
292
  * Split transcript text into chunks at natural boundaries (double newlines).
@@ -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/llm.d.ts CHANGED
@@ -69,6 +69,25 @@ export declare function claudeCliConfig(claudePath: string): LlmConfig;
69
69
  * Returns the model name if available, null otherwise.
70
70
  */
71
71
  export declare function probeOllama(baseUrl?: string): Promise<string | null>;
72
+ /**
73
+ * Pre-flight health check for a specific Ollama endpoint + model.
74
+ * Returns { ok, reason } so callers can log a clear abort message.
75
+ *
76
+ * - `ok: true` — endpoint reachable AND the requested model appears in
77
+ * `/api/tags`. Safe to proceed with a batch distillation run.
78
+ * - `ok: false, reason: "unreachable"` — network failure or non-2xx.
79
+ * - `ok: false, reason: "model_missing"` — endpoint is up but the
80
+ * model isn't listed (the exact case that caused data loss when
81
+ * mhac-pro's Ollama didn't have the distill model loaded).
82
+ *
83
+ * Matches on exact name OR name prefix ("qwen3.5:35b" matches "qwen3.5:35b-a3b").
84
+ */
85
+ export declare function probeOllamaModel(baseUrl: string, modelName: string): Promise<{
86
+ ok: true;
87
+ } | {
88
+ ok: false;
89
+ reason: "unreachable" | "model_missing";
90
+ }>;
72
91
  /**
73
92
  * For batch operations (nightly pipeline), prefer Ollama when available.
74
93
  * Claude CLI has strict rate limits that kill batch distillation.
package/dist/llm.js CHANGED
@@ -26,6 +26,7 @@ exports.resolveLlmConfigForCC = resolveLlmConfigForCC;
26
26
  exports.findClaudeBinary = findClaudeBinary;
27
27
  exports.claudeCliConfig = claudeCliConfig;
28
28
  exports.probeOllama = probeOllama;
29
+ exports.probeOllamaModel = probeOllamaModel;
29
30
  exports.preferOllamaForBatch = preferOllamaForBatch;
30
31
  const node_fs_1 = require("node:fs");
31
32
  const node_path_1 = require("node:path");
@@ -367,6 +368,35 @@ async function probeOllama(baseUrl = "http://localhost:11434") {
367
368
  return null;
368
369
  }
369
370
  }
371
+ /**
372
+ * Pre-flight health check for a specific Ollama endpoint + model.
373
+ * Returns { ok, reason } so callers can log a clear abort message.
374
+ *
375
+ * - `ok: true` — endpoint reachable AND the requested model appears in
376
+ * `/api/tags`. Safe to proceed with a batch distillation run.
377
+ * - `ok: false, reason: "unreachable"` — network failure or non-2xx.
378
+ * - `ok: false, reason: "model_missing"` — endpoint is up but the
379
+ * model isn't listed (the exact case that caused data loss when
380
+ * mhac-pro's Ollama didn't have the distill model loaded).
381
+ *
382
+ * Matches on exact name OR name prefix ("qwen3.5:35b" matches "qwen3.5:35b-a3b").
383
+ */
384
+ async function probeOllamaModel(baseUrl, modelName) {
385
+ try {
386
+ const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/tags`, {
387
+ signal: AbortSignal.timeout(5000),
388
+ });
389
+ if (!resp.ok)
390
+ return { ok: false, reason: "unreachable" };
391
+ const data = (await resp.json());
392
+ const models = data.models ?? [];
393
+ const found = models.some((m) => m.name === modelName || m.name.startsWith(modelName + ":"));
394
+ return found ? { ok: true } : { ok: false, reason: "model_missing" };
395
+ }
396
+ catch {
397
+ return { ok: false, reason: "unreachable" };
398
+ }
399
+ }
370
400
  /**
371
401
  * For batch operations (nightly pipeline), prefer Ollama when available.
372
402
  * Claude CLI has strict rate limits that kill batch distillation.
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,17 +172,41 @@ 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;
192
+ let hadTransientFailure = false;
193
+ // Pre-flight health check for a remote distill endpoint.
194
+ // If the distill provider is Ollama on a remote host and that host (or the
195
+ // required model) is unreachable, abort BEFORE touching any sessions —
196
+ // prevents the data-loss bug where lastRun advances past sessions that
197
+ // were never actually processed.
198
+ if (batches.length > 0 && llmConfig.distillBaseUrl && (llmConfig.distillProvider ?? llmConfig.provider) === "ollama") {
199
+ const distillModel = llmConfig.distillModel ?? llmConfig.model;
200
+ const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.distillBaseUrl, distillModel);
201
+ if (!health.ok) {
202
+ const reason = health.reason === "unreachable"
203
+ ? `distill endpoint unreachable (${llmConfig.distillBaseUrl})`
204
+ : `distill model not loaded (${distillModel} missing on ${llmConfig.distillBaseUrl})`;
205
+ console.error(`[hicortex] ABORT: ${reason} — will retry next run, lastRun unchanged`);
206
+ hadTransientFailure = true;
207
+ batches.length = 0; // Skip the distillation loop entirely
208
+ }
209
+ }
185
210
  // Detect safe chunk size based on model context window
186
211
  const chunkSize = await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.distillModel ?? llmConfig.model, llmConfig.baseUrl);
187
212
  for (const batch of batches) {
@@ -190,6 +215,20 @@ async function runNightly(options = {}) {
190
215
  console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): too short`);
191
216
  continue;
192
217
  }
218
+ // Server-mode per-session dedup: skip sessions already in the DB.
219
+ // Client mode gets this for free via the server's /ingest endpoint;
220
+ // server mode writes directly via storage.insertMemory and needs
221
+ // an explicit check. This makes retries of previously-failed runs
222
+ // idempotent.
223
+ if (!dryRun) {
224
+ const existing = db
225
+ .prepare("SELECT COUNT(*) as c FROM memories WHERE source_session = ?")
226
+ .get(batch.sessionId);
227
+ if (existing.c > 0) {
228
+ console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): already ingested`);
229
+ continue;
230
+ }
231
+ }
193
232
  console.log(`[hicortex] Distilling ${batch.sessionId.slice(0, 8)} (${batch.projectName}, ${batch.date})`);
194
233
  if (dryRun) {
195
234
  console.log(`[hicortex] [dry-run] Would distill ${transcript.length} chars`);
@@ -224,7 +263,8 @@ async function runNightly(options = {}) {
224
263
  }
225
264
  catch (err) {
226
265
  const msg = err instanceof Error ? err.message : String(err);
227
- console.error(`[hicortex] Distillation failed: ${msg}`);
266
+ console.error(`[hicortex] Distillation failed: ${msg} — will retry next run`);
267
+ hadTransientFailure = true;
228
268
  }
229
269
  }
230
270
  console.log(`[hicortex] Distillation complete: ${memoriesIngested} new memories`);
@@ -235,14 +275,28 @@ async function runNightly(options = {}) {
235
275
  console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
236
276
  (report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
237
277
  }
238
- // 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)
239
280
  if (!dryRun) {
240
- const injection = await (0, claude_md_js_1.injectLessons)(db, { stateDir });
241
- 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}`);
242
287
  }
243
288
  // Step 5: Update last-run timestamp
289
+ // CRITICAL: only advance lastRun if every session was processed without
290
+ // a transient failure. Otherwise failed sessions would be permanently
291
+ // lost — they'd be older than the new lastRun and never retried.
244
292
  if (!dryRun) {
245
- writeLastRun();
293
+ if (hadTransientFailure) {
294
+ console.warn(`[hicortex] Not advancing lastRun — one or more sessions failed. ` +
295
+ `They will be retried on the next run.`);
296
+ }
297
+ else {
298
+ writeLastRun();
299
+ }
246
300
  }
247
301
  console.log(`[hicortex] Nightly pipeline complete.`);
248
302
  }
@@ -329,6 +383,22 @@ async function runClientNightly(config, dryRun) {
329
383
  writeLastRun();
330
384
  return;
331
385
  }
386
+ // Pre-flight health check for a remote distill endpoint (client mode).
387
+ // If the distill provider is Ollama on a remote host and the required model
388
+ // isn't loaded, abort BEFORE touching any sessions — same data-loss fix
389
+ // as server mode.
390
+ let hadTransientFailure = false;
391
+ if (llmConfig.distillBaseUrl && (llmConfig.distillProvider ?? llmConfig.provider) === "ollama") {
392
+ const distillModel = llmConfig.distillModel ?? llmConfig.model;
393
+ const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.distillBaseUrl, distillModel);
394
+ if (!health.ok) {
395
+ const reason = health.reason === "unreachable"
396
+ ? `distill endpoint unreachable (${llmConfig.distillBaseUrl})`
397
+ : `distill model not loaded (${distillModel} missing on ${llmConfig.distillBaseUrl})`;
398
+ console.error(`[hicortex] ABORT: ${reason} — will retry next run, lastRun unchanged`);
399
+ return; // Don't touch lastRun; next trigger retries the same sessions
400
+ }
401
+ }
332
402
  // Distill each session and POST to server
333
403
  let memoriesIngested = 0;
334
404
  let sessionsSent = 0;
@@ -388,6 +458,7 @@ async function runClientNightly(config, dryRun) {
388
458
  }
389
459
  else {
390
460
  console.error(`[hicortex] Ingest failed (${resp.status}): ${result.error}`);
461
+ hadTransientFailure = true;
391
462
  }
392
463
  }
393
464
  if (sessionCount > 0) {
@@ -396,7 +467,8 @@ async function runClientNightly(config, dryRun) {
396
467
  }
397
468
  }
398
469
  catch (err) {
399
- console.error(`[hicortex] Failed: ${err instanceof Error ? err.message : String(err)}`);
470
+ console.error(`[hicortex] Distillation failed: ${err instanceof Error ? err.message : String(err)} — will retry next run`);
471
+ hadTransientFailure = true;
400
472
  }
401
473
  }
402
474
  // Inject lessons from server into CLAUDE.md
@@ -408,8 +480,17 @@ async function runClientNightly(config, dryRun) {
408
480
  console.error(`[hicortex] CLAUDE.md injection failed: ${err instanceof Error ? err.message : String(err)}`);
409
481
  }
410
482
  }
411
- if (!dryRun)
412
- writeLastRun();
483
+ // Only advance lastRun if every session was processed without a transient
484
+ // failure. Otherwise failed sessions would be permanently lost.
485
+ if (!dryRun) {
486
+ if (hadTransientFailure) {
487
+ console.warn(`[hicortex] Not advancing lastRun — one or more sessions failed. ` +
488
+ `They will be retried on the next run.`);
489
+ }
490
+ else {
491
+ writeLastRun();
492
+ }
493
+ }
413
494
  console.log(`[hicortex] Client nightly complete: ${memoriesIngested} memories from ${sessionsSent} sessions → ${serverUrl}`);
414
495
  }
415
496
  /**
@@ -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.5",
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.5",
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",