@gamaze/hicortex 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -42,7 +42,9 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  const node_path_1 = require("node:path");
44
44
  const db_js_1 = require("./db.js");
45
- const license_js_1 = require("./license.js");
45
+ const features_js_1 = require("./features.js");
46
+ const extensions_js_1 = require("./extensions.js");
47
+ const state_js_1 = require("./state.js");
46
48
  const llm_js_1 = require("./llm.js");
47
49
  const node_fs_1 = require("node:fs");
48
50
  const node_os_1 = require("node:os");
@@ -87,8 +89,10 @@ exports.default = {
87
89
  llm = new llm_js_1.LlmClient(llmConfig);
88
90
  log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model} ` +
89
91
  `(reflect: ${llmConfig.reflectModel})`);
90
- // License check (non-blocking)
91
- (0, license_js_1.validateLicense)(config.licenseKey, stateDir).catch((err) => log(`[hicortex] License validation failed: ${err}`));
92
+ // One-time migration of legacy state files (no-op if state.json exists)
93
+ (0, state_js_1.migrateLegacyState)(stateDir);
94
+ // License: init feature cache (sync after this returns)
95
+ await (0, features_js_1.initFeatures)(config.licenseKey, stateDir);
92
96
  // Schedule nightly consolidation
93
97
  const consolidateHour = config.consolidateHour ?? 2;
94
98
  cancelConsolidation = (0, consolidate_js_1.scheduleConsolidation)(db, llm, embedder_js_1.embed, consolidateHour);
@@ -123,9 +127,13 @@ exports.default = {
123
127
  const lessons = storage.getLessons(db, 7, ctx.project);
124
128
  if (lessons.length === 0)
125
129
  return {};
126
- const maxFeatures = (0, license_js_1.getFeatures)(stateDir);
127
- const maxLessons = maxFeatures.maxMemories === -1 ? 20 : 10;
128
- const formatted = lessons.slice(0, maxLessons).map((l) => {
130
+ const maxLessons = (0, features_js_1.lessonsLimit)();
131
+ const selected = await (0, extensions_js_1.getLessonSelector)().select(lessons, {
132
+ maxLessons,
133
+ project: ctx.project,
134
+ agentId: ctx.agentId,
135
+ });
136
+ const formatted = selected.map((l) => {
129
137
  // Extract just the lesson text from the structured content
130
138
  const match = l.content.match(/## Lesson: (.+)/);
131
139
  return match ? `- ${match[1]}` : `- ${l.content.slice(0, 200)}`;
@@ -136,9 +144,9 @@ exports.default = {
136
144
  "\n";
137
145
  // Daily friendly reminder when at memory cap
138
146
  const memCount = storage.countMemories(db);
139
- if (maxFeatures.maxMemories > 0 && memCount >= maxFeatures.maxMemories) {
147
+ if ((0, features_js_1.memoryCapReached)(memCount)) {
140
148
  context +=
141
- `\n---\nHicortex free tier: ${maxFeatures.maxMemories} memories stored. ` +
149
+ `\n---\nHicortex free tier: ${(0, features_js_1.maxMemoriesAllowed)()} memories stored. ` +
142
150
  `New memories can no longer be saved, and your agent has stopped learning and self-improving from new sessions. ` +
143
151
  `Existing memories, lessons, and search still work. ` +
144
152
  `Upgrade for unlimited usage: https://hicortex.gamaze.com/ ` +
