@gamaze/hicortex 0.15.3 → 0.16.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 +2 -1
- package/dist/consolidate.d.ts +4 -3
- package/dist/consolidate.js +33 -12
- package/dist/db.js +83 -3
- package/dist/eval/recall-sweep.d.ts +23 -0
- package/dist/eval/recall-sweep.js +288 -2
- package/dist/index.js +13 -4
- package/dist/init.d.ts +1 -1
- package/dist/init.js +53 -100
- package/dist/llm.d.ts +3 -1
- package/dist/llm.js +18 -4
- package/dist/mcp-server.js +23 -19
- package/dist/memory-instructions.js +1 -1
- package/dist/recall-hook-cli.d.ts +1 -1
- package/dist/recall-hook-cli.js +7 -2
- package/dist/recall-index.d.ts +33 -2
- package/dist/recall-index.js +61 -14
- package/dist/retrieval.d.ts +52 -1
- package/dist/retrieval.js +144 -23
- package/dist/storage.d.ts +56 -3
- package/dist/storage.js +93 -17
- package/dist/types.d.ts +4 -0
- package/dist/uninstall.js +18 -4
- package/hermes-plugin/hicortex/client.py +8 -4
- package/hermes-plugin/hicortex/config.py +11 -0
- package/hermes-plugin/hicortex/provider.py +11 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/hicortex-activate/SKILL.md +0 -53
- package/skills/hicortex-learn/SKILL.md +0 -40
package/dist/init.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - Register MCP server in CC settings
|
|
14
14
|
* - Install CC SessionStart hook for query-time lessons
|
|
15
15
|
* - Strip old static CLAUDE.md learnings block if present
|
|
16
|
-
* -
|
|
16
|
+
* - Remove legacy pre-0.10 CC commands (/learn, /hicortex-activate) if present
|
|
17
17
|
*/
|
|
18
18
|
import type { DomainDef } from "./types.js";
|
|
19
19
|
/**
|
package/dist/init.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - Register MCP server in CC settings
|
|
15
15
|
* - Install CC SessionStart hook for query-time lessons
|
|
16
16
|
* - Strip old static CLAUDE.md learnings block if present
|
|
17
|
-
* -
|
|
17
|
+
* - Remove legacy pre-0.10 CC commands (/learn, /hicortex-activate) if present
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
exports.GENERIC_DEFAULT_DOMAINS = void 0;
|
|
@@ -255,98 +255,50 @@ function allowHicortexTools() {
|
|
|
255
255
|
console.log(` ✓ Added Hicortex tool permissions to ${CC_SETTINGS}`);
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
// /learn
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
## Example
|
|
285
|
-
|
|
286
|
-
\`/learn always check provider docs before assuming an API uses the same auth scheme as OpenAI\`
|
|
287
|
-
|
|
288
|
-
Becomes a call to hicortex_ingest with:
|
|
289
|
-
- content: "LEARNING: always check provider docs before assuming an API uses the same auth scheme as OpenAI — header names and token formats vary widely (Bearer vs x-api-key vs custom)."
|
|
290
|
-
- memory_type: "lesson"
|
|
291
|
-
`;
|
|
292
|
-
const learnPath = (0, node_path_1.join)(CC_COMMANDS_DIR, "learn.md");
|
|
293
|
-
if ((0, node_fs_1.existsSync)(learnPath)) {
|
|
294
|
-
// Check if it's ours (contains hicortex_ingest)
|
|
295
|
-
const existing = (0, node_fs_1.readFileSync)(learnPath, "utf-8");
|
|
296
|
-
if (!existing.includes("hicortex_ingest") && !existing.includes("hicortex")) {
|
|
297
|
-
console.log(` ⚠ Skipping /learn — existing command found (not Hicortex). Won't overwrite.`);
|
|
258
|
+
function cleanupLegacyCcCommands() {
|
|
259
|
+
// Pre-0.10 installs wrote two CC slash commands that are now RETIRED:
|
|
260
|
+
// - /learn : manual immediate ingest. Capture has been automatic
|
|
261
|
+
// (nightly-from-logs) since 0.9; hicortex_ingest
|
|
262
|
+
// remains for *explicitly requested* learnings, but
|
|
263
|
+
// no longer warrants a slash command.
|
|
264
|
+
// - /hicortex-activate : registered a commercial license key. licenseKey
|
|
265
|
+
// gates nothing now (the per-install auth TOKEN is
|
|
266
|
+
// the credential, auto-generated at init), so the
|
|
267
|
+
// command is dead.
|
|
268
|
+
// Remove stale copies so upgraders don't keep dead commands. Idempotent —
|
|
269
|
+
// a best-effort cleanup of our own files; never throws.
|
|
270
|
+
for (const name of ["learn.md", "hicortex-activate.md"]) {
|
|
271
|
+
const p = (0, node_path_1.join)(CC_COMMANDS_DIR, name);
|
|
272
|
+
if (!(0, node_fs_1.existsSync)(p))
|
|
273
|
+
continue;
|
|
274
|
+
// Ownership guard: only remove a file we actually wrote. `learn.md` is a
|
|
275
|
+
// generic command name a user may own independently — deleting by filename
|
|
276
|
+
// alone would silently destroy their file. The retired writer always
|
|
277
|
+
// embedded "hicortex" (the ingest tool + prose); mirror the same marker the
|
|
278
|
+
// old installer used before overwriting. Skip + warn on anything else.
|
|
279
|
+
try {
|
|
280
|
+
if (!(0, node_fs_1.readFileSync)(p, "utf-8").toLowerCase().includes("hicortex")) {
|
|
281
|
+
console.log(` ⚠ Skipping ${name} in ${CC_COMMANDS_DIR} — not a Hicortex file, left untouched`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
298
284
|
}
|
|
299
|
-
|
|
300
|
-
|
|
285
|
+
catch {
|
|
286
|
+
// Unreadable — do not delete blind; leave it and move on.
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
(0, node_fs_1.rmSync)(p);
|
|
291
|
+
console.log(` ✓ Removed legacy command ${name} from ${CC_COMMANDS_DIR} (retired pre-0.10)`);
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
// ENOENT = already gone (fine, idempotent). Anything else (EACCES, EBUSY)
|
|
295
|
+
// is worth a line so a stuck stale file is diagnosable — but never fatal
|
|
296
|
+
// to init (best-effort cleanup of our own file).
|
|
297
|
+
if (err?.code !== "ENOENT") {
|
|
298
|
+
console.log(` ⚠ Could not remove legacy command ${name}: ${err?.message ?? err}`);
|
|
299
|
+
}
|
|
301
300
|
}
|
|
302
301
|
}
|
|
303
|
-
else {
|
|
304
|
-
(0, node_fs_1.writeFileSync)(learnPath, learnContent);
|
|
305
|
-
}
|
|
306
|
-
// /hicortex-activate command — registers a commercial license key for display in status
|
|
307
|
-
const activateContent = `---
|
|
308
|
-
name: hicortex-activate
|
|
309
|
-
description: Register a Hicortex commercial license key. Personal and noncommercial use is free; commercial use requires a per-seat license from hicortex.gamaze.com.
|
|
310
|
-
argument-hint: <license-key>
|
|
311
|
-
allowed-tools: Bash(mkdir:*), Bash(echo:*), Bash(launchctl:*), Bash(systemctl:*), Bash(curl:*), mcp__hicortex__hicortex_ingest, mcp__hicortex__hicortex_search, mcp__hicortex__hicortex_recent, mcp__hicortex__hicortex_lessons
|
|
312
|
-
---
|
|
313
|
-
|
|
314
|
-
# Register Hicortex Commercial License
|
|
315
|
-
|
|
316
|
-
## If key provided (e.g. /hicortex-activate hctx-abc123)
|
|
317
|
-
|
|
318
|
-
1. Write the key to the config file:
|
|
319
|
-
|
|
320
|
-
\`\`\`bash
|
|
321
|
-
mkdir -p ~/.hicortex
|
|
322
|
-
echo '{ "licenseKey": "THE_KEY_HERE" }' > ~/.hicortex/config.json
|
|
323
|
-
\`\`\`
|
|
324
|
-
|
|
325
|
-
2. Restart the server to apply:
|
|
326
|
-
|
|
327
|
-
On macOS:
|
|
328
|
-
\`\`\`bash
|
|
329
|
-
launchctl kickstart -k gui/$(id -u)/com.gamaze.hicortex
|
|
330
|
-
\`\`\`
|
|
331
|
-
|
|
332
|
-
On Linux:
|
|
333
|
-
\`\`\`bash
|
|
334
|
-
systemctl --user restart hicortex
|
|
335
|
-
\`\`\`
|
|
336
|
-
|
|
337
|
-
3. Verify the key is recognised:
|
|
338
|
-
\`\`\`bash
|
|
339
|
-
hicortex status
|
|
340
|
-
\`\`\`
|
|
341
|
-
|
|
342
|
-
4. Tell the user: "Commercial license registered. The license tier will appear in \`hicortex status\`."
|
|
343
|
-
|
|
344
|
-
## If no key provided
|
|
345
|
-
|
|
346
|
-
Tell them: "Hicortex is free for personal and noncommercial use. Commercial use requires a per-seat license — see https://hicortex.gamaze.com/. After purchase you will receive a key; pass it here and I'll register it."
|
|
347
|
-
`;
|
|
348
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(CC_COMMANDS_DIR, "hicortex-activate.md"), activateContent);
|
|
349
|
-
console.log(` ✓ Installed /learn and /hicortex-activate commands in ${CC_COMMANDS_DIR}`);
|
|
350
302
|
}
|
|
351
303
|
// ---------------------------------------------------------------------------
|
|
352
304
|
// Hermes setup
|
|
@@ -1211,7 +1163,6 @@ async function runInit(options = {}) {
|
|
|
1211
1163
|
actions.push("Register MCP server in CC settings");
|
|
1212
1164
|
if (d.hermesFound)
|
|
1213
1165
|
actions.push("Install Hermes plugin + configure");
|
|
1214
|
-
actions.push("Install /learn and /hicortex-activate commands");
|
|
1215
1166
|
actions.push("Install SessionStart hook (query-time lessons)");
|
|
1216
1167
|
if (actions.length === 0) {
|
|
1217
1168
|
console.log("Everything is already configured. Nothing to do.");
|
|
@@ -1294,8 +1245,8 @@ async function runInit(options = {}) {
|
|
|
1294
1245
|
}
|
|
1295
1246
|
// Ensure tool permissions are set (also needed for users upgrading from older versions)
|
|
1296
1247
|
allowHicortexTools();
|
|
1297
|
-
//
|
|
1298
|
-
|
|
1248
|
+
// Remove legacy pre-0.10 CC commands (/learn, /hicortex-activate) if present
|
|
1249
|
+
cleanupLegacyCcCommands();
|
|
1299
1250
|
// Setup Hermes if detected
|
|
1300
1251
|
if (d.hermesFound) {
|
|
1301
1252
|
// localhost bypass makes the token optional for co-located installs;
|
|
@@ -1325,13 +1276,15 @@ async function runInit(options = {}) {
|
|
|
1325
1276
|
// failures are swallowed inside sendLifecycleEvent.
|
|
1326
1277
|
await (0, telemetry_js_1.sendLifecycleEvent)("install", HICORTEX_HOME, readHomeConfig(HICORTEX_HOME), pkgVersion());
|
|
1327
1278
|
console.log("Next steps:");
|
|
1328
|
-
|
|
1279
|
+
// Counter-based so the list stays contiguous (1,2,3,4) whether or not Hermes
|
|
1280
|
+
// was detected — a conditional middle step used to leave a "1, 3, 4" gap.
|
|
1281
|
+
let step = 1;
|
|
1282
|
+
console.log(` ${step++}. Restart Claude Code to pick up the new MCP server and SessionStart hook`);
|
|
1329
1283
|
if (d.hermesFound) {
|
|
1330
|
-
console.log(
|
|
1284
|
+
console.log(` ${step++}. Activate the Hermes plugin: run \`hermes memory setup\`, select 'hicortex', then restart the gateway(s)`);
|
|
1331
1285
|
}
|
|
1332
|
-
console.log(
|
|
1333
|
-
console.log(
|
|
1334
|
-
console.log(` 5. Check server: curl ${serverUrl}/health`);
|
|
1286
|
+
console.log(` ${step++}. Ask your agent: 'What Hicortex tools do you have?'`);
|
|
1287
|
+
console.log(` ${step++}. Check server: curl ${serverUrl}/health`);
|
|
1335
1288
|
}
|
|
1336
1289
|
// ---------------------------------------------------------------------------
|
|
1337
1290
|
// Client Mode Init
|
|
@@ -1470,8 +1423,8 @@ async function runClientInit(serverUrl, agentName) {
|
|
|
1470
1423
|
registerCcMcp(serverUrl);
|
|
1471
1424
|
}
|
|
1472
1425
|
allowHicortexTools();
|
|
1473
|
-
// Step 5:
|
|
1474
|
-
|
|
1426
|
+
// Step 5: Remove legacy pre-0.10 CC commands if present
|
|
1427
|
+
cleanupLegacyCcCommands();
|
|
1475
1428
|
// Step 6: Install SessionStart hook for query-time lessons + the #192
|
|
1476
1429
|
// per-prompt pushed-recall hooks.
|
|
1477
1430
|
installSessionStartHook();
|
package/dist/llm.d.ts
CHANGED
|
@@ -191,8 +191,10 @@ export declare class RateLimitError extends Error {
|
|
|
191
191
|
}
|
|
192
192
|
export declare class LlmClient {
|
|
193
193
|
private config;
|
|
194
|
-
private rateLimitedUntil;
|
|
195
194
|
constructor(config: LlmConfig);
|
|
195
|
+
/** Endpoint identity for shared rate-limit state (provider + base URL). */
|
|
196
|
+
private get endpointKey();
|
|
197
|
+
private get rateLimitedUntil();
|
|
196
198
|
/** Check if we're currently rate limited */
|
|
197
199
|
get isRateLimited(): boolean;
|
|
198
200
|
private handleRateLimit;
|
package/dist/llm.js
CHANGED
|
@@ -466,12 +466,25 @@ class RateLimitError extends Error {
|
|
|
466
466
|
}
|
|
467
467
|
}
|
|
468
468
|
exports.RateLimitError = RateLimitError;
|
|
469
|
+
// Rate-limit backoff is shared across all LlmClient instances that target the
|
|
470
|
+
// same endpoint, keyed by provider@baseUrl. completeWithOverride() spins up a
|
|
471
|
+
// throwaway client per call for the distill/reflect/classify override tiers;
|
|
472
|
+
// with per-instance state each throwaway started un-rate-limited and re-hit a
|
|
473
|
+
// 429'd provider immediately, defeating the backoff on exactly the configs
|
|
474
|
+
// (e.g. z.ai via distillBaseUrl/reflectBaseUrl) that route through those tiers.
|
|
475
|
+
const rateLimitedUntilByEndpoint = new Map();
|
|
469
476
|
class LlmClient {
|
|
470
477
|
config;
|
|
471
|
-
rateLimitedUntil = 0;
|
|
472
478
|
constructor(config) {
|
|
473
479
|
this.config = config;
|
|
474
480
|
}
|
|
481
|
+
/** Endpoint identity for shared rate-limit state (provider + base URL). */
|
|
482
|
+
get endpointKey() {
|
|
483
|
+
return `${this.config.provider}@${this.config.baseUrl ?? ""}`;
|
|
484
|
+
}
|
|
485
|
+
get rateLimitedUntil() {
|
|
486
|
+
return rateLimitedUntilByEndpoint.get(this.endpointKey) ?? 0;
|
|
487
|
+
}
|
|
475
488
|
/** Check if we're currently rate limited */
|
|
476
489
|
get isRateLimited() {
|
|
477
490
|
return Date.now() < this.rateLimitedUntil;
|
|
@@ -482,9 +495,10 @@ class LlmClient {
|
|
|
482
495
|
const retryMs = retryAfter
|
|
483
496
|
? parseInt(retryAfter, 10) * 1000
|
|
484
497
|
: DEFAULT_RATE_LIMIT_RETRY_MS;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
498
|
+
const until = Date.now() + retryMs;
|
|
499
|
+
rateLimitedUntilByEndpoint.set(this.endpointKey, until);
|
|
500
|
+
console.log(`[hicortex] Rate limited by LLM provider (${this.endpointKey}). ` +
|
|
501
|
+
`Will retry after ${new Date(until).toISOString()}`);
|
|
488
502
|
throw new RateLimitError(retryMs);
|
|
489
503
|
}
|
|
490
504
|
/**
|
package/dist/mcp-server.js
CHANGED
|
@@ -128,27 +128,18 @@ function createMcpServer() {
|
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
130
|
// -- hicortex_get --
|
|
131
|
-
server.tool("hicortex_get", "Fetch ONE memory's full content by id — use this to lazy-load entries from the '## Memory recall (auto)' index or from search results whose snippet was not enough. Fetching a memory marks it as used (strengthens it), so fetch entries that could change your action — not every shown one. When the memory shapes your answer, cite it to the user (id + date + origin agent).", {
|
|
131
|
+
server.tool("hicortex_get", "Fetch ONE memory's full content by id — use this to lazy-load entries from the '## Memory recall (auto)' index or from search results whose snippet was not enough. Fetching a memory marks it as used (strengthens it), so fetch entries that could change your action — not every shown one. When the memory shapes your answer, cite it to the user (id + date + origin agent) — mark a fetched memory `FETCHED` and a one-line entry cited unread `SNIPPET`; don't pass SNIPPET off as established.", {
|
|
132
132
|
id: zod_1.z.string().describe("Memory id (as shown in recall index/search results)"),
|
|
133
133
|
}, async ({ id }) => {
|
|
134
134
|
if (!db)
|
|
135
135
|
return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
|
|
136
|
+
// Delegates to formatMemoryGetText → handleMemoryGet, so the citation
|
|
137
|
+
// (incl. the #204 FETCHED marker) is built in ONE place shared with the
|
|
138
|
+
// REST GET /memory path. CC reaches Hicortex through THIS MCP tool;
|
|
139
|
+
// before #207's fix it got a marker-less citation built inline here.
|
|
136
140
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const fullId = storage.resolveMemoryId(db, id);
|
|
140
|
-
const mem = fullId ? storage.getMemory(db, fullId) : null;
|
|
141
|
-
if (!mem)
|
|
142
|
-
return { content: [{ type: "text", text: `No memory with id ${id}` }], isError: true };
|
|
143
|
-
// Real use → full strengthen (access_count + hardening + prune shield).
|
|
144
|
-
storage.strengthenMemory(db, mem.id, new Date().toISOString());
|
|
145
|
-
// Provenance header (built-in citing norm, 0.14.1): id, type, project,
|
|
146
|
-
// ORIGIN AGENT (shared brain — the memory may come from another
|
|
147
|
-
// agent's session), and date, plus the explicit citation instruction.
|
|
148
|
-
const date = (mem.created_at ?? "").slice(0, 10);
|
|
149
|
-
const header = `[memory ${mem.id} | ${mem.memory_type ?? "episode"} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
|
|
150
|
-
`Cite as (memory ${String(mem.id).slice(0, 8)}, ${date}) where this shapes your answer; it may be stale — newer memories supersede older.`;
|
|
151
|
-
return { content: [{ type: "text", text: `${header}\n\n${mem.content ?? ""}` }] };
|
|
141
|
+
const r = (0, recall_index_js_1.formatMemoryGetText)(db, { id });
|
|
142
|
+
return { content: [{ type: "text", text: r.text }], isError: r.status !== 200 };
|
|
152
143
|
}
|
|
153
144
|
catch (err) {
|
|
154
145
|
return { content: [{ type: "text", text: `Get failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
@@ -513,14 +504,24 @@ async function startServer(options = {}) {
|
|
|
513
504
|
const app = (0, express_1.default)();
|
|
514
505
|
// Raise the body limit — whole-session denoised transcripts exceed the 100 kB default.
|
|
515
506
|
app.use(express_1.default.json({ limit: "25mb" }));
|
|
516
|
-
// CORS:
|
|
507
|
+
// CORS: reflect ONLY explicitly-allowlisted origins (config.corsAllowedOrigins),
|
|
508
|
+
// and never send Access-Control-Allow-Credentials. Reflecting any origin with
|
|
509
|
+
// credentials — combined with the localhost auth bypass and the default 0.0.0.0
|
|
510
|
+
// bind — let any web page the user visits read/mutate the memory store via
|
|
511
|
+
// fetch() to localhost with no token (the browser connects from 127.0.0.1, so
|
|
512
|
+
// the bypass grants access, and the reflected Allow-Origin let the page read the
|
|
513
|
+
// response). The bundled UIs (/viz, /context/ui) are same-origin and need no CORS
|
|
514
|
+
// headers at all; cross-origin access is opt-in per the hosted plan (#110 §2).
|
|
515
|
+
// Must run before auth so allowlisted preflight OPTIONS get their headers.
|
|
516
|
+
const corsAllowedOrigins = Array.isArray(savedConfig?.corsAllowedOrigins)
|
|
517
|
+
? savedConfig.corsAllowedOrigins.filter((o) => typeof o === "string")
|
|
518
|
+
: [];
|
|
517
519
|
app.use((req, res, next) => {
|
|
518
520
|
const origin = req.headers.origin;
|
|
519
|
-
if (origin) {
|
|
521
|
+
if (origin && corsAllowedOrigins.includes(origin)) {
|
|
520
522
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
521
523
|
res.setHeader("Access-Control-Allow-Methods", "GET, PUT, POST, OPTIONS");
|
|
522
524
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization");
|
|
523
|
-
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
524
525
|
res.setHeader("Vary", "Origin");
|
|
525
526
|
}
|
|
526
527
|
if (req.method === "OPTIONS") {
|
|
@@ -689,8 +690,11 @@ async function startServer(options = {}) {
|
|
|
689
690
|
return retrieval.retrieve(db, embedder_js_1.embed, query, {
|
|
690
691
|
limit,
|
|
691
692
|
noStrengthen: true,
|
|
693
|
+
// #203: project + mission_domains are SOFT affinity (zero-boost
|
|
694
|
+
// neutral), threaded into computeScore. privacy stays a hard filter.
|
|
692
695
|
project: filters?.project,
|
|
693
696
|
privacy: filters?.privacy,
|
|
697
|
+
missionDomains: filters?.mission_domains,
|
|
694
698
|
queryEmbedding: queryVec,
|
|
695
699
|
});
|
|
696
700
|
},
|
|
@@ -33,7 +33,7 @@ function renderMemoryInstructions() {
|
|
|
33
33
|
"Your long-term memory is Hicortex — shared across all agents and sessions.",
|
|
34
34
|
"- A `## Memory recall (auto)` index may arrive with prompts: it is a MENU, not content. Fetch a full memory with `hicortex_get(id)` when the entry could change how you handle the current task.",
|
|
35
35
|
"- Recall before assuming: `hicortex_search` for prior decisions/facts/preferences, `hicortex_recent` to catch up on a project.",
|
|
36
|
-
"- Cite any memory you rely on
|
|
36
|
+
"- Cite any memory you rely on by id + date, and mark it `FETCHED` (you read the full memory via `hicortex_get`) or `SNIPPET` (the one-line entry only). Don't present a SNIPPET citation as established. On conflicts, newer memories supersede older.",
|
|
37
37
|
"- Capture is automatic (nightly). Do not manually ingest routine content — `hicortex_ingest` is for explicitly requested learnings only.",
|
|
38
38
|
"- Never inspect, test, or modify memory/plugin/gateway infrastructure (configs, services, tokens). If a memory tool seems missing or broken, say so and stop.",
|
|
39
39
|
].join("\n");
|
|
@@ -23,6 +23,6 @@ interface HookPayload {
|
|
|
23
23
|
* there is nothing to send (no session id, or an unhandled event). Exported
|
|
24
24
|
* for tests.
|
|
25
25
|
*/
|
|
26
|
-
export declare function buildHookRequest(payload: HookPayload): Record<string, unknown> | null;
|
|
26
|
+
export declare function buildHookRequest(payload: HookPayload, cwd?: string): Record<string, unknown> | null;
|
|
27
27
|
export declare function runRecallHook(): Promise<void>;
|
|
28
28
|
export {};
|
package/dist/recall-hook-cli.js
CHANGED
|
@@ -17,6 +17,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
exports.buildHookRequest = buildHookRequest;
|
|
18
18
|
exports.runRecallHook = runRecallHook;
|
|
19
19
|
const lessons_context_js_1 = require("./lessons-context.js");
|
|
20
|
+
const node_path_1 = require("node:path");
|
|
20
21
|
const FETCH_TIMEOUT_MS = 1000;
|
|
21
22
|
/** Read all of stdin (CC pipes the hook payload JSON). */
|
|
22
23
|
async function readStdin() {
|
|
@@ -31,7 +32,7 @@ async function readStdin() {
|
|
|
31
32
|
* there is nothing to send (no session id, or an unhandled event). Exported
|
|
32
33
|
* for tests.
|
|
33
34
|
*/
|
|
34
|
-
function buildHookRequest(payload) {
|
|
35
|
+
function buildHookRequest(payload, cwd = process.cwd()) {
|
|
35
36
|
const sessionId = typeof payload.session_id === "string" && payload.session_id
|
|
36
37
|
? payload.session_id
|
|
37
38
|
: null;
|
|
@@ -43,7 +44,11 @@ function buildHookRequest(payload) {
|
|
|
43
44
|
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
44
45
|
if (!prompt)
|
|
45
46
|
return null;
|
|
46
|
-
|
|
47
|
+
// #203 scope: derive project from the session cwd so retrieval can apply a
|
|
48
|
+
// soft project-affinity boost. basename(cwd) matches capture's
|
|
49
|
+
// decodeProjectDirName for non-hyphenated dirs (the common case); a hyphen
|
|
50
|
+
// edge case is a pre-existing capture bug, filed separately.
|
|
51
|
+
return { session_id: sessionId, prompt, project: (0, node_path_1.basename)(cwd) };
|
|
47
52
|
}
|
|
48
53
|
async function runRecallHook() {
|
|
49
54
|
const cfg = (0, lessons_context_js_1.resolveConfig)();
|
package/dist/recall-index.d.ts
CHANGED
|
@@ -44,10 +44,18 @@ export declare function passesRelevanceGate(r: MemorySearchResult, minSimilarity
|
|
|
44
44
|
/** Recall filters a client may push per request (#193 review F1): a scoped
|
|
45
45
|
* plugin (Hermes privacy_filter / default_project) must be able to narrow
|
|
46
46
|
* recall exactly like the legacy /search prefetch did — dropping them
|
|
47
|
-
* silently would leak out-of-scope memory titles into the injected index.
|
|
47
|
+
* silently would leak out-of-scope memory titles into the injected index.
|
|
48
|
+
*
|
|
49
|
+
* #203: `project` and `mission_domains` are now SOFT affinity signals in
|
|
50
|
+
* retrieval (zero-boost neutral, never a filter / penalty); `privacy` stays a
|
|
51
|
+
* hard filter (security boundary). They ride the body → retrieveFn →
|
|
52
|
+
* retrieve() → computeScore path unchanged in shape. */
|
|
48
53
|
export interface RecallFilters {
|
|
49
54
|
project?: string;
|
|
50
55
|
privacy?: string[];
|
|
56
|
+
/** #203: Hermes mission domains (declared in plugin config). Soft domain
|
|
57
|
+
* affinity in computeScore via max overlapping memory_tags.weight. */
|
|
58
|
+
mission_domains?: string[];
|
|
51
59
|
}
|
|
52
60
|
export interface RecallIndexDeps {
|
|
53
61
|
db: Database.Database;
|
|
@@ -58,9 +66,15 @@ export interface RecallIndexDeps {
|
|
|
58
66
|
retrieveFn: (query: string, limit: number, filters: RecallFilters | undefined, sessionId: string) => Promise<MemorySearchResult[]>;
|
|
59
67
|
options?: RecallIndexOptions;
|
|
60
68
|
}
|
|
69
|
+
/** Normalize a request-supplied string-list param: array of strings or a CSV
|
|
70
|
+
* string → string[] | undefined. Anything else (or an empty result) means
|
|
71
|
+
* "absent" — never a partial guess. Shared by `parsePrivacyParam` and
|
|
72
|
+
* `mission_domains` (#203) so both accept `["A","B"]` and `"A, B"` alike. */
|
|
73
|
+
export declare function parseStringListParam(v: unknown): string[] | undefined;
|
|
61
74
|
/** Normalize a request-supplied privacy filter: array of strings or a CSV
|
|
62
75
|
* string → string[] | undefined. Anything else (or an empty result) means
|
|
63
|
-
* "no filter" — never a partial guess.
|
|
76
|
+
* "no filter" — never a partial guess. Delegates to parseStringListParam;
|
|
77
|
+
* kept as a named export for tests and handleMemoryGet callers. */
|
|
64
78
|
export declare function parsePrivacyParam(v: unknown): string[] | undefined;
|
|
65
79
|
/**
|
|
66
80
|
* Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
|
|
@@ -83,3 +97,20 @@ export declare function handleMemoryGet(db: Database.Database, query: {
|
|
|
83
97
|
id?: unknown;
|
|
84
98
|
privacy?: unknown;
|
|
85
99
|
}): RecallIndexResult;
|
|
100
|
+
/**
|
|
101
|
+
* MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
|
|
102
|
+
* text block the MCP tool returns (provenance header + the SHARED citation +
|
|
103
|
+
* content). Extracted from the MCP tool handler so its output — incl. the
|
|
104
|
+
* #204 FETCHED marker, which rides on handleMemoryGet's citation — is unit-
|
|
105
|
+
* testable. The citation string is built ONCE (handleMemoryGet); this only
|
|
106
|
+
* frames it, mirroring how /recall-index is shared across harnesses. The
|
|
107
|
+
* extraction closes the #207 gap (CC's MCP path had a marker-less citation
|
|
108
|
+
* built inline, while the REST path used handleMemoryGet — same contract, two
|
|
109
|
+
* implementations, one updated).
|
|
110
|
+
*/
|
|
111
|
+
export declare function formatMemoryGetText(db: Database.Database, query: {
|
|
112
|
+
id?: unknown;
|
|
113
|
+
}): {
|
|
114
|
+
status: number;
|
|
115
|
+
text: string;
|
|
116
|
+
};
|
package/dist/recall-index.js
CHANGED
|
@@ -56,9 +56,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
56
56
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
57
|
exports.memoryTitle = memoryTitle;
|
|
58
58
|
exports.passesRelevanceGate = passesRelevanceGate;
|
|
59
|
+
exports.parseStringListParam = parseStringListParam;
|
|
59
60
|
exports.parsePrivacyParam = parsePrivacyParam;
|
|
60
61
|
exports.handleRecallIndex = handleRecallIndex;
|
|
61
62
|
exports.handleMemoryGet = handleMemoryGet;
|
|
63
|
+
exports.formatMemoryGetText = formatMemoryGetText;
|
|
62
64
|
const storage = __importStar(require("./storage.js"));
|
|
63
65
|
const DEFAULT_MIN_SIMILARITY = 0.55;
|
|
64
66
|
const DEFAULT_MAX_ITEMS = 6;
|
|
@@ -87,7 +89,15 @@ function formatDate(iso) {
|
|
|
87
89
|
return `${dd}.${mm}.${d.getFullYear()}`;
|
|
88
90
|
}
|
|
89
91
|
function formatIndexLine(r) {
|
|
90
|
-
|
|
92
|
+
// Provenance (#202): date, scope (domain else project), ORIGIN AGENT, type.
|
|
93
|
+
// The origin agent lets a reader calibrate trust — "from my session" vs
|
|
94
|
+
// another agent/project — before fetching or acting on an entry.
|
|
95
|
+
const meta = [
|
|
96
|
+
formatDate(r.created_at),
|
|
97
|
+
r.domain ?? r.project ?? undefined,
|
|
98
|
+
r.source_agent ?? undefined,
|
|
99
|
+
r.memory_type,
|
|
100
|
+
]
|
|
91
101
|
.filter(Boolean)
|
|
92
102
|
.join(", ");
|
|
93
103
|
return `- [${r.id}] ${memoryTitle(r.content)}${meta ? ` (${meta})` : ""}`;
|
|
@@ -98,10 +108,11 @@ function passesRelevanceGate(r, minSimilarity) {
|
|
|
98
108
|
return true;
|
|
99
109
|
return typeof r.similarity === "number" && r.similarity >= minSimilarity;
|
|
100
110
|
}
|
|
101
|
-
/** Normalize a request-supplied
|
|
111
|
+
/** Normalize a request-supplied string-list param: array of strings or a CSV
|
|
102
112
|
* string → string[] | undefined. Anything else (or an empty result) means
|
|
103
|
-
* "
|
|
104
|
-
|
|
113
|
+
* "absent" — never a partial guess. Shared by `parsePrivacyParam` and
|
|
114
|
+
* `mission_domains` (#203) so both accept `["A","B"]` and `"A, B"` alike. */
|
|
115
|
+
function parseStringListParam(v) {
|
|
105
116
|
const items = Array.isArray(v)
|
|
106
117
|
? v.filter((x) => typeof x === "string")
|
|
107
118
|
: typeof v === "string"
|
|
@@ -110,6 +121,13 @@ function parsePrivacyParam(v) {
|
|
|
110
121
|
const cleaned = items.map((s) => s.trim()).filter(Boolean);
|
|
111
122
|
return cleaned.length > 0 ? cleaned : undefined;
|
|
112
123
|
}
|
|
124
|
+
/** Normalize a request-supplied privacy filter: array of strings or a CSV
|
|
125
|
+
* string → string[] | undefined. Anything else (or an empty result) means
|
|
126
|
+
* "no filter" — never a partial guess. Delegates to parseStringListParam;
|
|
127
|
+
* kept as a named export for tests and handleMemoryGet callers. */
|
|
128
|
+
function parsePrivacyParam(v) {
|
|
129
|
+
return parseStringListParam(v);
|
|
130
|
+
}
|
|
113
131
|
/**
|
|
114
132
|
* Handle a /recall-index request body. Thin Express adapter in mcp-server.ts;
|
|
115
133
|
* all behavior lives here so tests exercise it directly.
|
|
@@ -134,11 +152,15 @@ async function handleRecallIndex(deps, body) {
|
|
|
134
152
|
const maxItems = clampInt(deps.options?.maxItems, DEFAULT_MAX_ITEMS, 1, 20);
|
|
135
153
|
const minSimilarity = clampNumber(deps.options?.minSimilarity, DEFAULT_MIN_SIMILARITY, 0, 1);
|
|
136
154
|
const turn = deps.registry.beginTurn(sessionId);
|
|
137
|
-
// Optional client-side scoping (F1): project +
|
|
138
|
-
//
|
|
155
|
+
// Optional client-side scoping (F1 + #203): project + mission_domains (soft
|
|
156
|
+
// affinity) and privacy (hard filter) ride the body and are pushed into
|
|
157
|
+
// retrieval. project is cwd-derived (CC/OC) or gateway-supplied; mission_domains
|
|
158
|
+
// is Hermes-declared (plugin config). Neither excludes anything — both are
|
|
159
|
+
// zero-boost-neutral score terms in computeScore.
|
|
139
160
|
const filters = {
|
|
140
161
|
project: typeof req.project === "string" && req.project ? req.project : undefined,
|
|
141
162
|
privacy: parsePrivacyParam(req.privacy),
|
|
163
|
+
mission_domains: parseStringListParam(req.mission_domains),
|
|
142
164
|
};
|
|
143
165
|
let results;
|
|
144
166
|
try {
|
|
@@ -164,13 +186,15 @@ async function handleRecallIndex(deps, body) {
|
|
|
164
186
|
const lines = picked.map((r) => formatIndexLine(r));
|
|
165
187
|
const block = [
|
|
166
188
|
"## Memory recall (auto)",
|
|
167
|
-
// Provenance is BUILT IN
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
189
|
+
// Provenance is BUILT IN (owner decision 27.07, option D; extended #202/#204):
|
|
190
|
+
// - #202: origin agent in each one-liner (formatIndexLine) — trust calibration.
|
|
191
|
+
// - #204: confidence levels — FETCHED (read in full) vs SNIPPET (one-line entry only),
|
|
192
|
+
// so "the agent cited a memory" can no longer pass as "the agent read it".
|
|
193
|
+
// This header carries the SELECTION-time rules: supersession (the one moment
|
|
194
|
+
// competing dates are visible side by side) and cite-with-confidence. The
|
|
195
|
+
// full citation format rides on the hicortex_get response / GET /memory
|
|
196
|
+
// `citation` field (use-time, marked FETCHED).
|
|
197
|
+
"Possibly relevant memories — dates matter, newer supersedes older. Fetch with `hicortex_get(id)` when an entry could change your action. Cite what you rely on by id + date, and mark it `FETCHED` if you read the full memory or `SNIPPET` if you're citing the one-line entry unread — don't pass a SNIPPET citation off as established fact.",
|
|
174
198
|
...lines,
|
|
175
199
|
].join("\n");
|
|
176
200
|
return { status: 200, body: { block, shown: ids, turn } };
|
|
@@ -212,10 +236,33 @@ function handleMemoryGet(db, query) {
|
|
|
212
236
|
status: 200,
|
|
213
237
|
body: {
|
|
214
238
|
memory: mem,
|
|
215
|
-
citation: `(memory ${String(mem.id).slice(0, 8)}, ${date}, from ${mem.source_agent ?? "unknown"})`,
|
|
239
|
+
citation: `(memory ${String(mem.id).slice(0, 8)}, ${date}, from ${mem.source_agent ?? "unknown"}, FETCHED)`,
|
|
216
240
|
},
|
|
217
241
|
};
|
|
218
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* MCP `hicortex_get` presentation: handleMemoryGet's result framed as the
|
|
245
|
+
* text block the MCP tool returns (provenance header + the SHARED citation +
|
|
246
|
+
* content). Extracted from the MCP tool handler so its output — incl. the
|
|
247
|
+
* #204 FETCHED marker, which rides on handleMemoryGet's citation — is unit-
|
|
248
|
+
* testable. The citation string is built ONCE (handleMemoryGet); this only
|
|
249
|
+
* frames it, mirroring how /recall-index is shared across harnesses. The
|
|
250
|
+
* extraction closes the #207 gap (CC's MCP path had a marker-less citation
|
|
251
|
+
* built inline, while the REST path used handleMemoryGet — same contract, two
|
|
252
|
+
* implementations, one updated).
|
|
253
|
+
*/
|
|
254
|
+
function formatMemoryGetText(db, query) {
|
|
255
|
+
const r = handleMemoryGet(db, query);
|
|
256
|
+
if (r.status !== 200) {
|
|
257
|
+
return { status: r.status, text: String(r.body.error ?? `No memory with id ${query.id ?? ""}`) };
|
|
258
|
+
}
|
|
259
|
+
const mem = r.body.memory;
|
|
260
|
+
const citation = r.body.citation; // carries FETCHED (#204)
|
|
261
|
+
const date = (mem.created_at ?? "").slice(0, 10);
|
|
262
|
+
const header = `[memory ${mem.id} | ${mem.memory_type ?? "episode"} | ${mem.project ?? "-"} | from ${mem.source_agent ?? "unknown"} | ${date}]\n` +
|
|
263
|
+
`Cite as ${citation} where this shapes your answer; it may be stale — newer memories supersede older.`;
|
|
264
|
+
return { status: 200, text: `${header}\n\n${mem.content ?? ""}` };
|
|
265
|
+
}
|
|
219
266
|
function clampInt(v, dflt, min, max) {
|
|
220
267
|
const n = Number(v);
|
|
221
268
|
if (!Number.isFinite(n))
|