@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.
@@ -18,10 +18,10 @@ export declare function injectLessons(db: Database.Database, options?: {
18
18
  claudeMdPath?: string;
19
19
  stateDir?: string;
20
20
  project?: string;
21
- }): {
21
+ }): Promise<{
22
22
  lessonsCount: number;
23
23
  path: string;
24
- };
24
+ }>;
25
25
  /**
26
26
  * Remove the Hicortex Learnings block from CLAUDE.md.
27
27
  * Used by the uninstall command.
package/dist/claude-md.js CHANGED
@@ -49,7 +49,8 @@ const node_fs_1 = require("node:fs");
49
49
  const node_path_1 = require("node:path");
50
50
  const node_os_1 = require("node:os");
51
51
  const storage = __importStar(require("./storage.js"));
52
- const license_js_1 = require("./license.js");
52
+ const features_js_1 = require("./features.js");
53
+ const extensions_js_1 = require("./extensions.js");
53
54
  const START_MARKER = "<!-- HICORTEX-LEARNINGS:START -->";
54
55
  const END_MARKER = "<!-- HICORTEX-LEARNINGS:END -->";
55
56
  const DEFAULT_CLAUDE_MD = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
@@ -58,15 +59,16 @@ const DEFAULT_CLAUDE_MD = (0, node_path_1.join)((0, node_os_1.homedir)(), ".clau
58
59
  * Creates the file if it doesn't exist.
59
60
  * Replaces existing block if present, appends if not.
60
61
  */
