@gamaze/hicortex 0.10.1 → 0.11.0
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/README.md +42 -0
- package/THIRD_PARTY_NOTICES.md +108 -0
- package/assets/vendor/3d-force-graph.min.js +5 -0
- package/assets/vendor/force-graph.min.js +5 -0
- package/assets/vendor/three.core.min.js +6 -0
- package/assets/vendor/three.module.min.js +6 -0
- package/assets/viz.html +1126 -0
- package/dist/classify-domains.d.ts +98 -0
- package/dist/classify-domains.js +340 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +63 -0
- package/dist/consolidate.d.ts +139 -2
- package/dist/consolidate.js +302 -87
- package/dist/db.js +70 -0
- package/dist/domain-classify.d.ts +164 -0
- package/dist/domain-classify.js +300 -0
- package/dist/extensions.d.ts +12 -0
- package/dist/graph.d.ts +56 -0
- package/dist/graph.js +145 -0
- package/dist/index.js +1 -1
- package/dist/init.d.ts +25 -0
- package/dist/init.js +54 -0
- package/dist/lesson-selection.js +12 -5
- package/dist/lessons-context.js +2 -1
- package/dist/llm.d.ts +67 -0
- package/dist/llm.js +122 -0
- package/dist/mcp-server.js +82 -27
- package/dist/nightly-status.js +9 -28
- package/dist/nightly.js +42 -32
- package/dist/nofit.d.ts +111 -0
- package/dist/nofit.js +176 -0
- package/dist/prompts.d.ts +0 -5
- package/dist/prompts.js +5 -29
- package/dist/relink.d.ts +100 -0
- package/dist/relink.js +277 -0
- package/dist/retrieval.d.ts +16 -1
- package/dist/retrieval.js +34 -2
- package/dist/schema-prototypes.d.ts +149 -0
- package/dist/schema-prototypes.js +329 -0
- package/dist/state.d.ts +32 -0
- package/dist/state.js +29 -0
- package/dist/status.js +12 -19
- package/dist/storage.d.ts +44 -1
- package/dist/storage.js +70 -1
- package/dist/types.d.ts +90 -0
- package/dist/viz.d.ts +69 -0
- package/dist/viz.js +180 -0
- package/domains.example.json +36 -0
- package/package.json +6 -3
package/dist/graph.js
CHANGED
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
* we deal with (hundreds to low thousands of nodes).
|
|
11
11
|
*/
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.EXPORT_MAX_LIMIT = exports.EXPORT_DEFAULT_LIMIT = void 0;
|
|
13
14
|
exports.louvainCommunities = louvainCommunities;
|
|
14
15
|
exports.detectHubs = detectHubs;
|
|
15
16
|
exports.getNeighbors = getNeighbors;
|
|
17
|
+
exports.exportGraph = exportGraph;
|
|
16
18
|
exports.shortestPath = shortestPath;
|
|
19
|
+
const retrieval_js_1 = require("./retrieval.js");
|
|
17
20
|
function loadGraph(db) {
|
|
18
21
|
const rows = db
|
|
19
22
|
.prepare("SELECT source_id, target_id, strength FROM memory_links")
|
|
@@ -210,6 +213,148 @@ function getNeighbors(db, memoryId, limit = 10, relationship) {
|
|
|
210
213
|
return results;
|
|
211
214
|
}
|
|
212
215
|
// ---------------------------------------------------------------------------
|
|
216
|
+
// Full graph export (for GET /graph?op=export — the /viz page, #124)
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Default shows the whole graph for corpora up to 5k memories (owner request
|
|
219
|
+
// 05.07 — the strongest-500 default hid most edges). Payload note: nodes carry
|
|
220
|
+
// content up to 4000 chars, so 5k nodes can be a several-MB JSON response —
|
|
221
|
+
// acceptable on localhost/LAN, which is the /viz deployment model.
|
|
222
|
+
exports.EXPORT_DEFAULT_LIMIT = 5000;
|
|
223
|
+
exports.EXPORT_MAX_LIMIT = 10000;
|
|
224
|
+
/** First non-empty line of content, capped at 80 chars. */
|
|
225
|
+
function makeLabel(content) {
|
|
226
|
+
const firstLine = content.split("\n").find((l) => l.trim().length > 0) ?? content;
|
|
227
|
+
return firstLine.trim().slice(0, 80);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Assemble the full node/edge payload for the /viz page.
|
|
231
|
+
*
|
|
232
|
+
* Nodes are ranked by effective (decayed) strength and capped at `limit`.
|
|
233
|
+
* Edges include only links where BOTH endpoints made the cut.
|
|
234
|
+
* Link counts come from one aggregate query over memory_links (no per-row
|
|
235
|
+
* queries) and feed both the effectiveStrength hardening term and the
|
|
236
|
+
* per-node linkCount field.
|
|
237
|
+
*/
|
|
238
|
+
function exportGraph(db, options = {}) {
|
|
239
|
+
const limit = Math.min(Math.max(Math.floor(options.limit ?? exports.EXPORT_DEFAULT_LIMIT), 1), exports.EXPORT_MAX_LIMIT);
|
|
240
|
+
const now = new Date();
|
|
241
|
+
// Per-memory link counts — single aggregate query (same shape as detectHubs)
|
|
242
|
+
const linkCountRows = db
|
|
243
|
+
.prepare(`SELECT id, COUNT(*) as cnt FROM (
|
|
244
|
+
SELECT source_id AS id FROM memory_links
|
|
245
|
+
UNION ALL
|
|
246
|
+
SELECT target_id AS id FROM memory_links
|
|
247
|
+
) GROUP BY id`)
|
|
248
|
+
.all();
|
|
249
|
+
const linkCounts = new Map(linkCountRows.map((r) => [r.id, r.cnt]));
|
|
250
|
+
// Candidate memories — domain/type filters pushed into SQL
|
|
251
|
+
let sql = `SELECT id, content, memory_type, domain, project, base_strength,
|
|
252
|
+
last_accessed, access_count, created_at
|
|
253
|
+
FROM memories`;
|
|
254
|
+
const where = [];
|
|
255
|
+
const params = [];
|
|
256
|
+
if (options.domain) {
|
|
257
|
+
where.push("domain = ?");
|
|
258
|
+
params.push(options.domain);
|
|
259
|
+
}
|
|
260
|
+
if (options.type) {
|
|
261
|
+
where.push("memory_type = ?");
|
|
262
|
+
params.push(options.type);
|
|
263
|
+
}
|
|
264
|
+
if (options.tag) {
|
|
265
|
+
// Tag match (any weight) OR the domain fallback for never-multi-tagged
|
|
266
|
+
// rows — consistent with the tags payload fallback below.
|
|
267
|
+
where.push(`(id IN (SELECT memory_id FROM memory_tags WHERE tag = ?)
|
|
268
|
+
OR (domain = ? AND id NOT IN (SELECT DISTINCT memory_id FROM memory_tags)))`);
|
|
269
|
+
params.push(options.tag, options.tag);
|
|
270
|
+
}
|
|
271
|
+
if (where.length > 0)
|
|
272
|
+
sql += ` WHERE ${where.join(" AND ")}`;
|
|
273
|
+
const rows = db.prepare(sql).all(...params);
|
|
274
|
+
const hubIds = new Set(detectHubs(db).map((h) => h.id));
|
|
275
|
+
// Score, filter on effective strength, rank, cap
|
|
276
|
+
const scored = rows.map((row) => {
|
|
277
|
+
const linkCount = linkCounts.get(row.id) ?? 0;
|
|
278
|
+
const strength = (0, retrieval_js_1.effectiveStrength)(row.base_strength ?? 0.5, row.last_accessed, now, { accessCount: row.access_count ?? 0, linkCount });
|
|
279
|
+
return { row, strength, linkCount };
|
|
280
|
+
});
|
|
281
|
+
const filtered = options.minStrength !== undefined
|
|
282
|
+
? scored.filter((s) => s.strength >= options.minStrength)
|
|
283
|
+
: scored;
|
|
284
|
+
filtered.sort((a, b) => b.strength - a.strength);
|
|
285
|
+
const top = filtered.slice(0, limit);
|
|
286
|
+
// Multi-label tags (graded-schema spec) — fetch only for the capped node
|
|
287
|
+
// set. One query keyed on the included ids; grouped into per-node parallel
|
|
288
|
+
// arrays ORDERED BY WEIGHT DESC (NULL weights last, in insertion/relevance
|
|
289
|
+
// order). The derived primary remains on `domain` (the colour); `tags` is
|
|
290
|
+
// the full set, `tagWeights` the parallel association weights (rounded 4dp;
|
|
291
|
+
// 0 for NULL/not-yet-computed).
|
|
292
|
+
const topIds = top.map((t) => t.row.id);
|
|
293
|
+
const tagsByMemory = new Map();
|
|
294
|
+
if (topIds.length > 0) {
|
|
295
|
+
const idPlaceholders = topIds.map(() => "?").join(", ");
|
|
296
|
+
const tagRows = db
|
|
297
|
+
.prepare(`SELECT memory_id, tag, weight FROM memory_tags
|
|
298
|
+
WHERE memory_id IN (${idPlaceholders})
|
|
299
|
+
ORDER BY memory_id, (weight IS NULL) ASC, weight DESC, rowid ASC`)
|
|
300
|
+
.all(...topIds);
|
|
301
|
+
for (const r of tagRows) {
|
|
302
|
+
const arr = tagsByMemory.get(r.memory_id);
|
|
303
|
+
const entry = { tag: r.tag, weight: r.weight };
|
|
304
|
+
if (arr)
|
|
305
|
+
arr.push(entry);
|
|
306
|
+
else
|
|
307
|
+
tagsByMemory.set(r.memory_id, [entry]);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const nodes = top.map(({ row, strength, linkCount }) => {
|
|
311
|
+
const weighted = tagsByMemory.get(row.id)
|
|
312
|
+
?? (row.domain ? [{ tag: row.domain, weight: null }] : []);
|
|
313
|
+
return {
|
|
314
|
+
id: row.id,
|
|
315
|
+
label: makeLabel(row.content),
|
|
316
|
+
content: row.content.slice(0, 4000),
|
|
317
|
+
memory_type: row.memory_type,
|
|
318
|
+
domain: row.domain,
|
|
319
|
+
tags: weighted.map((w) => w.tag),
|
|
320
|
+
tagWeights: weighted.map((w) => (w.weight == null ? 0 : Math.round(w.weight * 10000) / 10000)),
|
|
321
|
+
project: row.project,
|
|
322
|
+
strength: Math.round(strength * 10000) / 10000,
|
|
323
|
+
linkCount,
|
|
324
|
+
isHub: hubIds.has(row.id),
|
|
325
|
+
created_at: row.created_at,
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
// Edges — only where both endpoints are in the included set
|
|
329
|
+
const included = new Set(nodes.map((n) => n.id));
|
|
330
|
+
const linkRows = db
|
|
331
|
+
.prepare("SELECT source_id, target_id, relationship, strength FROM memory_links")
|
|
332
|
+
.all();
|
|
333
|
+
const edges = linkRows
|
|
334
|
+
.filter((l) => included.has(l.source_id) && included.has(l.target_id))
|
|
335
|
+
.map((l) => ({
|
|
336
|
+
source: l.source_id,
|
|
337
|
+
target: l.target_id,
|
|
338
|
+
relationship: l.relationship,
|
|
339
|
+
strength: l.strength,
|
|
340
|
+
}));
|
|
341
|
+
// Filter dropdown values — distinct across the WHOLE DB, not the shown subset
|
|
342
|
+
const domains = db
|
|
343
|
+
.prepare("SELECT DISTINCT domain FROM memories WHERE domain IS NOT NULL AND domain != '' ORDER BY domain")
|
|
344
|
+
.all().map((r) => r.domain);
|
|
345
|
+
const types = db
|
|
346
|
+
.prepare("SELECT DISTINCT memory_type FROM memories WHERE memory_type IS NOT NULL AND memory_type != '' ORDER BY memory_type")
|
|
347
|
+
.all().map((r) => r.memory_type);
|
|
348
|
+
const total = db.prepare("SELECT COUNT(*) as cnt FROM memories").get().cnt;
|
|
349
|
+
return {
|
|
350
|
+
nodes,
|
|
351
|
+
edges,
|
|
352
|
+
domains,
|
|
353
|
+
types,
|
|
354
|
+
meta: { total, shown: nodes.length, edgeCount: edges.length },
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
213
358
|
// Shortest path (for MCP tool)
|
|
214
359
|
// ---------------------------------------------------------------------------
|
|
215
360
|
function shortestPath(db, fromId, toId, maxDepth = 5) {
|
package/dist/index.js
CHANGED
|
@@ -333,7 +333,7 @@ exports.default = {
|
|
|
333
333
|
target_id: { type: "string", description: "Target memory ID (required for path operation)" },
|
|
334
334
|
limit: { type: "number", description: "Max results (default 10)" },
|
|
335
335
|
domain: { type: "string", description: "Filter hubs by domain" },
|
|
336
|
-
relationship: { type: "string", description: "Filter neighbors by relationship type (e.g., CONTRADICTS, SUPERSEDES,
|
|
336
|
+
relationship: { type: "string", description: "Filter neighbors by relationship type (e.g., extends, relates_to; legacy data may also have CONTRADICTS, SUPERSEDES, updates)" },
|
|
337
337
|
},
|
|
338
338
|
required: ["operation"],
|
|
339
339
|
},
|
package/dist/init.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - Strip old static CLAUDE.md learnings block if present
|
|
16
16
|
* - Install CC custom commands (/learn, /hicortex-activate)
|
|
17
17
|
*/
|
|
18
|
+
import type { DomainDef } from "./types.js";
|
|
18
19
|
/**
|
|
19
20
|
* Parse a KEY=VALUE env file (e.g. ~/.hermes/.env or ~/.claude/settings.json env block).
|
|
20
21
|
* Handles: comments (#), quoted values, empty lines.
|
|
@@ -36,6 +37,30 @@ export declare function persistAuthToken(configPath: string): {
|
|
|
36
37
|
token: string;
|
|
37
38
|
generated: boolean;
|
|
38
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Generic default memory domains scaffolded by server-mode init (issue #150).
|
|
42
|
+
* Deliberately broad, high-level spheres — an editable STARTING POINT, not a
|
|
43
|
+
* taxonomy. Users narrow or replace them to match how THEY think (life areas
|
|
44
|
+
* or project/topic areas both work). There is NO fallback category: a no-fit
|
|
45
|
+
* memory is handled automatically by the weak-primary floor + decay lifecycle
|
|
46
|
+
* (see nofit.ts) — never by a catch-all domain.
|
|
47
|
+
*/
|
|
48
|
+
export declare const GENERIC_DEFAULT_DOMAINS: DomainDef[];
|
|
49
|
+
/**
|
|
50
|
+
* Scaffold the generic default `domains` list into config.json (server mode).
|
|
51
|
+
*
|
|
52
|
+
* Non-clobber (same philosophy as persistAuthToken): only writes when the
|
|
53
|
+
* config has NO `domains` key at all. An existing key — even an empty array —
|
|
54
|
+
* is user-owned and is never touched. Upgrading installs that re-run init get
|
|
55
|
+
* the scaffold too (they have no `domains` key yet); installs that never
|
|
56
|
+
* re-run init keep the legacy project-grouping behaviour.
|
|
57
|
+
*
|
|
58
|
+
* Prints its own hint lines (tested); returns whether it wrote the scaffold.
|
|
59
|
+
* Exported for testability.
|
|
60
|
+
*/
|
|
61
|
+
export declare function scaffoldDefaultDomains(configPath: string): {
|
|
62
|
+
scaffolded: boolean;
|
|
63
|
+
};
|
|
39
64
|
/**
|
|
40
65
|
* Install (or verify) the CC SessionStart hook that runs `hicortex lessons-context`.
|
|
41
66
|
* The hook fetches lessons from the configured server at session start and injects
|
package/dist/init.js
CHANGED
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
* - Install CC custom commands (/learn, /hicortex-activate)
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.GENERIC_DEFAULT_DOMAINS = void 0;
|
|
20
21
|
exports.parseEnvFile = parseEnvFile;
|
|
21
22
|
exports.generateAuthToken = generateAuthToken;
|
|
22
23
|
exports.persistAuthToken = persistAuthToken;
|
|
24
|
+
exports.scaffoldDefaultDomains = scaffoldDefaultDomains;
|
|
23
25
|
exports.installSessionStartHook = installSessionStartHook;
|
|
24
26
|
exports.runInit = runInit;
|
|
25
27
|
exports.resolveNightlyHour = resolveNightlyHour;
|
|
@@ -697,6 +699,52 @@ function persistAuthToken(configPath) {
|
|
|
697
699
|
(0, node_fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
698
700
|
return { token, generated: true };
|
|
699
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* Generic default memory domains scaffolded by server-mode init (issue #150).
|
|
704
|
+
* Deliberately broad, high-level spheres — an editable STARTING POINT, not a
|
|
705
|
+
* taxonomy. Users narrow or replace them to match how THEY think (life areas
|
|
706
|
+
* or project/topic areas both work). There is NO fallback category: a no-fit
|
|
707
|
+
* memory is handled automatically by the weak-primary floor + decay lifecycle
|
|
708
|
+
* (see nofit.ts) — never by a catch-all domain.
|
|
709
|
+
*/
|
|
710
|
+
exports.GENERIC_DEFAULT_DOMAINS = [
|
|
711
|
+
{ name: "Work", description: "Your job and professional life — employer, clients, workstreams" },
|
|
712
|
+
{ name: "Personal", description: "Private life — home, hobbies, everyday matters" },
|
|
713
|
+
{ name: "People", description: "Relationships — family, friends, social life, network" },
|
|
714
|
+
{ name: "Health", description: "Fitness, wellbeing, medical" },
|
|
715
|
+
{ name: "Finance", description: "Money — budgeting, spending, investing" },
|
|
716
|
+
];
|
|
717
|
+
/**
|
|
718
|
+
* Scaffold the generic default `domains` list into config.json (server mode).
|
|
719
|
+
*
|
|
720
|
+
* Non-clobber (same philosophy as persistAuthToken): only writes when the
|
|
721
|
+
* config has NO `domains` key at all. An existing key — even an empty array —
|
|
722
|
+
* is user-owned and is never touched. Upgrading installs that re-run init get
|
|
723
|
+
* the scaffold too (they have no `domains` key yet); installs that never
|
|
724
|
+
* re-run init keep the legacy project-grouping behaviour.
|
|
725
|
+
*
|
|
726
|
+
* Prints its own hint lines (tested); returns whether it wrote the scaffold.
|
|
727
|
+
* Exported for testability.
|
|
728
|
+
*/
|
|
729
|
+
function scaffoldDefaultDomains(configPath) {
|
|
730
|
+
let config = {};
|
|
731
|
+
try {
|
|
732
|
+
config = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
|
|
733
|
+
}
|
|
734
|
+
catch { /* new file */ }
|
|
735
|
+
if ("domains" in config) {
|
|
736
|
+
console.log(" ✓ Memory domains already configured — leaving your list as-is");
|
|
737
|
+
return { scaffolded: false };
|
|
738
|
+
}
|
|
739
|
+
config.domains = exports.GENERIC_DEFAULT_DOMAINS;
|
|
740
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(configPath), { recursive: true });
|
|
741
|
+
(0, node_fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2));
|
|
742
|
+
const names = exports.GENERIC_DEFAULT_DOMAINS.map((d) => d.name).join(", ");
|
|
743
|
+
console.log(` ✓ Memory domains scaffolded in ${configPath}`);
|
|
744
|
+
console.log(` Defaults: ${names}. Edit the \`domains\` list to match how YOU think —`);
|
|
745
|
+
console.log(` they can be life areas OR project/topic areas (see domains.example.json in the package).`);
|
|
746
|
+
return { scaffolded: true };
|
|
747
|
+
}
|
|
700
748
|
/**
|
|
701
749
|
* Determine the npm package specifier for the daemon.
|
|
702
750
|
* Uses tag-based resolution so restarts pick up new versions automatically.
|
|
@@ -1005,6 +1053,12 @@ async function runInit(options = {}) {
|
|
|
1005
1053
|
else {
|
|
1006
1054
|
console.log(` ✓ Auth token already configured`);
|
|
1007
1055
|
}
|
|
1056
|
+
// Scaffold the generic default memory domains (server mode only — domains
|
|
1057
|
+
// live in the server's config; a client's memories are classified by the
|
|
1058
|
+
// server). Non-clobber: an existing `domains` key is never touched.
|
|
1059
|
+
// Classification activates automatically once an LLM is configured; until
|
|
1060
|
+
// then domains sit inert (strict-skip path).
|
|
1061
|
+
scaffoldDefaultDomains(configPath);
|
|
1008
1062
|
// Install the nightly job (capture via localhost /distill + consolidation).
|
|
1009
1063
|
// Without it a server-mode install never captures or consolidates — the
|
|
1010
1064
|
// daemon only serves recall + /distill. Skips if a schedule already exists.
|
package/dist/lesson-selection.js
CHANGED
|
@@ -69,14 +69,21 @@ function parseDate(ts) {
|
|
|
69
69
|
const d = new Date(ts);
|
|
70
70
|
return isNaN(d.getTime()) ? null : d;
|
|
71
71
|
}
|
|
72
|
-
function projectMatch(lesson, targetProject, moduleIndex) {
|
|
73
|
-
if (!lesson.project)
|
|
74
|
-
return 0.0;
|
|
72
|
+
function projectMatch(lesson, targetProject, moduleIndex, targetDomainName) {
|
|
75
73
|
if (targetProject && lesson.project === targetProject)
|
|
76
74
|
return 1.0;
|
|
77
75
|
if (lesson.project === "global")
|
|
78
76
|
return 0.3;
|
|
79
|
-
//
|
|
77
|
+
// Content-mode same-domain boost: when the caller knows the current
|
|
78
|
+
// life-sphere domain, a lesson filed into the SAME domain scores 0.5.
|
|
79
|
+
// This is the content-classification analogue of the project-grouping
|
|
80
|
+
// moduleIndex boost below. Keyed on lesson.domain directly (no project map).
|
|
81
|
+
if (targetDomainName && lesson.domain && lesson.domain === targetDomainName) {
|
|
82
|
+
return 0.5;
|
|
83
|
+
}
|
|
84
|
+
if (!lesson.project)
|
|
85
|
+
return 0.0;
|
|
86
|
+
// Project-grouping same-domain boost: same domain via moduleIndex = 0.5.
|
|
80
87
|
if (targetProject && moduleIndex) {
|
|
81
88
|
const targetDomain = moduleIndex.domains.find((d) => d.projects.includes(targetProject));
|
|
82
89
|
if (targetDomain && targetDomain.projects.includes(lesson.project)) {
|
|
@@ -129,7 +136,7 @@ exports.domainAwareLessonSelector = {
|
|
|
129
136
|
const now = new Date();
|
|
130
137
|
// Score every lesson
|
|
131
138
|
const scored = lessons.map((lesson) => {
|
|
132
|
-
const pMatch = projectMatch(lesson, ctx.project, ctx.moduleIndex);
|
|
139
|
+
const pMatch = projectMatch(lesson, ctx.project, ctx.moduleIndex, ctx.domain);
|
|
133
140
|
const rScore = recencyScore(lesson, now);
|
|
134
141
|
const sScore = lesson.base_strength ?? 0.5;
|
|
135
142
|
const aScore = accessAffinity(lesson);
|
package/dist/lessons-context.js
CHANGED
|
@@ -83,7 +83,8 @@ async function fetchLessonsContext() {
|
|
|
83
83
|
for (const domain of moduleIndex.domains) {
|
|
84
84
|
const kwStr = domain.keywords.length > 0 ? `: ${domain.keywords.join(", ")}` : "";
|
|
85
85
|
parts.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount} lessons)${kwStr}`);
|
|
86
|
-
|
|
86
|
+
if (domain.projects.length > 0)
|
|
87
|
+
parts.push(` ${domain.projects.join(" | ")}`);
|
|
87
88
|
}
|
|
88
89
|
parts.push(`${index.total} memories, ${index.lessonCount} lessons, ${index.sourceCount} agents. Search with \`hicortex_search\`.`);
|
|
89
90
|
}
|
package/dist/llm.d.ts
CHANGED
|
@@ -29,6 +29,21 @@ export interface LlmConfig {
|
|
|
29
29
|
reflectBaseUrl?: string;
|
|
30
30
|
reflectApiKey?: string;
|
|
31
31
|
reflectProvider?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Optional separate model for memory tag classification (defaults to the
|
|
34
|
+
* reflect tier when unset — zero behavior change for existing installs).
|
|
35
|
+
* Chosen after an A/B benchmark where a dedicated classifier model
|
|
36
|
+
* materially outperformed the reflect model on this task.
|
|
37
|
+
*/
|
|
38
|
+
classifyModel?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Optional separate endpoint for classification. When only classifyModel is
|
|
41
|
+
* set, the classify model runs on the reflect endpoint (or the base endpoint
|
|
42
|
+
* when no reflect endpoint is configured).
|
|
43
|
+
*/
|
|
44
|
+
classifyBaseUrl?: string;
|
|
45
|
+
classifyApiKey?: string;
|
|
46
|
+
classifyProvider?: string;
|
|
32
47
|
}
|
|
33
48
|
/**
|
|
34
49
|
* Resolve LLM configuration from explicit config-file overrides or
|
|
@@ -53,6 +68,46 @@ export declare function resolveExplicitLlmConfig(overrides?: {
|
|
|
53
68
|
* the transition for any lingering call sites — remove after 0.10.0 ships.
|
|
54
69
|
*/
|
|
55
70
|
export declare const resolveLlmConfigForCC: typeof resolveExplicitLlmConfig;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve an LlmConfig from a saved ~/.hicortex/config.json object.
|
|
73
|
+
*
|
|
74
|
+
* This is the SINGLE config path used by pipeline runs (nightly consolidation
|
|
75
|
+
* and `hicortex relink`): named backends (claude-cli, ollama) first, then the
|
|
76
|
+
* explicit-config/env fallthrough via resolveExplicitLlmConfig, then the
|
|
77
|
+
* reflect endpoint overlay. Extracted verbatim from nightly.ts — behavior
|
|
78
|
+
* is identical to the pre-0.11 inline block.
|
|
79
|
+
*
|
|
80
|
+
* Returns `reason: "claude_binary_missing"` when claude-cli is configured but
|
|
81
|
+
* the binary can't be found, so callers can log a context-specific message.
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolveSavedLlmConfig(savedConfig: Record<string, unknown> | null): {
|
|
84
|
+
config: LlmConfig | null;
|
|
85
|
+
reason?: "claude_binary_missing";
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Endpoint + model that memory tag classification will ACTUALLY use, for
|
|
89
|
+
* pre-flight probing. Pure function — the single source of truth shared by
|
|
90
|
+
* the nightly's contentDomainsReady gate and `hicortex classify-domains`.
|
|
91
|
+
*
|
|
92
|
+
* Mirrors LlmClient.completeClassify's routing:
|
|
93
|
+
* - classify tier configured (classifyModel and/or classifyBaseUrl) →
|
|
94
|
+
* classifyBaseUrl ?? reflectBaseUrl, classifyModel ?? reflectModel
|
|
95
|
+
* - classify tier absent → the reflect tier (reflectBaseUrl/reflectModel),
|
|
96
|
+
* exactly what completeReflect uses
|
|
97
|
+
*
|
|
98
|
+
* Returns null when no probe applies: only a SEPARATE Ollama endpoint can go
|
|
99
|
+
* unreachable mid-run (API providers are cloud-reachable; the base endpoint
|
|
100
|
+
* is not pre-flighted anywhere, matching distill/reflect behavior).
|
|
101
|
+
*
|
|
102
|
+
* `tier` tells callers which configuration produced the target — "reflect"
|
|
103
|
+
* means the classification probe is identical to the reflect-stage probe and
|
|
104
|
+
* its result can be reused.
|
|
105
|
+
*/
|
|
106
|
+
export declare function resolveClassifyProbeTarget(config: LlmConfig): {
|
|
107
|
+
tier: "classify" | "reflect";
|
|
108
|
+
baseUrl: string;
|
|
109
|
+
model: string;
|
|
110
|
+
} | null;
|
|
56
111
|
/**
|
|
57
112
|
* Find the claude CLI binary. Returns the full path or null.
|
|
58
113
|
*/
|
|
@@ -132,6 +187,18 @@ export declare class LlmClient {
|
|
|
132
187
|
* Routes to distillBaseUrl/distillProvider if configured (e.g. remote Ollama with faster model).
|
|
133
188
|
*/
|
|
134
189
|
completeDistill(prompt: string, maxTokens?: number): Promise<string>;
|
|
190
|
+
/**
|
|
191
|
+
* Classification-tier completion (memory tag classification).
|
|
192
|
+
*
|
|
193
|
+
* Routing (same "optional dedicated model+baseUrl with fallback" pattern as
|
|
194
|
+
* completeDistill; Ollama calls inherit think:false via completeOllama):
|
|
195
|
+
* - Neither classifyModel nor classifyBaseUrl set → delegate to
|
|
196
|
+
* completeReflect (exactly the pre-classify-tier behavior).
|
|
197
|
+
* - classifyBaseUrl set → that endpoint, model classifyModel ?? reflectModel.
|
|
198
|
+
* - Only classifyModel set → the classify model on the reflect endpoint
|
|
199
|
+
* when one is configured, else on the base endpoint.
|
|
200
|
+
*/
|
|
201
|
+
completeClassify(prompt: string, maxTokens?: number): Promise<string>;
|
|
135
202
|
/**
|
|
136
203
|
* Complete with overridden baseUrl/apiKey/provider (used for reflect tier with separate endpoint).
|
|
137
204
|
* Creates a temporary LlmClient to avoid mutating shared config under concurrent calls.
|
package/dist/llm.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.LlmClient = exports.RateLimitError = exports.resolveLlmConfigForCC = void 0;
|
|
19
19
|
exports.resolveExplicitLlmConfig = resolveExplicitLlmConfig;
|
|
20
|
+
exports.resolveSavedLlmConfig = resolveSavedLlmConfig;
|
|
21
|
+
exports.resolveClassifyProbeTarget = resolveClassifyProbeTarget;
|
|
20
22
|
exports.findClaudeBinary = findClaudeBinary;
|
|
21
23
|
exports.claudeCliConfig = claudeCliConfig;
|
|
22
24
|
exports.probeOllama = probeOllama;
|
|
@@ -68,6 +70,102 @@ function resolveExplicitLlmConfig(overrides) {
|
|
|
68
70
|
* the transition for any lingering call sites — remove after 0.10.0 ships.
|
|
69
71
|
*/
|
|
70
72
|
exports.resolveLlmConfigForCC = resolveExplicitLlmConfig;
|
|
73
|
+
/**
|
|
74
|
+
* Resolve an LlmConfig from a saved ~/.hicortex/config.json object.
|
|
75
|
+
*
|
|
76
|
+
* This is the SINGLE config path used by pipeline runs (nightly consolidation
|
|
77
|
+
* and `hicortex relink`): named backends (claude-cli, ollama) first, then the
|
|
78
|
+
* explicit-config/env fallthrough via resolveExplicitLlmConfig, then the
|
|
79
|
+
* reflect endpoint overlay. Extracted verbatim from nightly.ts — behavior
|
|
80
|
+
* is identical to the pre-0.11 inline block.
|
|
81
|
+
*
|
|
82
|
+
* Returns `reason: "claude_binary_missing"` when claude-cli is configured but
|
|
83
|
+
* the binary can't be found, so callers can log a context-specific message.
|
|
84
|
+
*/
|
|
85
|
+
function resolveSavedLlmConfig(savedConfig) {
|
|
86
|
+
let llmConfig = null;
|
|
87
|
+
if (savedConfig?.llmBackend === "claude-cli") {
|
|
88
|
+
const claudePath = findClaudeBinary();
|
|
89
|
+
if (claudePath) {
|
|
90
|
+
llmConfig = claudeCliConfig(claudePath);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
return { config: null, reason: "claude_binary_missing" };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (savedConfig?.llmBackend === "ollama") {
|
|
97
|
+
llmConfig = {
|
|
98
|
+
baseUrl: savedConfig.llmBaseUrl ?? "http://localhost:11434",
|
|
99
|
+
apiKey: "",
|
|
100
|
+
model: savedConfig.llmModel ?? "qwen3.5:4b",
|
|
101
|
+
reflectModel: savedConfig.reflectModel ?? savedConfig.llmModel ?? "qwen3.5:4b",
|
|
102
|
+
provider: "ollama",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
llmConfig = resolveExplicitLlmConfig({
|
|
107
|
+
llmBaseUrl: savedConfig?.llmBaseUrl,
|
|
108
|
+
llmApiKey: savedConfig?.llmApiKey,
|
|
109
|
+
llmModel: savedConfig?.llmModel,
|
|
110
|
+
reflectModel: savedConfig?.reflectModel,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (llmConfig && savedConfig?.reflectBaseUrl) {
|
|
114
|
+
llmConfig.reflectBaseUrl = savedConfig.reflectBaseUrl;
|
|
115
|
+
llmConfig.reflectApiKey = savedConfig.reflectApiKey ?? llmConfig.apiKey;
|
|
116
|
+
llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
|
|
117
|
+
}
|
|
118
|
+
// Optional classify tier (memory tag classification). Same overlay pattern
|
|
119
|
+
// as distillModel/distillBaseUrl: when absent, completeClassify falls back
|
|
120
|
+
// to the reflect tier — zero behavior change for existing installs.
|
|
121
|
+
if (llmConfig && savedConfig?.classifyModel) {
|
|
122
|
+
llmConfig.classifyModel = savedConfig.classifyModel;
|
|
123
|
+
}
|
|
124
|
+
if (llmConfig && savedConfig?.classifyBaseUrl) {
|
|
125
|
+
llmConfig.classifyBaseUrl = savedConfig.classifyBaseUrl;
|
|
126
|
+
llmConfig.classifyApiKey = savedConfig.classifyApiKey ?? llmConfig.apiKey;
|
|
127
|
+
llmConfig.classifyProvider = savedConfig.classifyProvider ?? llmConfig.provider;
|
|
128
|
+
}
|
|
129
|
+
return { config: llmConfig };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Endpoint + model that memory tag classification will ACTUALLY use, for
|
|
133
|
+
* pre-flight probing. Pure function — the single source of truth shared by
|
|
134
|
+
* the nightly's contentDomainsReady gate and `hicortex classify-domains`.
|
|
135
|
+
*
|
|
136
|
+
* Mirrors LlmClient.completeClassify's routing:
|
|
137
|
+
* - classify tier configured (classifyModel and/or classifyBaseUrl) →
|
|
138
|
+
* classifyBaseUrl ?? reflectBaseUrl, classifyModel ?? reflectModel
|
|
139
|
+
* - classify tier absent → the reflect tier (reflectBaseUrl/reflectModel),
|
|
140
|
+
* exactly what completeReflect uses
|
|
141
|
+
*
|
|
142
|
+
* Returns null when no probe applies: only a SEPARATE Ollama endpoint can go
|
|
143
|
+
* unreachable mid-run (API providers are cloud-reachable; the base endpoint
|
|
144
|
+
* is not pre-flighted anywhere, matching distill/reflect behavior).
|
|
145
|
+
*
|
|
146
|
+
* `tier` tells callers which configuration produced the target — "reflect"
|
|
147
|
+
* means the classification probe is identical to the reflect-stage probe and
|
|
148
|
+
* its result can be reused.
|
|
149
|
+
*/
|
|
150
|
+
function resolveClassifyProbeTarget(config) {
|
|
151
|
+
const classifyConfigured = Boolean(config.classifyModel || config.classifyBaseUrl);
|
|
152
|
+
if (classifyConfigured) {
|
|
153
|
+
const baseUrl = config.classifyBaseUrl ?? config.reflectBaseUrl;
|
|
154
|
+
const model = config.classifyModel ?? config.reflectModel;
|
|
155
|
+
const provider = config.classifyBaseUrl
|
|
156
|
+
? (config.classifyProvider ?? config.provider)
|
|
157
|
+
: (config.reflectProvider ?? config.provider); // riding the reflect endpoint
|
|
158
|
+
if (baseUrl && provider === "ollama") {
|
|
159
|
+
return { tier: "classify", baseUrl, model };
|
|
160
|
+
}
|
|
161
|
+
return null; // base endpoint or API provider — no probe
|
|
162
|
+
}
|
|
163
|
+
// Classify tier absent — classification delegates to completeReflect.
|
|
164
|
+
if (config.reflectBaseUrl && (config.reflectProvider ?? config.provider) === "ollama") {
|
|
165
|
+
return { tier: "reflect", baseUrl: config.reflectBaseUrl, model: config.reflectModel ?? config.model };
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
71
169
|
function detectProvider(url) {
|
|
72
170
|
const u = url.toLowerCase();
|
|
73
171
|
if (u.includes("ollama") || u.includes(":11434"))
|
|
@@ -298,6 +396,30 @@ class LlmClient {
|
|
|
298
396
|
}
|
|
299
397
|
return this.complete(this.config.distillModel ?? this.config.model, prompt, maxTokens, 900_000);
|
|
300
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Classification-tier completion (memory tag classification).
|
|
401
|
+
*
|
|
402
|
+
* Routing (same "optional dedicated model+baseUrl with fallback" pattern as
|
|
403
|
+
* completeDistill; Ollama calls inherit think:false via completeOllama):
|
|
404
|
+
* - Neither classifyModel nor classifyBaseUrl set → delegate to
|
|
405
|
+
* completeReflect (exactly the pre-classify-tier behavior).
|
|
406
|
+
* - classifyBaseUrl set → that endpoint, model classifyModel ?? reflectModel.
|
|
407
|
+
* - Only classifyModel set → the classify model on the reflect endpoint
|
|
408
|
+
* when one is configured, else on the base endpoint.
|
|
409
|
+
*/
|
|
410
|
+
async completeClassify(prompt, maxTokens = 8192) {
|
|
411
|
+
if (!this.config.classifyModel && !this.config.classifyBaseUrl) {
|
|
412
|
+
return this.completeReflect(prompt, maxTokens);
|
|
413
|
+
}
|
|
414
|
+
const model = this.config.classifyModel ?? this.config.reflectModel;
|
|
415
|
+
if (this.config.classifyBaseUrl) {
|
|
416
|
+
return this.completeWithOverride(this.config.classifyBaseUrl, this.config.classifyApiKey ?? this.config.apiKey, this.config.classifyProvider ?? this.config.provider, model, prompt, maxTokens, 900_000);
|
|
417
|
+
}
|
|
418
|
+
if (this.config.reflectBaseUrl) {
|
|
419
|
+
return this.completeWithOverride(this.config.reflectBaseUrl, this.config.reflectApiKey ?? this.config.apiKey, this.config.reflectProvider ?? this.config.provider, model, prompt, maxTokens, 900_000);
|
|
420
|
+
}
|
|
421
|
+
return this.complete(model, prompt, maxTokens, 900_000);
|
|
422
|
+
}
|
|
301
423
|
/**
|
|
302
424
|
* Complete with overridden baseUrl/apiKey/provider (used for reflect tier with separate endpoint).
|
|
303
425
|
* Creates a temporary LlmClient to avoid mutating shared config under concurrent calls.
|