@gamaze/hicortex 0.12.1 → 0.13.1
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 +48 -8
- package/assets/context.html +115 -6
- package/dist/cli-args.d.ts +16 -0
- package/dist/cli-args.js +30 -0
- package/dist/cli.js +13 -1
- package/dist/context-cli.d.ts +14 -3
- package/dist/context-cli.js +71 -17
- package/dist/context-store.d.ts +108 -2
- package/dist/context-store.js +308 -16
- package/dist/distiller.d.ts +27 -1
- package/dist/distiller.js +81 -9
- package/dist/index.js +73 -28
- package/dist/init.d.ts +39 -0
- package/dist/init.js +171 -4
- package/dist/lessons-context.d.ts +56 -0
- package/dist/lessons-context.js +77 -18
- package/dist/llm.d.ts +24 -1
- package/dist/llm.js +119 -2
- package/dist/mcp-server.js +27 -5
- package/dist/nightly-status.js +4 -1
- package/dist/nightly.js +8 -0
- package/dist/status.d.ts +9 -0
- package/dist/status.js +29 -4
- package/dist/types.d.ts +20 -0
- package/hermes-plugin/hicortex/README.md +15 -2
- package/hermes-plugin/hicortex/client.py +7 -0
- package/hermes-plugin/hicortex/config.py +10 -0
- package/hermes-plugin/hicortex/plugin.yaml +2 -2
- package/hermes-plugin/hicortex/provider.py +134 -1
- package/package.json +1 -1
package/dist/lessons-context.js
CHANGED
|
@@ -23,9 +23,14 @@
|
|
|
23
23
|
* CC session, and a broken /context fetch must never blank the whole output.
|
|
24
24
|
*/
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.titleCaseSection = titleCaseSection;
|
|
27
|
+
exports.orderSectionNames = orderSectionNames;
|
|
28
|
+
exports.renderContextBlock = renderContextBlock;
|
|
29
|
+
exports.gateAndRenderContext = gateAndRenderContext;
|
|
26
30
|
exports.fetchLessonsContext = fetchLessonsContext;
|
|
27
31
|
const node_fs_1 = require("node:fs");
|
|
28
32
|
const node_path_1 = require("node:path");
|
|
33
|
+
const context_store_js_1 = require("./context-store.js");
|
|
29
34
|
const features_js_1 = require("./features.js");
|
|
30
35
|
const extensions_js_1 = require("./extensions.js");
|
|
31
36
|
const state_js_1 = require("./state.js");
|
|
@@ -50,7 +55,13 @@ function resolveConfig() {
|
|
|
50
55
|
const serverUrl = config.mode === "client" && typeof config.serverUrl === "string"
|
|
51
56
|
? config.serverUrl.replace(/\/+$/, "")
|
|
52
57
|
: `http://127.0.0.1:${config.port ?? DEFAULT_PORT}`;
|
|
53
|
-
|
|
58
|
+
// Per-agent context id (0.13) via the shared resolver, so the id sent here
|
|
59
|
+
// always matches what `hicortex status` reports. No configured agentName →
|
|
60
|
+
// agentId null → NO ?agent= (bare fetch): CC's default is the shared global
|
|
61
|
+
// context. A configured agentName that sanitizes to null → agentId null too
|
|
62
|
+
// (NO ?agent=), never a 400 that the fail-soft hook would silently swallow.
|
|
63
|
+
const agentName = (0, context_store_js_1.resolveAgentIdentity)(config).agentId;
|
|
64
|
+
return { serverUrl, authToken: config.authToken, home, agentName };
|
|
54
65
|
}
|
|
55
66
|
function authHeaders(authToken) {
|
|
56
67
|
return authToken ? { "Authorization": `Bearer ${authToken}` } : {};
|
|
@@ -112,6 +123,8 @@ async function fetchLessonsBlock(cfg) {
|
|
|
112
123
|
/**
|
|
113
124
|
* Title-case a section name for its heading: split on `-`/`_`, capitalize each
|
|
114
125
|
* word ("user" → "User", "my_notes" → "My Notes").
|
|
126
|
+
* Exported so the OC plugin (index.ts) renders the `## Context` block
|
|
127
|
+
* identically to the CC hook rather than duplicating the logic.
|
|
115
128
|
*/
|
|
116
129
|
function titleCaseSection(name) {
|
|
117
130
|
return name
|
|
@@ -124,7 +137,7 @@ function titleCaseSection(name) {
|
|
|
124
137
|
* Stable section ordering: `user` first, then `rules` (the seeded primary
|
|
125
138
|
* sections, spec §8), then every other section alphabetically. Server-side
|
|
126
139
|
* enumeration order (readdirSync) is FS-dependent, so we sort here for a
|
|
127
|
-
* deterministic injection block.
|
|
140
|
+
* deterministic injection block. Exported for reuse by the OC plugin.
|
|
128
141
|
*/
|
|
129
142
|
function orderSectionNames(names) {
|
|
130
143
|
const primaries = ["user", "rules"].filter((p) => names.includes(p));
|
|
@@ -132,23 +145,13 @@ function orderSectionNames(names) {
|
|
|
132
145
|
return [...primaries, ...rest];
|
|
133
146
|
}
|
|
134
147
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
148
|
+
* Render the `## Context` block from a resolved section map, or null when there
|
|
149
|
+
* is nothing to inject (no sections, or every section blank after trimming).
|
|
150
|
+
* Pure — no gating, no I/O. Shared verbatim by the CC hook and the OC plugin so
|
|
151
|
+
* both harnesses emit an identical block. Sections are ordered (user, rules,
|
|
152
|
+
* then alphabetical) and rendered under title-cased `###` headings.
|
|
138
153
|
*/
|
|
139
|
-
|
|
140
|
-
const resp = await fetch(`${cfg.serverUrl}/context`, {
|
|
141
|
-
headers: authHeaders(cfg.authToken),
|
|
142
|
-
signal: AbortSignal.timeout(3000),
|
|
143
|
-
});
|
|
144
|
-
if (!resp.ok)
|
|
145
|
-
return null;
|
|
146
|
-
const data = await resp.json();
|
|
147
|
-
// Self-gate: only inject when this harness is in the server-resolved list.
|
|
148
|
-
const clients = Array.isArray(data.clients) ? data.clients : [];
|
|
149
|
-
if (!clients.includes(THIS_HARNESS))
|
|
150
|
-
return null;
|
|
151
|
-
const sections = data.sections;
|
|
154
|
+
function renderContextBlock(sections) {
|
|
152
155
|
if (!sections || typeof sections !== "object" || Array.isArray(sections))
|
|
153
156
|
return null;
|
|
154
157
|
const names = orderSectionNames(Object.keys(sections));
|
|
@@ -163,6 +166,62 @@ async function fetchContextBlock(cfg) {
|
|
|
163
166
|
return null;
|
|
164
167
|
return ["## Context", "", ...bodyParts].join("\n");
|
|
165
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Gate a GET /context response and render the `## Context` block, or null when
|
|
171
|
+
* nothing should be injected: `harness` not in the server-resolved `clients`,
|
|
172
|
+
* an empty/blank section set, or — when `requireAgentEcho` — a response that
|
|
173
|
+
* does not echo `agent`. The SINGLE gate used by both CC and OC so the two can
|
|
174
|
+
* never drift (the Python Hermes plugin `provider.py::_context_block` mirrors
|
|
175
|
+
* this logic — keep them in sync).
|
|
176
|
+
*
|
|
177
|
+
* `requireAgentEcho` is the old-server guard, and it is the CALLER's decision:
|
|
178
|
+
* - OC passes `agentId !== null` — when it actually sent an id, a 0.12 server
|
|
179
|
+
* that ignores `?agent=` (200 global, no echo) must NOT leak global context
|
|
180
|
+
* into every persona; on a bare fetch (no id) the guard is off (amendment
|
|
181
|
+
* A2).
|
|
182
|
+
* - CC passes `false` ALWAYS and deliberately (see the call site): a thin CC
|
|
183
|
+
* client auto-upgrades via npx BEFORE bedrock does, so during the upgrade
|
|
184
|
+
* window it talks to a 0.12 server that cannot hold ANY per-agent config —
|
|
185
|
+
* global IS the operator's intended state there, and a guard would instead
|
|
186
|
+
* blank ALL context for every CC session in that window.
|
|
187
|
+
*/
|
|
188
|
+
function gateAndRenderContext(data, harness, opts) {
|
|
189
|
+
if (!data || typeof data !== "object")
|
|
190
|
+
return null;
|
|
191
|
+
const clients = Array.isArray(data.clients) ? data.clients : [];
|
|
192
|
+
if (!clients.includes(harness))
|
|
193
|
+
return null;
|
|
194
|
+
if (opts.requireAgentEcho && typeof data.agent !== "string")
|
|
195
|
+
return null;
|
|
196
|
+
return renderContextBlock(data.sections ?? {});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Fetch /context and build the `## Context` block, or null when nothing should
|
|
200
|
+
* be injected: non-2xx, this harness not in `clients`, no sections, or all
|
|
201
|
+
* sections empty. Throws propagate to the caller's fail-soft catch.
|
|
202
|
+
*/
|
|
203
|
+
async function fetchContextBlock(cfg) {
|
|
204
|
+
// Send ?agent= only when we have a valid id; the server does the merge and
|
|
205
|
+
// returns the resolved sections, so the hook stays dumb (no client-side mode
|
|
206
|
+
// logic). A null id (CC's default: no configured agentName, or a configured
|
|
207
|
+
// value that sanitizes to nothing) → bare /context → the shared global set.
|
|
208
|
+
const url = cfg.agentName
|
|
209
|
+
? `${cfg.serverUrl}/context?agent=${encodeURIComponent(cfg.agentName)}`
|
|
210
|
+
: `${cfg.serverUrl}/context`;
|
|
211
|
+
const resp = await fetch(url, {
|
|
212
|
+
headers: authHeaders(cfg.authToken),
|
|
213
|
+
signal: AbortSignal.timeout(3000),
|
|
214
|
+
});
|
|
215
|
+
if (!resp.ok)
|
|
216
|
+
return null;
|
|
217
|
+
const data = await resp.json();
|
|
218
|
+
// CC deliberately passes requireAgentEcho: false (NOT the OC/Hermes old-server
|
|
219
|
+
// guard). A thin CC client auto-upgrades via npx BEFORE bedrock does, so
|
|
220
|
+
// mid-upgrade it may hit a 0.12 server that returns global context with no
|
|
221
|
+
// `agent` echo — and a 0.12 server cannot hold per-agent config, so global is
|
|
222
|
+
// the intended state. Guarding here would blank ALL CC context in that window.
|
|
223
|
+
return gateAndRenderContext(data, THIS_HARNESS, { requireAgentEcho: false });
|
|
224
|
+
}
|
|
166
225
|
/**
|
|
167
226
|
* Fetch context + lessons concurrently and return the combined Markdown block,
|
|
168
227
|
* or null when neither yields anything (nothing to inject; caller prints
|
package/dist/llm.d.ts
CHANGED
|
@@ -68,6 +68,25 @@ export declare function resolveExplicitLlmConfig(overrides?: {
|
|
|
68
68
|
* the transition for any lingering call sites — remove after 0.10.0 ships.
|
|
69
69
|
*/
|
|
70
70
|
export declare const resolveLlmConfigForCC: typeof resolveExplicitLlmConfig;
|
|
71
|
+
export type { ModelTierOverride } from "./types.js";
|
|
72
|
+
/**
|
|
73
|
+
* Normalize a nested `models: { <tier>: {model,baseUrl,apiKey,provider} }` block
|
|
74
|
+
* onto the flat `llm*` / `distill*` / `reflect*` / `classify*` keys the resolver
|
|
75
|
+
* already consumes. Nested overrides WIN over any flat key of the same name; every
|
|
76
|
+
* non-mapped key (llmBackend, licenseKey, distillFallback, contextClients, …)
|
|
77
|
+
* is preserved via spread. Pure: returns the SAME reference when there is no
|
|
78
|
+
* `models` key, so this is a provable no-op for every existing install.
|
|
79
|
+
*
|
|
80
|
+
* Robust to a malformed config.json: a config that parses to a scalar, array,
|
|
81
|
+
* or null is returned untouched (matching the pre-0.13.1 optional-chaining
|
|
82
|
+
* tolerance — this function must never throw at server/nightly boot).
|
|
83
|
+
*
|
|
84
|
+
* Fail-explicit (warn + skip, never throw): an invalid `models` value, an
|
|
85
|
+
* unknown tier name, a non-object tier value, a non-string field value, a tier
|
|
86
|
+
* apiKey/provider set without a baseUrl (they are baseUrl-gated downstream), and
|
|
87
|
+
* a dead score apiKey/provider under an ollama base.
|
|
88
|
+
*/
|
|
89
|
+
export declare function applyModelsBlock(saved: Record<string, unknown> | null): Record<string, unknown> | null;
|
|
71
90
|
/**
|
|
72
91
|
* Resolve an LlmConfig from a saved ~/.hicortex/config.json object.
|
|
73
92
|
*
|
|
@@ -79,8 +98,12 @@ export declare const resolveLlmConfigForCC: typeof resolveExplicitLlmConfig;
|
|
|
79
98
|
*
|
|
80
99
|
* Returns `reason: "claude_binary_missing"` when claude-cli is configured but
|
|
81
100
|
* the binary can't be found, so callers can log a context-specific message.
|
|
101
|
+
*
|
|
102
|
+
* `findBinary` is injectable (defaults to the real `findClaudeBinary`) so the
|
|
103
|
+
* claude-cli branch — including the missing-binary passthrough — can be pinned
|
|
104
|
+
* deterministically in tests without depending on the host filesystem.
|
|
82
105
|
*/
|
|
83
|
-
export declare function resolveSavedLlmConfig(savedConfig: Record<string, unknown> | null): {
|
|
106
|
+
export declare function resolveSavedLlmConfig(savedConfig: Record<string, unknown> | null, findBinary?: () => string | null): {
|
|
84
107
|
config: LlmConfig | null;
|
|
85
108
|
reason?: "claude_binary_missing";
|
|
86
109
|
};
|
package/dist/llm.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
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.applyModelsBlock = applyModelsBlock;
|
|
20
21
|
exports.resolveSavedLlmConfig = resolveSavedLlmConfig;
|
|
21
22
|
exports.resolveClassifyProbeTarget = resolveClassifyProbeTarget;
|
|
22
23
|
exports.findClaudeBinary = findClaudeBinary;
|
|
@@ -70,6 +71,117 @@ function resolveExplicitLlmConfig(overrides) {
|
|
|
70
71
|
* the transition for any lingering call sites — remove after 0.10.0 ships.
|
|
71
72
|
*/
|
|
72
73
|
exports.resolveLlmConfigForCC = resolveExplicitLlmConfig;
|
|
74
|
+
/**
|
|
75
|
+
* Map from a `models.<tier>` name to the flat config keys it feeds. The base
|
|
76
|
+
* tier is `score` — score IS the base model today (completeFast reads
|
|
77
|
+
* config.model), so it lands on the llm* keys and its `provider` is ignored
|
|
78
|
+
* (the base provider comes from llmBackend / detectProvider, not config).
|
|
79
|
+
* Tiers with a `provider` key (distill/reflect/classify) apply their apiKey +
|
|
80
|
+
* provider through a baseUrl-gated overlay downstream; `score` (no provider
|
|
81
|
+
* key) rides the base resolution.
|
|
82
|
+
*/
|
|
83
|
+
const MODELS_TIER_KEYS = {
|
|
84
|
+
score: { model: "llmModel", baseUrl: "llmBaseUrl", apiKey: "llmApiKey" },
|
|
85
|
+
distill: { model: "distillModel", baseUrl: "distillBaseUrl", apiKey: "distillApiKey", provider: "distillProvider" },
|
|
86
|
+
reflect: { model: "reflectModel", baseUrl: "reflectBaseUrl", apiKey: "reflectApiKey", provider: "reflectProvider" },
|
|
87
|
+
classify: { model: "classifyModel", baseUrl: "classifyBaseUrl", apiKey: "classifyApiKey", provider: "classifyProvider" },
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Normalize a nested `models: { <tier>: {model,baseUrl,apiKey,provider} }` block
|
|
91
|
+
* onto the flat `llm*` / `distill*` / `reflect*` / `classify*` keys the resolver
|
|
92
|
+
* already consumes. Nested overrides WIN over any flat key of the same name; every
|
|
93
|
+
* non-mapped key (llmBackend, licenseKey, distillFallback, contextClients, …)
|
|
94
|
+
* is preserved via spread. Pure: returns the SAME reference when there is no
|
|
95
|
+
* `models` key, so this is a provable no-op for every existing install.
|
|
96
|
+
*
|
|
97
|
+
* Robust to a malformed config.json: a config that parses to a scalar, array,
|
|
98
|
+
* or null is returned untouched (matching the pre-0.13.1 optional-chaining
|
|
99
|
+
* tolerance — this function must never throw at server/nightly boot).
|
|
100
|
+
*
|
|
101
|
+
* Fail-explicit (warn + skip, never throw): an invalid `models` value, an
|
|
102
|
+
* unknown tier name, a non-object tier value, a non-string field value, a tier
|
|
103
|
+
* apiKey/provider set without a baseUrl (they are baseUrl-gated downstream), and
|
|
104
|
+
* a dead score apiKey/provider under an ollama base.
|
|
105
|
+
*/
|
|
106
|
+
function applyModelsBlock(saved) {
|
|
107
|
+
// Guard the container itself first — `"models" in saved` throws a TypeError on
|
|
108
|
+
// a truthy non-object (config.json = `true`/`5`/`"x"`); such configs must pass
|
|
109
|
+
// through so the boot path degrades to recall-only exactly as before.
|
|
110
|
+
if (typeof saved !== "object" || saved === null || Array.isArray(saved))
|
|
111
|
+
return saved;
|
|
112
|
+
if (!("models" in saved))
|
|
113
|
+
return saved;
|
|
114
|
+
const models = saved.models;
|
|
115
|
+
if (typeof models !== "object" || models === null || Array.isArray(models)) {
|
|
116
|
+
console.warn(`[hicortex] Ignoring invalid "models" config: expected an object of per-tier overrides, got ${Array.isArray(models) ? "array" : models === null ? "null" : typeof models}`);
|
|
117
|
+
return saved;
|
|
118
|
+
}
|
|
119
|
+
const ollamaBase = saved.llmBackend === "ollama";
|
|
120
|
+
const mapped = {};
|
|
121
|
+
for (const [tier, value] of Object.entries(models)) {
|
|
122
|
+
const keys = MODELS_TIER_KEYS[tier];
|
|
123
|
+
if (!keys) {
|
|
124
|
+
console.warn(`[hicortex] Ignoring unknown "models" tier "${tier}" (expected: score, distill, reflect, classify)`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
128
|
+
console.warn(`[hicortex] Ignoring invalid "models.${tier}" override: expected an object with model/baseUrl/apiKey/provider`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const o = value;
|
|
132
|
+
// Per-field string validation: a non-string value would map verbatim and
|
|
133
|
+
// fail opaquely downstream (e.g. baseUrl: 11434), so drop it with a warning.
|
|
134
|
+
const strField = (name) => {
|
|
135
|
+
const v = o[name];
|
|
136
|
+
if (v === undefined)
|
|
137
|
+
return undefined;
|
|
138
|
+
if (typeof v !== "string") {
|
|
139
|
+
console.warn(`[hicortex] Ignoring non-string "models.${tier}.${name}" (expected a string)`);
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
return v;
|
|
143
|
+
};
|
|
144
|
+
const model = strField("model");
|
|
145
|
+
const baseUrl = strField("baseUrl");
|
|
146
|
+
const apiKey = strField("apiKey");
|
|
147
|
+
const provider = strField("provider");
|
|
148
|
+
if (model !== undefined)
|
|
149
|
+
mapped[keys.model] = model;
|
|
150
|
+
if (baseUrl !== undefined)
|
|
151
|
+
mapped[keys.baseUrl] = baseUrl;
|
|
152
|
+
if (keys.provider) {
|
|
153
|
+
// Overlay tier (distill/reflect/classify): the downstream overlay only
|
|
154
|
+
// consumes apiKey/provider when the tier ALSO sets its own baseUrl.
|
|
155
|
+
// Without one, they would silently bill to the base key — so warn + drop.
|
|
156
|
+
if ((apiKey !== undefined || provider !== undefined) && baseUrl === undefined) {
|
|
157
|
+
console.warn(`[hicortex] Ignoring "models.${tier}" apiKey/provider without a baseUrl: they only take effect when the tier sets its own baseUrl`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
if (apiKey !== undefined)
|
|
161
|
+
mapped[keys.apiKey] = apiKey;
|
|
162
|
+
if (provider !== undefined)
|
|
163
|
+
mapped[keys.provider] = provider;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// score = base tier: no separate provider key, and apiKey rides llmApiKey.
|
|
168
|
+
if (provider !== undefined) {
|
|
169
|
+
console.warn(`[hicortex] Ignoring "models.score.provider": the base provider comes from llmBackend (or is auto-detected from the endpoint)`);
|
|
170
|
+
}
|
|
171
|
+
if (apiKey !== undefined) {
|
|
172
|
+
if (ollamaBase) {
|
|
173
|
+
// The ollama base path hardcodes an empty api key and never reads
|
|
174
|
+
// llmApiKey, so score.apiKey is dead there.
|
|
175
|
+
console.warn(`[hicortex] Ignoring "models.score.apiKey": the base ollama path sends no api key`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
mapped[keys.apiKey] = apiKey;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { ...saved, ...mapped };
|
|
184
|
+
}
|
|
73
185
|
/**
|
|
74
186
|
* Resolve an LlmConfig from a saved ~/.hicortex/config.json object.
|
|
75
187
|
*
|
|
@@ -81,11 +193,16 @@ exports.resolveLlmConfigForCC = resolveExplicitLlmConfig;
|
|
|
81
193
|
*
|
|
82
194
|
* Returns `reason: "claude_binary_missing"` when claude-cli is configured but
|
|
83
195
|
* the binary can't be found, so callers can log a context-specific message.
|
|
196
|
+
*
|
|
197
|
+
* `findBinary` is injectable (defaults to the real `findClaudeBinary`) so the
|
|
198
|
+
* claude-cli branch — including the missing-binary passthrough — can be pinned
|
|
199
|
+
* deterministically in tests without depending on the host filesystem.
|
|
84
200
|
*/
|
|
85
|
-
function resolveSavedLlmConfig(savedConfig) {
|
|
201
|
+
function resolveSavedLlmConfig(savedConfig, findBinary = findClaudeBinary) {
|
|
202
|
+
savedConfig = applyModelsBlock(savedConfig);
|
|
86
203
|
let llmConfig = null;
|
|
87
204
|
if (savedConfig?.llmBackend === "claude-cli") {
|
|
88
|
-
const claudePath =
|
|
205
|
+
const claudePath = findBinary();
|
|
89
206
|
if (claudePath) {
|
|
90
207
|
llmConfig = claudeCliConfig(claudePath);
|
|
91
208
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -81,6 +81,10 @@ let stateDir = "";
|
|
|
81
81
|
// Resolved contextClients list (spec §2) — the harness names allowed to inject
|
|
82
82
|
// the standing context layer. Echoed by GET /context so each hook self-gates.
|
|
83
83
|
let contextClients = ["cc"];
|
|
84
|
+
// Resolved contextAgents map (0.13) — agent id → mode (override/global/off).
|
|
85
|
+
// Read once at boot (like contextClients); the drop-in-a-dir presence path is
|
|
86
|
+
// per-request, so only explicit config entries need a daemon restart to apply.
|
|
87
|
+
let contextAgents = {};
|
|
84
88
|
// Cache detectChunkSize results keyed by "<provider>/<model>@<baseUrl>" so we
|
|
85
89
|
// probe each endpoint once per server boot rather than once per /distill request.
|
|
86
90
|
const chunkSizeCache = new Map();
|
|
@@ -328,7 +332,7 @@ async function startServer(options = {}) {
|
|
|
328
332
|
// Named backends (claude-cli, ollama) → immediate config; everything else
|
|
329
333
|
// goes through resolveExplicitLlmConfig which requires a user-chosen provider.
|
|
330
334
|
// If nothing is configured: start recall-only with an unmissable warning.
|
|
331
|
-
const savedConfig = readConfigFile(stateDir);
|
|
335
|
+
const savedConfig = (0, llm_js_1.applyModelsBlock)(readConfigFile(stateDir));
|
|
332
336
|
if (savedConfig?.llmBackend === "claude-cli") {
|
|
333
337
|
const claudePath = (0, llm_js_1.findClaudeBinary)();
|
|
334
338
|
if (claudePath) {
|
|
@@ -436,6 +440,16 @@ async function startServer(options = {}) {
|
|
|
436
440
|
console.warn(`[hicortex] Ignoring unknown contextClients: ${resolvedClients.dropped.join(", ")} ` +
|
|
437
441
|
`(known: cc, hermes, oc)`);
|
|
438
442
|
}
|
|
443
|
+
// Per-agent context (0.13): resolve the config-declared modes. Warn once per
|
|
444
|
+
// boot on dropped entries (bad key or bad mode) so typos surface. NOTE: this
|
|
445
|
+
// map is boot-time; editing contextAgents needs a daemon restart. Dropping an
|
|
446
|
+
// agents/<id> dir onto disk takes effect immediately (per-request presence).
|
|
447
|
+
const resolvedAgents = (0, context_store_js_1.resolveContextAgents)(savedConfig?.contextAgents);
|
|
448
|
+
contextAgents = resolvedAgents.agents;
|
|
449
|
+
if (resolvedAgents.dropped.length > 0) {
|
|
450
|
+
console.warn(`[hicortex] Ignoring invalid contextAgents entries: ${resolvedAgents.dropped.join(", ")} ` +
|
|
451
|
+
`(keys must match ^[a-z0-9][a-z0-9_-]*$; modes must be override|global|off)`);
|
|
452
|
+
}
|
|
439
453
|
// Express app
|
|
440
454
|
const app = (0, express_1.default)();
|
|
441
455
|
// Raise the body limit — whole-session denoised transcripts exceed the 100 kB default.
|
|
@@ -620,7 +634,7 @@ async function startServer(options = {}) {
|
|
|
620
634
|
// which the tests exercise directly — no mirror-app drift.
|
|
621
635
|
app.get("/context", (req, res) => {
|
|
622
636
|
try {
|
|
623
|
-
const r = (0, context_store_js_1.handleContextGet)((0, node_path_1.join)(stateDir, "context"), contextClients, req.query);
|
|
637
|
+
const r = (0, context_store_js_1.handleContextGet)((0, node_path_1.join)(stateDir, "context"), contextClients, req.query, contextAgents);
|
|
624
638
|
res.status(r.status).json(r.body);
|
|
625
639
|
}
|
|
626
640
|
catch (err) {
|
|
@@ -629,7 +643,7 @@ async function startServer(options = {}) {
|
|
|
629
643
|
});
|
|
630
644
|
app.put("/context", (req, res) => {
|
|
631
645
|
try {
|
|
632
|
-
const r = (0, context_store_js_1.handleContextPut)((0, node_path_1.join)(stateDir, "context"), req.body);
|
|
646
|
+
const r = (0, context_store_js_1.handleContextPut)((0, node_path_1.join)(stateDir, "context"), req.body, req.query, contextAgents);
|
|
633
647
|
if (r.warn)
|
|
634
648
|
console.warn(`[hicortex] ${r.warn}`);
|
|
635
649
|
res.status(r.status).json(r.body);
|
|
@@ -703,7 +717,11 @@ async function startServer(options = {}) {
|
|
|
703
717
|
? `${session_id}${segment_id ? `#${segment_id}` : ""}`
|
|
704
718
|
: undefined;
|
|
705
719
|
try {
|
|
706
|
-
|
|
720
|
+
// Collect gate-dropped entries so they can ride back in the response and
|
|
721
|
+
// land in the caller's file-persisted nightly log (#156 audit trail); the
|
|
722
|
+
// server-side per-entry console.log in distillChunk stays as well.
|
|
723
|
+
const dropped = [];
|
|
724
|
+
const entries = await (0, distiller_js_1.distillSession)(llm, conversationText, project ?? "unknown", date, chunkSize, dropped);
|
|
707
725
|
const ids = [];
|
|
708
726
|
for (let i = 0; i < entries.length; i++) {
|
|
709
727
|
const entry = entries[i];
|
|
@@ -722,7 +740,11 @@ async function startServer(options = {}) {
|
|
|
722
740
|
});
|
|
723
741
|
ids.push(id);
|
|
724
742
|
}
|
|
725
|
-
res.status(201).json({
|
|
743
|
+
res.status(201).json({
|
|
744
|
+
ids,
|
|
745
|
+
distilled: ids.length,
|
|
746
|
+
dropped: dropped.map((d) => (d.length > 120 ? `${d.slice(0, 120)}…` : d)),
|
|
747
|
+
});
|
|
726
748
|
}
|
|
727
749
|
catch (err) {
|
|
728
750
|
res.status(500).json({ error: "Distillation failed", message: err instanceof Error ? err.message : String(err) });
|
package/dist/nightly-status.js
CHANGED
|
@@ -18,6 +18,7 @@ const node_os_1 = require("node:os");
|
|
|
18
18
|
const node_child_process_1 = require("node:child_process");
|
|
19
19
|
const db_js_1 = require("./db.js");
|
|
20
20
|
const state_js_1 = require("./state.js");
|
|
21
|
+
const llm_js_1 = require("./llm.js");
|
|
21
22
|
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
22
23
|
const CONFIG_PATH = (0, node_path_1.join)(HICORTEX_HOME, "config.json");
|
|
23
24
|
async function showNightlyStatus() {
|
|
@@ -36,7 +37,9 @@ async function showNightlyStatus() {
|
|
|
36
37
|
}
|
|
37
38
|
// LLM config
|
|
38
39
|
try {
|
|
39
|
-
|
|
40
|
+
// No `?? {}` coercion: a null/invalid parse must fall through to the catch
|
|
41
|
+
// below (as it did pre-0.13.1) rather than print a fabricated-healthy status.
|
|
42
|
+
const config = (0, llm_js_1.applyModelsBlock)(JSON.parse((0, node_fs_1.readFileSync)(CONFIG_PATH, "utf-8")));
|
|
40
43
|
const backend = config.llmBackend ?? "auto-detect";
|
|
41
44
|
const model = config.llmModel ?? "default";
|
|
42
45
|
const mode = config.mode === "client" ? "client → " + (config.serverUrl ?? "?") : "server (local)";
|
package/dist/nightly.js
CHANGED
|
@@ -228,6 +228,10 @@ async function runNightly(options = {}) {
|
|
|
228
228
|
const data = await resp.json();
|
|
229
229
|
memoriesIngested += data.distilled ?? 0;
|
|
230
230
|
console.log(`[hicortex] → ${data.distilled ?? 0} memories extracted`);
|
|
231
|
+
// Durable audit trail (#156): the server truncates each dropped entry.
|
|
232
|
+
for (const d of data.dropped ?? []) {
|
|
233
|
+
console.log(`[hicortex] Substance gate: dropped "${d}"`);
|
|
234
|
+
}
|
|
231
235
|
}
|
|
232
236
|
else if (resp.status === 429) {
|
|
233
237
|
const data = await resp.json();
|
|
@@ -444,6 +448,10 @@ async function runClientNightly(config, dryRun) {
|
|
|
444
448
|
memoriesIngested += count;
|
|
445
449
|
sessionsSent++;
|
|
446
450
|
console.log(`[hicortex] → ${count} memories sent to server`);
|
|
451
|
+
// Durable audit trail (#156): the server truncates each dropped entry.
|
|
452
|
+
for (const d of data.dropped ?? []) {
|
|
453
|
+
console.log(`[hicortex] Substance gate: dropped "${d}"`);
|
|
454
|
+
}
|
|
447
455
|
}
|
|
448
456
|
else if (resp.status === 401) {
|
|
449
457
|
console.error(`[hicortex] Auth failed. Check authToken in ~/.hicortex/config.json`);
|
package/dist/status.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hicortex status — show current configuration and stats.
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* The value shown after "Agent name:" in `hicortex status` (#179, A3). Reports
|
|
6
|
+
* EXACTLY what the CC hook resolves (shared `resolveAgentIdentity`), so the
|
|
7
|
+
* operator never keys `contextAgents`/`agents/<id>/` on an id the install does
|
|
8
|
+
* not actually send. Unset → the install sends no `?agent=` and shares the
|
|
9
|
+
* global context (CC default). A configured-but-unsanitizable value is called
|
|
10
|
+
* out as invalid (the hook sends none) rather than silently accepted.
|
|
11
|
+
*/
|
|
12
|
+
export declare function statusAgentLine(config: Record<string, unknown>): string;
|
|
4
13
|
export declare function runStatus(): Promise<void>;
|
package/dist/status.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Hicortex status — show current configuration and stats.
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.statusAgentLine = statusAgentLine;
|
|
6
7
|
exports.runStatus = runStatus;
|
|
7
8
|
const paths_js_1 = require("./paths.js");
|
|
8
9
|
const node_fs_1 = require("node:fs");
|
|
@@ -12,9 +13,29 @@ const node_child_process_1 = require("node:child_process");
|
|
|
12
13
|
const db_js_1 = require("./db.js");
|
|
13
14
|
const features_js_1 = require("./features.js");
|
|
14
15
|
const state_js_1 = require("./state.js");
|
|
16
|
+
const context_store_js_1 = require("./context-store.js");
|
|
15
17
|
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
16
18
|
const CC_SETTINGS = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "settings.json");
|
|
17
19
|
const OC_CONFIG = (0, node_path_1.join)((0, node_os_1.homedir)(), ".openclaw", "openclaw.json");
|
|
20
|
+
/**
|
|
21
|
+
* The value shown after "Agent name:" in `hicortex status` (#179, A3). Reports
|
|
22
|
+
* EXACTLY what the CC hook resolves (shared `resolveAgentIdentity`), so the
|
|
23
|
+
* operator never keys `contextAgents`/`agents/<id>/` on an id the install does
|
|
24
|
+
* not actually send. Unset → the install sends no `?agent=` and shares the
|
|
25
|
+
* global context (CC default). A configured-but-unsanitizable value is called
|
|
26
|
+
* out as invalid (the hook sends none) rather than silently accepted.
|
|
27
|
+
*/
|
|
28
|
+
function statusAgentLine(config) {
|
|
29
|
+
const id = (0, context_store_js_1.resolveAgentIdentity)(config);
|
|
30
|
+
switch (id.source) {
|
|
31
|
+
case "configured":
|
|
32
|
+
return id.agentId;
|
|
33
|
+
case "invalid-config":
|
|
34
|
+
return `(invalid configured value "${id.rawConfigured}" — fix config.agentName; hook sends none)`;
|
|
35
|
+
default: // unset
|
|
36
|
+
return "(not set — global context)";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
18
39
|
async function runStatus() {
|
|
19
40
|
console.log("Hicortex Status");
|
|
20
41
|
console.log("─".repeat(40));
|
|
@@ -42,11 +63,12 @@ async function runStatus() {
|
|
|
42
63
|
let licenseKey = "";
|
|
43
64
|
let savedAuthToken = "";
|
|
44
65
|
let isClientMode = false;
|
|
66
|
+
let parsedConfig = {};
|
|
45
67
|
try {
|
|
46
|
-
|
|
47
|
-
licenseKey =
|
|
48
|
-
savedAuthToken =
|
|
49
|
-
isClientMode =
|
|
68
|
+
parsedConfig = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
|
|
69
|
+
licenseKey = parsedConfig.licenseKey ?? "";
|
|
70
|
+
savedAuthToken = parsedConfig.authToken ?? "";
|
|
71
|
+
isClientMode = parsedConfig.mode === "client";
|
|
50
72
|
}
|
|
51
73
|
catch { /* no config */ }
|
|
52
74
|
const validated = (0, features_js_1.getValidatedLicense)();
|
|
@@ -65,6 +87,9 @@ async function runStatus() {
|
|
|
65
87
|
else if (!isClientMode && !savedAuthToken) {
|
|
66
88
|
console.log(`Auth token: not configured (run: npx @gamaze/hicortex init)`);
|
|
67
89
|
}
|
|
90
|
+
// Per-agent context id (#179) — the id this install sends as ?agent= and the
|
|
91
|
+
// key operators use for contextAgents / agents/<id>/ dirs.
|
|
92
|
+
console.log(`Agent name: ${statusAgentLine(parsedConfig)}`);
|
|
68
93
|
console.log();
|
|
69
94
|
// Adapters
|
|
70
95
|
console.log("Adapters:");
|
package/dist/types.d.ts
CHANGED
|
@@ -115,6 +115,16 @@ export interface ConsolidationReport {
|
|
|
115
115
|
calls_by_stage: Record<string, number>;
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* A single per-stage override inside the nested `models` server-config block.
|
|
120
|
+
* Consumed by `applyModelsBlock` (llm.ts), which re-exports this type.
|
|
121
|
+
*/
|
|
122
|
+
export interface ModelTierOverride {
|
|
123
|
+
model?: string;
|
|
124
|
+
baseUrl?: string;
|
|
125
|
+
apiKey?: string;
|
|
126
|
+
provider?: string;
|
|
127
|
+
}
|
|
118
128
|
/** Plugin configuration from openclaw.plugin.json configSchema. */
|
|
119
129
|
export interface HicortexConfig {
|
|
120
130
|
licenseKey?: string;
|
|
@@ -145,6 +155,16 @@ export interface HicortexConfig {
|
|
|
145
155
|
classifyApiKey?: string;
|
|
146
156
|
/** Optional provider for the classify endpoint (defaults to the base provider). */
|
|
147
157
|
classifyProvider?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Server config (NOT an OC-plugin key): nested per-stage model overrides.
|
|
160
|
+
* `{ score|distill|reflect|classify: { model?, baseUrl?, apiKey?, provider? } }`.
|
|
161
|
+
* Normalized onto the flat `llm*` / `distill*` / `reflect*` / `classify*`
|
|
162
|
+
* keys at read time (see applyModelsBlock in llm.ts); nested wins, and the
|
|
163
|
+
* flat keys remain supported at lower precedence. Happy path is a single model via
|
|
164
|
+
* `llmModel`; use this block only for per-stage routing. `score.provider` is
|
|
165
|
+
* ignored (base provider comes from llmBackend).
|
|
166
|
+
*/
|
|
167
|
+
models?: Record<string, ModelTierOverride>;
|
|
148
168
|
/** @deprecated Consolidation is owned by the server nightly. */
|
|
149
169
|
consolidateHour?: number;
|
|
150
170
|
/** @deprecated The OC plugin no longer opens its own database. */
|
|
@@ -14,11 +14,24 @@ Gives [Hermes](https://github.com/nousresearch/hermes-agent) agents self-learnin
|
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| `prefetch(query)` | recall relevant memories before each turn | `GET /search` |
|
|
16
16
|
| `queue_prefetch(query)` | background recall for the next turn | `GET /search` |
|
|
17
|
-
| `system_prompt_block()` | inject distilled lessons + memory index | `GET /lessons` |
|
|
17
|
+
| `system_prompt_block()` | inject per-agent standing context + distilled lessons + memory index | `GET /context`, `GET /lessons` |
|
|
18
18
|
| `get_tool_schemas()` | exposes the 8 unified tools | see tool table below |
|
|
19
19
|
|
|
20
20
|
That's the whole surface. No `sync_turn`, no compaction/session-end capture — those are intentionally absent.
|
|
21
21
|
|
|
22
|
+
### Per-agent standing context (0.13)
|
|
23
|
+
|
|
24
|
+
`system_prompt_block()` also injects the hand-edited **standing context layer** (`## Context`, above the lessons block) — "who you are + how to work", distinct from episodic memory. The server resolves it **per agent**: this profile's own sections override the global set (`override`), or it can be `global` or `off`. See the main repo's `/context` layer docs.
|
|
25
|
+
|
|
26
|
+
The plugin sends its **profile name** as `?agent=`, resolved in this order:
|
|
27
|
+
|
|
28
|
+
1. `agent_name` in the plugin config (explicit override);
|
|
29
|
+
2. the `HERMES_PROFILE` environment variable;
|
|
30
|
+
3. a `HERMES_HOME` ending in `profiles/<name>` (the per-profile install path);
|
|
31
|
+
4. none → the global context (backward compatible).
|
|
32
|
+
|
|
33
|
+
Leave `agent_name` blank to auto-derive (2–4). Context injection needs a Hicortex server **≥ 0.13**; against an older server the plugin detects the missing per-agent support and injects no context (lessons are unaffected). Context and lessons fail soft independently — a context failure never costs the lessons block.
|
|
34
|
+
|
|
22
35
|
### Tools (unified 8)
|
|
23
36
|
|
|
24
37
|
| Tool | REST call | Description |
|
|
@@ -57,7 +70,7 @@ hermes memory setup # select "hicortex", enter the server URL/token when promp
|
|
|
57
70
|
|
|
58
71
|
Run it once per profile if you use Hermes profiles. Hermes allows **one** external memory provider at a time, so disable Honcho (or any other) first, then restart the gateway.
|
|
59
72
|
|
|
60
|
-
Config fields (`hicortex_url`, `default_project`, `recall_limit`, `privacy_filter`) can also be written to `$HERMES_HOME/plugins/hicortex/config.json` directly. The auth token is a **secret** — set it via env, not the JSON file:
|
|
73
|
+
Config fields (`hicortex_url`, `default_project`, `recall_limit`, `privacy_filter`, `agent_name`) can also be written to `$HERMES_HOME/plugins/hicortex/config.json` directly. `agent_name` pins the per-agent context id for this profile (leave blank to auto-derive — see [Per-agent standing context](#per-agent-standing-context-013)). The auth token is a **secret** — set it via env, not the JSON file:
|
|
61
74
|
|
|
62
75
|
```bash
|
|
63
76
|
export HICORTEX_AUTH_TOKEN=hctx-default-token # or your custom token
|
|
@@ -96,6 +96,13 @@ class HicortexClient:
|
|
|
96
96
|
def lessons(self) -> dict[str, Any]:
|
|
97
97
|
return self._get("/lessons")
|
|
98
98
|
|
|
99
|
+
def context(self, agent: Optional[str] = None) -> dict[str, Any]:
|
|
100
|
+
"""Standing context layer (L2). When ``agent`` is set, the server
|
|
101
|
+
resolves the per-agent scope and echoes ``agent``/``mode`` (0.13); a
|
|
102
|
+
pre-0.13 server ignores the param and returns the global set with no
|
|
103
|
+
echo — the caller uses that echo as an old-server guard."""
|
|
104
|
+
return self._get("/context", {"agent": agent})
|
|
105
|
+
|
|
99
106
|
def index(self) -> dict[str, Any]:
|
|
100
107
|
return self._get("/index")
|
|
101
108
|
|
|
@@ -57,6 +57,16 @@ CONFIG_SCHEMA: list[dict[str, Any]] = [
|
|
|
57
57
|
"default": "WORK,PERSONAL",
|
|
58
58
|
"required": False,
|
|
59
59
|
},
|
|
60
|
+
{
|
|
61
|
+
"key": "agent_name",
|
|
62
|
+
"label": "Agent name (per-agent context)",
|
|
63
|
+
"description": (
|
|
64
|
+
"Identity sent as ?agent= when fetching the standing context layer, "
|
|
65
|
+
"so this profile gets its own context (0.13). Leave blank to "
|
|
66
|
+
"auto-derive from the running profile (HERMES_PROFILE / HERMES_HOME)."
|
|
67
|
+
),
|
|
68
|
+
"required": False,
|
|
69
|
+
},
|
|
60
70
|
# NOTE: recall-only plugin — no capture config. Capture is handled by the
|
|
61
71
|
# nightly server-side reader of each agent's session store.
|
|
62
72
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
name: hicortex
|
|
2
|
-
version: 0.
|
|
3
|
-
description: "Self-learning memory for Hermes agents — every session is distilled into lessons overnight, and your agent wakes up wiser. Injects fresh lessons each turn and exposes the full 8-tool memory surface (search, recent, ingest, lessons, index, graph, update, delete) via a shared Hicortex server. Stdlib-only."
|
|
2
|
+
version: 0.6.0
|
|
3
|
+
description: "Self-learning memory for Hermes agents — every session is distilled into lessons overnight, and your agent wakes up wiser. Injects fresh lessons each turn plus a per-agent standing context block, and exposes the full 8-tool memory surface (search, recent, ingest, lessons, index, graph, update, delete) via a shared Hicortex server. Stdlib-only."
|
|
4
4
|
pip_dependencies: []
|
|
5
5
|
hooks: []
|
|
6
6
|
requires_env:
|