61
- function injectLessons(db, options = {}) {
62
+ async function injectLessons(db, options = {}) {
62
63
  const claudeMdPath = options.claudeMdPath ?? DEFAULT_CLAUDE_MD;
63
- const stateDir = options.stateDir ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
64
64
  // Determine limits based on license
65
- const features = (0, license_js_1.getFeatures)(stateDir);
66
- const maxLessons = features.maxMemories === -1 ? 10 : 5;
65
+ const maxLessons = (0, features_js_1.lessonsLimit)();
67
66
  // --- Lessons ---
68
67
  const lessons = storage.getLessons(db, 30, options.project);
69
- const selected = lessons.slice(0, maxLessons);
68
+ const selected = await (0, extensions_js_1.getLessonSelector)().select(lessons, {
69
+ maxLessons,
70
+ project: options.project,
71
+ });
70
72
  const lessonLines = selected.map((l) => {
71
73
  const titleMatch = l.content.match(/## Lesson: (.+)/);
72
74
  const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
@@ -43,13 +43,11 @@ exports.parseJsonLenient = parseJsonLenient;
43
43
  exports.runConsolidation = runConsolidation;
44
44
  exports.msUntilHour = msUntilHour;
45
45
  exports.scheduleConsolidation = scheduleConsolidation;
46
- const node_fs_1 = require("node:fs");
47
- const node_path_1 = require("node:path");
48
- const node_os_1 = require("node:os");
49
46
  const retrieval_js_1 = require("./retrieval.js");
50
47
  const storage = __importStar(require("./storage.js"));
51
48
  const prompts_js_1 = require("./prompts.js");
52
- const LAST_CONSOLIDATED_PATH = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex", "last-consolidated.txt");
49
+ const features_js_1 = require("./features.js");
50
+ const state_js_1 = require("./state.js");
53
51
  // Default config constants (matching Python config.py)
54
52
  const CONSOLIDATE_MAX_LLM_CALLS = 200;
55
53
  const CONSOLIDATE_PRUNE_MIN_AGE_DAYS = 90;
@@ -130,12 +128,7 @@ function parseJsonLenient(text, fallback) {
130
128
  // Stage 1: Pre-check
131
129
  // ---------------------------------------------------------------------------
132
130
  function readLastConsolidated() {
133
- try {
134
- return (0, node_fs_1.readFileSync)(LAST_CONSOLIDATED_PATH, "utf-8").trim();
135
- }
136
- catch {
137
- return "";
138
- }
131
+ return (0, state_js_1.loadState)().lastConsolidated ?? "";
139
132
  }
140
133
  function stagePrecheck(db) {
141
134
  const lastTs = readLastConsolidated();
@@ -273,10 +266,8 @@ async function stageReflection(db, memories, llm, budget, embedFn, dryRun) {
273
266
  };
274
267
  try {
275
268
  // Check memory cap before storing lesson
276
- const { getFeatures } = await import("./license.js");
277
- const features = getFeatures("");
278
- if (features.maxMemories > 0 && storage.countMemories(db) >= features.maxMemories) {
279
- console.log(`[hicortex] Free tier limit (${features.maxMemories} memories). ` +
269
+ if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
270
+ console.log(`[hicortex] Free tier limit (${(0, features_js_1.maxMemoriesAllowed)()} memories). ` +
280
271
  `Existing memories and lessons still work. New lessons won't be saved. ` +
281
272
  `Upgrade for unlimited usage: https://hicortex.gamaze.com/`);
282
273
  break;
@@ -445,14 +436,10 @@ async function runConsolidation(db, llm, embedFn, dryRun = false) {
445
436
  }
446
437
  // Update last-consolidated timestamp
447
438
  if (!dryRun && report.status === "completed") {
448
- try {
449
- const dir = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "memory");
450
- (0, node_fs_1.mkdirSync)(dir, { recursive: true });
451
- (0, node_fs_1.writeFileSync)(LAST_CONSOLIDATED_PATH, new Date().toISOString());
452
- }
453
- catch {
454
- // Non-fatal
455
- }
439
+ (0, state_js_1.updateState)((s) => {
440
+ s.lastConsolidated = new Date().toISOString();
441
+ return s;
442
+ });
456
443
  }
457
444
  report.budget = budget.summary();
458
445
  report.completed_at = new Date().toISOString();
package/dist/db.d.ts CHANGED
@@ -18,6 +18,8 @@ export declare function resolveDbPath(override?: string): string;
18
18
  * Returns the open Database instance (caller manages lifetime).
19
19
  */
20
20
  export declare function initDb(dbPath: string): Database.Database;
21
+ /** Read the currently-applied schema version. Returns 0 if no migrations applied. */
22
+ export declare function getSchemaVersion(db: Database.Database): number;
21
23
  /**
22
24
  * Return database statistics.
23
25
  */
package/dist/db.js CHANGED
@@ -9,6 +9,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.resolveDbPath = resolveDbPath;
11
11
  exports.initDb = initDb;
12
+ exports.getSchemaVersion = getSchemaVersion;
12
13
  exports.getStats = getStats;
13
14
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
14
15
  const node_fs_1 = require("node:fs");
@@ -175,18 +176,96 @@ function initDb(dbPath) {
175
176
  return db;
176
177
  }
177
178
  /**
178
- * Apply schema migrations for existing databases.
179
+ * Helper: check if a column exists on a table without aborting.
180
+ * Used by migrations to stay idempotent across partially-migrated databases.
181
+ */
182
+ function hasColumn(db, table, column) {
183
+ const cols = db.pragma(`table_info(${table})`);
184
+ return cols.some((c) => c.name === column);
185
+ }
186
+ /**
187
+ * Migrations are append-only. Add new entries at the end with monotonically
188
+ * increasing version numbers. Each migration runs in a single transaction
189
+ * and the schema_version row is inserted only on success.
190
+ *
191
+ * IMPORTANT: never edit a migration after it has shipped — write a new one.
192
+ *
193
+ * IMPORTANT: sqlite-vec virtual tables (memory_vectors) and FTS5 virtual
194
+ * tables (memories_fts) cannot be ALTER'd. If a future Pro feature needs
195
+ * per-vector metadata, add a sidecar table and JOIN, do not try to extend
196
+ * the virtual table in place.
197
+ */
198
+ const MIGRATIONS = [
199
+ {
200
+ version: 1,
201
+ name: "add_ingested_at",
202
+ up: (db) => {
203
+ if (!hasColumn(db, "memories", "ingested_at")) {
204
+ db.exec("ALTER TABLE memories ADD COLUMN ingested_at TIMESTAMP");
205
+ db.exec("UPDATE memories SET ingested_at = created_at WHERE ingested_at IS NULL");
206
+ }
207
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_ingested ON memories(ingested_at)");
208
+ },
209
+ },
210
+ {
211
+ version: 2,
212
+ name: "add_updated_at",
213
+ up: (db) => {
214
+ if (!hasColumn(db, "memories", "updated_at")) {
215
+ db.exec("ALTER TABLE memories ADD COLUMN updated_at TIMESTAMP");
216
+ }
217
+ },
218
+ },
219
+ ];
220
+ /**
221
+ * Run all pending migrations against the database.
222
+ *
223
+ * Creates the schema_version tracking table if missing, then applies any
224
+ * migration whose version > the current max. Each migration runs inside its
225
+ * own transaction so a partial failure rolls back cleanly. Existing databases
226
+ * with the relevant columns already present pass through as no-ops because
227
+ * each migration's up() is idempotent.
179
228
  */
180
229
  function migrate(db) {
181
- const cols = db.pragma("table_info(memories)");
182
- const colNames = new Set(cols.map((c) => c.name));
183
- if (!colNames.has("ingested_at")) {
184
- db.exec("ALTER TABLE memories ADD COLUMN ingested_at TIMESTAMP");
185
- db.exec("UPDATE memories SET ingested_at = created_at");
186
- db.exec("CREATE INDEX IF NOT EXISTS idx_memories_ingested ON memories(ingested_at)");
230
+ db.exec(`
231
+ CREATE TABLE IF NOT EXISTS schema_version (
232
+ version INTEGER PRIMARY KEY,
233
+ name TEXT NOT NULL,
234
+ applied_at TEXT NOT NULL
235
+ );
236
+ `);
237
+ const row = db
238
+ .prepare("SELECT MAX(version) as v FROM schema_version")
239
+ .get();
240
+ const currentVersion = row.v ?? 0;
241
+ const pending = MIGRATIONS.filter((m) => m.version > currentVersion);
242
+ if (pending.length === 0)
243
+ return;
244
+ for (const m of pending) {
245
+ const tx = db.transaction(() => {
246
+ m.up(db);
247
+ db.prepare("INSERT INTO schema_version (version, name, applied_at) VALUES (?, ?, ?)").run(m.version, m.name, new Date().toISOString());
248
+ });
249
+ try {
250
+ tx();
251
+ console.log(`[hicortex] Applied migration ${m.version}: ${m.name}`);
252
+ }
253
+ catch (err) {
254
+ const msg = err instanceof Error ? err.message : String(err);
255
+ throw new Error(`Migration ${m.version} (${m.name}) failed: ${msg}`);
256
+ }
187
257
  }
188
- if (!colNames.has("updated_at")) {
189
- db.exec("ALTER TABLE memories ADD COLUMN updated_at TIMESTAMP");
258
+ }
259
+ /** Read the currently-applied schema version. Returns 0 if no migrations applied. */
260
+ function getSchemaVersion(db) {
261
+ try {
262
+ const row = db
263
+ .prepare("SELECT MAX(version) as v FROM schema_version")
264
+ .get();
265
+ return row.v ?? 0;
266
+ }
267
+ catch {
268
+ return 0;
190
269
  }
191
270
  }
192
271
  /**
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Extension interfaces for the OSS/Pro split.
3
+ *
4
+ * The OSS client defines these interfaces and ships default implementations
5
+ * (the current behaviour). Pro features (in src/pro/, never published) provide
6
+ * alternative implementations that are loaded at runtime via dynamic import,
7
+ * gated by the license check in features.ts.
8
+ *
9
+ * Why these specific interfaces:
10
+ *
11
+ * LessonSelector — there are exactly 3 lesson selection sites in the OSS
12
+ * code (claude-md.ts:injectLessons, index.ts:before_agent_start,
13
+ * nightly.ts:injectLessonsFromServer). All three currently do
14
+ * `lessons.slice(0, maxLessons)` after a DB or HTTP fetch. The Pro
15
+ * selector ranks lessons against project context, recency, and
16
+ * effectiveness scores instead of dumb-truncating.
17
+ *
18
+ * PromptStrategy — the current prompts.ts exports three pure functions
19
+ * (distillation, reflection, importanceScoring). The Pro variant has
20
+ * prescriptive prompts (`when X do Y` format) and re-trained reflection
21
+ * that produces a richer schema. The strategy bundles the prompt WITH
22
+ * its parser so a Pro prompt with a different output schema cannot
23
+ * silently fail when the OSS consumer parses it with the wrong shape.
24
+ *
25
+ * NOT in this file (deliberately deferred):
26
+ * - ContextAssembler — over-abstracted; the real seam is buildProjectContext
27
+ * in claude-md.ts, not the whole assembler. Will add when needed.
28
+ * - LessonValidator — speculative; no validation exists today. Will add
29
+ * when the first Pro use case demands it.
30
+ */
31
+ /**
32
+ * Minimum fields a lesson must have for the selector to work.
33
+ *
34
+ * The default selector only needs `content`. Pro selectors may need more
35
+ * (project for in-project weighting, base_strength for ranking, etc.).
36
+ *
37
+ * Memory satisfies this interface (it's a structural subset), and the
38
+ * client-mode HTTP shape `{content, created_at, base_strength, access_count}`
39
+ * also satisfies it. The selector is generic over T so call sites get back
40
+ * the same shape they passed in.
41
+ */
42
+ export interface SelectableLesson {
43
+ content: string;
44
+ project?: string | null;
45
+ created_at?: string;
46
+ base_strength?: number;
47
+ access_count?: number;
48
+ id?: string;
49
+ memory_type?: string;
50
+ }
51
+ export interface LessonSelectorContext {
52
+ /** Maximum number of lessons to return. Caller decides this from features.lessonsLimit(). */
53
+ maxLessons: number;
54
+ /** Current project, if known. Pro selectors weight in-project lessons higher. */
55
+ project?: string | null;
56
+ /** Optional: agent id, for cross-agent learning context (Pro). */
57
+ agentId?: string;
58
+ /** Optional: current task description, for relevance scoring (Pro). */
59
+ currentTask?: string;
60
+ }
61
+ export interface LessonSelector {
62
+ /**
63
+ * Pick `ctx.maxLessons` lessons from the candidate pool.
64
+ * Default impl: take the first N (caller passes them in priority order).
65
+ * Pro impl: rank by relevance to ctx.project / ctx.currentTask / effectiveness.
66
+ *
67
+ * Generic so the output type matches the input type — Memory[] in returns
68
+ * Memory[] out, partial-shape in returns partial-shape out.
69
+ */
70
+ select<T extends SelectableLesson>(lessons: T[], ctx: LessonSelectorContext): T[] | Promise<T[]>;
71
+ }
72
+ /**
73
+ * Default LessonSelector — preserves current OSS behaviour exactly.
74
+ * `slice(0, maxLessons)` over the candidate pool, no re-ranking.
75
+ */
76
+ export declare const defaultLessonSelector: LessonSelector;
77
+ /** Output schema produced by the reflection prompt and consumed by consolidate.ts. */
78
+ export interface ReflectionLesson {
79
+ lesson: string;
80
+ type: "reinforce" | "correct" | "principle" | string;
81
+ project: string;
82
+ severity: "critical" | "important" | "minor" | string;
83
+ confidence: "high" | "medium" | "low" | string;
84
+ source_pattern?: string;
85
+ }
86
+ export interface PromptStrategy {
87
+ /** Build the distillation prompt (transcript → memories). */
88
+ distillation(project: string, date: string, transcript: string): string;
89
+ /** Build the reflection prompt (recent memories → lessons). */
90
+ reflection(memoriesBlock: string, recentLessons?: string): string;
91
+ /** Build the importance scoring prompt (batch of memories → scores). */
92
+ importanceScoring(memoriesBlock: string): string;
93
+ /**
94
+ * Parse the LLM's reflection output. Bundled with the prompt so a Pro prompt
95
+ * with a different output schema cannot silently fail downstream.
96
+ */
97
+ parseReflection(raw: string): ReflectionLesson[];
98
+ /**
99
+ * Parse the LLM's importance scoring output.
100
+ * @param raw The LLM response
101
+ * @param expectedCount The number of memories scored — used to pad/trim
102
+ * @returns Array of scores (length === expectedCount), each in [0, 1]
103
+ */
104
+ parseImportanceScores(raw: string, expectedCount: number): number[];
105
+ }
106
+ export declare const defaultPromptStrategy: PromptStrategy;
107
+ /**
108
+ * Holder for the active extension implementations. The OSS client always uses
109
+ * the defaults; Pro features (in src/pro/, loaded via dynamic import) replace
110
+ * these at boot if a valid license is present.
111
+ *
112
+ * Wiring will happen via setExtensions() called from src/pro/ after license
113
+ * validation (not yet implemented in OSS). Until Pro code exists and is loaded,
114
+ * every call site uses the defaults — zero behavioural change for OSS users.
115
+ */
116
+ declare let activeExtensions: {
117
+ selector: LessonSelector;
118
+ prompts: PromptStrategy;
119
+ };
120
+ /** Replace the active extensions (called from features.ts when Pro loads). */
121
+ export declare function setExtensions(ext: Partial<typeof activeExtensions>): void;
122
+ /** Get the active LessonSelector (default unless Pro is loaded). */
123
+ export declare function getLessonSelector(): LessonSelector;
124
+ /** Get the active PromptStrategy (default unless Pro is loaded). */
125
+ export declare function getPromptStrategy(): PromptStrategy;
126
+ export {};
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ /**
3
+ * Extension interfaces for the OSS/Pro split.
4
+ *
5
+ * The OSS client defines these interfaces and ships default implementations
6
+ * (the current behaviour). Pro features (in src/pro/, never published) provide
7
+ * alternative implementations that are loaded at runtime via dynamic import,
8
+ * gated by the license check in features.ts.
9
+ *
10
+ * Why these specific interfaces:
11
+ *
12
+ * LessonSelector — there are exactly 3 lesson selection sites in the OSS
13
+ * code (claude-md.ts:injectLessons, index.ts:before_agent_start,
14
+ * nightly.ts:injectLessonsFromServer). All three currently do
15
+ * `lessons.slice(0, maxLessons)` after a DB or HTTP fetch. The Pro
16
+ * selector ranks lessons against project context, recency, and
17
+ * effectiveness scores instead of dumb-truncating.
18
+ *
19
+ * PromptStrategy — the current prompts.ts exports three pure functions
20
+ * (distillation, reflection, importanceScoring). The Pro variant has
21
+ * prescriptive prompts (`when X do Y` format) and re-trained reflection
22
+ * that produces a richer schema. The strategy bundles the prompt WITH
23
+ * its parser so a Pro prompt with a different output schema cannot
24
+ * silently fail when the OSS consumer parses it with the wrong shape.
25
+ *
26
+ * NOT in this file (deliberately deferred):
27
+ * - ContextAssembler — over-abstracted; the real seam is buildProjectContext
28
+ * in claude-md.ts, not the whole assembler. Will add when needed.
29
+ * - LessonValidator — speculative; no validation exists today. Will add
30
+ * when the first Pro use case demands it.
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.defaultPromptStrategy = exports.defaultLessonSelector = void 0;
34
+ exports.setExtensions = setExtensions;
35
+ exports.getLessonSelector = getLessonSelector;
36
+ exports.getPromptStrategy = getPromptStrategy;
37
+ /**
38
+ * Default LessonSelector — preserves current OSS behaviour exactly.
39
+ * `slice(0, maxLessons)` over the candidate pool, no re-ranking.
40
+ */
41
+ exports.defaultLessonSelector = {
42
+ select(lessons, ctx) {
43
+ return lessons.slice(0, ctx.maxLessons);
44
+ },
45
+ };
46
+ // ---------------------------------------------------------------------------
47
+ // Default PromptStrategy — wraps the current prompts.ts and the lenient JSON
48
+ // parser from consolidate.ts. Preserves current OSS behaviour exactly.
49
+ // ---------------------------------------------------------------------------
50
+ const prompts_js_1 = require("./prompts.js");
51
+ /**
52
+ * Lenient JSON parser — tolerates markdown fences and indexed list formats.
53
+ * Extracted from consolidate.ts to keep prompt+parser together.
54
+ */
55
+ function parseJsonLenient(text, fallback) {
56
+ text = text.trim();
57
+ // Strip markdown code fences
58
+ if (text.startsWith("```")) {
59
+ const lines = text.split("\n");
60
+ const stripped = lines.slice(1);
61
+ if (stripped.length > 0 && stripped[stripped.length - 1].trim() === "```") {
62
+ stripped.pop();
63
+ }
64
+ text = stripped.join("\n").trim();
65
+ }
66
+ try {
67
+ return JSON.parse(text);
68
+ }
69
+ catch {
70
+ // Fall through
71
+ }
72
+ // Handle "[0] 0.7\n[1] 0.6\n..." indexed format
73
+ const indexed = [...text.matchAll(/\[\d+\]\s*([\d.]+)/g)];
74
+ if (indexed.length > 0) {
75
+ try {
76
+ return indexed.map((m) => parseFloat(m[1]));
77
+ }
78
+ catch {
79
+ // Fall through
80
+ }
81
+ }
82
+ return fallback;
83
+ }
84
+ exports.defaultPromptStrategy = {
85
+ distillation: prompts_js_1.distillation,
86
+ reflection: prompts_js_1.reflection,
87
+ importanceScoring: prompts_js_1.importanceScoring,
88
+ parseReflection(raw) {
89
+ const parsed = parseJsonLenient(raw, []);
90
+ if (!Array.isArray(parsed))
91
+ return [];
92
+ const out = [];
93
+ for (const item of parsed) {
94
+ if (typeof item !== "object" || item === null)
95
+ continue;
96
+ const lo = item;
97
+ const lessonText = String(lo.lesson ?? "");
98
+ if (!lessonText)
99
+ continue;
100
+ out.push({
101
+ lesson: lessonText,
102
+ type: String(lo.type ?? "principle"),
103
+ project: String(lo.project ?? "global"),
104
+ severity: String(lo.severity ?? "important"),
105
+ confidence: String(lo.confidence ?? "medium"),
106
+ source_pattern: lo.source_pattern ? String(lo.source_pattern) : undefined,
107
+ });
108
+ }
109
+ return out;
110
+ },
111
+ parseImportanceScores(raw, expectedCount) {
112
+ let scores = parseJsonLenient(raw, null);
113
+ if (!Array.isArray(scores)) {
114
+ scores = new Array(expectedCount).fill(0.5);
115
+ }
116
+ while (scores.length < expectedCount)
117
+ scores.push(0.5);
118
+ scores = scores.slice(0, expectedCount);
119
+ return scores.map((s) => {
120
+ const v = Number(s);
121
+ if (isNaN(v))
122
+ return 0.5;
123
+ return Math.max(0, Math.min(1, v));
124
+ });
125
+ },
126
+ };
127
+ // ---------------------------------------------------------------------------
128
+ // Loader — used by call sites to get either the OSS default or a Pro override
129
+ // ---------------------------------------------------------------------------
130
+ /**
131
+ * Holder for the active extension implementations. The OSS client always uses
132
+ * the defaults; Pro features (in src/pro/, loaded via dynamic import) replace
133
+ * these at boot if a valid license is present.
134
+ *
135
+ * Wiring will happen via setExtensions() called from src/pro/ after license
136
+ * validation (not yet implemented in OSS). Until Pro code exists and is loaded,
137
+ * every call site uses the defaults — zero behavioural change for OSS users.
138
+ */
139
+ let activeExtensions = {
140
+ selector: exports.defaultLessonSelector,
141
+ prompts: exports.defaultPromptStrategy,
142
+ };
143
+ /** Replace the active extensions (called from features.ts when Pro loads). */
144
+ function setExtensions(ext) {
145
+ activeExtensions = { ...activeExtensions, ...ext };
146
+ }
147
+ /** Get the active LessonSelector (default unless Pro is loaded). */
148
+ function getLessonSelector() {
149
+ return activeExtensions.selector;
150
+ }
151
+ /** Get the active PromptStrategy (default unless Pro is loaded). */
152
+ function getPromptStrategy() {
153
+ return activeExtensions.prompts;
154
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Centralized feature gating — single source of truth for tier-dependent values.
3
+ *
4
+ * Why this exists:
5
+ * - getFeatures() in license.ts was sync but validateLicense was async, creating
6
+ * a race where Pro users got free-tier features during the validation window.
7
+ * - License checks were scattered across 8+ call sites with subtly different
8
+ * handling (e.g., consolidate.ts:308 had a dynamic import in a hot loop to
9
+ * dodge a circular import).
10
+ *
11
+ * All feature decisions now flow through this module. Call initFeatures() once at
12
+ * process boot before serving any requests; sync getters become deterministic.
13
+ */
14
+ import type { LicenseInfo } from "./types.js";
15
+ /**
16
+ * Initialize the feature cache. Call ONCE at process boot before any feature
17
+ * gating queries. Race fix:
18
+ * 1. Synchronously load persisted tier from disk (instant, deterministic)
19
+ * 2. If no persisted tier and we have a key, AWAIT first validation
20
+ * 3. If persisted tier exists, kick off background re-validation
21
+ *
22
+ * After this returns, sync getters (isPro, lessonsLimit, etc.) are deterministic
23
+ * and reflect the user's actual tier — no more "free during validation window".
24
+ */
25
+ export declare function initFeatures(licenseKey: string | undefined, stateDir?: string): Promise<void>;
26
+ /** Are we on a paid tier (Pro, Team, Lifetime)? */
27
+ export declare function isPro(): boolean;
28
+ /** Memory count cap. -1 = unlimited (paid). */
29
+ export declare function maxMemoriesAllowed(): number;
30
+ /** Has the memory cap been hit? Pass current count from caller. */
31
+ export declare function memoryCapReached(currentCount: number): boolean;
32
+ /** Number of lessons to inject into CLAUDE.md / before_agent_start. */
33
+ export declare function lessonsLimit(): number;
34
+ /** Is remote /ingest allowed? Free + Team yes, Pro (single-machine) no. */
35
+ export declare function remoteIngestAllowed(): boolean;
36
+ /** Direct read of the underlying features (for callers that need the full record). */
37
+ export declare function getCurrentFeatures(): LicenseInfo["features"];
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ /**
3
+ * Centralized feature gating — single source of truth for tier-dependent values.
4
+ *
5
+ * Why this exists:
6
+ * - getFeatures() in license.ts was sync but validateLicense was async, creating
7
+ * a race where Pro users got free-tier features during the validation window.
8
+ * - License checks were scattered across 8+ call sites with subtly different
9
+ * handling (e.g., consolidate.ts:308 had a dynamic import in a hot loop to
10
+ * dodge a circular import).
11
+ *
12
+ * All feature decisions now flow through this module. Call initFeatures() once at
13
+ * process boot before serving any requests; sync getters become deterministic.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.initFeatures = initFeatures;
17
+ exports.isPro = isPro;
18
+ exports.maxMemoriesAllowed = maxMemoriesAllowed;
19
+ exports.memoryCapReached = memoryCapReached;
20
+ exports.lessonsLimit = lessonsLimit;
21
+ exports.remoteIngestAllowed = remoteIngestAllowed;
22
+ exports.getCurrentFeatures = getCurrentFeatures;
23
+ const node_path_1 = require("node:path");
24
+ const node_os_1 = require("node:os");
25
+ const license_js_1 = require("./license.js");
26
+ const state_js_1 = require("./state.js");
27
+ const DEFAULT_STATE_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
28
+ const FREE_FEATURES = {
29
+ reflection: true,
30
+ vectorSearch: true,
31
+ maxMemories: 250,
32
+ crossAgent: true,
33
+ remoteIngest: true,
34
+ };
35
+ let currentFeatures = FREE_FEATURES;
36
+ let initialized = false;
37
+ function persistTier(stateDir, info) {
38
+ (0, state_js_1.updateState)((s) => {
39
+ s.tier = {
40
+ tier: info.tier,
41
+ validatedAt: new Date().toISOString(),
42
+ features: info.features,
43
+ };
44
+ return s;
45
+ }, stateDir);
46
+ }
47
+ /**
48
+ * Initialize the feature cache. Call ONCE at process boot before any feature
49
+ * gating queries. Race fix:
50
+ * 1. Synchronously load persisted tier from disk (instant, deterministic)
51
+ * 2. If no persisted tier and we have a key, AWAIT first validation
52
+ * 3. If persisted tier exists, kick off background re-validation
53
+ *
54
+ * After this returns, sync getters (isPro, lessonsLimit, etc.) are deterministic
55
+ * and reflect the user's actual tier — no more "free during validation window".
56
+ */
57
+ async function initFeatures(licenseKey, stateDir = DEFAULT_STATE_DIR) {
58
+ if (initialized)
59
+ return;
60
+ initialized = true;
61
+ // Step 1: Load persisted tier from state.json (instant)
62
+ const persisted = (0, state_js_1.loadState)(stateDir).tier;
63
+ if (persisted) {
64
+ currentFeatures = persisted.features;
65
+ }
66
+ else {
67
+ currentFeatures = FREE_FEATURES;
68
+ }
69
+ // Step 2: No key → free tier, done
70
+ if (!licenseKey)
71
+ return;
72
+ // Step 3: Validate
73
+ if (!persisted) {
74
+ // First-time: AWAIT validation so the very first request sees the right tier
75
+ try {
76
+ const info = await (0, license_js_1.validateLicense)(licenseKey, stateDir);
77
+ currentFeatures = info.features;
78
+ if (info.valid) {
79
+ persistTier(stateDir, info);
80
+ }
81
+ }
82
+ catch {
83
+ // Validation failed (network, etc.) — stay on free
84
+ }
85
+ }
86
+ else {
87
+ // Already have a persisted tier; re-validate in background
88
+ (0, license_js_1.validateLicense)(licenseKey, stateDir)
89
+ .then((info) => {
90
+ currentFeatures = info.features;
91
+ if (info.valid) {
92
+ persistTier(stateDir, info);
93
+ }
94
+ })
95
+ .catch(() => {
96
+ // Keep persisted features
97
+ });
98
+ }
99
+ }
100
+ // ---------------------------------------------------------------------------
101
+ // Public API — sync getters used everywhere in the codebase
102
+ // ---------------------------------------------------------------------------
103
+ /** Are we on a paid tier (Pro, Team, Lifetime)? */
104
+ function isPro() {
105
+ return currentFeatures.maxMemories === -1;
106
+ }
107
+ /** Memory count cap. -1 = unlimited (paid). */
108
+ function maxMemoriesAllowed() {
109
+ return currentFeatures.maxMemories;
110
+ }
111
+ /** Has the memory cap been hit? Pass current count from caller. */
112
+ function memoryCapReached(currentCount) {
113
+ const max = maxMemoriesAllowed();
114
+ return max > 0 && currentCount >= max;
115
+ }
116
+ /** Number of lessons to inject into CLAUDE.md / before_agent_start. */
117
+ function lessonsLimit() {
118
+ return isPro() ? 20 : 10;
119
+ }
120
+ /** Is remote /ingest allowed? Free + Team yes, Pro (single-machine) no. */
121
+ function remoteIngestAllowed() {
122
+ return currentFeatures.remoteIngest !== false;
123
+ }
124
+ /** Direct read of the underlying features (for callers that need the full record). */
125
+ function getCurrentFeatures() {
126
+ return currentFeatures;
127
+ }