@gamaze/hicortex 0.7.0 → 0.10.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.
Files changed (54) hide show
  1. package/README.md +57 -39
  2. package/dist/claude-md.d.ts +9 -21
  3. package/dist/claude-md.js +9 -241
  4. package/dist/cli.d.ts +3 -2
  5. package/dist/cli.js +29 -11
  6. package/dist/consolidate.js +76 -26
  7. package/dist/db.js +24 -0
  8. package/dist/embedder.d.ts +11 -0
  9. package/dist/embedder.js +27 -0
  10. package/dist/extensions.d.ts +41 -88
  11. package/dist/extensions.js +36 -61
  12. package/dist/features.d.ts +21 -25
  13. package/dist/features.js +47 -83
  14. package/dist/graph.d.ts +1 -1
  15. package/dist/graph.js +13 -7
  16. package/dist/hermes-transcript-reader.d.ts +27 -0
  17. package/dist/hermes-transcript-reader.js +134 -0
  18. package/dist/index.d.ts +16 -4
  19. package/dist/index.js +252 -344
  20. package/dist/init.d.ts +41 -1
  21. package/dist/init.js +545 -190
  22. package/dist/lesson-selection.d.ts +62 -0
  23. package/dist/lesson-selection.js +159 -0
  24. package/dist/lessons-context.d.ts +17 -0
  25. package/dist/lessons-context.js +96 -0
  26. package/dist/llm.d.ts +42 -29
  27. package/dist/llm.js +89 -270
  28. package/dist/mcp-server.d.ts +0 -1
  29. package/dist/mcp-server.js +407 -88
  30. package/dist/nightly.d.ts +9 -6
  31. package/dist/nightly.js +197 -357
  32. package/dist/oc-transcript-reader.d.ts +20 -0
  33. package/dist/oc-transcript-reader.js +61 -0
  34. package/dist/pi-transcript-reader.d.ts +1 -0
  35. package/dist/prompts.d.ts +5 -0
  36. package/dist/prompts.js +29 -0
  37. package/dist/status.js +22 -2
  38. package/dist/storage.d.ts +7 -1
  39. package/dist/storage.js +28 -7
  40. package/dist/transcript-reader.d.ts +19 -0
  41. package/dist/transcript-reader.js +17 -3
  42. package/dist/types.d.ts +16 -0
  43. package/dist/types.js +7 -0
  44. package/dist/uninstall.js +31 -1
  45. package/hermes-plugin/hicortex/README.md +77 -0
  46. package/hermes-plugin/hicortex/__init__.py +17 -0
  47. package/hermes-plugin/hicortex/client.py +162 -0
  48. package/hermes-plugin/hicortex/config.py +105 -0
  49. package/hermes-plugin/hicortex/plugin.yaml +12 -0
  50. package/hermes-plugin/hicortex/provider.py +432 -0
  51. package/openclaw.plugin.json +17 -44
  52. package/package.json +7 -5
  53. package/dist/pro-loader.d.ts +0 -33
  54. package/dist/pro-loader.js +0 -187
package/dist/nightly.js CHANGED
@@ -2,12 +2,14 @@
2
2
  /**
3
3
  * Nightly pipeline — manual trigger or called by the persistent server.
4
4
  *
5
- * Steps:
6
- * 1. Read new CC transcripts since last run
7
- * 2. Distill each session into memories via LLM
8
- * 3. Run consolidation (scoring, reflection, linking, decay)
9
- * 4. Inject lessons into CLAUDE.md
10
- * 5. Update last-run timestamp
5
+ * Steps (0.9.0+):
6
+ * 1. Read new harness transcripts since last run
7
+ * 2. Denoise + POST each session to /distill (server captures for itself via localhost)
8
+ * 3. Run consolidation (scoring, reflection, linking, decay) — server mode only
9
+ * 4. Update last-run timestamp
10
+ *
11
+ * Every machine (server + clients) uses the same capture path: denoise locally,
12
+ * POST to /distill. No local LLM required for capture; distillation is server-side.
11
13
  */
12
14
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
15
  if (k2 === undefined) k2 = k;
@@ -59,10 +61,10 @@ const storage = __importStar(require("./storage.js"));
59
61
  const distiller_js_1 = require("./distiller.js");
60
62
  const consolidate_js_1 = require("./consolidate.js");
61
63
  const transcript_reader_js_1 = require("./transcript-reader.js");
64
+ const hermes_transcript_reader_js_1 = require("./hermes-transcript-reader.js");
62
65
  const pi_transcript_reader_js_1 = require("./pi-transcript-reader.js");
63
- const claude_md_js_1 = require("./claude-md.js");
66
+ const oc_transcript_reader_js_1 = require("./oc-transcript-reader.js");
64
67
  const features_js_1 = require("./features.js");
65
- const extensions_js_1 = require("./extensions.js");
66
68
  const state_js_1 = require("./state.js");
67
69
  const telemetry_js_1 = require("./telemetry.js");
68
70
  const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
@@ -101,40 +103,44 @@ function writeLastRun(stateDir = HICORTEX_HOME) {
101
103
  }
