@gamaze/hicortex 0.13.2 → 0.14.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 +15 -1
- package/dist/cli.js +12 -0
- package/dist/consolidate.d.ts +1 -1
- package/dist/consolidate.js +1 -1
- package/dist/context-cli.js +1 -1
- package/dist/db.js +14 -0
- package/dist/domain-classify.d.ts +3 -3
- package/dist/domain-classify.js +3 -3
- package/dist/hermes-transcript-reader.d.ts +1 -1
- package/dist/hermes-transcript-reader.js +1 -1
- package/dist/init.d.ts +7 -0
- package/dist/init.js +44 -16
- package/dist/lessons-context.d.ts +15 -1
- package/dist/lessons-context.js +5 -2
- package/dist/mcp-server.js +91 -6
- package/dist/nightly.js +5 -0
- package/dist/pi-transcript-reader.d.ts +3 -3
- package/dist/pi-transcript-reader.js +5 -5
- package/dist/recall-hook-cli.d.ts +28 -0
- package/dist/recall-hook-cli.js +77 -0
- package/dist/recall-index.d.ts +54 -0
- package/dist/recall-index.js +163 -0
- package/dist/recall-registry.d.ts +48 -0
- package/dist/recall-registry.js +99 -0
- package/dist/retrieval.d.ts +39 -1
- package/dist/retrieval.js +122 -22
- package/dist/storage.d.ts +8 -1
- package/dist/storage.js +27 -1
- package/dist/transcript-reader.d.ts +1 -1
- package/dist/transcript-reader.js +2 -2
- package/dist/types.d.ts +6 -0
- package/domains.example.json +53 -13
- package/hermes-plugin/hicortex/provider.py +4 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,8 +66,11 @@ The plugin connects to `http://127.0.0.1:8787` by default. For a remote server,
|
|
|
66
66
|
| When | What | How |
|
|
67
67
|
|------|------|-----|
|
|
68
68
|
| Agent start | Standing context (`## Context`) + recent lessons fetched fresh and injected | CC SessionStart hook (calls `hicortex lessons-context`) / Hermes plugin `system_prompt_block` (+ `prefetch` recall) / OC `before_agent_start` hook |
|
|
69
|
+
| Every prompt (0.14) | A compact **recall index** of relevant memories is injected — one line per memory; the agent lazy-loads full content with `hicortex_get` only when needed | CC UserPromptSubmit hook (calls `hicortex recall-hook` → server `POST /recall-index`). Turn-based dedup per session; resets on new session/compaction. Fail-soft, 1 s timeout |
|
|
69
70
|
| Nightly | Denoise sessions → POST /distill → server distills + embeds + stores → consolidate (score, reflect, link, decay) | Automatic pipeline — no manual steps |
|
|
70
71
|
|
|
72
|
+
**Exposure vs use (0.14):** appearing in the recall index only marks a memory as *shown* (it stops decaying while topically active); fetching it with `hicortex_get` marks it as *used* (durable strengthening). Memory importance is driven by what agents actually use, not by what was pushed at them.
|
|
73
|
+
|
|
71
74
|
## Memory Domains & Tags
|
|
72
75
|
|
|
73
76
|
Domains are your top-level memory spheres — the handful of areas your life or work actually splits into. Every memory gets **multiple weighted tags** from your domain list plus one **primary** domain, so a memory that spans areas (a work project that touches your finances) lives in both instead of being forced into one bin. Domains drive the knowledge index, graph coloring, and lesson selection.
|
|
@@ -107,9 +110,10 @@ The run is resumable — interrupt it any time and it continues where it stopped
|
|
|
107
110
|
|
|
108
111
|
## Agent Tools (MCP)
|
|
109
112
|
|
|
110
|
-
|
|
113
|
+
9 tools available via MCP:
|
|
111
114
|
|
|
112
115
|
- **hicortex_search** — Semantic search across all stored memories
|
|
116
|
+
- **hicortex_get** — Fetch one memory's full content by id (0.14) — the lazy-load counterpart of the recall index; fetching marks the memory as used
|
|
113
117
|
- **hicortex_recent** — Get recent decisions and project state (queryless recall; renamed in 0.12)
|
|
114
118
|
- **hicortex_ingest** — Store a memory directly
|
|
115
119
|
- **hicortex_lessons** — Get actionable lessons from reflection
|
|
@@ -203,6 +207,14 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
203
207
|
| `contextAgents` | Per-agent context modes (0.13): `{ "<id>": "override" \| "global" \| "off" }`. Absent + no `agents/<id>/` dir → every agent gets the global set. Boot-time (restart to apply) — see [Per-agent context](#per-agent-context-013) |
|
|
204
208
|
| `agentName` | This install's per-agent context id sent as `?agent=`. **Unset by default** (CC shares the global context — no `?agent=` sent). Explicit opt-in via `init --agent-name <name>`; `init --agent-name ""` clears it. An empty/whitespace value equals unset |
|
|
205
209
|
| `nightlyHour` | Local hour (0–23) for the nightly job installed by `init` (defaults: client 2, server 3). Applied on fresh installs; existing schedules are never overwritten |
|
|
210
|
+
| `decayHalfLifeDays` | Memory decay half-life in days at reference importance (default: 365). Larger = slower forgetting; importance, access, and links slow it further |
|
|
211
|
+
| `searchLimit` / `recentLimit` | Default result counts for search (8) and recent (12) |
|
|
212
|
+
| `recentWindowDays` | Candidate window for recent recall (default: 180) |
|
|
213
|
+
| `coldExposureSlots` | Top-k slots reservable for never-accessed memories so the long tail gets exposure (default: 2) |
|
|
214
|
+
| `recallMaxItems` | Max lines in the pushed recall index (default: 6) |
|
|
215
|
+
| `recallMinSimilarity` | Relevance floor for index entries (default: 0.55; text-search matches always pass) |
|
|
216
|
+
| `recallReshowTurns` | Turns before an already-shown memory may reappear in the same session (default: 30) |
|
|
217
|
+
| `recallMinPromptChars` | Prompts shorter than this skip the recall index (default: 20) |
|
|
206
218
|
| `telemetry` | Anonymous usage telemetry, `false` to opt out |
|
|
207
219
|
|
|
208
220
|
Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze.com/docs/configuration.html)
|
|
@@ -214,6 +226,8 @@ Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze
|
|
|
214
226
|
| `/health` | GET | No | Server status, memory count, version |
|
|
215
227
|
| `/distill` | POST | Yes | Canonical capture endpoint (0.9.0+). Accepts denoised session text (`text` string or `messages` array), distills server-side, stores. Used by both server-mode and client-mode nightly jobs. |
|
|
216
228
|
| `/search` | GET | Yes | Semantic memory search |
|
|
229
|
+
| `/recall-index` | POST | Yes | Pushed recall index (0.14): `{session_id, prompt}` → compact one-line-per-memory block (or `null`); `{session_id, reset: true}` clears the session's dedup state. Appearing in the index marks memories *shown*, never *used* |
|
|
230
|
+
| `/memory` | GET | Yes | Fetch one memory by `?id=` (0.14). Marks it as used (strengthens) — the lazy-load counterpart of `/recall-index` |
|
|
217
231
|
| `/recent` | GET | Yes | Recent memories, queryless recall (renamed from `/context` in 0.12) |
|
|
218
232
|
| `/context` | GET / PUT | Yes | Standing [context layer](#context-layer): read all sections / partial-upsert named sections. `?agent=<id>` selects a [per-agent scope](#per-agent-context-013) (server resolves override/global/off + merge); invalid id → 400. Recall-style query params on GET → 400 (use `/recent`) |
|
|
219
233
|
| `/context/ui` | GET | No* | Web editor for the context layer (shell served without auth, like `/viz`; data via `/context`) |
|
package/dist/cli.js
CHANGED
|
@@ -165,6 +165,17 @@ switch (command) {
|
|
|
165
165
|
});
|
|
166
166
|
});
|
|
167
167
|
break;
|
|
168
|
+
case "recall-hook":
|
|
169
|
+
// CC UserPromptSubmit + SessionStart hook: pushed recall index (#192).
|
|
170
|
+
// Reads the CC hook payload from stdin, POSTs to the server, prints the
|
|
171
|
+
// index block (UserPromptSubmit) or resets session dedup (SessionStart).
|
|
172
|
+
// Fail-soft — any error = silent exit 0, never blocks a CC session.
|
|
173
|
+
import("./recall-hook-cli.js").then(({ runRecallHook }) => {
|
|
174
|
+
runRecallHook()
|
|
175
|
+
.then(() => process.exit(0))
|
|
176
|
+
.catch(() => process.exit(0));
|
|
177
|
+
}).catch(() => process.exit(0));
|
|
178
|
+
break;
|
|
168
179
|
case "lessons-context":
|
|
169
180
|
// CC SessionStart hook: fetch lessons from the configured server and print
|
|
170
181
|
// a Markdown block to stdout. Fail-soft — any error = silent exit 0 so a
|
|
@@ -196,6 +207,7 @@ Commands:
|
|
|
196
207
|
relink Resumable link-discovery pass over the ENTIRE corpus (server mode)
|
|
197
208
|
classify-domains Backfill content-based domain tags over the corpus (server mode, needs config.domains)
|
|
198
209
|
lessons-context Fetch lessons and print Markdown to stdout (CC SessionStart hook)
|
|
210
|
+
recall-hook Pushed recall index for the current prompt (CC UserPromptSubmit/SessionStart hook)
|
|
199
211
|
context Standing context layer (show|edit) against the configured server
|
|
200
212
|
status Show current configuration and stats
|
|
201
213
|
uninstall Remove CC integration (preserves DB)
|
package/dist/consolidate.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { type DomainDef } from "./domain-classify.js";
|
|
|
12
12
|
* Minimum COSINE similarity for a link candidate.
|
|
13
13
|
*
|
|
14
14
|
* Calibration (2026-07): measured top-10 neighbor cosine histogram on the
|
|
15
|
-
*
|
|
15
|
+
* ~3000-memory production corpus. Typical top-1 neighbor cosine:
|
|
16
16
|
* median 0.823, p10 0.743, p90 0.902. Threshold 0.75 combined with the
|
|
17
17
|
* top-3 cap yields ≈ 2.2 candidate links/memory. The previous value (0.55)
|
|
18
18
|
* lived on an accidental 1−L2 scale where it required cosine > 0.90 — a
|
package/dist/consolidate.js
CHANGED
|
@@ -65,7 +65,7 @@ const CONSOLIDATE_PRUNE_MIN_AGE_DAYS = 90;
|
|
|
65
65
|
* Minimum COSINE similarity for a link candidate.
|
|
66
66
|
*
|
|
67
67
|
* Calibration (2026-07): measured top-10 neighbor cosine histogram on the
|
|
68
|
-
*
|
|
68
|
+
* ~3000-memory production corpus. Typical top-1 neighbor cosine:
|
|
69
69
|
* median 0.823, p10 0.743, p90 0.902. Threshold 0.75 combined with the
|
|
70
70
|
* top-3 cap yields ≈ 2.2 candidate links/memory. The previous value (0.55)
|
|
71
71
|
* lived on an accidental 1−L2 scale where it required cosine > 0.90 — a
|
package/dist/context-cli.js
CHANGED
|
@@ -261,7 +261,7 @@ function extractAgentFlag(args) {
|
|
|
261
261
|
// A missing value (end of args) or the next token being another flag is a
|
|
262
262
|
// typo — never let it silently fall through to the global scope.
|
|
263
263
|
if (val === undefined || val.startsWith("-")) {
|
|
264
|
-
throw new ContextCliError("--agent requires a value, e.g. --agent
|
|
264
|
+
throw new ContextCliError("--agent requires a value, e.g. --agent alice");
|
|
265
265
|
}
|
|
266
266
|
agent = val;
|
|
267
267
|
i++;
|
package/dist/db.js
CHANGED
|
@@ -321,6 +321,20 @@ const MIGRATIONS = [
|
|
|
321
321
|
`);
|
|
322
322
|
},
|
|
323
323
|
},
|
|
324
|
+
{
|
|
325
|
+
version: 8,
|
|
326
|
+
name: "add_shown_count",
|
|
327
|
+
up: (db) => {
|
|
328
|
+
// #192 recall alignment: exposure tracking separate from use. shown_count
|
|
329
|
+
// counts appearances in the pushed recall index (/recall-index), which
|
|
330
|
+
// refreshes last_accessed (mild strengthen: decay clock resets) but does
|
|
331
|
+
// NOT touch access_count — hardening, the prune shield, and the adoption
|
|
332
|
+
// metric (uses per showing) stay driven by real use only.
|
|
333
|
+
if (!hasColumn(db, "memories", "shown_count")) {
|
|
334
|
+
db.exec("ALTER TABLE memories ADD COLUMN shown_count INTEGER DEFAULT 0");
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
},
|
|
324
338
|
];
|
|
325
339
|
/**
|
|
326
340
|
* Run all pending migrations against the database.
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* ---------------
|
|
6
6
|
* The nightly's legacy `stageDomainCuration` groups PROJECTS into domains and
|
|
7
7
|
* assigns every memory its project's domain. For an owner whose "projects" are
|
|
8
|
-
* often AGENT names (
|
|
8
|
+
* often AGENT names (alice, bob, ...), one agent produces memories spanning
|
|
9
9
|
* many life areas, so life-memories get smeared under the agent. This module
|
|
10
10
|
* classifies a single memory into life-spheres by its CONTENT, drawn from a
|
|
11
11
|
* user-curated vocabulary in ~/.hicortex/config.json (`domains`).
|
|
12
12
|
*
|
|
13
13
|
* GRADED SCHEMA TAGS (spec 2026-07-07, supersedes the LLM-picked primary from
|
|
14
|
-
* PR #152/#153): a memory genuinely spans spheres — "set up
|
|
14
|
+
* PR #152/#153): a memory genuinely spans spheres — "set up the server for the
|
|
15
15
|
* agent fleet" is both Hardware AND Ventures. The classifier now returns ONLY
|
|
16
16
|
* the discrete part:
|
|
17
17
|
* - `tags`: 0..N vocabulary names that genuinely apply, MOST-RELEVANT FIRST
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*
|
|
32
32
|
* The `project` name is passed to the classifier as a HINT (content wins;
|
|
33
33
|
* project only breaks ties). This rescues terse technical memories from
|
|
34
|
-
* projects
|
|
34
|
+
* projects whose content alone reads as ambiguous.
|
|
35
35
|
*
|
|
36
36
|
* The classifier makes ONE constrained LLM call per memory (via the classify
|
|
37
37
|
* tier — classifyModel/classifyBaseUrl when configured, else the reflect
|
package/dist/domain-classify.js
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* ---------------
|
|
7
7
|
* The nightly's legacy `stageDomainCuration` groups PROJECTS into domains and
|
|
8
8
|
* assigns every memory its project's domain. For an owner whose "projects" are
|
|
9
|
-
* often AGENT names (
|
|
9
|
+
* often AGENT names (alice, bob, ...), one agent produces memories spanning
|
|
10
10
|
* many life areas, so life-memories get smeared under the agent. This module
|
|
11
11
|
* classifies a single memory into life-spheres by its CONTENT, drawn from a
|
|
12
12
|
* user-curated vocabulary in ~/.hicortex/config.json (`domains`).
|
|
13
13
|
*
|
|
14
14
|
* GRADED SCHEMA TAGS (spec 2026-07-07, supersedes the LLM-picked primary from
|
|
15
|
-
* PR #152/#153): a memory genuinely spans spheres — "set up
|
|
15
|
+
* PR #152/#153): a memory genuinely spans spheres — "set up the server for the
|
|
16
16
|
* agent fleet" is both Hardware AND Ventures. The classifier now returns ONLY
|
|
17
17
|
* the discrete part:
|
|
18
18
|
* - `tags`: 0..N vocabulary names that genuinely apply, MOST-RELEVANT FIRST
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
*
|
|
33
33
|
* The `project` name is passed to the classifier as a HINT (content wins;
|
|
34
34
|
* project only breaks ties). This rescues terse technical memories from
|
|
35
|
-
* projects
|
|
35
|
+
* projects whose content alone reads as ambiguous.
|
|
36
36
|
*
|
|
37
37
|
* The classifier makes ONE constrained LLM call per memory (via the classify
|
|
38
38
|
* tier — classifyModel/classifyBaseUrl when configured, else the reflect
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Hermes transcript reader — the nightly capture path for Nous Research Hermes.
|
|
3
3
|
*
|
|
4
4
|
* Hermes stores conversation in a SQLite state DB, one per profile:
|
|
5
|
-
* ~/.hermes/profiles/<profile>/state.db (per-profile agents:
|
|
5
|
+
* ~/.hermes/profiles/<profile>/state.db (per-profile agents: alice, bob, carol)
|
|
6
6
|
* ~/.hermes/state.db (global, non-profile setups)
|
|
7
7
|
*
|
|
8
8
|
* Schema (relevant columns):
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Hermes transcript reader — the nightly capture path for Nous Research Hermes.
|
|
4
4
|
*
|
|
5
5
|
* Hermes stores conversation in a SQLite state DB, one per profile:
|
|
6
|
-
* ~/.hermes/profiles/<profile>/state.db (per-profile agents:
|
|
6
|
+
* ~/.hermes/profiles/<profile>/state.db (per-profile agents: alice, bob, carol)
|
|
7
7
|
* ~/.hermes/state.db (global, non-profile setups)
|
|
8
8
|
*
|
|
9
9
|
* Schema (relevant columns):
|
package/dist/init.d.ts
CHANGED
|
@@ -119,6 +119,13 @@ export declare function isEphemeralNpxPath(binPath: string): boolean;
|
|
|
119
119
|
* @param settingsPath Override for the settings.json path (used in tests; defaults to CC_SETTINGS).
|
|
120
120
|
*/
|
|
121
121
|
export declare function installSessionStartHook(settingsPath?: string): void;
|
|
122
|
+
/**
|
|
123
|
+
* Install (or verify) the #192 pushed-recall hooks: `hicortex recall-hook`
|
|
124
|
+
* under UserPromptSubmit (per-prompt recall index) AND under SessionStart
|
|
125
|
+
* (per-session dedup reset — the CLI dispatches on the payload's
|
|
126
|
+
* hook_event_name, so one command serves both events).
|
|
127
|
+
*/
|
|
128
|
+
export declare function installRecallHooks(settingsPath?: string): void;
|
|
122
129
|
export declare function runInit(options?: {
|
|
123
130
|
serverUrl?: string;
|
|
124
131
|
agentName?: string;
|
package/dist/init.js
CHANGED
|
@@ -27,6 +27,7 @@ exports.decideAgentName = decideAgentName;
|
|
|
27
27
|
exports.scaffoldDefaultDomains = scaffoldDefaultDomains;
|
|
28
28
|
exports.isEphemeralNpxPath = isEphemeralNpxPath;
|
|
29
29
|
exports.installSessionStartHook = installSessionStartHook;
|
|
30
|
+
exports.installRecallHooks = installRecallHooks;
|
|
30
31
|
exports.runInit = runInit;
|
|
31
32
|
exports.resolveNightlyHour = resolveNightlyHour;
|
|
32
33
|
const paths_js_1 = require("./paths.js");
|
|
@@ -960,10 +961,30 @@ function resolveBinaryArgs() {
|
|
|
960
961
|
* @param settingsPath Override for the settings.json path (used in tests; defaults to CC_SETTINGS).
|
|
961
962
|
*/
|
|
962
963
|
function installSessionStartHook(settingsPath) {
|
|
964
|
+
installCcHook("SessionStart", "lessons-context", 10, settingsPath);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Install (or verify) the #192 pushed-recall hooks: `hicortex recall-hook`
|
|
968
|
+
* under UserPromptSubmit (per-prompt recall index) AND under SessionStart
|
|
969
|
+
* (per-session dedup reset — the CLI dispatches on the payload's
|
|
970
|
+
* hook_event_name, so one command serves both events).
|
|
971
|
+
*/
|
|
972
|
+
function installRecallHooks(settingsPath) {
|
|
973
|
+
installCcHook("UserPromptSubmit", "recall-hook", 3, settingsPath);
|
|
974
|
+
installCcHook("SessionStart", "recall-hook", 3, settingsPath);
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Shared CC-hook installer: add `hicortex <subcommand>` under the given hook
|
|
978
|
+
* event in ~/.claude/settings.json. Idempotent per (event, subcommand): skips
|
|
979
|
+
* if any existing entry for that event already runs the subcommand. `timeout`
|
|
980
|
+
* is CC's hook-process kill timeout in SECONDS (the network timeout inside the
|
|
981
|
+
* command is separate and shorter).
|
|
982
|
+
*/
|
|
983
|
+
function installCcHook(eventName, subcommand, timeout, settingsPath) {
|
|
963
984
|
const targetPath = settingsPath ?? CC_SETTINGS;
|
|
964
985
|
const binaryArgs = resolveBinaryArgs();
|
|
965
|
-
//
|
|
966
|
-
const command = [...binaryArgs,
|
|
986
|
+
// e.g. "/path/to/hicortex lessons-context" or "npx -y @gamaze/hicortex recall-hook"
|
|
987
|
+
const command = [...binaryArgs, subcommand].join(" ");
|
|
967
988
|
let settings = {};
|
|
968
989
|
if ((0, node_fs_1.existsSync)(targetPath)) {
|
|
969
990
|
try {
|
|
@@ -971,23 +992,26 @@ function installSessionStartHook(settingsPath) {
|
|
|
971
992
|
}
|
|
972
993
|
catch {
|
|
973
994
|
// File exists but is malformed — do NOT overwrite (would destroy the user's entire CC config).
|
|
974
|
-
console.log(` ⚠ ${targetPath} exists but is not valid JSON — skipping
|
|
995
|
+
console.log(` ⚠ ${targetPath} exists but is not valid JSON — skipping ${eventName} hook.`);
|
|
975
996
|
console.log(` Fix the file, then re-run init, or add the hook manually:`);
|
|
976
997
|
console.log(` command: "${command}"`);
|
|
977
998
|
return;
|
|
978
999
|
}
|
|
979
1000
|
}
|
|
980
|
-
// Ensure hooks object and
|
|
1001
|
+
// Ensure hooks object and the event array exist
|
|
981
1002
|
if (!settings.hooks || typeof settings.hooks !== "object") {
|
|
982
1003
|
settings.hooks = {};
|
|
983
1004
|
}
|
|
984
1005
|
const hooks = settings.hooks;
|
|
985
|
-
if (!Array.isArray(hooks
|
|
986
|
-
hooks
|
|
987
|
-
}
|
|
988
|
-
const
|
|
989
|
-
// Idempotent: skip if any existing entry's command
|
|
990
|
-
|
|
1006
|
+
if (!Array.isArray(hooks[eventName])) {
|
|
1007
|
+
hooks[eventName] = [];
|
|
1008
|
+
}
|
|
1009
|
+
const entries = hooks[eventName];
|
|
1010
|
+
// Idempotent: skip if any existing entry's command runs this subcommand.
|
|
1011
|
+
// Word-boundary guard so "recall-hook" never matches a hypothetical
|
|
1012
|
+
// "recall-hook-foo" command.
|
|
1013
|
+
const subcommandRe = new RegExp(`(^|\\s)${subcommand}(\\s|$)`);
|
|
1014
|
+
const alreadyInstalled = entries.some((entry) => {
|
|
991
1015
|
if (typeof entry !== "object" || entry === null)
|
|
992
1016
|
return false;
|
|
993
1017
|
const e = entry;
|
|
@@ -997,21 +1021,21 @@ function installSessionStartHook(settingsPath) {
|
|
|
997
1021
|
if (typeof h !== "object" || h === null)
|
|
998
1022
|
return false;
|
|
999
1023
|
const hook = h;
|
|
1000
|
-
return typeof hook.command === "string" && hook.command
|
|
1024
|
+
return typeof hook.command === "string" && subcommandRe.test(hook.command);
|
|
1001
1025
|
});
|
|
1002
1026
|
}
|
|
1003
1027
|
return false;
|
|
1004
1028
|
});
|
|
1005
1029
|
if (alreadyInstalled) {
|
|
1006
|
-
console.log(` ✓
|
|
1030
|
+
console.log(` ✓ ${eventName} hook (${subcommand}) already installed`);
|
|
1007
1031
|
return;
|
|
1008
1032
|
}
|
|
1009
|
-
|
|
1010
|
-
hooks: [{ type: "command", command, timeout
|
|
1033
|
+
entries.push({
|
|
1034
|
+
hooks: [{ type: "command", command, timeout }],
|
|
1011
1035
|
});
|
|
1012
1036
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(targetPath), { recursive: true });
|
|
1013
1037
|
(0, node_fs_1.writeFileSync)(targetPath, JSON.stringify(settings, null, 2));
|
|
1014
|
-
console.log(` ✓ Installed
|
|
1038
|
+
console.log(` ✓ Installed ${eventName} hook: ${command}`);
|
|
1015
1039
|
}
|
|
1016
1040
|
function installLaunchd(binaryArgs) {
|
|
1017
1041
|
const plistDir = (0, node_path_1.join)((0, node_os_1.homedir)(), "Library", "LaunchAgents");
|
|
@@ -1263,6 +1287,8 @@ async function runInit(options = {}) {
|
|
|
1263
1287
|
// Install CC SessionStart hook for query-time lesson injection.
|
|
1264
1288
|
// Lessons are now fetched live at session start — no static CLAUDE.md block needed.
|
|
1265
1289
|
installSessionStartHook();
|
|
1290
|
+
// #192: per-prompt pushed recall index + session dedup reset.
|
|
1291
|
+
installRecallHooks();
|
|
1266
1292
|
// Strip the old static lessons block from CLAUDE.md (0.9.0 migration).
|
|
1267
1293
|
// Lessons are now delivered via the SessionStart hook instead.
|
|
1268
1294
|
const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
|
|
@@ -1418,8 +1444,10 @@ async function runClientInit(serverUrl, agentName) {
|
|
|
1418
1444
|
allowHicortexTools();
|
|
1419
1445
|
// Step 5: Install CC commands
|
|
1420
1446
|
installCcCommands();
|
|
1421
|
-
// Step 6: Install SessionStart hook for query-time lessons
|
|
1447
|
+
// Step 6: Install SessionStart hook for query-time lessons + the #192
|
|
1448
|
+
// per-prompt pushed-recall hooks.
|
|
1422
1449
|
installSessionStartHook();
|
|
1450
|
+
installRecallHooks();
|
|
1423
1451
|
// Strip the old static CLAUDE.md lessons block if present (0.9.0 migration).
|
|
1424
1452
|
const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
|
|
1425
1453
|
if ((0, claude_md_js_1.removeLessonsBlock)(claudeMdPath)) {
|
|
@@ -33,6 +33,20 @@ export interface ContextResponse {
|
|
|
33
33
|
agent?: string;
|
|
34
34
|
mode?: string;
|
|
35
35
|
}
|
|
36
|
+
export interface ResolvedConfig {
|
|
37
|
+
serverUrl: string;
|
|
38
|
+
authToken: string | undefined;
|
|
39
|
+
home: string;
|
|
40
|
+
/** Per-agent context id sent as ?agent= (0.13); null → global (no param). */
|
|
41
|
+
agentName: string | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read ~/.hicortex/config.json and resolve the server URL + auth token, or
|
|
45
|
+
* null when there is no usable config (server not set up yet — fail soft).
|
|
46
|
+
* Exported for reuse by the recall-hook CLI (#192) so the two CC hooks can
|
|
47
|
+
* never resolve the server differently.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveConfig(): ResolvedConfig | null;
|
|
36
50
|
/**
|
|
37
51
|
* Title-case a section name for its heading: split on `-`/`_`, capitalize each
|
|
38
52
|
* word ("user" → "User", "my_notes" → "My Notes").
|
|
@@ -69,7 +83,7 @@ export declare function renderContextBlock(sections: Record<string, string>): st
|
|
|
69
83
|
* into every persona; on a bare fetch (no id) the guard is off (amendment
|
|
70
84
|
* A2).
|
|
71
85
|
* - CC passes `false` ALWAYS and deliberately (see the call site): a thin CC
|
|
72
|
-
* client auto-upgrades via npx BEFORE
|
|
86
|
+
* client auto-upgrades via npx BEFORE the server does, so during the upgrade
|
|
73
87
|
* window it talks to a 0.12 server that cannot hold ANY per-agent config —
|
|
74
88
|
* global IS the operator's intended state there, and a guard would instead
|
|
75
89
|
* blank ALL context for every CC session in that window.
|
package/dist/lessons-context.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
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.resolveConfig = resolveConfig;
|
|
26
27
|
exports.titleCaseSection = titleCaseSection;
|
|
27
28
|
exports.orderSectionNames = orderSectionNames;
|
|
28
29
|
exports.renderContextBlock = renderContextBlock;
|
|
@@ -41,6 +42,8 @@ const THIS_HARNESS = "cc";
|
|
|
41
42
|
/**
|
|
42
43
|
* Read ~/.hicortex/config.json and resolve the server URL + auth token, or
|
|
43
44
|
* null when there is no usable config (server not set up yet — fail soft).
|
|
45
|
+
* Exported for reuse by the recall-hook CLI (#192) so the two CC hooks can
|
|
46
|
+
* never resolve the server differently.
|
|
44
47
|
*/
|
|
45
48
|
function resolveConfig() {
|
|
46
49
|
const home = (0, paths_js_1.hicortexHome)();
|
|
@@ -180,7 +183,7 @@ function renderContextBlock(sections) {
|
|
|
180
183
|
* into every persona; on a bare fetch (no id) the guard is off (amendment
|
|
181
184
|
* A2).
|
|
182
185
|
* - CC passes `false` ALWAYS and deliberately (see the call site): a thin CC
|
|
183
|
-
* client auto-upgrades via npx BEFORE
|
|
186
|
+
* client auto-upgrades via npx BEFORE the server does, so during the upgrade
|
|
184
187
|
* window it talks to a 0.12 server that cannot hold ANY per-agent config —
|
|
185
188
|
* global IS the operator's intended state there, and a guard would instead
|
|
186
189
|
* blank ALL context for every CC session in that window.
|
|
@@ -216,7 +219,7 @@ async function fetchContextBlock(cfg) {
|
|
|
216
219
|
return null;
|
|
217
220
|
const data = await resp.json();
|
|
218
221
|
// CC deliberately passes requireAgentEcho: false (NOT the OC/Hermes old-server
|
|
219
|
-
// guard). A thin CC client auto-upgrades via npx BEFORE
|
|
222
|
+
// guard). A thin CC client auto-upgrades via npx BEFORE the server does, so
|
|
220
223
|
// mid-upgrade it may hit a 0.12 server that returns global context with no
|
|
221
224
|
// `agent` echo — and a 0.12 server cannot hold per-agent config, so global is
|
|
222
225
|
// the intended state. Guarding here would blank ALL CC context in that window.
|
package/dist/mcp-server.js
CHANGED
|
@@ -64,6 +64,8 @@ const graph_js_1 = require("./graph.js");
|
|
|
64
64
|
const viz_js_1 = require("./viz.js");
|
|
65
65
|
const context_store_js_1 = require("./context-store.js");
|
|
66
66
|
const retrieval = __importStar(require("./retrieval.js"));
|
|
67
|
+
const recall_registry_js_1 = require("./recall-registry.js");
|
|
68
|
+
const recall_index_js_1 = require("./recall-index.js");
|
|
67
69
|
const seed_lesson_js_1 = require("./seed-lesson.js");
|
|
68
70
|
const distiller_js_1 = require("./distiller.js");
|
|
69
71
|
// ---------------------------------------------------------------------------
|
|
@@ -85,6 +87,9 @@ let contextClients = ["cc"];
|
|
|
85
87
|
// Read once at boot (like contextClients); the drop-in-a-dir presence path is
|
|
86
88
|
// per-request, so only explicit config entries need a daemon restart to apply.
|
|
87
89
|
let contextAgents = {};
|
|
90
|
+
// Pushed-recall dedup registry (#192) + options; configured at boot.
|
|
91
|
+
let recallRegistry = new recall_registry_js_1.SessionRecallRegistry();
|
|
92
|
+
let recallIndexOptions = {};
|
|
88
93
|
// Cache detectChunkSize results keyed by "<provider>/<model>@<baseUrl>" so we
|
|
89
94
|
// probe each endpoint once per server boot rather than once per /distill request.
|
|
90
95
|
const chunkSizeCache = new Map();
|
|
@@ -103,9 +108,9 @@ function createMcpServer() {
|
|
|
103
108
|
version: VERSION,
|
|
104
109
|
});
|
|
105
110
|
// -- hicortex_search --
|
|
106
|
-
server.tool("hicortex_search", "Search long-term memory
|
|
111
|
+
server.tool("hicortex_search", "Search shared long-term memory (all agents, all sessions). CALL THIS BEFORE assuming, guessing, or asking the user about anything that may have come up before: prior decisions, preferences, project facts, people, hardware, past incidents. If you are about to write 'I don't have information about…', search first.", {
|
|
107
112
|
query: zod_1.z.string().describe("Search query text"),
|
|
108
|
-
limit: zod_1.z.coerce.number().optional().describe("Max results (default
|
|
113
|
+
limit: zod_1.z.coerce.number().optional().describe("Max results (default: server config searchLimit)"),
|
|
109
114
|
project: zod_1.z.string().optional().describe("Filter by project name"),
|
|
110
115
|
}, async ({ query, limit, project }) => {
|
|
111
116
|
if (!db)
|
|
@@ -118,10 +123,29 @@ function createMcpServer() {
|
|
|
118
123
|
return { content: [{ type: "text", text: `Search failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
119
124
|
}
|
|
120
125
|
});
|
|
126
|
+
// -- hicortex_get --
|
|
127
|
+
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 only fetch what you actually need.", {
|
|
128
|
+
id: zod_1.z.string().describe("Memory id (as shown in recall index/search results)"),
|
|
129
|
+
}, async ({ id }) => {
|
|
130
|
+
if (!db)
|
|
131
|
+
return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
|
|
132
|
+
try {
|
|
133
|
+
const mem = storage.getMemory(db, id);
|
|
134
|
+
if (!mem)
|
|
135
|
+
return { content: [{ type: "text", text: `No memory with id ${id}` }], isError: true };
|
|
136
|
+
// Real use → full strengthen (access_count + hardening + prune shield).
|
|
137
|
+
storage.strengthenMemory(db, id, new Date().toISOString());
|
|
138
|
+
const header = `[${mem.memory_type ?? "episode"}] ${mem.project ?? ""} ${mem.created_at ?? ""}`.trim();
|
|
139
|
+
return { content: [{ type: "text", text: `${header}\n\n${mem.content ?? ""}` }] };
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
return { content: [{ type: "text", text: `Get failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
|
|
143
|
+
}
|
|
144
|
+
});
|
|
121
145
|
// -- hicortex_recent --
|
|
122
|
-
server.tool("hicortex_recent", "Get recent memories, optionally filtered by project.
|
|
146
|
+
server.tool("hicortex_recent", "Get recent memories, optionally filtered by project. CALL THIS AT THE START of substantive work on a project to catch up on its latest state — cheaper than asking the user what happened.", {
|
|
123
147
|
project: zod_1.z.string().optional().describe("Filter by project name"),
|
|
124
|
-
limit: zod_1.z.coerce.number().optional().describe("Max results (default
|
|
148
|
+
limit: zod_1.z.coerce.number().optional().describe("Max results (default: server config recentLimit)"),
|
|
125
149
|
}, async ({ project, limit }) => {
|
|
126
150
|
if (!db)
|
|
127
151
|
return { content: [{ type: "text", text: "Hicortex not initialized" }], isError: true };
|
|
@@ -446,6 +470,21 @@ async function startServer(options = {}) {
|
|
|
446
470
|
// agents/<id> dir onto disk takes effect immediately (per-request presence).
|
|
447
471
|
const resolvedAgents = (0, context_store_js_1.resolveContextAgents)(savedConfig?.contextAgents);
|
|
448
472
|
contextAgents = resolvedAgents.agents;
|
|
473
|
+
// #192 recall/decay alignment: decay speed + recall breadth + pushed-recall
|
|
474
|
+
// knobs, ALL from config (see retrieval.ts configureRecall for the key list)
|
|
475
|
+
// so calibration is a config edit + restart, never a release.
|
|
476
|
+
retrieval.configureDecay({ halfLifeDays: savedConfig?.decayHalfLifeDays });
|
|
477
|
+
const recallCfg = retrieval.configureRecall(savedConfig);
|
|
478
|
+
console.log(`[hicortex] Recall: k=${recallCfg.searchLimit}/recent=${recallCfg.recentLimit}` +
|
|
479
|
+
`/window=${recallCfg.recentWindowDays}d/cold=${recallCfg.coldExposureSlots}`);
|
|
480
|
+
recallRegistry = new recall_registry_js_1.SessionRecallRegistry({
|
|
481
|
+
reshowTurns: savedConfig?.recallReshowTurns,
|
|
482
|
+
});
|
|
483
|
+
recallIndexOptions = {
|
|
484
|
+
minSimilarity: savedConfig?.recallMinSimilarity,
|
|
485
|
+
maxItems: savedConfig?.recallMaxItems,
|
|
486
|
+
minPromptLength: savedConfig?.recallMinPromptChars,
|
|
487
|
+
};
|
|
449
488
|
if (resolvedAgents.dropped.length > 0) {
|
|
450
489
|
console.warn(`[hicortex] Ignoring invalid contextAgents entries: ${resolvedAgents.dropped.join(", ")} ` +
|
|
451
490
|
`(keys must match ^[a-z0-9][a-z0-9_-]*$; modes must be override|global|off)`);
|
|
@@ -582,7 +621,8 @@ async function startServer(options = {}) {
|
|
|
582
621
|
res.status(400).json({ error: "Missing 'query'" });
|
|
583
622
|
return;
|
|
584
623
|
}
|
|
585
|
-
|
|
624
|
+
// No hardcoded default: absent limit → config-driven (searchLimit).
|
|
625
|
+
const limit = req.query.limit ? Number(req.query.limit) : undefined;
|
|
586
626
|
const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
|
|
587
627
|
const privacy = typeof req.query.privacy === "string" && req.query.privacy
|
|
588
628
|
? req.query.privacy.split(",").map((s) => s.trim()).filter(Boolean)
|
|
@@ -595,6 +635,50 @@ async function startServer(options = {}) {
|
|
|
595
635
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
596
636
|
}
|
|
597
637
|
});
|
|
638
|
+
// REST /recall-index — pushed recall index (#192). One recall logic for all
|
|
639
|
+
// harnesses: CC UserPromptSubmit hook, Hermes/OC per-turn plugins. Returns a
|
|
640
|
+
// compact index block (or null); exposure is recorded as shown_count +
|
|
641
|
+
// last_accessed, NOT access_count (that stays reserved for hicortex_get /
|
|
642
|
+
// GET /memory — real use). {reset: true} clears the session's dedup state
|
|
643
|
+
// (SessionStart/compaction).
|
|
644
|
+
app.post("/recall-index", async (req, res) => {
|
|
645
|
+
if (!db) {
|
|
646
|
+
res.status(503).json({ error: "Server not initialized" });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const r = await (0, recall_index_js_1.handleRecallIndex)({
|
|
650
|
+
db,
|
|
651
|
+
registry: recallRegistry,
|
|
652
|
+
retrieveFn: (query, limit) => retrieval.retrieve(db, embedder_js_1.embed, query, { limit, noStrengthen: true }),
|
|
653
|
+
options: recallIndexOptions,
|
|
654
|
+
}, req.body);
|
|
655
|
+
res.status(r.status).json(r.body);
|
|
656
|
+
});
|
|
657
|
+
// REST /memory?id= — fetch one memory's full content (lazy-load counterpart
|
|
658
|
+
// of /recall-index for REST clients: Hermes/OC plugins). Marks it as used.
|
|
659
|
+
app.get("/memory", (req, res) => {
|
|
660
|
+
if (!db) {
|
|
661
|
+
res.status(503).json({ error: "Server not initialized" });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const id = typeof req.query.id === "string" ? req.query.id : "";
|
|
665
|
+
if (!id) {
|
|
666
|
+
res.status(400).json({ error: "Missing 'id'" });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
const mem = storage.getMemory(db, id);
|
|
671
|
+
if (!mem) {
|
|
672
|
+
res.status(404).json({ error: `No memory with id ${id}` });
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
storage.strengthenMemory(db, id, new Date().toISOString());
|
|
676
|
+
res.json({ memory: mem });
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
680
|
+
}
|
|
681
|
+
});
|
|
598
682
|
// REST /recent — recent memories, optionally filtered by project.
|
|
599
683
|
app.get("/recent", (req, res) => {
|
|
600
684
|
if (!db) {
|
|
@@ -602,7 +686,8 @@ async function startServer(options = {}) {
|
|
|
602
686
|
return;
|
|
603
687
|
}
|
|
604
688
|
const project = typeof req.query.project === "string" && req.query.project ? req.query.project : undefined;
|
|
605
|
-
|
|
689
|
+
// No hardcoded default: absent limit → config-driven (recentLimit).
|
|
690
|
+
const limit = req.query.limit ? Number(req.query.limit) : undefined;
|
|
606
691
|
const privacy = typeof req.query.privacy === "string" && req.query.privacy
|
|
607
692
|
? req.query.privacy.split(",").map((s) => s.trim()).filter(Boolean)
|
|
608
693
|
: undefined;
|
package/dist/nightly.js
CHANGED
|
@@ -66,6 +66,7 @@ const hermes_transcript_reader_js_1 = require("./hermes-transcript-reader.js");
|
|
|
66
66
|
const pi_transcript_reader_js_1 = require("./pi-transcript-reader.js");
|
|
67
67
|
const oc_transcript_reader_js_1 = require("./oc-transcript-reader.js");
|
|
68
68
|
const features_js_1 = require("./features.js");
|
|
69
|
+
const retrieval_js_1 = require("./retrieval.js");
|
|
69
70
|
const state_js_1 = require("./state.js");
|
|
70
71
|
const capture_cursors_js_1 = require("./capture-cursors.js");
|
|
71
72
|
const capture_js_1 = require("./capture.js");
|
|
@@ -209,6 +210,10 @@ async function runNightly(options = {}) {
|
|
|
209
210
|
}
|
|
210
211
|
const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
|
|
211
212
|
const port = savedConfig?.port ?? 8787;
|
|
213
|
+
// #192: consolidation's decay/prune stage must score with the same clock as
|
|
214
|
+
// the server's retrieval path (config decayHalfLifeDays, default 365).
|
|
215
|
+
(0, retrieval_js_1.configureDecay)({ halfLifeDays: savedConfig?.decayHalfLifeDays });
|
|
216
|
+
(0, retrieval_js_1.configureRecall)(savedConfig);
|
|
212
217
|
const modeLabel = captureOnly ? " (capture-only)" : dryRun ? " (dry run)" : "";
|
|
213
218
|
console.log(`[hicortex] Nightly pipeline starting${modeLabel}`);
|
|
214
219
|
if (captureOnly) {
|
|
@@ -16,14 +16,14 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Directory layout:
|
|
18
18
|
* ~/.pi/agent/sessions/
|
|
19
|
-
* --home-
|
|
19
|
+
* --home-alice-projects-myagent--/
|
|
20
20
|
* 2026-04-10T18-37-44-615Z_<uuid>.jsonl
|
|
21
21
|
* 2026-04-11T07-51-28-282Z_<uuid>.jsonl
|
|
22
22
|
* --home-agents-Development-MAIC--/
|
|
23
23
|
* ...
|
|
24
24
|
*
|
|
25
|
-
* The encoded-cwd uses double-dash separators: /home/
|
|
26
|
-
* becomes --home-
|
|
25
|
+
* The encoded-cwd uses double-dash separators: /home/alice/projects/myagent
|
|
26
|
+
* becomes --home-alice-projects-myagent--. The session header's `cwd` field
|
|
27
27
|
* is the canonical path; the directory name is a filesystem-safe encoding.
|
|
28
28
|
*/
|
|
29
29
|
import type { TranscriptBatch, CursorMap } from "./transcript-reader.js";
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Directory layout:
|
|
19
19
|
* ~/.pi/agent/sessions/
|
|
20
|
-
* --home-
|
|
20
|
+
* --home-alice-projects-myagent--/
|
|
21
21
|
* 2026-04-10T18-37-44-615Z_<uuid>.jsonl
|
|
22
22
|
* 2026-04-11T07-51-28-282Z_<uuid>.jsonl
|
|
23
23
|
* --home-agents-Development-MAIC--/
|
|
24
24
|
* ...
|
|
25
25
|
*
|
|
26
|
-
* The encoded-cwd uses double-dash separators: /home/
|
|
27
|
-
* becomes --home-
|
|
26
|
+
* The encoded-cwd uses double-dash separators: /home/alice/projects/myagent
|
|
27
|
+
* becomes --home-alice-projects-myagent--. The session header's `cwd` field
|
|
28
28
|
* is the canonical path; the directory name is a filesystem-safe encoding.
|
|
29
29
|
*/
|
|
30
30
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -157,7 +157,7 @@ function readPiTranscripts(since, sessionsDir = DEFAULT_PI_SESSIONS_DIR, cursors
|
|
|
157
157
|
}
|
|
158
158
|
/**
|
|
159
159
|
* Extract the last path segment from a cwd as the project name.
|
|
160
|
-
* /home/
|
|
160
|
+
* /home/alice/projects/myagent → "myagent"
|
|
161
161
|
* Falls back to decoding the directory name if cwd is empty.
|
|
162
162
|
*/
|
|
163
163
|
function deriveProjectName(cwd, encodedDir) {
|
|
@@ -165,7 +165,7 @@ function deriveProjectName(cwd, encodedDir) {
|
|
|
165
165
|
const segments = cwd.split("/").filter(Boolean);
|
|
166
166
|
return segments[segments.length - 1] ?? "unknown";
|
|
167
167
|
}
|
|
168
|
-
// Decode the Pi directory encoding: --home-
|
|
168
|
+
// Decode the Pi directory encoding: --home-alice-projects-myagent-- → myagent
|
|
169
169
|
const decoded = encodedDir.replace(/^--/, "").replace(/--$/, "").split("-");
|
|
170
170
|
return decoded[decoded.length - 1] ?? "unknown";
|
|
171
171
|
}
|