@gamaze/hicortex 0.11.1 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/assets/context.html +487 -0
- package/dist/classify-domains.js +2 -2
- package/dist/cli.js +18 -0
- package/dist/context-cli.d.ts +58 -0
- package/dist/context-cli.js +240 -0
- package/dist/context-store.d.ts +120 -0
- package/dist/context-store.js +321 -0
- package/dist/db.js +2 -1
- package/dist/features.js +2 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +39 -21
- package/dist/init.d.ts +9 -0
- package/dist/init.js +41 -11
- package/dist/lessons-context.d.ts +20 -7
- package/dist/lessons-context.js +120 -30
- package/dist/mcp-server.js +71 -9
- package/dist/nightly-status.js +8 -1
- package/dist/nightly.js +21 -2
- package/dist/paths.d.ts +1 -0
- package/dist/paths.js +17 -0
- package/dist/relink.js +2 -2
- package/dist/retrieval.d.ts +1 -1
- package/dist/retrieval.js +2 -2
- package/dist/state.js +2 -2
- package/dist/status.js +2 -1
- package/dist/uninstall.js +31 -14
- package/dist/viz.d.ts +15 -0
- package/dist/viz.js +48 -0
- package/hermes-plugin/hicortex/README.md +4 -5
- package/hermes-plugin/hicortex/__init__.py +1 -1
- package/hermes-plugin/hicortex/client.py +2 -2
- package/hermes-plugin/hicortex/plugin.yaml +2 -2
- package/hermes-plugin/hicortex/provider.py +8 -21
- package/package.json +2 -2
- package/skills/hicortex-memory/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ The run is resumable — interrupt it any time and it continues where it stopped
|
|
|
110
110
|
8 tools available via MCP:
|
|
111
111
|
|
|
112
112
|
- **hicortex_search** — Semantic search across all stored memories
|
|
113
|
-
- **
|
|
113
|
+
- **hicortex_recent** — Get recent decisions and project state (queryless recall; renamed in 0.12)
|
|
114
114
|
- **hicortex_ingest** — Store a memory directly
|
|
115
115
|
- **hicortex_lessons** — Get actionable lessons from reflection
|
|
116
116
|
- **hicortex_index** — Get the knowledge domain index (what topics are stored)
|
|
@@ -120,6 +120,15 @@ The run is resumable — interrupt it any time and it continues where it stopped
|
|
|
120
120
|
|
|
121
121
|
Skills: `/learn` to save explicit learnings.
|
|
122
122
|
|
|
123
|
+
## Context Layer
|
|
124
|
+
|
|
125
|
+
Beyond auto-distilled memories and lessons, Hicortex holds a **hand-edited context layer** — standing "who you are + how to work" Markdown injected into every session at start. Unlike memories, it is **never distilled, scored, or decayed**: what you write stays verbatim until you change it.
|
|
126
|
+
|
|
127
|
+
- **Storage:** plain files on the server at `~/.hicortex/context/*.md` — one file per section (recommended starter sections `user.md` + `rules.md`, which you create — nothing is pre-populated; add more by dropping in a file). It lives outside the memories table; consolidation never touches it.
|
|
128
|
+
- **Edit:** the web editor at `http://localhost:8787/context/ui` (one tab per section, Save), or the CLI `hicortex context show [name]` / `hicortex context edit <name>`.
|
|
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
|
+
- **Deletion** is filesystem-only — remove the file on the server (as the daemon user).
|
|
131
|
+
|
|
123
132
|
## CLI Commands
|
|
124
133
|
|
|
125
134
|
```bash
|
|
@@ -130,6 +139,8 @@ npx @gamaze/hicortex nightly # Run distill + consolidate (full
|
|
|
130
139
|
npx @gamaze/hicortex nightly --capture-only # Capture only, skip consolidation (safe for sub-daily runs)
|
|
131
140
|
npx @gamaze/hicortex nightly --dry-run # Preview without changes
|
|
132
141
|
npx @gamaze/hicortex classify-domains # Backfill domain tags over the corpus (see Memory Domains & Tags)
|
|
142
|
+
npx @gamaze/hicortex context show [name] # Print the standing context layer (see Context Layer)
|
|
143
|
+
npx @gamaze/hicortex context edit <name> # Edit a context section in $EDITOR
|
|
133
144
|
npx @gamaze/hicortex status # Show config, DB stats
|
|
134
145
|
npx @gamaze/hicortex uninstall # Remove CC integration (keeps DB)
|
|
135
146
|
```
|
|
@@ -174,6 +185,7 @@ Config at `~/.hicortex/config.json`. Created by `init`. Key options:
|
|
|
174
185
|
| `domains` | Your memory domain list (`[{name, description}]`). Scaffolded by `init`; edit freely — see [Memory Domains & Tags](#memory-domains--tags) |
|
|
175
186
|
| `weakPrimaryFloor` | Minimum similarity for a no-fit memory to keep a weak domain association (default: 0.45) |
|
|
176
187
|
| `moduleIndexTokenBudget` | Max tokens for domain index in lessons context (default: 500) |
|
|
188
|
+
| `contextClients` | Which harnesses inject the [context layer](#context-layer) at session start (default `["cc"]`; `"all"` or any subset of `cc`/`hermes`/`oc`) |
|
|
177
189
|
| `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 |
|
|
178
190
|
| `telemetry` | Anonymous usage telemetry, `false` to opt out |
|
|
179
191
|
|
|
@@ -186,7 +198,9 @@ Full docs: [hicortex.gamaze.com/docs/configuration.html](https://hicortex.gamaze
|
|
|
186
198
|
| `/health` | GET | No | Server status, memory count, version |
|
|
187
199
|
| `/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. |
|
|
188
200
|
| `/search` | GET | Yes | Semantic memory search |
|
|
189
|
-
| `/
|
|
201
|
+
| `/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`) |
|
|
203
|
+
| `/context/ui` | GET | No* | Web editor for the context layer (shell served without auth, like `/viz`; data via `/context`) |
|
|
190
204
|
| `/lessons` | GET | Yes | Lessons + memory index (used by CC SessionStart hook) |
|
|
191
205
|
| `/ingest` | POST | Yes | Legacy: accept a single pre-distilled memory from older clients |
|
|
192
206
|
| `/sse` | GET | Yes | MCP SSE stream for agent connections |
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Hicortex — context</title>
|
|
7
|
+
<!--
|
|
8
|
+
Hicortex standing-context editor (#165, 0.12 — spec 2026-07-12 §5).
|
|
9
|
+
Self-contained: inline CSS/JS, ZERO external requests (CSP-safe). The PRIMARY
|
|
10
|
+
edit surface for the context layer.
|
|
11
|
+
|
|
12
|
+
One tab per section, tabs built dynamically from GET /context `sections`
|
|
13
|
+
(the server serves any allowlist-named *.md as a section — nothing hardcoded).
|
|
14
|
+
Each tab is a <textarea>; Save PUTs only the active section (partial upsert).
|
|
15
|
+
|
|
16
|
+
The page SHELL is public (exempted in createAuthMiddleware like /viz); the
|
|
17
|
+
GET/PUT /context data calls are bearer-only with a localhost bypass, so on the
|
|
18
|
+
server box no token is needed. Remote browsers hand off a token via
|
|
19
|
+
?token=<t> (stripped from the URL on load), localStorage, or a 401 prompt.
|
|
20
|
+
|
|
21
|
+
Security: section content is only ever read/written through textarea .value —
|
|
22
|
+
never innerHTML. Tab labels and messages use textContent. No markdown preview
|
|
23
|
+
in v1 (spec §5).
|
|
24
|
+
-->
|
|
25
|
+
<style>
|
|
26
|
+
:root {
|
|
27
|
+
--bg: #ffffff;
|
|
28
|
+
--panel: #f6f7f9;
|
|
29
|
+
--panel-border: #d9dce2;
|
|
30
|
+
--text: #1c1f26;
|
|
31
|
+
--text-dim: #656d7a;
|
|
32
|
+
--accent: #2f6fb0;
|
|
33
|
+
--accent-text: #ffffff;
|
|
34
|
+
--danger: #c0392b;
|
|
35
|
+
--ok: #2f8f4e;
|
|
36
|
+
--tab-active: #ffffff;
|
|
37
|
+
}
|
|
38
|
+
@media (prefers-color-scheme: dark) {
|
|
39
|
+
:root {
|
|
40
|
+
--bg: #0f1117;
|
|
41
|
+
--panel: #161a23;
|
|
42
|
+
--panel-border: #262c3a;
|
|
43
|
+
--text: #d5dae4;
|
|
44
|
+
--text-dim: #8b93a5;
|
|
45
|
+
--accent: #5b9dd9;
|
|
46
|
+
--accent-text: #0b0d12;
|
|
47
|
+
--danger: #e05555;
|
|
48
|
+
--ok: #5fbf77;
|
|
49
|
+
--tab-active: #0f1117;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/* Explicit theme override wins over the media default in BOTH directions. */
|
|
53
|
+
:root[data-theme="light"] {
|
|
54
|
+
--bg: #ffffff; --panel: #f6f7f9; --panel-border: #d9dce2;
|
|
55
|
+
--text: #1c1f26; --text-dim: #656d7a; --accent: #2f6fb0;
|
|
56
|
+
--accent-text: #ffffff; --danger: #c0392b; --ok: #2f8f4e; --tab-active: #ffffff;
|
|
57
|
+
}
|
|
58
|
+
:root[data-theme="dark"] {
|
|
59
|
+
--bg: #0f1117; --panel: #161a23; --panel-border: #262c3a;
|
|
60
|
+
--text: #d5dae4; --text-dim: #8b93a5; --accent: #5b9dd9;
|
|
61
|
+
--accent-text: #0b0d12; --danger: #e05555; --ok: #5fbf77; --tab-active: #0f1117;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
65
|
+
html, body { height: 100%; }
|
|
66
|
+
body {
|
|
67
|
+
background: var(--bg);
|
|
68
|
+
color: var(--text);
|
|
69
|
+
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
70
|
+
display: flex; flex-direction: column;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
header {
|
|
74
|
+
display: flex; align-items: baseline; gap: 12px;
|
|
75
|
+
padding: 12px 18px; border-bottom: 1px solid var(--panel-border);
|
|
76
|
+
}
|
|
77
|
+
header h1 { font-size: 15px; font-weight: 600; white-space: nowrap; }
|
|
78
|
+
header .meta { color: var(--text-dim); font-size: 12px; white-space: nowrap; }
|
|
79
|
+
header .spacer { flex: 1; }
|
|
80
|
+
|
|
81
|
+
main { flex: 1; display: flex; flex-direction: column; padding: 14px 18px; gap: 12px; min-height: 0; }
|
|
82
|
+
|
|
83
|
+
#tabs {
|
|
84
|
+
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
|
|
85
|
+
border-bottom: 1px solid var(--panel-border); padding-bottom: 0;
|
|
86
|
+
}
|
|
87
|
+
.tab {
|
|
88
|
+
background: transparent; color: var(--text-dim);
|
|
89
|
+
border: 1px solid transparent; border-bottom: none;
|
|
90
|
+
border-radius: 6px 6px 0 0; padding: 6px 14px; cursor: pointer;
|
|
91
|
+
font: inherit; font-size: 13px; position: relative; top: 1px;
|
|
92
|
+
}
|
|
93
|
+
.tab:hover { color: var(--text); }
|
|
94
|
+
.tab.active {
|
|
95
|
+
color: var(--text); background: var(--tab-active);
|
|
96
|
+
border-color: var(--panel-border); border-bottom: 1px solid var(--tab-active);
|
|
97
|
+
}
|
|
98
|
+
.tab .dirty { color: var(--accent); margin-left: 5px; }
|
|
99
|
+
|
|
100
|
+
#addwrap { margin-left: auto; display: flex; gap: 6px; align-items: center; }
|
|
101
|
+
#addname {
|
|
102
|
+
background: var(--bg); color: var(--text);
|
|
103
|
+
border: 1px solid var(--panel-border); border-radius: 5px;
|
|
104
|
+
padding: 5px 8px; font: inherit; font-size: 13px; width: 150px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
button.btn {
|
|
108
|
+
background: var(--accent); color: var(--accent-text);
|
|
109
|
+
border: none; border-radius: 5px; padding: 6px 14px;
|
|
110
|
+
font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
|
|
111
|
+
}
|
|
112
|
+
button.btn:hover { filter: brightness(1.08); }
|
|
113
|
+
button.btn.secondary {
|
|
114
|
+
background: transparent; color: var(--text);
|
|
115
|
+
border: 1px solid var(--panel-border); font-weight: 500;
|
|
116
|
+
}
|
|
117
|
+
button.btn:disabled { opacity: 0.5; cursor: default; filter: none; }
|
|
118
|
+
|
|
119
|
+
#editor { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
|
120
|
+
textarea {
|
|
121
|
+
flex: 1; width: 100%; min-height: 240px; resize: vertical;
|
|
122
|
+
background: var(--panel); color: var(--text);
|
|
123
|
+
border: 1px solid var(--panel-border); border-radius: 6px;
|
|
124
|
+
padding: 12px; font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
125
|
+
tab-size: 2;
|
|
126
|
+
}
|
|
127
|
+
textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
|
|
128
|
+
|
|
129
|
+
#bar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
130
|
+
#bar .grow { flex: 1; }
|
|
131
|
+
#msg { font-size: 13px; min-height: 18px; }
|
|
132
|
+
#msg.ok { color: var(--ok); }
|
|
133
|
+
#msg.err { color: var(--danger); }
|
|
134
|
+
#msg.dim { color: var(--text-dim); }
|
|
135
|
+
|
|
136
|
+
#empty { color: var(--text-dim); padding: 24px 0; }
|
|
137
|
+
#empty code { background: var(--panel); padding: 1px 5px; border-radius: 3px; }
|
|
138
|
+
|
|
139
|
+
/* token overlay */
|
|
140
|
+
#overlay {
|
|
141
|
+
position: fixed; inset: 0; display: none; z-index: 30;
|
|
142
|
+
align-items: center; justify-content: center;
|
|
143
|
+
background: rgba(0, 0, 0, 0.55);
|
|
144
|
+
}
|
|
145
|
+
#overlay.open { display: flex; }
|
|
146
|
+
#overlay .box {
|
|
147
|
+
background: var(--panel); border: 1px solid var(--panel-border);
|
|
148
|
+
border-radius: 8px; padding: 22px; max-width: 420px; width: 90%;
|
|
149
|
+
}
|
|
150
|
+
#overlay .box h2 { font-size: 15px; margin-bottom: 8px; }
|
|
151
|
+
#overlay .box p { color: var(--text-dim); margin-bottom: 12px; font-size: 13px; }
|
|
152
|
+
#overlay .box code { background: var(--bg); padding: 1px 5px; border-radius: 3px; }
|
|
153
|
+
#overlay input {
|
|
154
|
+
width: 100%; background: var(--bg); color: var(--text);
|
|
155
|
+
border: 1px solid var(--panel-border); border-radius: 5px;
|
|
156
|
+
padding: 8px; font: inherit; margin-bottom: 10px;
|
|
157
|
+
}
|
|
158
|
+
#token-err { color: var(--danger); font-size: 13px; margin-top: 8px; display: none; }
|
|
159
|
+
</style>
|
|
160
|
+
</head>
|
|
161
|
+
<body>
|
|
162
|
+
<header>
|
|
163
|
+
<h1>Hicortex — context</h1>
|
|
164
|
+
<span class="meta" id="clients"></span>
|
|
165
|
+
<div class="spacer"></div>
|
|
166
|
+
<span class="meta" id="updated"></span>
|
|
167
|
+
</header>
|
|
168
|
+
|
|
169
|
+
<main>
|
|
170
|
+
<div id="tabs">
|
|
171
|
+
<div id="addwrap">
|
|
172
|
+
<input type="text" id="addname" placeholder="new-section-name" autocomplete="off" spellcheck="false">
|
|
173
|
+
<button class="btn secondary" id="add">Add</button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div id="empty" style="display:none">
|
|
178
|
+
No sections yet. Add one above (e.g. <code>user</code> or <code>rules</code>) — a lowercase name
|
|
179
|
+
of letters, digits, <code>-</code> or <code>_</code>.
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div id="editor" style="display:none">
|
|
183
|
+
<textarea id="area" spellcheck="false" placeholder="# Markdown…"></textarea>
|
|
184
|
+
<div id="bar" style="margin-top:12px">
|
|
185
|
+
<button class="btn" id="save">Save</button>
|
|
186
|
+
<span class="grow"></span>
|
|
187
|
+
<span id="msg" class="dim"></span>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</main>
|
|
191
|
+
|
|
192
|
+
<div id="overlay">
|
|
193
|
+
<div class="box">
|
|
194
|
+
<h2>Authentication required</h2>
|
|
195
|
+
<p>This server needs a token for remote access.
|
|
196
|
+
Run <code>hicortex status</code> on the server to get it.</p>
|
|
197
|
+
<input type="password" id="token-input" placeholder="hctx-…" autocomplete="off">
|
|
198
|
+
<button class="btn" id="token-submit">Connect</button>
|
|
199
|
+
<div id="token-err">Still unauthorized — check the token.</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<script>
|
|
204
|
+
"use strict";
|
|
205
|
+
(function () {
|
|
206
|
+
// =========================================================================
|
|
207
|
+
// Section-name allowlist — mirrors the server contract (context-store.ts
|
|
208
|
+
// SECTION_NAME_RE / SECTION_NAME_MAX). Client check is UX; the server also
|
|
209
|
+
// enforces it and is the authority.
|
|
210
|
+
// =========================================================================
|
|
211
|
+
var NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
212
|
+
var NAME_MAX = 64;
|
|
213
|
+
function validName(n) {
|
|
214
|
+
return typeof n === "string" && n.length >= 1 && n.length <= NAME_MAX && NAME_RE.test(n);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// =========================================================================
|
|
218
|
+
// Auth token handling — mirrors assets/viz.html: ?token= handoff (stripped
|
|
219
|
+
// from history immediately), localStorage persistence, in-page 401 prompt.
|
|
220
|
+
// On localhost the server bypasses auth, so tokenless fetches just work and
|
|
221
|
+
// the prompt only appears on a real 401.
|
|
222
|
+
// =========================================================================
|
|
223
|
+
var token = null;
|
|
224
|
+
(function initToken() {
|
|
225
|
+
var params = new URLSearchParams(window.location.search);
|
|
226
|
+
var fromUrl = params.get("token");
|
|
227
|
+
if (fromUrl) {
|
|
228
|
+
token = fromUrl;
|
|
229
|
+
try { localStorage.setItem("hicortexToken", fromUrl); } catch (e) { /* private mode */ }
|
|
230
|
+
params.delete("token");
|
|
231
|
+
var qs = params.toString();
|
|
232
|
+
history.replaceState(null, "", window.location.pathname + (qs ? "?" + qs : ""));
|
|
233
|
+
} else {
|
|
234
|
+
try { token = localStorage.getItem("hicortexToken"); } catch (e) { token = null; }
|
|
235
|
+
}
|
|
236
|
+
})();
|
|
237
|
+
|
|
238
|
+
function authHeaders(extra) {
|
|
239
|
+
var h = extra ? Object.assign({}, extra) : {};
|
|
240
|
+
if (token) h["Authorization"] = "Bearer " + token;
|
|
241
|
+
return h;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// =========================================================================
|
|
245
|
+
// DOM refs
|
|
246
|
+
// =========================================================================
|
|
247
|
+
var tabsEl = document.getElementById("tabs");
|
|
248
|
+
var addWrap = document.getElementById("addwrap");
|
|
249
|
+
var addName = document.getElementById("addname");
|
|
250
|
+
var addBtn = document.getElementById("add");
|
|
251
|
+
var emptyEl = document.getElementById("empty");
|
|
252
|
+
var editorEl = document.getElementById("editor");
|
|
253
|
+
var area = document.getElementById("area");
|
|
254
|
+
var saveBtn = document.getElementById("save");
|
|
255
|
+
var msgEl = document.getElementById("msg");
|
|
256
|
+
var clientsEl = document.getElementById("clients");
|
|
257
|
+
var updatedEl = document.getElementById("updated");
|
|
258
|
+
var overlay = document.getElementById("overlay");
|
|
259
|
+
var tokenInput = document.getElementById("token-input");
|
|
260
|
+
var tokenErr = document.getElementById("token-err");
|
|
261
|
+
|
|
262
|
+
// =========================================================================
|
|
263
|
+
// State — sections is name -> { saved, draft, isNew }
|
|
264
|
+
// =========================================================================
|
|
265
|
+
var sections = {}; // name -> { saved: string, draft: string, isNew: bool }
|
|
266
|
+
var order = []; // tab order (section names)
|
|
267
|
+
var active = null; // active section name
|
|
268
|
+
var afterAuth = null; // action to retry after a successful token prompt
|
|
269
|
+
|
|
270
|
+
function setMsg(text, cls) {
|
|
271
|
+
msgEl.textContent = text || "";
|
|
272
|
+
msgEl.className = cls || "dim";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isDirty(name) {
|
|
276
|
+
var s = sections[name];
|
|
277
|
+
return !!s && (s.isNew || s.draft !== s.saved);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// =========================================================================
|
|
281
|
+
// Rendering — tab labels are textContent only (never innerHTML). Section
|
|
282
|
+
// content lives exclusively in the textarea .value.
|
|
283
|
+
// =========================================================================
|
|
284
|
+
function renderTabs() {
|
|
285
|
+
// Remove existing tab buttons but keep the add-section control.
|
|
286
|
+
var btns = tabsEl.querySelectorAll(".tab");
|
|
287
|
+
for (var i = 0; i < btns.length; i++) tabsEl.removeChild(btns[i]);
|
|
288
|
+
|
|
289
|
+
order.forEach(function (name) {
|
|
290
|
+
var b = document.createElement("button");
|
|
291
|
+
b.className = "tab" + (name === active ? " active" : "");
|
|
292
|
+
b.type = "button";
|
|
293
|
+
b.appendChild(document.createTextNode(name));
|
|
294
|
+
if (isDirty(name)) {
|
|
295
|
+
var dot = document.createElement("span");
|
|
296
|
+
dot.className = "dirty";
|
|
297
|
+
dot.textContent = "•"; // bullet = unsaved
|
|
298
|
+
b.appendChild(dot);
|
|
299
|
+
}
|
|
300
|
+
b.addEventListener("click", function () { selectTab(name); });
|
|
301
|
+
tabsEl.insertBefore(b, addWrap);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
var hasTabs = order.length > 0;
|
|
305
|
+
emptyEl.style.display = hasTabs ? "none" : "block";
|
|
306
|
+
editorEl.style.display = hasTabs && active ? "flex" : "none";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function selectTab(name) {
|
|
310
|
+
// Persist the current textarea into the draft before switching.
|
|
311
|
+
if (active && sections[active]) sections[active].draft = area.value;
|
|
312
|
+
active = name;
|
|
313
|
+
area.value = sections[name] ? sections[name].draft : "";
|
|
314
|
+
setMsg("", "dim");
|
|
315
|
+
renderTabs();
|
|
316
|
+
area.focus();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function updateMeta(clients, updatedAt) {
|
|
320
|
+
if (Array.isArray(clients)) {
|
|
321
|
+
clientsEl.textContent = clients.length ? "clients: " + clients.join(", ") : "clients: none";
|
|
322
|
+
}
|
|
323
|
+
if (updatedAt !== undefined) {
|
|
324
|
+
updatedEl.textContent = updatedAt ? "updated " + updatedAt : "";
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// =========================================================================
|
|
329
|
+
// Load — GET /context, build tabs from the returned sections keys.
|
|
330
|
+
// =========================================================================
|
|
331
|
+
function load() {
|
|
332
|
+
setMsg("Loading…", "dim");
|
|
333
|
+
fetch("/context", { headers: authHeaders() })
|
|
334
|
+
.then(function (resp) {
|
|
335
|
+
if (resp.status === 401) { afterAuth = load; showTokenPrompt(); throw new Error("unauthorized"); }
|
|
336
|
+
if (!resp.ok) {
|
|
337
|
+
return resp.json().catch(function () { return {}; }).then(function (body) {
|
|
338
|
+
throw new Error(body.error || ("Server error " + resp.status));
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return resp.json();
|
|
342
|
+
})
|
|
343
|
+
.then(function (data) {
|
|
344
|
+
overlay.classList.remove("open");
|
|
345
|
+
var incoming = data.sections || {};
|
|
346
|
+
var names = Object.keys(incoming).sort();
|
|
347
|
+
sections = {};
|
|
348
|
+
order = [];
|
|
349
|
+
names.forEach(function (n) {
|
|
350
|
+
sections[n] = { saved: incoming[n], draft: incoming[n], isNew: false };
|
|
351
|
+
order.push(n);
|
|
352
|
+
});
|
|
353
|
+
// Keep the active tab if it still exists, else pick the first.
|
|
354
|
+
if (!active || order.indexOf(active) < 0) active = order.length ? order[0] : null;
|
|
355
|
+
if (active) area.value = sections[active].draft;
|
|
356
|
+
updateMeta(data.clients, data.updated_at || null);
|
|
357
|
+
renderTabs();
|
|
358
|
+
setMsg(order.length ? "" : "", "dim");
|
|
359
|
+
})
|
|
360
|
+
.catch(function (err) {
|
|
361
|
+
if (err.message === "unauthorized") return;
|
|
362
|
+
setMsg("Failed to load: " + err.message, "err");
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// =========================================================================
|
|
367
|
+
// Save — PUT /context with ONLY the active section (partial upsert).
|
|
368
|
+
// =========================================================================
|
|
369
|
+
function save() {
|
|
370
|
+
if (!active) return;
|
|
371
|
+
if (sections[active]) sections[active].draft = area.value;
|
|
372
|
+
var name = active;
|
|
373
|
+
var value = area.value;
|
|
374
|
+
|
|
375
|
+
if (!validName(name)) {
|
|
376
|
+
setMsg("Invalid section name: " + name, "err");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
var payload = { sections: {} };
|
|
381
|
+
payload.sections[name] = value;
|
|
382
|
+
|
|
383
|
+
saveBtn.disabled = true;
|
|
384
|
+
setMsg("Saving…", "dim");
|
|
385
|
+
fetch("/context", {
|
|
386
|
+
method: "PUT",
|
|
387
|
+
headers: authHeaders({ "Content-Type": "application/json" }),
|
|
388
|
+
body: JSON.stringify(payload),
|
|
389
|
+
})
|
|
390
|
+
.then(function (resp) {
|
|
391
|
+
if (resp.status === 401) {
|
|
392
|
+
afterAuth = save;
|
|
393
|
+
showTokenPrompt();
|
|
394
|
+
throw new Error("unauthorized");
|
|
395
|
+
}
|
|
396
|
+
return resp.json().catch(function () { return {}; }).then(function (body) {
|
|
397
|
+
if (!resp.ok) throw new Error(body.error || ("Server error " + resp.status));
|
|
398
|
+
return body;
|
|
399
|
+
});
|
|
400
|
+
})
|
|
401
|
+
.then(function (body) {
|
|
402
|
+
overlay.classList.remove("open");
|
|
403
|
+
var s = sections[name];
|
|
404
|
+
if (s) { s.saved = value; s.isNew = false; }
|
|
405
|
+
updateMeta(undefined, body.updated_at || null);
|
|
406
|
+
setMsg("Saved" + (body.updated_at ? " · " + body.updated_at : ""), "ok");
|
|
407
|
+
renderTabs();
|
|
408
|
+
})
|
|
409
|
+
.catch(function (err) {
|
|
410
|
+
if (err.message !== "unauthorized") setMsg("Save failed: " + err.message, "err");
|
|
411
|
+
})
|
|
412
|
+
.then(function () { saveBtn.disabled = false; });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// =========================================================================
|
|
416
|
+
// Add a new section (client-side; created on first Save). Validated against
|
|
417
|
+
// the same allowlist BEFORE it can be edited — inline error otherwise.
|
|
418
|
+
// =========================================================================
|
|
419
|
+
function addSection() {
|
|
420
|
+
var name = addName.value.trim();
|
|
421
|
+
if (!validName(name)) {
|
|
422
|
+
setMsg("Invalid name — use lowercase letters, digits, - or _ (max 64, no leading - or _).", "err");
|
|
423
|
+
addName.focus();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (sections[name]) {
|
|
427
|
+
// Already exists — just switch to it.
|
|
428
|
+
addName.value = "";
|
|
429
|
+
selectTab(name);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
sections[name] = { saved: "", draft: "", isNew: true };
|
|
433
|
+
order.push(name);
|
|
434
|
+
addName.value = "";
|
|
435
|
+
selectTab(name);
|
|
436
|
+
setMsg("New section “" + name + "” — edit and Save to create it.", "dim");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// =========================================================================
|
|
440
|
+
// Token prompt (remote only — localhost bypasses auth)
|
|
441
|
+
// =========================================================================
|
|
442
|
+
function showTokenPrompt() {
|
|
443
|
+
setMsg("", "dim");
|
|
444
|
+
overlay.classList.add("open");
|
|
445
|
+
tokenInput.focus();
|
|
446
|
+
}
|
|
447
|
+
function submitToken() {
|
|
448
|
+
var v = tokenInput.value.trim();
|
|
449
|
+
if (!v) return;
|
|
450
|
+
token = v;
|
|
451
|
+
try { localStorage.setItem("hicortexToken", v); } catch (e) { /* ignore */ }
|
|
452
|
+
tokenErr.style.display = "none";
|
|
453
|
+
// Verify with a cheap GET before closing; retry the pending action on success.
|
|
454
|
+
fetch("/context", { headers: authHeaders() }).then(function (resp) {
|
|
455
|
+
if (resp.status === 401) { tokenErr.style.display = "block"; return; }
|
|
456
|
+
overlay.classList.remove("open");
|
|
457
|
+
var next = afterAuth || load;
|
|
458
|
+
afterAuth = null;
|
|
459
|
+
next();
|
|
460
|
+
}).catch(function () { tokenErr.style.display = "block"; });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// =========================================================================
|
|
464
|
+
// Wiring
|
|
465
|
+
// =========================================================================
|
|
466
|
+
saveBtn.addEventListener("click", save);
|
|
467
|
+
addBtn.addEventListener("click", addSection);
|
|
468
|
+
addName.addEventListener("keydown", function (e) { if (e.key === "Enter") addSection(); });
|
|
469
|
+
area.addEventListener("input", function () {
|
|
470
|
+
if (active && sections[active]) sections[active].draft = area.value;
|
|
471
|
+
renderTabs();
|
|
472
|
+
});
|
|
473
|
+
// Cmd/Ctrl+S saves the active tab.
|
|
474
|
+
window.addEventListener("keydown", function (e) {
|
|
475
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === "s" || e.key === "S")) {
|
|
476
|
+
e.preventDefault();
|
|
477
|
+
save();
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
document.getElementById("token-submit").addEventListener("click", submitToken);
|
|
481
|
+
tokenInput.addEventListener("keydown", function (e) { if (e.key === "Enter") submitToken(); });
|
|
482
|
+
|
|
483
|
+
load();
|
|
484
|
+
})();
|
|
485
|
+
</script>
|
|
486
|
+
</body>
|
|
487
|
+
</html>
|
package/dist/classify-domains.js
CHANGED
|
@@ -73,9 +73,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
73
73
|
})();
|
|
74
74
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
75
75
|
exports.runClassifyDomains = runClassifyDomains;
|
|
76
|
+
const paths_js_1 = require("./paths.js");
|
|
76
77
|
const node_fs_1 = require("node:fs");
|
|
77
78
|
const node_path_1 = require("node:path");
|
|
78
|
-
const node_os_1 = require("node:os");
|
|
79
79
|
const db_js_1 = require("./db.js");
|
|
80
80
|
const state_js_1 = require("./state.js");
|
|
81
81
|
const storage = __importStar(require("./storage.js"));
|
|
@@ -84,7 +84,7 @@ const domain_classify_js_1 = require("./domain-classify.js");
|
|
|
84
84
|
const consolidate_js_1 = require("./consolidate.js");
|
|
85
85
|
const schema_prototypes_js_1 = require("./schema-prototypes.js");
|
|
86
86
|
const nofit_js_1 = require("./nofit.js");
|
|
87
|
-
const HICORTEX_HOME = (0,
|
|
87
|
+
const HICORTEX_HOME = (0, paths_js_1.hicortexHome)();
|
|
88
88
|
function readConfig(stateDir) {
|
|
89
89
|
try {
|
|
90
90
|
return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(stateDir, "config.json"), "utf-8"));
|
package/dist/cli.js
CHANGED
|
@@ -114,6 +114,21 @@ switch (command) {
|
|
|
114
114
|
});
|
|
115
115
|
break;
|
|
116
116
|
}
|
|
117
|
+
case "context": {
|
|
118
|
+
// Standing context layer edit surface (spec §6): show|edit against the
|
|
119
|
+
// configured server. Secondary to the /context/ui Web UI; for headless boxes.
|
|
120
|
+
const args = process.argv.slice(3);
|
|
121
|
+
import("./context-cli.js").then(({ runContextCommand, ContextCliError }) => {
|
|
122
|
+
runContextCommand(args).catch((err) => {
|
|
123
|
+
if (err instanceof ContextCliError)
|
|
124
|
+
console.error(err.message);
|
|
125
|
+
else
|
|
126
|
+
console.error("[hicortex] context command failed:", err instanceof Error ? err.message : err);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
117
132
|
case "status":
|
|
118
133
|
import("./status.js").then(({ runStatus }) => {
|
|
119
134
|
runStatus().catch((err) => {
|
|
@@ -159,6 +174,7 @@ Commands:
|
|
|
159
174
|
relink Resumable link-discovery pass over the ENTIRE corpus (server mode)
|
|
160
175
|
classify-domains Backfill content-based domain tags over the corpus (server mode, needs config.domains)
|
|
161
176
|
lessons-context Fetch lessons and print Markdown to stdout (CC SessionStart hook)
|
|
177
|
+
context Standing context layer (show|edit) against the configured server
|
|
162
178
|
status Show current configuration and stats
|
|
163
179
|
uninstall Remove CC integration (preserves DB)
|
|
164
180
|
|
|
@@ -174,6 +190,8 @@ Options:
|
|
|
174
190
|
classify-domains --all Reclassify every memory (default: only NULL/stale-domain rows)
|
|
175
191
|
classify-domains --batch <n> Memories per batch (default: 200)
|
|
176
192
|
classify-domains --reset Restart from the beginning (ignore saved cursor)
|
|
193
|
+
context show [name] Print all context sections, or just <name> (raw, pipeable)
|
|
194
|
+
context edit <name> Edit a section in $EDITOR; PUT only if changed
|
|
177
195
|
|
|
178
196
|
Examples:
|
|
179
197
|
npx @gamaze/hicortex server
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-cli — `hicortex context show|edit`, the secondary/headless edit
|
|
3
|
+
* surface for the standing context layer (spec 2026-07-12 §6). The Web UI
|
|
4
|
+
* (`/context/ui`) is primary; this exists for boxes without a browser.
|
|
5
|
+
*
|
|
6
|
+
* hicortex context show [name] GET /context → print all sections, or one
|
|
7
|
+
* hicortex context edit <name> GET section → $EDITOR → PUT if changed
|
|
8
|
+
*
|
|
9
|
+
* URL/token resolution mirrors lessons-context.ts:44-49 (client mode →
|
|
10
|
+
* config.serverUrl; server mode → http://127.0.0.1:<port>; token from
|
|
11
|
+
* config.authToken) — explicitly NOT the hardcoded 127.0.0.1:8787 of
|
|
12
|
+
* status.ts. Fails soft with a clear message + non-zero exit on any server
|
|
13
|
+
* error, distinguishing a down server from an HTTP error (esp. 404 = server
|
|
14
|
+
* too old / wrong endpoint), like the OC plugin's describeGetFailure.
|
|
15
|
+
*/
|
|
16
|
+
/** Thrown for any expected, user-facing failure. cli.ts prints .message + exits 1. */
|
|
17
|
+
export declare class ContextCliError extends Error {
|
|
18
|
+
}
|
|
19
|
+
export interface ContextServerTarget {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
authToken?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the server URL + token from a parsed config object. Pure + exported
|
|
25
|
+
* so it is unit-testable without a live config. Follows lessons-context.ts.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveContextTarget(config: Record<string, unknown>): ContextServerTarget;
|
|
28
|
+
/** Read ~/.hicortex/config.json (or $HICORTEX_HOME/config.json). Missing → {}. */
|
|
29
|
+
export declare function loadConfig(): Record<string, unknown>;
|
|
30
|
+
/** The Save decision: PUT only when the edited content differs. Pure + tested. */
|
|
31
|
+
export declare function sectionChanged(before: string, after: string): boolean;
|
|
32
|
+
export interface ContextGetResponse {
|
|
33
|
+
sections: Record<string, string>;
|
|
34
|
+
updated_at: string | null;
|
|
35
|
+
clients: string[];
|
|
36
|
+
}
|
|
37
|
+
/** GET /context. Throws ContextCliError with a clear message on any failure. */
|
|
38
|
+
export declare function getContext(target: ContextServerTarget): Promise<ContextGetResponse>;
|
|
39
|
+
/** 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>;
|
|
41
|
+
/** Readable rendering of every section + the resolved clients line (show, no name). */
|
|
42
|
+
export declare function formatAllSections(data: ContextGetResponse): string;
|
|
43
|
+
/** Raw markdown for one section (show <name>), or null if it does not exist. */
|
|
44
|
+
export declare function formatOneSection(data: ContextGetResponse, name: string): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Default editor spawn: opens `file` in the first available editor, blocking
|
|
47
|
+
* until it exits. Returns true if an editor ran, false if none was found.
|
|
48
|
+
* Injectable so tests never actually spawn an editor.
|
|
49
|
+
*/
|
|
50
|
+
export type EditorSpawn = (file: string) => boolean;
|
|
51
|
+
/**
|
|
52
|
+
* `edit <name>`: validate name (fast-fail; the server enforces too) → fetch
|
|
53
|
+
* current content → $EDITOR on a temp file → PUT only if changed. Temp file is
|
|
54
|
+
* always cleaned up. `spawn` is injectable for tests.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runEdit(name: string, spawn?: EditorSpawn): Promise<void>;
|
|
57
|
+
/** Dispatch for `hicortex context <sub>`. Throws ContextCliError on bad usage/failure. */
|
|
58
|
+
export declare function runContextCommand(args: string[]): Promise<void>;
|