102
104
  async function runNightly(options = {}) {
103
105
  const dryRun = options.dryRun ?? false;
106
+ const captureOnly = options.captureOnly ?? false;
104
107
  const stateDir = options.stateDir ?? HICORTEX_HOME;
105
108
  // One-time migration of legacy state files (no-op if state.json exists)
106
109
  (0, state_js_1.migrateLegacyState)(stateDir);
107
110
  // Check mode: client or server
108
111
  const savedConfig = readNightlyConfig(stateDir);
109
112
  if (savedConfig?.mode === "client") {
113
+ // --capture-only is accepted in client mode but irrelevant: client nightly
114
+ // is already capture-only (no consolidation step).
110
115
  await runClientNightly(savedConfig, dryRun);
111
116
  return;
112
117
  }
113
118
  const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
114
- console.log(`[hicortex] Nightly pipeline starting${dryRun ? " (dry run)" : ""}`);
119
+ const port = savedConfig?.port ?? 8787;
120
+ const modeLabel = captureOnly ? " (capture-only)" : dryRun ? " (dry run)" : "";
121
+ console.log(`[hicortex] Nightly pipeline starting${modeLabel}`);
122
+ if (captureOnly) {
123
+ console.log(`[hicortex] capture-only run — consolidation skipped`);
124
+ }
115
125
  console.log(`[hicortex] DB: ${dbPath}`);
116
- // Init DB
126
+ // Init DB — consolidation reads the DB directly; capture goes via HTTP.
117
127
  const db = (0, db_js_1.initDb)(dbPath);
118
128
  try {
119
129
  // License: read from config file or env var, init feature cache
120
130
  const licenseKey = readConfigLicenseKey(stateDir) ?? process.env.HICORTEX_LICENSE_KEY;
121
131
  await (0, features_js_1.initFeatures)(licenseKey, stateDir);
122
- // Init LLM: check config.json first, then auto-detect
123
- let llmConfig;
124
- const savedConfig = readNightlyConfig(stateDir);
132
+ // Init LLM for consolidation (scoring + reflection). Capture (distillation)
133
+ // is handled by the running daemon over /distill — no local distill LLM needed.
134
+ // No LLM → capture loop still runs (sessions POST to /distill, which will 503
135
+ // transient-fail and hold the watermark), but consolidation is skipped.
136
+ let llmConfig = null;
125
137
  if (savedConfig?.llmBackend === "claude-cli") {
126
138
  const claudePath = (0, llm_js_1.findClaudeBinary)();
127
139
  if (claudePath) {
128
140
  llmConfig = (0, llm_js_1.claudeCliConfig)(claudePath);
129
141
  }
130
142
  else {
131
- console.warn("[hicortex] claude-cli configured but binary not found, falling back");
132
- llmConfig = (0, llm_js_1.resolveLlmConfigForCC)({
133
- llmBaseUrl: savedConfig?.llmBaseUrl,
134
- llmApiKey: savedConfig?.llmApiKey,
135
- llmModel: savedConfig?.llmModel,
136
- reflectModel: savedConfig?.reflectModel,
137
- });
143
+ console.warn("[hicortex] claude-cli configured but binary not found consolidation skipped");
138
144
  }
139
145
  }
140
146
  else if (savedConfig?.llmBackend === "ollama") {
@@ -147,167 +153,138 @@ async function runNightly(options = {}) {
147
153
  };
148
154
  }
149
155
  else {
150
- llmConfig = (0, llm_js_1.resolveLlmConfigForCC)({
156
+ llmConfig = (0, llm_js_1.resolveExplicitLlmConfig)({
151
157
  llmBaseUrl: savedConfig?.llmBaseUrl,
152
158
  llmApiKey: savedConfig?.llmApiKey,
153
159
  llmModel: savedConfig?.llmModel,
154
160
  reflectModel: savedConfig?.reflectModel,
155
161
  });
156
162
  }
157
- // Apply distill and reflect overrides from config
158
- if (savedConfig?.distillModel) {
159
- llmConfig.distillModel = savedConfig.distillModel;
160
- }
161
- if (savedConfig?.distillBaseUrl) {
162
- llmConfig.distillBaseUrl = savedConfig.distillBaseUrl;
163
- llmConfig.distillApiKey = savedConfig.distillApiKey ?? llmConfig.apiKey;
164
- llmConfig.distillProvider = savedConfig.distillProvider ?? llmConfig.provider;
165
- }
166
- if (savedConfig?.reflectBaseUrl) {
163
+ if (llmConfig && savedConfig?.reflectBaseUrl) {
167
164
  llmConfig.reflectBaseUrl = savedConfig.reflectBaseUrl;
168
165
  llmConfig.reflectApiKey = savedConfig.reflectApiKey ?? llmConfig.apiKey;
169
166
  llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
170
167
  }
171
- // Auto-detect Ollama for batch distillation (claude-cli has rate limits)
172
- llmConfig = await (0, llm_js_1.preferOllamaForBatch)(llmConfig);
173
- if (llmConfig.provider === "ollama") {
174
- console.log(`[hicortex] Auto-detected local Ollama (${llmConfig.model}) — using for batch distillation`);
175
- }
176
- const llm = new llm_js_1.LlmClient(llmConfig);
177
- const distillInfo = llmConfig.distillBaseUrl
178
- ? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
179
- : llmConfig.distillModel ?? "";
180
- console.log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}`);
181
- // Step 1: Read new transcripts (CC + Pi)
168
+ const llm = llmConfig ? new llm_js_1.LlmClient(llmConfig) : null;
169
+ // Step 1: Read new transcripts (CC + Hermes + Pi + OpenClaw)
182
170
  const since = readLastRun();
183
171
  console.log(`[hicortex] Reading transcripts since ${since.toISOString()}`);
184
172
  const ccBatches = (0, transcript_reader_js_1.readCcTranscripts)(since);
173
+ const hermesBatches = (0, hermes_transcript_reader_js_1.readHermesSessions)(since);
174
+ // Pi is a supported harness in the product (readPiTranscripts no-ops when
175
+ // ~/.pi/agent/sessions is absent). Retired only on specific deployments by
176
+ // simply having no Pi session files — not removed from the pipeline.
185
177
  const piBatches = (0, pi_transcript_reader_js_1.readPiTranscripts)(since);
186
- const batches = [...ccBatches, ...piBatches];
178
+ // OpenClaw persists sessions in the Pi v3 format at ~/.openclaw/agents/;
179
+ // no-ops when OC isn't installed.
180
+ const ocBatches = (0, oc_transcript_reader_js_1.readOcTranscripts)(since);
181
+ const batches = [...ccBatches, ...hermesBatches, ...piBatches, ...ocBatches];
187
182
  if (ccBatches.length > 0)
188
183
  console.log(`[hicortex] Found ${ccBatches.length} CC session(s)`);
184
+ if (hermesBatches.length > 0)
185
+ console.log(`[hicortex] Found ${hermesBatches.length} Hermes session(s)`);
189
186
  if (piBatches.length > 0)
190
187
  console.log(`[hicortex] Found ${piBatches.length} Pi session(s)`);
188
+ if (ocBatches.length > 0)
189
+ console.log(`[hicortex] Found ${ocBatches.length} OpenClaw session(s)`);
191
190
  console.log(`[hicortex] Total: ${batches.length} new session(s)`);
192
191
  if (batches.length === 0 && !dryRun) {
193
- // Still run consolidation — there may be unscored memories from OC
194
- console.log(`[hicortex] No new transcripts. Running consolidation only.`);
192
+ // Still run consolidation (unless capture-only) — there may be unscored memories from OC.
193
+ console.log(captureOnly
194
+ ? `[hicortex] No new transcripts. Nothing to capture.`
195
+ : `[hicortex] No new transcripts. Running consolidation only.`);
195
196
  }
196
- // Step 2: Distill each session
197
+ // Step 2: Denoise and POST each session to the local daemon via /distill.
198
+ // The dedup check and distillation quality (35B) are the server's concern.
197
199
  let memoriesIngested = 0;
198
200
  let hadTransientFailure = false;
199
- // Pre-flight health check for a remote distill endpoint.
200
- // If the distill provider is Ollama on a remote host and that host (or the
201
- // required model) is unreachable, abort BEFORE touching any sessions —
202
- // prevents the data-loss bug where lastRun advances past sessions that
203
- // were never actually processed.
204
- if (batches.length > 0 && llmConfig.distillBaseUrl && (llmConfig.distillProvider ?? llmConfig.provider) === "ollama") {
205
- const distillModel = llmConfig.distillModel ?? llmConfig.model;
206
- const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.distillBaseUrl, distillModel);
207
- if (!health.ok) {
208
- const reason = health.reason === "unreachable"
209
- ? `distill endpoint unreachable (${llmConfig.distillBaseUrl})`
210
- : `distill model not loaded (${distillModel} missing on ${llmConfig.distillBaseUrl})`;
211
- console.error(`[hicortex] ABORT: ${reason} — will retry next run, lastRun unchanged`);
212
- hadTransientFailure = true;
213
- batches.length = 0; // Skip the distillation loop entirely
214
- }
215
- }
216
- // Detect safe chunk size based on model context window
217
- const chunkSize = await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.distillModel ?? llmConfig.model, llmConfig.baseUrl);
218
201
  for (const batch of batches) {
219
202
  const transcript = (0, distiller_js_1.extractConversationText)(batch.entries);
220
203
  if (transcript.length < 200) {
221
204
  console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): too short`);
222
205
  continue;
223
206
  }
224
- // Server-mode per-session dedup: skip sessions already in the DB.
225
- // Client mode gets this for free via the server's /ingest endpoint;
226
- // server mode writes directly via storage.insertMemory and needs
227
- // an explicit check. This makes retries of previously-failed runs
228
- // idempotent.
229
- if (!dryRun) {
230
- const existing = db
231
- .prepare("SELECT COUNT(*) as c FROM memories WHERE source_session = ?")
232
- .get(batch.sessionId);
233
- if (existing.c > 0) {
234
- console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): already ingested`);
235
- continue;
236
- }
237
- }
238
- console.log(`[hicortex] Distilling ${batch.sessionId.slice(0, 8)} (${batch.projectName}, ${batch.date})`);
207
+ console.log(`[hicortex] Capturing ${batch.sessionId.slice(0, 8)} (${batch.projectName}, ${batch.date})`);
239
208
  if (dryRun) {
240
- console.log(`[hicortex] [dry-run] Would distill ${transcript.length} chars`);
209
+ console.log(`[hicortex] [dry-run] Would POST ${transcript.length} chars to /distill`);
241
210
  continue;
242
211
  }
243
- // Check cap before distilling
244
- if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
245
- console.log(`[hicortex] Free tier limit (${(0, features_js_1.maxMemoriesAllowed)()} memories). Skipping new ingestion. ` +
246
- `Upgrade: https://hicortex.gamaze.com/`);
247
- break;
248
- }
249
212
  try {
250
- const entries = await (0, distiller_js_1.distillSession)(llm, transcript, batch.projectName, batch.date, chunkSize);
251
- for (const entry of entries) {
252
- try {
253
- const embedding = await (0, embedder_js_1.embed)(entry);
254
- storage.insertMemory(db, entry, embedding, {
255
- sourceAgent: `claude-code/${batch.projectName}`,
256
- sourceSession: batch.sessionId,
257
- project: batch.projectName,
258
- privacy: "WORK",
259
- memoryType: "episode",
260
- });
261
- memoriesIngested++;
262
- }
263
- catch (err) {
264
- const msg = err instanceof Error ? err.message : String(err);
265
- console.error(`[hicortex] Failed to ingest entry: ${msg}`);
213
+ const resp = await fetch(`http://127.0.0.1:${port}/distill`, {
214
+ method: "POST",
215
+ headers: { "Content-Type": "application/json" },
216
+ body: JSON.stringify({
217
+ text: transcript,
218
+ source_agent: batch.sourceAgent ?? `claude-code/${batch.projectName}`,
219
+ project: batch.projectName,
220
+ session_id: batch.sessionId,
221
+ session_date: batch.date,
222
+ privacy: "WORK",
223
+ }),
224
+ // Synchronous 35B distillation of a large session can take minutes.
225
+ signal: AbortSignal.timeout(20 * 60 * 1000),
226
+ });
227
+ if (resp.status === 200) {
228
+ const data = await resp.json();
229
+ if (data.skipped) {
230
+ console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): already ingested`);
266
231
  }
267
232
  }
268
- console.log(`[hicortex] → ${entries.length} memories extracted`);
233
+ else if (resp.status === 201) {
234
+ const data = await resp.json();
235
+ memoriesIngested += data.distilled ?? 0;
236
+ console.log(`[hicortex] → ${data.distilled ?? 0} memories extracted`);
237
+ }
238
+ else if (resp.status === 429) {
239
+ const data = await resp.json();
240
+ console.log(`[hicortex] Memory limit reached: ${data.error}. Stopping capture.`);
241
+ break;
242
+ }
243
+ else {
244
+ const data = await resp.json().catch(() => ({}));
245
+ console.error(`[hicortex] /distill returned ${resp.status}: ${data.error ?? "unknown error"} — will retry next run`);
246
+ hadTransientFailure = true;
247
+ }
269
248
  }
270
249
  catch (err) {
271
250
  const msg = err instanceof Error ? err.message : String(err);
272
- console.error(`[hicortex] Distillation failed: ${msg} — will retry next run`);
251
+ console.error(`[hicortex] Capture failed: ${msg} — will retry next run`);
273
252
  hadTransientFailure = true;
274
253
  }
275
254
  }
276
- console.log(`[hicortex] Distillation complete: ${memoriesIngested} new memories`);
277
- // Step 3: Consolidation
278
- if (!dryRun) {
279
- // Pre-flight health check for the reflect endpoint.
280
- // If reflectBaseUrl points to a remote Ollama and it's down (MBP offline),
281
- // skip reflection entirely instead of waiting through 3 retries (~3.5 min).
282
- // Scoring + linking + decay still run (they use the local model or don't need LLM).
283
- let skipReflection = false;
284
- if (llmConfig.reflectBaseUrl && (llmConfig.reflectProvider ?? llmConfig.provider) === "ollama") {
285
- const reflectModel = llmConfig.reflectModel ?? llmConfig.model;
286
- const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.reflectBaseUrl, reflectModel);
287
- if (!health.ok) {
288
- const reason = health.reason === "unreachable"
289
- ? `reflect endpoint unreachable (${llmConfig.reflectBaseUrl})`
290
- : `reflect model not loaded (${reflectModel} missing on ${llmConfig.reflectBaseUrl})`;
291
- console.warn(`[hicortex] ${reason} skipping reflection, scoring + linking will still run`);
292
- skipReflection = true;
255
+ console.log(`[hicortex] Capture complete: ${memoriesIngested} new memories`);
256
+ // Step 3: Consolidation — skipped in capture-only mode, dry-run, or no LLM.
257
+ // Runs even if capture had transient failures (opens DB directly, independent
258
+ // of the HTTP capture path). Full nightly only — capture-only runs are
259
+ // intended to run more frequently than once daily.
260
+ if (!dryRun && !captureOnly) {
261
+ if (!llm || !llmConfig) {
262
+ console.error("[hicortex] consolidation skipped: no LLM configured — run npx @gamaze/hicortex init");
263
+ }
264
+ else {
265
+ // Pre-flight health check for the reflect endpoint.
266
+ // If reflectBaseUrl points to a remote Ollama and it's down (MBP offline),
267
+ // skip reflection entirely instead of waiting through 3 retries (~3.5 min).
268
+ // Scoring + linking + decay still run.
269
+ let skipReflection = false;
270
+ if (llmConfig.reflectBaseUrl && (llmConfig.reflectProvider ?? llmConfig.provider) === "ollama") {
271
+ const reflectModel = llmConfig.reflectModel ?? llmConfig.model;
272
+ const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.reflectBaseUrl, reflectModel);
273
+ if (!health.ok) {
274
+ const reason = health.reason === "unreachable"
275
+ ? `reflect endpoint unreachable (${llmConfig.reflectBaseUrl})`
276
+ : `reflect model not loaded (${reflectModel} missing on ${llmConfig.reflectBaseUrl})`;
277
+ console.warn(`[hicortex] ${reason} — skipping reflection, scoring + linking will still run`);
278
+ skipReflection = true;
279
+ }
293
280
  }
281
+ console.log(`[hicortex] Running consolidation...`);
282
+ const report = await (0, consolidate_js_1.runConsolidation)(db, llm, embedder_js_1.embed, dryRun, skipReflection);
283
+ console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
284
+ (report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
294
285
  }
295
- console.log(`[hicortex] Running consolidation...`);
296
- const report = await (0, consolidate_js_1.runConsolidation)(db, llm, embedder_js_1.embed, dryRun, skipReflection);
297
- console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
298
- (report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
299
- }
300
- // Step 4: Inject lessons into the target file (CLAUDE.md or EXPERIENCE.md
301
- // or custom path — configurable via lessonTarget in config.json)
302
- if (!dryRun) {
303
- const lessonTarget = savedConfig?.lessonTarget;
304
- const injection = await (0, claude_md_js_1.injectLessons)(db, {
305
- claudeMdPath: lessonTarget,
306
- stateDir,
307
- });
308
- console.log(`[hicortex] Lessons updated: ${injection.lessonsCount} lessons at ${injection.path}`);
309
286
  }
310
- // Step 5: Update last-run timestamp
287
+ // Step 4: Update last-run timestamp.
311
288
  // CRITICAL: only advance lastRun if every session was processed without
312
289
  // a transient failure. Otherwise failed sessions would be permanently
313
290
  // lost — they'd be older than the new lastRun and never retried.
@@ -321,11 +298,16 @@ async function runNightly(options = {}) {
321
298
  }
322
299
  }
323
300
  console.log(`[hicortex] Nightly pipeline complete.`);
324
- // Step 6: Anonymous telemetry (fire-and-forget, opt-out via config)
325
- if (!dryRun && (0, telemetry_js_1.isTelemetryEnabled)(savedConfig)) {
326
- const agentType = piBatches.length > 0 && ccBatches.length > 0 ? "mixed"
327
- : piBatches.length > 0 ? "pi"
328
- : "cc";
301
+ // Anonymous telemetry (fire-and-forget, full nightly only).
302
+ // Capture-only runs are excluded to avoid inflating install pings.
303
+ if (!dryRun && !captureOnly && (0, telemetry_js_1.isTelemetryEnabled)(savedConfig)) {
304
+ const kinds = [
305
+ ccBatches.length > 0 && "cc",
306
+ hermesBatches.length > 0 && "hermes",
307
+ piBatches.length > 0 && "pi",
308
+ ocBatches.length > 0 && "oc",
309
+ ].filter(Boolean);
310
+ const agentType = kinds.length > 1 ? "mixed" : (kinds[0] ?? "cc");
329
311
  await (0, telemetry_js_1.sendTelemetry)({
330
312
  id: (0, telemetry_js_1.getTelemetryId)(stateDir),
331
313
  v: VERSION,
@@ -343,7 +325,7 @@ async function runNightly(options = {}) {
343
325
  }
344
326
  }
345
327
  // ---------------------------------------------------------------------------
346
- // Client Mode Nightly — distill locally, POST to remote server
328
+ // Client Mode Nightly — denoise locally, POST to remote server's /distill
347
329
  // ---------------------------------------------------------------------------
348
330
  async function runClientNightly(config, dryRun) {
349
331
  const serverUrl = config.serverUrl.replace(/\/+$/, "");
@@ -363,81 +345,33 @@ async function runClientNightly(config, dryRun) {
363
345
  console.error(`[hicortex] Aborting. Will retry next run.`);
364
346
  return; // Don't update last-run so we retry
365
347
  }
366
- // Init LLM for local distillation
367
- let llmConfig;
368
- if (config.llmBackend === "claude-cli") {
369
- const claudePath = (0, llm_js_1.findClaudeBinary)();
370
- if (claudePath) {
371
- llmConfig = (0, llm_js_1.claudeCliConfig)(claudePath);
372
- }
373
- else {
374
- llmConfig = (0, llm_js_1.resolveLlmConfigForCC)();
375
- }
376
- }
377
- else if (config.llmBackend === "ollama") {
378
- llmConfig = {
379
- baseUrl: config.llmBaseUrl ?? "http://localhost:11434",
380
- apiKey: "",
381
- model: config.llmModel ?? "qwen3.5:4b",
382
- reflectModel: config.reflectModel ?? config.llmModel ?? "qwen3.5:4b",
383
- provider: "ollama",
384
- };
385
- }
386
- else {
387
- llmConfig = (0, llm_js_1.resolveLlmConfigForCC)({
388
- llmBaseUrl: config.llmBaseUrl,
389
- llmApiKey: config.llmApiKey,
390
- llmModel: config.llmModel,
391
- });
392
- }
393
- if (config.distillModel) {
394
- llmConfig.distillModel = config.distillModel;
395
- }
396
- if (config.distillBaseUrl) {
397
- llmConfig.distillBaseUrl = config.distillBaseUrl;
398
- llmConfig.distillApiKey = config.distillApiKey ?? llmConfig.apiKey;
399
- llmConfig.distillProvider = config.distillProvider ?? llmConfig.provider;
400
- }
401
- // Auto-detect Ollama for batch distillation when claude-cli was resolved (fallback)
402
- llmConfig = await (0, llm_js_1.preferOllamaForBatch)(llmConfig);
403
- if (llmConfig.provider === "ollama" && !config.distillBaseUrl) {
404
- console.log(`[hicortex] Auto-detected local Ollama (${llmConfig.model}) — using for batch distillation`);
405
- }
406
- const llm = new llm_js_1.LlmClient(llmConfig);
407
- const distillInfo = llmConfig.distillBaseUrl
408
- ? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
409
- : llmConfig.distillModel ?? "";
410
- console.log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}`);
411
- // Detect safe chunk size based on model context window
412
- const chunkSize = await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.distillModel ?? llmConfig.model, llmConfig.baseUrl);
413
- // Read new CC transcripts
348
+ // No local LLM needed distillation happens on the server.
349
+ // Read new transcripts (CC + Hermes + Pi + OpenClaw). Client reads local
350
+ // logs, denoises, and POSTs the denoised text to the server's /distill
351
+ // endpoint. All readers no-op when their harness isn't installed.
414
352
  const since = readLastRun();
415
- console.log(`[hicortex] Reading CC transcripts since ${since.toISOString()}`);
416
- const batches = (0, transcript_reader_js_1.readCcTranscripts)(since);
417
- console.log(`[hicortex] Found ${batches.length} new session(s)`);
353
+ console.log(`[hicortex] Reading transcripts since ${since.toISOString()}`);
354
+ const ccBatches = (0, transcript_reader_js_1.readCcTranscripts)(since);
355
+ const hermesBatches = (0, hermes_transcript_reader_js_1.readHermesSessions)(since);
356
+ const piBatches = (0, pi_transcript_reader_js_1.readPiTranscripts)(since);
357
+ const ocBatches = (0, oc_transcript_reader_js_1.readOcTranscripts)(since);
358
+ const batches = [...ccBatches, ...hermesBatches, ...piBatches, ...ocBatches];
359
+ if (ccBatches.length > 0)
360
+ console.log(`[hicortex] Found ${ccBatches.length} CC session(s)`);
361
+ if (hermesBatches.length > 0)
362
+ console.log(`[hicortex] Found ${hermesBatches.length} Hermes session(s)`);
363
+ if (piBatches.length > 0)
364
+ console.log(`[hicortex] Found ${piBatches.length} Pi session(s)`);
365
+ if (ocBatches.length > 0)
366
+ console.log(`[hicortex] Found ${ocBatches.length} OpenClaw session(s)`);
367
+ console.log(`[hicortex] Total: ${batches.length} new session(s)`);
418
368
  if (batches.length === 0) {
419
- console.log(`[hicortex] Nothing to distill.`);
369
+ console.log(`[hicortex] Nothing to capture.`);
420
370
  if (!dryRun)
421
371
  writeLastRun();
422
372
  return;
423
373
  }
424
- // Pre-flight health check for a remote distill endpoint (client mode).
425
- // If the distill provider is Ollama on a remote host and the required model
426
- // isn't loaded, abort BEFORE touching any sessions — same data-loss fix
427
- // as server mode.
428
374
  let hadTransientFailure = false;
429
- if (llmConfig.distillBaseUrl && (llmConfig.distillProvider ?? llmConfig.provider) === "ollama") {
430
- const distillModel = llmConfig.distillModel ?? llmConfig.model;
431
- const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.distillBaseUrl, distillModel);
432
- if (!health.ok) {
433
- const reason = health.reason === "unreachable"
434
- ? `distill endpoint unreachable (${llmConfig.distillBaseUrl})`
435
- : `distill model not loaded (${distillModel} missing on ${llmConfig.distillBaseUrl})`;
436
- console.error(`[hicortex] ABORT: ${reason} — will retry next run, lastRun unchanged`);
437
- return; // Don't touch lastRun; next trigger retries the same sessions
438
- }
439
- }
440
- // Distill each session and POST to server
441
375
  let memoriesIngested = 0;
442
376
  let sessionsSent = 0;
443
377
  for (const batch of batches) {
@@ -446,78 +380,62 @@ async function runClientNightly(config, dryRun) {
446
380
  console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): too short`);
447
381
  continue;
448
382
  }
449
- console.log(`[hicortex] Distilling ${batch.sessionId.slice(0, 8)} (${batch.projectName}, ${batch.date})`);
383
+ console.log(`[hicortex] Capturing ${batch.sessionId.slice(0, 8)} (${batch.projectName}, ${batch.date})`);
450
384
  if (dryRun) {
451
- console.log(`[hicortex] [dry-run] Would distill ${transcript.length} chars`);
385
+ console.log(`[hicortex] [dry-run] Would POST ${transcript.length} chars to ${serverUrl}/distill`);
452
386
  continue;
453
387
  }
454
388
  try {
455
- const entries = await (0, distiller_js_1.distillSession)(llm, transcript, batch.projectName, batch.date, chunkSize);
456
- if (entries.length === 0) {
457
- console.log(`[hicortex] → No memories extracted`);
458
- continue;
459
- }
460
- // POST each extracted memory to the server
461
- let sessionCount = 0;
462
- for (const entry of entries) {
463
- const resp = await fetch(`${serverUrl}/ingest`, {
464
- method: "POST",
465
- headers: {
466
- "Content-Type": "application/json",
467
- ...(authToken ? { "Authorization": `Bearer ${authToken}` } : {}),
468
- },
469
- body: JSON.stringify({
470
- content: entry,
471
- source_agent: `claude-code/${batch.projectName}`,
472
- project: batch.projectName,
473
- memory_type: "episode",
474
- privacy: "WORK",
475
- source_session: batch.sessionId,
476
- session_date: batch.date,
477
- }),
478
- signal: AbortSignal.timeout(30_000),
479
- });
480
- const result = await resp.json();
481
- if (resp.status === 201) {
482
- sessionCount++;
483
- memoriesIngested++;
484
- }
485
- else if (result.skipped) {
486
- console.log(`[hicortex] → Already ingested (${result.existing_count} existing)`);
487
- break;
488
- }
489
- else if (resp.status === 401) {
490
- console.error(`[hicortex] Auth failed. Check authToken in ~/.hicortex/config.json`);
491
- return;
492
- }
493
- else if (resp.status === 429) {
494
- console.log(`[hicortex] Server memory limit reached.`);
495
- return;
496
- }
497
- else {
498
- console.error(`[hicortex] Ingest failed (${resp.status}): ${result.error}`);
499
- hadTransientFailure = true;
389
+ const resp = await fetch(`${serverUrl}/distill`, {
390
+ method: "POST",
391
+ headers: {
392
+ "Content-Type": "application/json",
393
+ ...(authToken ? { "Authorization": `Bearer ${authToken}` } : {}),
394
+ },
395
+ body: JSON.stringify({
396
+ text: transcript,
397
+ source_agent: batch.sourceAgent ?? `claude-code/${batch.projectName}`,
398
+ project: batch.projectName,
399
+ session_id: batch.sessionId,
400
+ session_date: batch.date,
401
+ privacy: "WORK",
402
+ }),
403
+ // Synchronous 35B distillation of a large session can take minutes.
404
+ signal: AbortSignal.timeout(20 * 60 * 1000),
405
+ });
406
+ if (resp.status === 200) {
407
+ const data = await resp.json();
408
+ if (data.skipped) {
409
+ console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)}: already ingested on server`);
500
410
  }
501
411
  }
502
- if (sessionCount > 0) {
412
+ else if (resp.status === 201) {
413
+ const data = await resp.json();
414
+ const count = data.distilled ?? 0;
415
+ memoriesIngested += count;
503
416
  sessionsSent++;
504
- console.log(`[hicortex] → ${sessionCount} memories sent to server`);
417
+ console.log(`[hicortex] → ${count} memories sent to server`);
418
+ }
419
+ else if (resp.status === 401) {
420
+ console.error(`[hicortex] Auth failed. Check authToken in ~/.hicortex/config.json`);
421
+ return; // No point retrying with wrong credentials
422
+ }
423
+ else if (resp.status === 429) {
424
+ const data = await resp.json().catch(() => ({}));
425
+ console.log(`[hicortex] Server memory limit reached: ${data.error}`);
426
+ return;
427
+ }
428
+ else {
429
+ const data = await resp.json().catch(() => ({}));
430
+ console.error(`[hicortex] /distill returned ${resp.status}: ${data.error ?? "unknown error"} — will retry next run`);
431
+ hadTransientFailure = true;
505
432
  }
506
433
  }
507
434
  catch (err) {
508
- console.error(`[hicortex] Distillation failed: ${err instanceof Error ? err.message : String(err)} — will retry next run`);
435
+ console.error(`[hicortex] Capture failed: ${err instanceof Error ? err.message : String(err)} — will retry next run`);
509
436
  hadTransientFailure = true;
510
437
  }
511
438
  }
