@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.
- package/dist/claude-md.d.ts +2 -2
- package/dist/claude-md.js +8 -6
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +23 -9
- package/dist/consolidate.js +9 -22
- package/dist/db.d.ts +2 -0
- package/dist/db.js +88 -9
- package/dist/distiller.d.ts +4 -2
- package/dist/distiller.js +31 -10
- 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 +20 -35
- package/dist/license.d.ts +13 -3
- package/dist/license.js +30 -41
- package/dist/mcp-server.js +50 -18
- package/dist/nightly-status.d.ts +11 -0
- package/dist/nightly-status.js +167 -0
- package/dist/nightly.js +102 -22
- package/dist/state.d.ts +64 -0
- package/dist/state.js +162 -0
- package/package.json +7 -3
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/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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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>
|
|
81
|
-
server --host <h>
|
|
82
|
-
nightly --dry-run
|
|
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);
|
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
|
/**
|
package/dist/distiller.d.ts
CHANGED
|
@@ -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
|
|
11
|
-
* -
|
|
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
|
|
18
|
-
* -
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|