@gamaze/hicortex 0.4.3 → 0.4.5
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/claude-md.d.ts +2 -2
- package/dist/claude-md.js +8 -6
- package/dist/consolidate.js +9 -22
- package/dist/db.d.ts +2 -0
- package/dist/db.js +88 -9
- package/dist/extensions.d.ts +126 -0
- package/dist/extensions.js +154 -0
- package/dist/features.d.ts +37 -0
- package/dist/features.js +127 -0
- package/dist/index.js +21 -57
- package/dist/init.d.ts +1 -1
- package/dist/init.js +4 -4
- package/dist/license.d.ts +13 -3
- package/dist/license.js +30 -41
- package/dist/llm.d.ts +8 -7
- package/dist/llm.js +33 -48
- package/dist/mcp-server.js +16 -18
- package/dist/nightly.js +28 -32
- package/dist/state.d.ts +64 -0
- package/dist/state.js +162 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +7 -3
- package/skills/hicortex-learn/SKILL.md +2 -2
package/dist/claude-md.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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+)/);
|
package/dist/consolidate.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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"];
|
package/dist/features.js
ADDED
|
@@ -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
|
+
}
|