@gamaze/hicortex 0.12.1 → 0.13.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 +20 -1
- 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/index.js +73 -28
- package/dist/init.d.ts +31 -0
- package/dist/init.js +156 -2
- package/dist/lessons-context.d.ts +56 -0
- package/dist/lessons-context.js +77 -18
- package/dist/mcp-server.js +16 -2
- package/dist/status.d.ts +9 -0
- package/dist/status.js +29 -4
- 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/README.md
CHANGED
|
@@ -129,6 +129,20 @@ Beyond auto-distilled memories and lessons, Hicortex holds a **hand-edited conte
|
|
|
129
129
|
- **Delivery:** injected into the harnesses listed in `contextClients` (default `["cc"]` — Claude Code only in v1; `"all"` or any subset of `cc`/`hermes`/`oc`).
|
|
130
130
|
- **Deletion** is filesystem-only — remove the file on the server (as the daemon user).
|
|
131
131
|
|
|
132
|
+
### Per-agent context (0.13)
|
|
133
|
+
|
|
134
|
+
One server serves a fleet of distinct-persona agents. Each agent can have its **own** context, resolved server-side into one of three modes:
|
|
135
|
+
|
|
136
|
+
- **`override`** (default when an `agents/<id>/` dir exists) — the agent's sections win **per section name**, falling back to the global set for any section it doesn't define.
|
|
137
|
+
- **`global`** — the shared global set (the 0.12 behavior).
|
|
138
|
+
- **`off`** — inject nothing for that agent.
|
|
139
|
+
|
|
140
|
+
- **Agent id:** Hermes and OC scope per profile/agent automatically. **CC is global by default** — it sends no `?agent=`, so all your CC machines share one global context (one user = one identity across machines). `agentName` is an explicit opt-in: set it with `init --agent-name <name>` and CC will send `?agent=<config.agentName>`; clear it with `init --agent-name ""` to return to global. The id is on the same strict allowlist as section names (it becomes a path). Shown by `hicortex status` (or `(not set — global context)` when unset).
|
|
141
|
+
- **Storage:** per-agent sections live at `~/.hicortex/context/agents/<id>/*.md`; the global reader never descends into `agents/`.
|
|
142
|
+
- **Config:** `contextAgents` maps agent id → mode; a dropped-in `agents/<id>/` dir alone means `override` with no config. Editing `contextAgents` needs a daemon restart; dropping in a dir takes effect immediately.
|
|
143
|
+
- **Edit:** the web editor's scope selector (`Global` | `<agent>`; inherited sections shown dimmed), or `hicortex context show|edit --agent <id>`.
|
|
144
|
+
- **Backward compatible:** no `?agent=` and no `contextAgents`/`agents/` dir → every agent gets the global set.
|
|
145
|
+
|
|
132
146
|
## CLI Commands
|
|
133
147
|
|
|
134
148
|
```bash
|
|
@@ -141,6 +155,9 @@ npx @gamaze/hicortex nightly --dry-run # Preview without changes
|
|
|
141
155
|
npx @gamaze/hicortex classify-domains # Backfill domain tags over the corpus (see Memory Domains & Tags)
|
|
142
156
|
npx @gamaze/hicortex context show [name] # Print the standing context layer (see Context Layer)
|
|
143
157
|
npx @gamaze/hicortex context edit <name> # Edit a context section in $EDITOR
|
|
158
|
+
npx @gamaze/hicortex context show --agent <id> # Show a specific agent's resolved context (0.13)
|
|
159
|
+
npx @gamaze/hicortex init --agent-name <name> # Opt in to a per-agent context id (default: unset — shared global context)
|
|
160
|
+
npx @gamaze/hicortex init --agent-name "" # Clear it back to global context
|
|
144
161
|
npx @gamaze/hicortex status # Show config, DB stats
|
|
145
162
|
npx @gamaze/hicortex uninstall # Remove CC integration (keeps DB)
|
|
146
163
|
```
|
|
@@ -186,6 +203,8 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
186
203
|
| `weakPrimaryFloor` | Minimum similarity for a no-fit memory to keep a weak domain association (default: 0.45) |
|
|
187
204
|
| `moduleIndexTokenBudget` | Max tokens for domain index in lessons context (default: 500) |
|
|
188
205
|
| `contextClients` | Which harnesses inject the [context layer](#context-layer) at session start (default `["cc"]`; `"all"` or any subset of `cc`/`hermes`/`oc`) |
|
|
206
|
+
| `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) |
|
|
207
|
+
| `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 |
|
|
189
208
|
| `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 |
|
|
190
209
|
| `telemetry` | Anonymous usage telemetry, `false` to opt out |
|
|
191
210
|
|
|
@@ -199,7 +218,7 @@ Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze
|
|
|
199
218
|
| `/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. |
|
|
200
219
|
| `/search` | GET | Yes | Semantic memory search |
|
|
201
220
|
| `/recent` | GET | Yes | Recent memories, queryless recall (renamed from `/context` in 0.12) |
|
|
202
|
-
| `/context` | GET / PUT | Yes | Standing [context layer](#context-layer): read all sections / partial-upsert named sections. Recall-style query params on GET → 400 (use `/recent`) |
|
|
221
|
+
| `/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`) |
|
|
203
222
|
| `/context/ui` | GET | No* | Web editor for the context layer (shell served without auth, like `/viz`; data via `/context`) |
|
|
204
223
|
| `/lessons` | GET | Yes | Lessons + memory index (used by CC SessionStart hook) |
|
|
205
224
|
| `/ingest` | POST | Yes | Legacy: accept a single pre-distilled memory from older clients |
|
package/assets/context.html
CHANGED
|
@@ -77,6 +77,13 @@
|
|
|
77
77
|
header h1 { font-size: 15px; font-weight: 600; white-space: nowrap; }
|
|
78
78
|
header .meta { color: var(--text-dim); font-size: 12px; white-space: nowrap; }
|
|
79
79
|
header .spacer { flex: 1; }
|
|
80
|
+
header select, header #newagent {
|
|
81
|
+
background: var(--bg); color: var(--text);
|
|
82
|
+
border: 1px solid var(--panel-border); border-radius: 5px;
|
|
83
|
+
padding: 4px 6px; font: inherit; font-size: 12px;
|
|
84
|
+
}
|
|
85
|
+
header #newagent { width: 120px; }
|
|
86
|
+
header .btn.small { padding: 4px 10px; font-size: 12px; }
|
|
80
87
|
|
|
81
88
|
main { flex: 1; display: flex; flex-direction: column; padding: 14px 18px; gap: 12px; min-height: 0; }
|
|
82
89
|
|
|
@@ -95,7 +102,9 @@
|
|
|
95
102
|
color: var(--text); background: var(--tab-active);
|
|
96
103
|
border-color: var(--panel-border); border-bottom: 1px solid var(--tab-active);
|
|
97
104
|
}
|
|
105
|
+
.tab.inherited { opacity: 0.6; font-style: italic; }
|
|
98
106
|
.tab .dirty { color: var(--accent); margin-left: 5px; }
|
|
107
|
+
.tab .inh { color: var(--text-dim); margin-left: 5px; font-size: 11px; }
|
|
99
108
|
|
|
100
109
|
#addwrap { margin-left: auto; display: flex; gap: 6px; align-items: center; }
|
|
101
110
|
#addname {
|
|
@@ -161,8 +170,13 @@
|
|
|
161
170
|
<body>
|
|
162
171
|
<header>
|
|
163
172
|
<h1>Hicortex — context</h1>
|
|
173
|
+
<label class="meta" for="scope">scope</label>
|
|
174
|
+
<select id="scope" title="Which agent's context to edit"></select>
|
|
175
|
+
<input type="text" id="newagent" placeholder="new-agent-id" autocomplete="off" spellcheck="false">
|
|
176
|
+
<button class="btn secondary small" id="addagent">Add agent</button>
|
|
164
177
|
<span class="meta" id="clients"></span>
|
|
165
178
|
<div class="spacer"></div>
|
|
179
|
+
<span class="meta" id="mode"></span>
|
|
166
180
|
<span class="meta" id="updated"></span>
|
|
167
181
|
</header>
|
|
168
182
|
|
|
@@ -255,6 +269,10 @@
|
|
|
255
269
|
var msgEl = document.getElementById("msg");
|
|
256
270
|
var clientsEl = document.getElementById("clients");
|
|
257
271
|
var updatedEl = document.getElementById("updated");
|
|
272
|
+
var modeEl = document.getElementById("mode");
|
|
273
|
+
var scopeSel = document.getElementById("scope");
|
|
274
|
+
var newAgentInput = document.getElementById("newagent");
|
|
275
|
+
var addAgentBtn = document.getElementById("addagent");
|
|
258
276
|
var overlay = document.getElementById("overlay");
|
|
259
277
|
var tokenInput = document.getElementById("token-input");
|
|
260
278
|
var tokenErr = document.getElementById("token-err");
|
|
@@ -267,6 +285,20 @@
|
|
|
267
285
|
var active = null; // active section name
|
|
268
286
|
var afterAuth = null; // action to retry after a successful token prompt
|
|
269
287
|
|
|
288
|
+
// Per-agent scope (0.13). scope = null → the global set; otherwise an agent
|
|
289
|
+
// id, and edits target PUT /context?agent=<id>. `agents` is the id -> mode
|
|
290
|
+
// map from the bare (global) GET; `origins` is the id -> "global"|"agent"
|
|
291
|
+
// provenance from an agent-scope GET (used to dim inherited tabs).
|
|
292
|
+
var scope = null;
|
|
293
|
+
var agents = {};
|
|
294
|
+
var origins = {};
|
|
295
|
+
|
|
296
|
+
// ?agent= query for the active scope. Written so the literal fetch("/context"
|
|
297
|
+
// survives the self-containment test; the server does the resolution.
|
|
298
|
+
function scopeQS() {
|
|
299
|
+
return scope ? "?agent=" + encodeURIComponent(scope) : "";
|
|
300
|
+
}
|
|
301
|
+
|
|
270
302
|
function setMsg(text, cls) {
|
|
271
303
|
msgEl.textContent = text || "";
|
|
272
304
|
msgEl.className = cls || "dim";
|
|
@@ -288,9 +320,18 @@
|
|
|
288
320
|
|
|
289
321
|
order.forEach(function (name) {
|
|
290
322
|
var b = document.createElement("button");
|
|
291
|
-
|
|
323
|
+
// In an agent scope, a section still coming from the global set is
|
|
324
|
+
// inherited — dim it and mark it. Saving it PUTs to the agent (override).
|
|
325
|
+
var inherited = scope && origins[name] === "global";
|
|
326
|
+
b.className = "tab" + (name === active ? " active" : "") + (inherited ? " inherited" : "");
|
|
292
327
|
b.type = "button";
|
|
293
328
|
b.appendChild(document.createTextNode(name));
|
|
329
|
+
if (inherited) {
|
|
330
|
+
var inh = document.createElement("span");
|
|
331
|
+
inh.className = "inh";
|
|
332
|
+
inh.textContent = "(inherited)";
|
|
333
|
+
b.appendChild(inh);
|
|
334
|
+
}
|
|
294
335
|
if (isDirty(name)) {
|
|
295
336
|
var dot = document.createElement("span");
|
|
296
337
|
dot.className = "dirty";
|
|
@@ -316,13 +357,33 @@
|
|
|
316
357
|
area.focus();
|
|
317
358
|
}
|
|
318
359
|
|
|
319
|
-
function updateMeta(clients, updatedAt) {
|
|
360
|
+
function updateMeta(clients, updatedAt, mode) {
|
|
320
361
|
if (Array.isArray(clients)) {
|
|
321
362
|
clientsEl.textContent = clients.length ? "clients: " + clients.join(", ") : "clients: none";
|
|
322
363
|
}
|
|
323
364
|
if (updatedAt !== undefined) {
|
|
324
365
|
updatedEl.textContent = updatedAt ? "updated " + updatedAt : "";
|
|
325
366
|
}
|
|
367
|
+
if (mode !== undefined) {
|
|
368
|
+
modeEl.textContent = mode ? "mode: " + mode : "";
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Rebuild the scope <select> from the known agents map. Options are added via
|
|
373
|
+
// textContent only (never innerHTML); the current scope stays selected.
|
|
374
|
+
function populateScopeOptions() {
|
|
375
|
+
while (scopeSel.firstChild) scopeSel.removeChild(scopeSel.firstChild);
|
|
376
|
+
var globalOpt = document.createElement("option");
|
|
377
|
+
globalOpt.value = "";
|
|
378
|
+
globalOpt.textContent = "Global";
|
|
379
|
+
scopeSel.appendChild(globalOpt);
|
|
380
|
+
Object.keys(agents).sort().forEach(function (id) {
|
|
381
|
+
var o = document.createElement("option");
|
|
382
|
+
o.value = id;
|
|
383
|
+
o.textContent = id + " (" + agents[id] + ")";
|
|
384
|
+
scopeSel.appendChild(o);
|
|
385
|
+
});
|
|
386
|
+
scopeSel.value = scope || "";
|
|
326
387
|
}
|
|
327
388
|
|
|
328
389
|
// =========================================================================
|
|
@@ -330,7 +391,7 @@
|
|
|
330
391
|
// =========================================================================
|
|
331
392
|
function load() {
|
|
332
393
|
setMsg("Loading…", "dim");
|
|
333
|
-
fetch("/context", { headers: authHeaders() })
|
|
394
|
+
fetch("/context" + scopeQS(), { headers: authHeaders() })
|
|
334
395
|
.then(function (resp) {
|
|
335
396
|
if (resp.status === 401) { afterAuth = load; showTokenPrompt(); throw new Error("unauthorized"); }
|
|
336
397
|
if (!resp.ok) {
|
|
@@ -342,6 +403,11 @@
|
|
|
342
403
|
})
|
|
343
404
|
.then(function (data) {
|
|
344
405
|
overlay.classList.remove("open");
|
|
406
|
+
// The bare (global) response carries the `agents` map; an agent-scope
|
|
407
|
+
// response carries `origins`. Retain the last-known agents list when the
|
|
408
|
+
// current response omits it so the selector stays populated.
|
|
409
|
+
if (scope === null && data.agents && typeof data.agents === "object") agents = data.agents;
|
|
410
|
+
origins = (data.origins && typeof data.origins === "object") ? data.origins : {};
|
|
345
411
|
var incoming = data.sections || {};
|
|
346
412
|
var names = Object.keys(incoming).sort();
|
|
347
413
|
sections = {};
|
|
@@ -353,7 +419,8 @@
|
|
|
353
419
|
// Keep the active tab if it still exists, else pick the first.
|
|
354
420
|
if (!active || order.indexOf(active) < 0) active = order.length ? order[0] : null;
|
|
355
421
|
if (active) area.value = sections[active].draft;
|
|
356
|
-
updateMeta(data.clients, data.updated_at || null);
|
|
422
|
+
updateMeta(data.clients, data.updated_at || null, data.mode || "");
|
|
423
|
+
populateScopeOptions();
|
|
357
424
|
renderTabs();
|
|
358
425
|
setMsg(order.length ? "" : "", "dim");
|
|
359
426
|
})
|
|
@@ -382,7 +449,7 @@
|
|
|
382
449
|
|
|
383
450
|
saveBtn.disabled = true;
|
|
384
451
|
setMsg("Saving…", "dim");
|
|
385
|
-
fetch("/context", {
|
|
452
|
+
fetch("/context" + scopeQS(), {
|
|
386
453
|
method: "PUT",
|
|
387
454
|
headers: authHeaders({ "Content-Type": "application/json" }),
|
|
388
455
|
body: JSON.stringify(payload),
|
|
@@ -402,7 +469,13 @@
|
|
|
402
469
|
overlay.classList.remove("open");
|
|
403
470
|
var s = sections[name];
|
|
404
471
|
if (s) { s.saved = value; s.isNew = false; }
|
|
405
|
-
|
|
472
|
+
// In an agent scope the just-saved section is now an override (no longer
|
|
473
|
+
// inherited); a first save also registers the agent in the selector.
|
|
474
|
+
if (scope) {
|
|
475
|
+
origins[name] = "agent";
|
|
476
|
+
if (!agents[scope]) { agents[scope] = body.mode || "override"; populateScopeOptions(); }
|
|
477
|
+
}
|
|
478
|
+
updateMeta(undefined, body.updated_at || null, body.mode);
|
|
406
479
|
setMsg("Saved" + (body.updated_at ? " · " + body.updated_at : ""), "ok");
|
|
407
480
|
renderTabs();
|
|
408
481
|
})
|
|
@@ -436,6 +509,39 @@
|
|
|
436
509
|
setMsg("New section “" + name + "” — edit and Save to create it.", "dim");
|
|
437
510
|
}
|
|
438
511
|
|
|
512
|
+
// =========================================================================
|
|
513
|
+
// Scope switching — reload the selected agent's (or the global) context.
|
|
514
|
+
// Warns before discarding unsaved edits in the current scope.
|
|
515
|
+
// =========================================================================
|
|
516
|
+
function anyDirty() {
|
|
517
|
+
return order.some(function (n) { return isDirty(n); });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function switchScope(next) {
|
|
521
|
+
if (anyDirty() && !window.confirm("Discard unsaved changes and switch scope?")) {
|
|
522
|
+
scopeSel.value = scope || "";
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
scope = next || null;
|
|
526
|
+
active = null; // let load() pick the first tab in the new scope
|
|
527
|
+
load();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function addAgent() {
|
|
531
|
+
var id = newAgentInput.value.trim();
|
|
532
|
+
if (!validName(id)) {
|
|
533
|
+
setMsg("Invalid agent id — lowercase letters, digits, - or _ (max 64, no leading - or _).", "err");
|
|
534
|
+
newAgentInput.focus();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
newAgentInput.value = "";
|
|
538
|
+
// The agent dir is created server-side on the first Save; until then the
|
|
539
|
+
// scope resolves to the inherited global set. Register it locally so the
|
|
540
|
+
// selector shows it immediately.
|
|
541
|
+
if (!agents[id]) agents[id] = "override";
|
|
542
|
+
switchScope(id);
|
|
543
|
+
}
|
|
544
|
+
|
|
439
545
|
// =========================================================================
|
|
440
546
|
// Token prompt (remote only — localhost bypasses auth)
|
|
441
547
|
// =========================================================================
|
|
@@ -466,6 +572,9 @@
|
|
|
466
572
|
saveBtn.addEventListener("click", save);
|
|
467
573
|
addBtn.addEventListener("click", addSection);
|
|
468
574
|
addName.addEventListener("keydown", function (e) { if (e.key === "Enter") addSection(); });
|
|
575
|
+
scopeSel.addEventListener("change", function () { switchScope(scopeSel.value); });
|
|
576
|
+
addAgentBtn.addEventListener("click", addAgent);
|
|
577
|
+
newAgentInput.addEventListener("keydown", function (e) { if (e.key === "Enter") addAgent(); });
|
|
469
578
|
area.addEventListener("input", function () {
|
|
470
579
|
if (active && sections[active]) sections[active].draft = area.value;
|
|
471
580
|
renderTabs();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny, dependency-free argv helpers for cli.ts. Kept in their own module so
|
|
3
|
+
* cli.ts can import them statically without pulling in a heavy command module
|
|
4
|
+
* (its command handlers stay lazily `import()`-ed).
|
|
5
|
+
*/
|
|
6
|
+
/** Thrown when a value-flag is present but missing its value. */
|
|
7
|
+
export declare class MissingFlagValueError extends Error {
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Read a required value for `flag` from `argv`. Returns undefined when the flag
|
|
11
|
+
* is absent; throws MissingFlagValueError when it is present without a value
|
|
12
|
+
* (end of args) OR the next token is itself a flag (starts with "-"). The
|
|
13
|
+
* latter guard stops `--agent-name --server <url>` from silently swallowing
|
|
14
|
+
* `--server` as the agent name — a typo must be loud.
|
|
15
|
+
*/
|
|
16
|
+
export declare function readValueFlag(argv: string[], flag: string): string | undefined;
|
package/dist/cli-args.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tiny, dependency-free argv helpers for cli.ts. Kept in their own module so
|
|
4
|
+
* cli.ts can import them statically without pulling in a heavy command module
|
|
5
|
+
* (its command handlers stay lazily `import()`-ed).
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.MissingFlagValueError = void 0;
|
|
9
|
+
exports.readValueFlag = readValueFlag;
|
|
10
|
+
/** Thrown when a value-flag is present but missing its value. */
|
|
11
|
+
class MissingFlagValueError extends Error {
|
|
12
|
+
}
|
|
13
|
+
exports.MissingFlagValueError = MissingFlagValueError;
|
|
14
|
+
/**
|
|
15
|
+
* Read a required value for `flag` from `argv`. Returns undefined when the flag
|
|
16
|
+
* is absent; throws MissingFlagValueError when it is present without a value
|
|
17
|
+
* (end of args) OR the next token is itself a flag (starts with "-"). The
|
|
18
|
+
* latter guard stops `--agent-name --server <url>` from silently swallowing
|
|
19
|
+
* `--server` as the agent name — a typo must be loud.
|
|
20
|
+
*/
|
|
21
|
+
function readValueFlag(argv, flag) {
|
|
22
|
+
const i = argv.indexOf(flag);
|
|
23
|
+
if (i === -1)
|
|
24
|
+
return undefined;
|
|
25
|
+
const val = argv[i + 1];
|
|
26
|
+
if (val === undefined || val.startsWith("-")) {
|
|
27
|
+
throw new MissingFlagValueError(`${flag} requires a value`);
|
|
28
|
+
}
|
|
29
|
+
return val;
|
|
30
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* uninstall Clean removal of CC integration
|
|
15
15
|
*/
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
const cli_args_js_1 = require("./cli-args.js");
|
|
17
18
|
const command = process.argv[2];
|
|
18
19
|
switch (command) {
|
|
19
20
|
case "server": {
|
|
@@ -32,8 +33,16 @@ switch (command) {
|
|
|
32
33
|
case "init": {
|
|
33
34
|
const serverArg = process.argv.indexOf("--server");
|
|
34
35
|
const serverUrl = serverArg !== -1 ? process.argv[serverArg + 1] : undefined;
|
|
36
|
+
let agentName;
|
|
37
|
+
try {
|
|
38
|
+
agentName = (0, cli_args_js_1.readValueFlag)(process.argv, "--agent-name");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
console.error("[hicortex] init: --agent-name requires a value, e.g. --agent-name lenovo");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
35
44
|
import("./init.js").then(({ runInit }) => {
|
|
36
|
-
runInit({ serverUrl }).catch((err) => {
|
|
45
|
+
runInit({ serverUrl, agentName }).catch((err) => {
|
|
37
46
|
console.error("[hicortex] Init failed:", err);
|
|
38
47
|
process.exit(1);
|
|
39
48
|
});
|
|
@@ -170,6 +179,8 @@ Commands:
|
|
|
170
179
|
Scaffolds 5 editable default memory domains (Work, Personal,
|
|
171
180
|
People, Health, Finance) in ~/.hicortex/config.json
|
|
172
181
|
init --server <url> Set up as client (remote server)
|
|
182
|
+
init --agent-name <name> Opt in to a per-agent context id (default: unset — shared global context)
|
|
183
|
+
Pass --agent-name "" to clear it back to global
|
|
173
184
|
nightly Run nightly denoise + capture + consolidate
|
|
174
185
|
relink Resumable link-discovery pass over the ENTIRE corpus (server mode)
|
|
175
186
|
classify-domains Backfill content-based domain tags over the corpus (server mode, needs config.domains)
|
|
@@ -192,6 +203,7 @@ Options:
|
|
|
192
203
|
classify-domains --reset Restart from the beginning (ignore saved cursor)
|
|
193
204
|
context show [name] Print all context sections, or just <name> (raw, pipeable)
|
|
194
205
|
context edit <name> Edit a section in $EDITOR; PUT only if changed
|
|
206
|
+
context … --agent <id> Target a per-agent scope instead of the global set
|
|
195
207
|
|
|
196
208
|
Examples:
|
|
197
209
|
npx @gamaze/hicortex server
|
package/dist/context-cli.d.ts
CHANGED
|
@@ -33,11 +33,14 @@ export interface ContextGetResponse {
|
|
|
33
33
|
sections: Record<string, string>;
|
|
34
34
|
updated_at: string | null;
|
|
35
35
|
clients: string[];
|
|
36
|
+
/** Present only for an agent-scoped read (0.13). */
|
|
37
|
+
agent?: string;
|
|
38
|
+
mode?: string;
|
|
36
39
|
}
|
|
37
40
|
/** GET /context. Throws ContextCliError with a clear message on any failure. */
|
|
38
|
-
export declare function getContext(target: ContextServerTarget): Promise<ContextGetResponse>;
|
|
41
|
+
export declare function getContext(target: ContextServerTarget, agent?: string): Promise<ContextGetResponse>;
|
|
39
42
|
/** PUT one section. Throws ContextCliError with a clear message on any failure. */
|
|
40
|
-
export declare function putSection(target: ContextServerTarget, name: string, content: string): Promise<void>;
|
|
43
|
+
export declare function putSection(target: ContextServerTarget, name: string, content: string, agent?: string): Promise<void>;
|
|
41
44
|
/** Readable rendering of every section + the resolved clients line (show, no name). */
|
|
42
45
|
export declare function formatAllSections(data: ContextGetResponse): string;
|
|
43
46
|
/** Raw markdown for one section (show <name>), or null if it does not exist. */
|
|
@@ -53,6 +56,14 @@ export type EditorSpawn = (file: string) => boolean;
|
|
|
53
56
|
* current content → $EDITOR on a temp file → PUT only if changed. Temp file is
|
|
54
57
|
* always cleaned up. `spawn` is injectable for tests.
|
|
55
58
|
*/
|
|
56
|
-
export declare function runEdit(name: string, spawn?: EditorSpawn): Promise<void>;
|
|
59
|
+
export declare function runEdit(name: string, spawn?: EditorSpawn, agent?: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Split out a `--agent <id>` flag (anywhere in argv) from the positional args.
|
|
62
|
+
* The flag omitted → the global scope.
|
|
63
|
+
*/
|
|
64
|
+
export declare function extractAgentFlag(args: string[]): {
|
|
65
|
+
agent?: string;
|
|
66
|
+
rest: string[];
|
|
67
|
+
};
|
|
57
68
|
/** Dispatch for `hicortex context <sub>`. Throws ContextCliError on bad usage/failure. */
|
|
58
69
|
export declare function runContextCommand(args: string[]): Promise<void>;
|
package/dist/context-cli.js
CHANGED
|
@@ -24,6 +24,7 @@ exports.putSection = putSection;
|
|
|
24
24
|
exports.formatAllSections = formatAllSections;
|
|
25
25
|
exports.formatOneSection = formatOneSection;
|
|
26
26
|
exports.runEdit = runEdit;
|
|
27
|
+
exports.extractAgentFlag = extractAgentFlag;
|
|
27
28
|
exports.runContextCommand = runContextCommand;
|
|
28
29
|
const node_fs_1 = require("node:fs");
|
|
29
30
|
const node_path_1 = require("node:path");
|
|
@@ -61,6 +62,10 @@ function loadConfig() {
|
|
|
61
62
|
function sectionChanged(before, after) {
|
|
62
63
|
return before !== after;
|
|
63
64
|
}
|
|
65
|
+
/** Append `?agent=<id>` to a /context URL when an agent scope is targeted. */
|
|
66
|
+
function contextUrl(baseUrl, agent) {
|
|
67
|
+
return `${baseUrl}/context${agent ? `?agent=${encodeURIComponent(agent)}` : ""}`;
|
|
68
|
+
}
|
|
64
69
|
function authHeaders(token) {
|
|
65
70
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
66
71
|
}
|
|
@@ -89,10 +94,10 @@ async function readBodyError(resp) {
|
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
/** GET /context. Throws ContextCliError with a clear message on any failure. */
|
|
92
|
-
async function getContext(target) {
|
|
97
|
+
async function getContext(target, agent) {
|
|
93
98
|
let resp;
|
|
94
99
|
try {
|
|
95
|
-
resp = await fetch(
|
|
100
|
+
resp = await fetch(contextUrl(target.baseUrl, agent), {
|
|
96
101
|
headers: authHeaders(target.authToken),
|
|
97
102
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
98
103
|
});
|
|
@@ -103,13 +108,20 @@ async function getContext(target) {
|
|
|
103
108
|
if (!resp.ok) {
|
|
104
109
|
throw new ContextCliError(`GET /context failed — ${describeFailure(resp.status, await readBodyError(resp))}`);
|
|
105
110
|
}
|
|
106
|
-
|
|
111
|
+
const data = (await resp.json());
|
|
112
|
+
// Old-server echo guard: a pre-0.13 server ignores ?agent= and answers 200
|
|
113
|
+
// with the GLOBAL set. Editing off that (then PUTting) would clobber shared
|
|
114
|
+
// context. Refuse unless the server echoes the agent it was asked for.
|
|
115
|
+
if (agent && data.agent !== agent) {
|
|
116
|
+
throw new ContextCliError(`server did not echo agent '${agent}' — it likely predates per-agent context (needs 0.13+)`);
|
|
117
|
+
}
|
|
118
|
+
return data;
|
|
107
119
|
}
|
|
108
120
|
/** PUT one section. Throws ContextCliError with a clear message on any failure. */
|
|
109
|
-
async function putSection(target, name, content) {
|
|
121
|
+
async function putSection(target, name, content, agent) {
|
|
110
122
|
let resp;
|
|
111
123
|
try {
|
|
112
|
-
resp = await fetch(
|
|
124
|
+
resp = await fetch(contextUrl(target.baseUrl, agent), {
|
|
113
125
|
method: "PUT",
|
|
114
126
|
headers: { "Content-Type": "application/json", ...authHeaders(target.authToken) },
|
|
115
127
|
body: JSON.stringify({ sections: { [name]: content } }),
|
|
@@ -122,6 +134,17 @@ async function putSection(target, name, content) {
|
|
|
122
134
|
if (!resp.ok) {
|
|
123
135
|
throw new ContextCliError(`PUT /context failed — ${describeFailure(resp.status, await readBodyError(resp))}`);
|
|
124
136
|
}
|
|
137
|
+
// Old-server echo guard on the WRITE too: a pre-0.13 server silently accepts
|
|
138
|
+
// the PUT as a GLOBAL write and returns 200 without echoing `agent`. Treat a
|
|
139
|
+
// missing echo as a failed agent write (the global section may have been
|
|
140
|
+
// clobbered — surface it loudly rather than reporting a false success).
|
|
141
|
+
if (agent) {
|
|
142
|
+
const body = (await resp.json().catch(() => ({})));
|
|
143
|
+
if (body.agent !== agent) {
|
|
144
|
+
throw new ContextCliError(`PUT /context?agent=${agent} — server did not echo the agent; it likely predates per-agent ` +
|
|
145
|
+
`context (needs 0.13+). It may have written to the GLOBAL scope — verify the server version.`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
125
148
|
}
|
|
126
149
|
// ---------------------------------------------------------------------------
|
|
127
150
|
// Pure formatters (tested directly; no I/O)
|
|
@@ -140,6 +163,8 @@ function formatAllSections(data) {
|
|
|
140
163
|
}
|
|
141
164
|
}
|
|
142
165
|
parts.push(`clients: ${data.clients.join(", ") || "(none)"}`);
|
|
166
|
+
if (data.agent)
|
|
167
|
+
parts.push(`agent: ${data.agent}${data.mode ? ` (mode: ${data.mode})` : ""}`);
|
|
143
168
|
return parts.join("\n");
|
|
144
169
|
}
|
|
145
170
|
/** Raw markdown for one section (show <name>), or null if it does not exist. */
|
|
@@ -176,8 +201,8 @@ const defaultSpawn = (file) => {
|
|
|
176
201
|
// ---------------------------------------------------------------------------
|
|
177
202
|
// Commands
|
|
178
203
|
// ---------------------------------------------------------------------------
|
|
179
|
-
async function runShow(name) {
|
|
180
|
-
const data = await getContext(resolveContextTarget(loadConfig()));
|
|
204
|
+
async function runShow(name, agent) {
|
|
205
|
+
const data = await getContext(resolveContextTarget(loadConfig()), agent);
|
|
181
206
|
if (name) {
|
|
182
207
|
const body = formatOneSection(data, name);
|
|
183
208
|
if (body === null) {
|
|
@@ -195,12 +220,12 @@ async function runShow(name) {
|
|
|
195
220
|
* current content → $EDITOR on a temp file → PUT only if changed. Temp file is
|
|
196
221
|
* always cleaned up. `spawn` is injectable for tests.
|
|
197
222
|
*/
|
|
198
|
-
async function runEdit(name, spawn = defaultSpawn) {
|
|
223
|
+
async function runEdit(name, spawn = defaultSpawn, agent) {
|
|
199
224
|
if (!(0, context_store_js_1.isValidSectionName)(name)) {
|
|
200
225
|
throw new ContextCliError(`Invalid section name '${name}'. Must match ^[a-z0-9][a-z0-9_-]*$ (max ${context_store_js_1.SECTION_NAME_MAX} chars).`);
|
|
201
226
|
}
|
|
202
227
|
const target = resolveContextTarget(loadConfig());
|
|
203
|
-
const data = await getContext(target);
|
|
228
|
+
const data = await getContext(target, agent);
|
|
204
229
|
const before = data.sections[name] ?? "";
|
|
205
230
|
const dir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "hicortex-ctx-"));
|
|
206
231
|
const file = (0, node_path_1.join)(dir, `${name}.md`);
|
|
@@ -215,26 +240,55 @@ async function runEdit(name, spawn = defaultSpawn) {
|
|
|
215
240
|
process.stdout.write("no changes\n");
|
|
216
241
|
return;
|
|
217
242
|
}
|
|
218
|
-
await putSection(target, name, after);
|
|
219
|
-
|
|
243
|
+
await putSection(target, name, after, agent);
|
|
244
|
+
// Saving to an agent scope creates/extends that agent's override.
|
|
245
|
+
process.stdout.write(`Saved section '${name}'${agent ? ` for agent '${agent}' (override)` : ""}.\n`);
|
|
220
246
|
}
|
|
221
247
|
finally {
|
|
222
248
|
(0, node_fs_1.rmSync)(dir, { recursive: true, force: true });
|
|
223
249
|
}
|
|
224
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Split out a `--agent <id>` flag (anywhere in argv) from the positional args.
|
|
253
|
+
* The flag omitted → the global scope.
|
|
254
|
+
*/
|
|
255
|
+
function extractAgentFlag(args) {
|
|
256
|
+
const rest = [];
|
|
257
|
+
let agent;
|
|
258
|
+
for (let i = 0; i < args.length; i++) {
|
|
259
|
+
if (args[i] === "--agent") {
|
|
260
|
+
const val = args[i + 1];
|
|
261
|
+
// A missing value (end of args) or the next token being another flag is a
|
|
262
|
+
// typo — never let it silently fall through to the global scope.
|
|
263
|
+
if (val === undefined || val.startsWith("-")) {
|
|
264
|
+
throw new ContextCliError("--agent requires a value, e.g. --agent lenny");
|
|
265
|
+
}
|
|
266
|
+
agent = val;
|
|
267
|
+
i++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
rest.push(args[i]);
|
|
271
|
+
}
|
|
272
|
+
return { agent, rest };
|
|
273
|
+
}
|
|
225
274
|
/** Dispatch for `hicortex context <sub>`. Throws ContextCliError on bad usage/failure. */
|
|
226
275
|
async function runContextCommand(args) {
|
|
227
|
-
const
|
|
276
|
+
const { agent, rest } = extractAgentFlag(args);
|
|
277
|
+
// Fast-fail on a bad agent id before any HTTP call (the server enforces too).
|
|
278
|
+
if (agent !== undefined && !(0, context_store_js_1.isValidAgentId)(agent)) {
|
|
279
|
+
throw new ContextCliError(`Invalid agent id '${agent}'. Must match ^[a-z0-9][a-z0-9_-]*$ (max ${context_store_js_1.SECTION_NAME_MAX} chars).`);
|
|
280
|
+
}
|
|
281
|
+
const sub = rest[0];
|
|
228
282
|
switch (sub) {
|
|
229
283
|
case "show":
|
|
230
|
-
await runShow(
|
|
284
|
+
await runShow(rest[1], agent);
|
|
231
285
|
return;
|
|
232
286
|
case "edit":
|
|
233
|
-
if (!
|
|
234
|
-
throw new ContextCliError("Usage: hicortex context edit <name>");
|
|
235
|
-
await runEdit(
|
|
287
|
+
if (!rest[1])
|
|
288
|
+
throw new ContextCliError("Usage: hicortex context edit <name> [--agent <id>]");
|
|
289
|
+
await runEdit(rest[1], undefined, agent);
|
|
236
290
|
return;
|
|
237
291
|
default:
|
|
238
|
-
throw new ContextCliError("Usage: hicortex context <show [name] | edit <name>>");
|
|
292
|
+
throw new ContextCliError("Usage: hicortex context <show [name] | edit <name>> [--agent <id>]");
|
|
239
293
|
}
|
|
240
294
|
}
|