@gamaze/hicortex 0.4.2 → 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+)/);
package/dist/cli.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * server Start the MCP HTTP/SSE server (persistent daemon)
7
7
  * init Detect existing setup and configure for CC/OC
8
8
  * nightly Run distill + consolidate + inject lessons (manual trigger)
9
+ * nightly --status Show nightly pipeline health check
9
10
  * status Show config, DB stats, adapter status
10
11
  * uninstall Clean removal of CC integration
11
12
  */
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * server Start the MCP HTTP/SSE server (persistent daemon)
8
8
  * init Detect existing setup and configure for CC/OC
9
9
  * nightly Run distill + consolidate + inject lessons (manual trigger)
10
+ * nightly --status Show nightly pipeline health check
10
11
  * status Show config, DB stats, adapter status
11
12
  * uninstall Clean removal of CC integration
12
13
  */
@@ -38,13 +39,24 @@ switch (command) {
38
39
  break;
39
40
  }
40
41
  case "nightly": {
41
- const dryRun = process.argv.includes("--dry-run");
42
- import("./nightly.js").then(({ runNightly }) => {
43
- runNightly({ dryRun }).catch((err) => {
44
- console.error("[hicortex] Nightly pipeline failed:", err);
45
- process.exit(1);
42
+ const args = process.argv.slice(3);
43
+ if (args.includes("--status")) {
44
+ import("./nightly-status.js").then(({ showNightlyStatus }) => {
45
+ showNightlyStatus().catch((err) => {
46
+ console.error("[hicortex] Status check failed:", err);
47
+ process.exit(1);
48
+ });
46
49
  });
47
- });
50
+ }
51
+ else {
52
+ const dryRun = args.includes("--dry-run");
53
+ import("./nightly.js").then(({ runNightly }) => {
54
+ runNightly({ dryRun }).catch((err) => {
55
+ console.error("[hicortex] Nightly pipeline failed:", err);
56
+ process.exit(1);
57
+ });
58
+ });
59
+ }
48
60
  break;
49
61
  }
50
62
  case "status":
@@ -77,13 +89,15 @@ Commands:
77
89
  uninstall Remove CC integration (preserves DB)
78
90
 
79
91
  Options:
80
- server --port <n> Port (default: 8787)
81
- server --host <h> Host (default: 127.0.0.1)
82
- nightly --dry-run Preview without changes
92
+ server --port <n> Port (default: 8787)
93
+ server --host <h> Host (default: 127.0.0.1)
94
+ nightly --dry-run Preview without changes
95
+ nightly --status Show nightly pipeline health
83
96
 
84
97
  Examples:
85
98
  npx @gamaze/hicortex server
86
99
  npx @gamaze/hicortex init
100
+ npx @gamaze/hicortex nightly --status
87
101
  npx @gamaze/hicortex init --server https://myserver.example.com
88
102
  npx @gamaze/hicortex status`);
89
103
  process.exit(command ? 1 : 0);
@@ -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
  /**
@@ -7,8 +7,10 @@ import type { LlmClient } from "./llm.js";
7
7
  /**
8
8
  * Estimate a safe chunk size in chars based on the LLM provider and model.
9
9
  * - API providers (Anthropic, OpenAI, claude-cli): no chunking needed (large context windows)
10
- * - Ollama: query /api/show for context_length, use ~60% for chunks (leaving room for prompt + generation)
11
- * - Fallback: 20K chars (~5K tokens) — safe for 4B models with 32K context
10
+ * - Ollama: query /api/show for context_length AND parameter_count, cap based on both
11
+ * - Small models (<8B params): max 20K chars (~5K tokens) — keeps CPU inference under ~60s
12
+ * - Larger models: up to 60K chars (~15K tokens)
13
+ * - Fallback: 20K chars
12
14
  */
13
15
  export declare function detectChunkSize(provider: string, model: string, baseUrl?: string): Promise<number>;
14
16
  /**
package/dist/distiller.js CHANGED
@@ -11,11 +11,18 @@ exports.distillSession = distillSession;
11
11
  const prompts_js_1 = require("./prompts.js");
12
12
  const MAX_TRANSCRIPT_CHARS = 80_000;
13
13
  const MIN_CONVERSATION_CHARS = 200;
14
+ // Chunk size limits by model parameter count (for local/CPU inference)
15
+ // Small models are slow on CPU — cap input size to keep inference under ~60s
16
+ const SMALL_MODEL_PARAMS = 8_000_000_000; // 8B — threshold for "small"
17
+ const SMALL_MODEL_MAX_CHUNK_CHARS = 20_000; // ~5K tokens — safe for 4-8B on CPU
18
+ const LARGE_MODEL_MAX_CHUNK_CHARS = 60_000; // ~15K tokens — ok for 8B+ on GPU or API
14
19
  /**
15
20
  * Estimate a safe chunk size in chars based on the LLM provider and model.
16
21
  * - API providers (Anthropic, OpenAI, claude-cli): no chunking needed (large context windows)
17
- * - Ollama: query /api/show for context_length, use ~60% for chunks (leaving room for prompt + generation)
18
- * - Fallback: 20K chars (~5K tokens) — safe for 4B models with 32K context
22
+ * - Ollama: query /api/show for context_length AND parameter_count, cap based on both
23
+ * - Small models (<8B params): max 20K chars (~5K tokens) — keeps CPU inference under ~60s
24
+ * - Larger models: up to 60K chars (~15K tokens)
25
+ * - Fallback: 20K chars
19
26
  */
20
27
  async function detectChunkSize(provider, model, baseUrl) {
21
28
  // API-based providers handle large contexts natively — no chunking needed
@@ -33,16 +40,30 @@ async function detectChunkSize(provider, model, baseUrl) {
33
40
  });
34
41
  if (resp.ok) {
35
42
  const data = await resp.json();
36
- // Try to extract context length from model_info
37
43
  const info = data.model_info ?? {};
44
+ // Extract parameter count for speed-aware capping
45
+ const paramKey = Object.keys(info).find((k) => k.endsWith("parameter_count"));
46
+ const paramCount = paramKey && typeof info[paramKey] === "number"
47
+ ? info[paramKey]
48
+ : 0;
49
+ const isSmallModel = paramCount > 0 && paramCount < SMALL_MODEL_PARAMS;
50
+ // Extract context length for context-aware capping
38
51
  const ctxKey = Object.keys(info).find((k) => k.endsWith("context_length") || k.endsWith("context_window"));
39
- if (ctxKey && typeof info[ctxKey] === "number") {
40
- const contextTokens = info[ctxKey];
41
- // Use 60% of context for chunk input (~4 chars/token)
42
- const chunkChars = Math.floor(contextTokens * 0.6 * 4);
43
- console.log(`[hicortex] Model context: ${contextTokens} tokens, chunk size: ${chunkChars} chars`);
44
- return Math.min(chunkChars, MAX_TRANSCRIPT_CHARS);
45
- }
52
+ const contextTokens = ctxKey && typeof info[ctxKey] === "number"
53
+ ? info[ctxKey]
54
+ : 0;
55
+ // Determine max chunk size based on model size (speed constraint)
56
+ // Unknown param count defaults to conservative (small model) — safe for any hardware
57
+ const maxBySpeed = !isSmallModel && paramCount > 0 ? LARGE_MODEL_MAX_CHUNK_CHARS : SMALL_MODEL_MAX_CHUNK_CHARS;
58
+ // Determine max chunk size based on context window (fits-in-context constraint)
59
+ const maxByContext = contextTokens > 0
60
+ ? Math.floor(contextTokens * 0.6 * 4) // 60% of context, ~4 chars/token
61
+ : MAX_TRANSCRIPT_CHARS;
62
+ const chunkChars = Math.min(maxBySpeed, maxByContext);
63
+ console.log(`[hicortex] Model: ${paramCount > 0 ? `${(paramCount / 1e9).toFixed(1)}B params` : "unknown size"}, ` +
64
+ `context: ${contextTokens > 0 ? `${contextTokens} tokens` : "unknown"}, ` +
65
+ `chunk size: ${chunkChars} chars${isSmallModel ? " (small model cap)" : ""}`);
66
+ return chunkChars;
46
67
  }
47
68
  }
48
69
  catch {
@@ -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
+ }