@@ -169,10 +177,8 @@ exports.default = {
169
177
  if (entries.length === 0)
170
178
  return;
171
179
  // Check license cap
172
- const features = (0, license_js_1.getFeatures)(stateDir);
173
- if (features.maxMemories > 0 &&
174
- storage.countMemories(db) >= features.maxMemories) {
175
- console.warn(`[hicortex] Free tier limit reached (${features.maxMemories} memories). ` +
180
+ if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
181
+ console.warn(`[hicortex] Free tier limit reached (${(0, features_js_1.maxMemoriesAllowed)()} memories). ` +
176
182
  `Search and lessons still work, but new memories won't be saved. ` +
177
183
  `Upgrade for unlimited usage: https://hicortex.gamaze.com/`);
178
184
  return;
@@ -299,12 +305,11 @@ exports.default = {
299
305
  async execute(_callId, args, context) {
300
306
  if (!db)
301
307
  return { error: "Hicortex not initialized" };
302
- const features = (0, license_js_1.getFeatures)(stateDir);
303
- if (features.maxMemories > 0 && storage.countMemories(db) >= features.maxMemories) {
308
+ if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
304
309
  return {
305
310
  content: [{
306
311
  type: "text",
307
- text: `Free tier limit reached (${features.maxMemories} memories). ` +
312
+ text: `Free tier limit reached (${(0, features_js_1.maxMemoriesAllowed)()} memories). ` +
308
313
  `Your existing memories and lessons still work — search and recall are unaffected. ` +
309
314
  `New memories won't be saved until you upgrade.\n\n` +
310
315
  `Upgrade for unlimited usage: https://hicortex.gamaze.com/`
@@ -529,26 +534,6 @@ function persistProviderConfig(llmConfig, log) {
529
534
  // Non-fatal — config works in memory even if we can't persist
530
535
  }
531
536
  }
532
- /**
533
- * Track when the memory cap was first hit. Returns days since cap was reached.
534
- * Stores timestamp in stateDir/cap-hit.txt on first detection.
535
- */
536
- function getDaysSinceCapHit(dir) {
537
- const capFile = (0, node_path_1.join)(dir, "cap-hit.txt");
538
- try {
539
- const ts = (0, node_fs_1.readFileSync)(capFile, "utf-8").trim();
540
- const hitDate = new Date(ts);
541
- return Math.floor((Date.now() - hitDate.getTime()) / (1000 * 60 * 60 * 24));
542
- }
543
- catch {
544
- // First time hitting cap — record it
545
- try {
546
- (0, node_fs_1.writeFileSync)(capFile, new Date().toISOString());
547
- }
548
- catch { /* non-fatal */ }
549
- return 0;
550
- }
551
- }
552
537
  const HICORTEX_TOOLS = [
553
538
  "hicortex_search",
554
539
  "hicortex_context",
package/dist/license.d.ts CHANGED
@@ -1,5 +1,15 @@
1
+ /**
2
+ * License API client.
3
+ *
4
+ * This module is a thin wrapper over the validation HTTP endpoint at
5
+ * https://hicortex.gamaze.com/api/validate. Persistence and feature gating
6
+ * live in features.ts + state.ts; this file does NOT touch disk.
7
+ *
8
+ * Offline grace: when the API is unreachable, we fall back to the cached
9
+ * tier in state.json (written by features.ts on the last successful
10
+ * validation). If the cached tier was validated within OFFLINE_GRACE_DAYS,
11
+ * we treat it as still valid.
12
+ */
1
13
  import type { LicenseInfo } from "./types.js";
2
- /** Validate a license key against the Hicortex API */
14
+ /** Validate a license key against the Hicortex API. */
3
15
  export declare function validateLicense(key: string | undefined, stateDir: string): Promise<LicenseInfo>;
4
- /** Get current features, using cache or free tier defaults */
5
- export declare function getFeatures(stateDir: string): LicenseInfo["features"];
package/dist/license.js CHANGED
@@ -1,13 +1,23 @@
1
1
  "use strict";
2
+ /**
3
+ * License API client.
4
+ *
5
+ * This module is a thin wrapper over the validation HTTP endpoint at
6
+ * https://hicortex.gamaze.com/api/validate. Persistence and feature gating
7
+ * live in features.ts + state.ts; this file does NOT touch disk.
8
+ *
9
+ * Offline grace: when the API is unreachable, we fall back to the cached
10
+ * tier in state.json (written by features.ts on the last successful
11
+ * validation). If the cached tier was validated within OFFLINE_GRACE_DAYS,
12
+ * we treat it as still valid.
13
+ */
2
14
  Object.defineProperty(exports, "__esModule", { value: true });
3
15
  exports.validateLicense = validateLicense;
4
- exports.getFeatures = getFeatures;
5
- const node_fs_1 = require("node:fs");
6
- const node_path_1 = require("node:path");
16
+ const state_js_1 = require("./state.js");
7
17
  const VALIDATE_URL = "https://hicortex.gamaze.com/api/validate";
8
18
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
9
19
  const OFFLINE_GRACE_DAYS = 7;
10
- // In-memory cache
20
+ // In-memory cache for the current process
11
21
  let cachedLicense = null;
12
22
  let cacheTimestamp = 0;
13
23
  const FREE_LICENSE = {
@@ -21,12 +31,12 @@ const FREE_LICENSE = {
21
31
  remoteIngest: true,
22
32
  },
23
33
  };
24
- /** Validate a license key against the Hicortex API */
34
+ /** Validate a license key against the Hicortex API. */
25
35
  async function validateLicense(key, stateDir) {
26
36
  // No key = free tier
27
37
  if (!key)
28
38
  return FREE_LICENSE;
29
- // Check in-memory cache
39
+ // Check in-memory cache (24h TTL)
30
40
  if (cachedLicense && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
31
41
  return cachedLicense;
32
42
  }
@@ -41,57 +51,36 @@ async function validateLicense(key, stateDir) {
41
51
  throw new Error(`HTTP ${resp.status}`);
42
52
  }
43
53
  const data = (await resp.json());
44
- // Cache result
45
54
  cachedLicense = data;
46
55
  cacheTimestamp = Date.now();
47
- // Persist last successful validation timestamp for offline grace
48
- if (data.valid) {
49
- persistValidationTimestamp(stateDir);
50
- }
51
56
  return data;
52
57
  }
53
58
  catch {
54
- // Network failure — check offline grace period
55
- return offlineFallback(key, stateDir);
56
- }
57
- }
58
- /** Get current features, using cache or free tier defaults */
59
- function getFeatures(stateDir) {
60
- if (cachedLicense)
61
- return cachedLicense.features;
62
- return FREE_LICENSE.features;
63
- }
64
- function persistValidationTimestamp(stateDir) {
65
- try {
66
- (0, node_fs_1.writeFileSync)((0, node_path_1.join)(stateDir, "license-validated.txt"), new Date().toISOString());
67
- }
68
- catch {
69
- // Non-critical
59
+ // Network failure — check offline grace period via state.tier
60
+ return offlineFallback(stateDir);
70
61
  }
71
62
  }
72
- function offlineFallback(key, stateDir) {
73
- const tsPath = (0, node_path_1.join)(stateDir, "license-validated.txt");
74
- if (!(0, node_fs_1.existsSync)(tsPath))
63
+ /**
64
+ * Offline fallback: if state.tier was validated within the grace period,
65
+ * return its cached features as if validation succeeded. Otherwise, free tier.
66
+ */
67
+ function offlineFallback(stateDir) {
68
+ const persisted = (0, state_js_1.loadState)(stateDir).tier;
69
+ if (!persisted)
75
70
  return FREE_LICENSE;
76
71
  try {
77
- const lastValidated = new Date((0, node_fs_1.readFileSync)(tsPath, "utf-8").trim());
72
+ const lastValidated = new Date(persisted.validatedAt);
78
73
  const daysSince = (Date.now() - lastValidated.getTime()) / (1000 * 60 * 60 * 24);
79
74
  if (daysSince <= OFFLINE_GRACE_DAYS) {
80
- // Within grace period — assume last known state was valid
81
- return cachedLicense ?? {
75
+ return {
82
76
  valid: true,
83
- tier: "pro",
84
- features: {
85
- reflection: true,
86
- vectorSearch: true,
87
- maxMemories: -1,
88
- crossAgent: true,
89
- },
77
+ tier: persisted.tier,
78
+ features: persisted.features,
90
79
  };
91
80
  }
92
81
  }
93
82
  catch {
94
- // Corrupted file
83
+ // Corrupted timestamp — fall through
95
84
  }
96
85
  return FREE_LICENSE;
97
86
  }
@@ -55,7 +55,8 @@ const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
55
55
  const zod_1 = require("zod");
56
56
  const db_js_1 = require("./db.js");
57
57
  const llm_js_1 = require("./llm.js");
58
- const license_js_1 = require("./license.js");
58
+ const features_js_1 = require("./features.js");
59
+ const state_js_1 = require("./state.js");
59
60
  const embedder_js_1 = require("./embedder.js");
60
61
  const storage = __importStar(require("./storage.js"));
61
62
  const retrieval = __importStar(require("./retrieval.js"));
@@ -121,12 +122,11 @@ function createMcpServer() {
121
122
  }, async ({ content, project, memory_type }) => {
122
123
  if (!db)
123
124
  return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
124
- const features = (0, license_js_1.getFeatures)(stateDir);
125
- if (features.maxMemories > 0 && storage.countMemories(db) >= features.maxMemories) {
125
+ if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
126
126
  return {
127
127
  content: [{
128
128
  type: "text",
129
- text: `Free tier limit reached (${features.maxMemories} memories). ` +
129
+ text: `Free tier limit reached (${(0, features_js_1.maxMemoriesAllowed)()} memories). ` +
130
130
  `Your existing memories and lessons still work — search and recall are unaffected. ` +
131
131
  `New memories won't be saved until you upgrade.\n\n` +
132
132
  `Upgrade for unlimited usage: https://hicortex.gamaze.com/`
@@ -296,11 +296,13 @@ async function startServer(options = {}) {
296
296
  ? `${llmConfig.reflectProvider}/${llmConfig.reflectModel}@${llmConfig.reflectBaseUrl}`
297
297
  : llmConfig.reflectModel;
298
298
  console.log(`[hicortex] LLM fast: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}, reflect: ${reflectInfo}`);
299
- // License: read from options, config file, or env var
299
+ // One-time migration of legacy state files (no-op if state.json exists)
300
+ (0, state_js_1.migrateLegacyState)(stateDir);
301
+ // License: read from options, config file, or env var, init feature cache
300
302
  const licenseKey = options.licenseKey
301
303
  ?? savedConfig?.licenseKey
302
304
  ?? process.env.HICORTEX_LICENSE_KEY;
303
- (0, license_js_1.validateLicense)(licenseKey, stateDir).catch((err) => console.log(`[hicortex] License validation failed: ${err}`));
305
+ await (0, features_js_1.initFeatures)(licenseKey, stateDir);
304
306
  if (licenseKey) {
305
307
  console.log(`[hicortex] License key configured`);
306
308
  }
@@ -411,15 +413,12 @@ async function startServer(options = {}) {
411
413
  // Pro license blocks remote ingest (upgrade to Team for multi-client)
412
414
  const ip = req.ip ?? req.socket.remoteAddress ?? "";
413
415
  const isLocal = ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
414
- if (!isLocal) {
415
- const features = (0, license_js_1.getFeatures)(stateDir);
416
- if (features.remoteIngest === false) {
417
- res.status(403).json({
418
- error: "Pro license is single-machine. Upgrade to Team for multi-client remote ingestion.",
419
- upgrade: "https://hicortex.gamaze.com/",
420
- });
421
- return;
422
- }
416
+ if (!isLocal && !(0, features_js_1.remoteIngestAllowed)()) {
417
+ res.status(403).json({
418
+ error: "Pro license is single-machine. Upgrade to Team for multi-client remote ingestion.",
419
+ upgrade: "https://hicortex.gamaze.com/",
420
+ });
421
+ return;
423
422
  }
424
423
  const { content, source_agent, project, memory_type, privacy, source_session, session_date } = req.body ?? {};
425
424
  if (!content || typeof content !== "string") {
@@ -440,9 +439,8 @@ async function startServer(options = {}) {
440
439
  }
441
440
  }
442
441
  // License check
443
- const features = (0, license_js_1.getFeatures)(stateDir);
444
- if (features.maxMemories > 0 && storage.countMemories(db) >= features.maxMemories) {
445
- res.status(429).json({ error: "Memory limit reached", limit: features.maxMemories });
442
+ if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
443
+ res.status(429).json({ error: "Memory limit reached", limit: (0, features_js_1.maxMemoriesAllowed)() });
446
444
  return;
447
445
  }
448
446
  try {
package/dist/nightly.js CHANGED
@@ -55,9 +55,10 @@ const distiller_js_1 = require("./distiller.js");
55
55
  const consolidate_js_1 = require("./consolidate.js");
56
56
  const transcript_reader_js_1 = require("./transcript-reader.js");
57
57
  const claude_md_js_1 = require("./claude-md.js");
58
- const license_js_1 = require("./license.js");
58
+ const features_js_1 = require("./features.js");
59
+ const extensions_js_1 = require("./extensions.js");
60
+ const state_js_1 = require("./state.js");
59
61
  const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
60
- const LAST_RUN_PATH = (0, node_path_1.join)(HICORTEX_HOME, "nightly-last-run.txt");
61
62
  function readNightlyConfig(stateDir) {
62
63
  try {
63
64
  const configPath = (0, node_path_1.join)(stateDir, "config.json");
@@ -78,25 +79,24 @@ function readConfigLicenseKey(stateDir) {
78
79
  return undefined;
79
80
  }
80
81
  }
81
- function readLastRun() {
82
- try {
83
- const ts = (0, node_fs_1.readFileSync)(LAST_RUN_PATH, "utf-8").trim();
84
- const d = new Date(ts);
85
- if (!isNaN(d.getTime()))
86
- return d;
87
- }
88
- catch {
89
- // No file — first run
90
- }
91
- return new Date(0); // Process everything
82
+ function readLastRun(stateDir = HICORTEX_HOME) {
83
+ const ts = (0, state_js_1.loadState)(stateDir).lastNightly;
84
+ if (!ts)
85
+ return new Date(0); // First run — process everything
86
+ const d = new Date(ts);
87
+ return isNaN(d.getTime()) ? new Date(0) : d;
92
88
  }
93
- function writeLastRun() {
94
- (0, node_fs_1.mkdirSync)(HICORTEX_HOME, { recursive: true });
95
- (0, node_fs_1.writeFileSync)(LAST_RUN_PATH, new Date().toISOString());
89
+ function writeLastRun(stateDir = HICORTEX_HOME) {
90
+ (0, state_js_1.updateState)((s) => {
91
+ s.lastNightly = new Date().toISOString();
92
+ return s;
93
+ }, stateDir);
96
94
  }
97
95
  async function runNightly(options = {}) {
98
96
  const dryRun = options.dryRun ?? false;
99
97
  const stateDir = options.stateDir ?? HICORTEX_HOME;
98
+ // One-time migration of legacy state files (no-op if state.json exists)
99
+ (0, state_js_1.migrateLegacyState)(stateDir);
100
100
  // Check mode: client or server
101
101
  const savedConfig = readNightlyConfig(stateDir);
102
102
  if (savedConfig?.mode === "client") {
@@ -109,9 +109,9 @@ async function runNightly(options = {}) {
109
109
  // Init DB
110
110
  const db = (0, db_js_1.initDb)(dbPath);
111
111
  try {
112
- // License: read from config file or env var
112
+ // License: read from config file or env var, init feature cache
113
113
  const licenseKey = readConfigLicenseKey(stateDir) ?? process.env.HICORTEX_LICENSE_KEY;
114
- await (0, license_js_1.validateLicense)(licenseKey, stateDir);
114
+ await (0, features_js_1.initFeatures)(licenseKey, stateDir);
115
115
  // Init LLM: check config.json first, then auto-detect
116
116
  let llmConfig;
117
117
  const savedConfig = readNightlyConfig(stateDir);
@@ -182,7 +182,6 @@ async function runNightly(options = {}) {
182
182
  }
183
183
  // Step 2: Distill each session
184
184
  let memoriesIngested = 0;
185
- const features = (0, license_js_1.getFeatures)(stateDir);
186
185
  // Detect safe chunk size based on model context window
187
186
  const chunkSize = await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.distillModel ?? llmConfig.model, llmConfig.baseUrl);
188
187
  for (const batch of batches) {
@@ -197,8 +196,8 @@ async function runNightly(options = {}) {
197
196
  continue;
198
197
  }
199
198
  // Check cap before distilling
200
- if (features.maxMemories > 0 && storage.countMemories(db) >= features.maxMemories) {
201
- console.log(`[hicortex] Free tier limit (${features.maxMemories} memories). Skipping new ingestion. ` +
199
+ if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
200
+ console.log(`[hicortex] Free tier limit (${(0, features_js_1.maxMemoriesAllowed)()} memories). Skipping new ingestion. ` +
202
201
  `Upgrade: https://hicortex.gamaze.com/`);
203
202
  break;
204
203
  }
@@ -238,7 +237,7 @@ async function runNightly(options = {}) {
238
237
  }
239
238
  // Step 4: Inject lessons into CLAUDE.md
240
239
  if (!dryRun) {
241
- const injection = (0, claude_md_js_1.injectLessons)(db, { stateDir });
240
+ const injection = await (0, claude_md_js_1.injectLessons)(db, { stateDir });
242
241
  console.log(`[hicortex] CLAUDE.md updated: ${injection.lessonsCount} lessons at ${injection.path}`);
243
242
  }
244
243
  // Step 5: Update last-run timestamp
@@ -427,8 +426,8 @@ async function injectLessonsFromServer(serverUrl, authToken) {
427
426
  return;
428
427
  }
429
428
  const data = await resp.json();
430
- const maxLessons = 10;
431
- const selected = data.lessons.slice(0, maxLessons);
429
+ const maxLessons = (0, features_js_1.lessonsLimit)();
430
+ const selected = await (0, extensions_js_1.getLessonSelector)().select(data.lessons, { maxLessons });
432
431
  // Format lessons
433
432
  const lessonLines = selected.map((l) => {
434
433
  const titleMatch = l.content.match(/## Lesson: (.+)/);
@@ -462,14 +461,11 @@ async function injectLessonsFromServer(serverUrl, authToken) {
462
461
  }
463
462
  blockParts.push(END_MARKER);
464
463
  const block = blockParts.join("\n");
465
- // Write to CLAUDE.md
466
- const { readFileSync, writeFileSync, mkdirSync } = await import("node:fs");
467
- const { join, dirname } = await import("node:path");
468
- const { homedir } = await import("node:os");
469
- const claudeMdPath = join(homedir(), ".claude", "CLAUDE.md");
464
+ // Write to CLAUDE.md (uses fs/path/os already imported at top of file)
465
+ const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
470
466
  let content = "";
471
467
  try {
472
- content = readFileSync(claudeMdPath, "utf-8");
468
+ content = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
473
469
  }
474
470
  catch { }
475
471
  const startIdx = content.indexOf(START_MARKER);
@@ -484,7 +480,7 @@ async function injectLessonsFromServer(serverUrl, authToken) {
484
480
  content += "\n";
485
481
  content += block + "\n";
486
482
  }
487
- mkdirSync(dirname(claudeMdPath), { recursive: true });
488
- writeFileSync(claudeMdPath, content);
483
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(claudeMdPath), { recursive: true });
484
+ (0, node_fs_1.writeFileSync)(claudeMdPath, content);
489
485
  console.log(`[hicortex] CLAUDE.md updated: ${lessonLines.length} lessons, ${data.index.total} memories indexed`);
490
486
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Centralized state management — single ~/.hicortex/state.json file.
3
+ *
4
+ * Replaces 4 separate state files used by previous versions:
5
+ * - nightly-last-run.txt → state.lastNightly
6
+ * - last-consolidated.txt → state.lastConsolidated
7
+ * - tier.json → state.tier
8
+ * - license-validated.txt → state.tier.validatedAt (subsumed)
9
+ *
10
+ * Why one file:
11
+ * - Atomic writes (write to temp + rename)
12
+ * - Easier debugging (one file to inspect)
13
+ * - No filesystem chatter from multiple separate writes
14
+ * - Single migration path going forward
15
+ *
16
+ * Note: ~/.hicortex/config.json is intentionally NOT merged here. Config is
17
+ * user-edited and tracked separately from machine state.
18
+ */
19
+ import type { LicenseInfo } from "./types.js";
20
+ /** Persisted tier information — reflects the last successful validation. */
21
+ export interface PersistedTier {
22
+ /** Tier name from the validation API response. */
23
+ tier: LicenseInfo["tier"];
24
+ /** ISO timestamp of when this tier was validated against the API. */
25
+ validatedAt: string;
26
+ /** Cached features object — used by features.ts and offline fallback. */
27
+ features: LicenseInfo["features"];
28
+ }
29
+ export interface HicortexState {
30
+ /** ISO timestamp of the last nightly transcript scan watermark. */
31
+ lastNightly?: string;
32
+ /** ISO timestamp of the last consolidation pipeline run. */
33
+ lastConsolidated?: string;
34
+ /** Last-known license tier (replaces tier.json + license-validated.txt). */
35
+ tier?: PersistedTier;
36
+ }
37
+ /**
38
+ * Load the state file. Returns an empty state if the file is missing
39
+ * or corrupted (callers should handle missing fields with defaults).
40
+ */
41
+ export declare function loadState(stateDir?: string): HicortexState;
42
+ /**
43
+ * Atomically write the state file. Uses write-to-temp + rename so a crash
44
+ * during write cannot leave a half-written state.json on disk.
45
+ */
46
+ export declare function saveState(state: HicortexState, stateDir?: string): void;
47
+ /**
48
+ * Read-modify-write helper. The updater receives the current state and
49
+ * returns the next state (or void if it mutates in place).
50
+ */
51
+ export declare function updateState(updater: (state: HicortexState) => HicortexState | void, stateDir?: string): HicortexState;
52
+ /**
53
+ * One-time migration from the 4 legacy state files to state.json.
54
+ *
55
+ * Behaviour:
56
+ * 1. If state.json already exists, do nothing (and clean up any leftover
57
+ * legacy files from a previously interrupted migration).
58
+ * 2. Otherwise, read whichever legacy files exist, build a HicortexState,
59
+ * write state.json, and delete the legacy files.
60
+ *
61
+ * Idempotent: safe to call on every boot.
62
+ * Returns true if migration ran, false if state.json already existed.
63
+ */
64
+ export declare function migrateLegacyState(stateDir?: string): boolean;