512
- // Inject lessons from server into CLAUDE.md
513
- if (!dryRun) {
514
- try {
515
- await injectLessonsFromServer(serverUrl, authToken);
516
- }
517
- catch (err) {
518
- console.error(`[hicortex] CLAUDE.md injection failed: ${err instanceof Error ? err.message : String(err)}`);
519
- }
520
- }
521
439
  // Only advance lastRun if every session was processed without a transient
522
440
  // failure. Otherwise failed sessions would be permanently lost.
523
441
  if (!dryRun) {
@@ -532,100 +450,22 @@ async function runClientNightly(config, dryRun) {
532
450
  console.log(`[hicortex] Client nightly complete: ${memoriesIngested} memories from ${sessionsSent} sessions → ${serverUrl}`);
533
451
  // Anonymous telemetry (fire-and-forget, opt-out via config)
534
452
  if (!dryRun && (0, telemetry_js_1.isTelemetryEnabled)(config)) {
453
+ const kinds = [
454
+ ccBatches.length > 0 && "cc",
455
+ hermesBatches.length > 0 && "hermes",
456
+ piBatches.length > 0 && "pi",
457
+ ocBatches.length > 0 && "oc",
458
+ ].filter(Boolean);
459
+ const agentType = kinds.length > 1 ? "mixed" : (kinds[0] ?? "cc");
535
460
  await (0, telemetry_js_1.sendTelemetry)({
536
461
  id: (0, telemetry_js_1.getTelemetryId)(HICORTEX_HOME),
537
462
  v: VERSION,
538
463
  mode: "client",
539
- agent: "cc", // client mode is always CC-originated currently
464
+ agent: agentType,
540
465
  mem: memoriesIngested,
541
- lessons: 0, // client doesn't know lesson count
466
+ lessons: 0, // client doesn't have direct DB access
542
467
  sessions: batches.length,
543
468
  ok: !hadTransientFailure,
544
469
  });
545
470
  }
546
471
  }
547
- /**
548
- * Fetch lessons + memory index from server and inject into CLAUDE.md.
549
- * Client mode equivalent of the server's injectLessons(db, ...).
550
- */
551
- async function injectLessonsFromServer(serverUrl, authToken) {
552
- const resp = await fetch(`${serverUrl}/lessons`, {
553
- headers: authToken ? { "Authorization": `Bearer ${authToken}` } : {},
554
- signal: AbortSignal.timeout(10_000),
555
- });
556
- if (!resp.ok) {
557
- console.log(`[hicortex] Could not fetch lessons from server (${resp.status})`);
558
- return;
559
- }
560
- const data = await resp.json();
561
- const maxLessons = (0, features_js_1.lessonsLimit)();
562
- // Use moduleIndex from server response, fall back to local state
563
- const moduleIndex = data.moduleIndex ?? (0, state_js_1.loadState)().moduleIndex;
564
- const selected = await (0, extensions_js_1.getLessonSelector)().select(data.lessons, { maxLessons, moduleIndex });
565
- // Format lessons
566
- const lessonLines = selected.map((l) => {
567
- const titleMatch = l.content.match(/## Lesson: (.+)/);
568
- const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
569
- const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
570
- const title = titleMatch ? titleMatch[1] : l.content.slice(0, 150);
571
- const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
572
- return `- ${title}${meta ? ` (${meta})` : ""}`;
573
- });
574
- // Format module/project index
575
- let indexLines;
576
- if (moduleIndex && moduleIndex.domains.length > 0) {
577
- indexLines = [];
578
- for (const domain of moduleIndex.domains) {
579
- const kwStr = domain.keywords.length > 0 ? `: ${domain.keywords.join(", ")}` : "";
580
- indexLines.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount} lessons)${kwStr}`);
581
- indexLines.push(` ${domain.projects.join(" | ")}`);
582
- }
583
- }
584
- else {
585
- indexLines = [data.index.projects.map(p => `${p.name}: ${p.count}`).join(" | ")];
586
- }
587
- // Build block
588
- const START_MARKER = "<!-- HICORTEX-LEARNINGS:START -->";
589
- const END_MARKER = "<!-- HICORTEX-LEARNINGS:END -->";
590
- const blockParts = [START_MARKER, "## Hicortex Memory"];
591
- blockParts.push("", "You have access to shared long-term memory across all agents and sessions.", "BEFORE making decisions, search memory: `hicortex_search` for prior decisions on the same topic.", "Use `hicortex_context` at session start for recent project state.");
592
- if (lessonLines.length > 0) {
593
- blockParts.push("", "### Lessons (updated nightly)");
594
- blockParts.push(...lessonLines);
595
- }
596
- else {
597
- blockParts.push("", "### Getting Started");
598
- blockParts.push("- Search past decisions with `hicortex_search` before starting work");
599
- blockParts.push("- Save important decisions with `hicortex_ingest`");
600
- blockParts.push("- Lessons will appear here after the first nightly run");
601
- }
602
- if (indexLines.length > 0) {
603
- blockParts.push("", "### Memory Index");
604
- blockParts.push(...indexLines);
605
- blockParts.push(`${data.index.total} memories, ${data.index.lessonCount} lessons, ${data.index.sourceCount} agents. Search with \`hicortex_search\`.`);
606
- }
607
- blockParts.push(END_MARKER);
608
- const block = blockParts.join("\n");
609
- // Write to CLAUDE.md (uses fs/path/os already imported at top of file)
610
- const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
611
- let content = "";
612
- try {
613
- content = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
614
- }
615
- catch { }
616
- const startIdx = content.indexOf(START_MARKER);
617
- const endIdx = content.indexOf(END_MARKER);
618
- if (startIdx !== -1 && endIdx !== -1) {
619
- content = content.slice(0, startIdx) + block + content.slice(endIdx + END_MARKER.length);
620
- }
621
- else {
622
- if (content.length > 0 && !content.endsWith("\n"))
623
- content += "\n";
624
- if (content.length > 0)
625
- content += "\n";
626
- content += block + "\n";
627
- }
628
- (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(claudeMdPath), { recursive: true });
629
- (0, node_fs_1.writeFileSync)(claudeMdPath, content);
630
- console.log(`[hicortex] CLAUDE.md updated: ${lessonLines.length} lessons, ${data.index.total} memories indexed`);
631
- }