@jhizzard/termdeck 0.2.4 → 0.3.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 +12 -4
- package/package.json +2 -1
- package/packages/cli/src/init-rumen.js +99 -10
- package/packages/client/public/app.js +2315 -0
- package/packages/client/public/index.html +39 -3280
- package/packages/client/public/style.css +1439 -0
- package/packages/server/src/index.js +175 -3
- package/packages/server/src/rag.js +7 -1
- package/packages/server/src/session.js +38 -5
- package/packages/server/src/setup/rumen/functions/rumen-tick/index.ts +6 -1
- package/packages/server/src/setup/rumen/migrations/001_rumen_tables.sql +39 -3
package/README.md
CHANGED
|
@@ -127,11 +127,19 @@ Restart Claude Code. Six MCP tools appear: `memory_remember`, `memory_recall`, `
|
|
|
127
127
|
|
|
128
128
|
### Tier 3 — Add Rumen for async learning
|
|
129
129
|
|
|
130
|
-
Rumen is a separate npm package — `@jhizzard/rumen@0.
|
|
130
|
+
Rumen is a separate npm package — `@jhizzard/rumen@0.3.6` — that ships as a Supabase Edge Function designed to run on a 15-minute `pg_cron` schedule. It's the async reflection layer over Mnestra: it reads recent session memories, cross-references them with your entire historical corpus via hybrid search, synthesizes insights via Claude Haiku, and writes the results back into `rumen_insights` (a new table alongside Mnestra's `memory_items`). TermDeck's Flashback and Claude Code's `memory_recall` both automatically benefit because insights flow back into the same database.
|
|
131
131
|
|
|
132
|
-
Rumen
|
|
132
|
+
**Rumen is live.** First full-kickstart run against a production Mnestra store on 2026-04-15 19:47 UTC: **111 sessions processed, 111 insights generated** in one pass. Insights surfaced patterns like "the error detection regex in Flashback misses `No such file or directory` — same class of blind spot as X" and "Practice sessions exist as a separate model but frontend components were built and never wired into the schedule view." The cognitive loop is closed.
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
Deploy with **one command**:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
termdeck init --rumen
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The wizard auto-resolves the latest published `@jhizzard/rumen` version from npm at deploy time, applies the self-healing SQL migration, stages the Edge Function with the correct version pin, deploys via the Supabase CLI, sets function secrets, applies the `pg_cron` schedule, and POSTs a manual test invocation. Prerequisites: Supabase CLI, Deno, and a `DATABASE_URL` pointing at a Mnestra-compatible Postgres. Full walkthrough including the five setup gotchas (the hidden IPv4 toggle in the Supabase Connect modal, the literal-string password bug, `DATABASE_URL` only / no `DIRECT_URL`, the macOS-13 + Homebrew + Deno incompatibility, and schema drift handling) is at **[rumen/install.md](https://github.com/jhizzard/rumen/blob/main/install.md)**.
|
|
141
|
+
|
|
142
|
+
**Why you'd want Rumen:** without it, Flashback only surfaces memories that structurally match the current context (same project, similar error). With Rumen, Flashback surfaces **cross-project patterns** that Haiku synthesized while you were away — "the CORS fix you applied in Project A probably solves this error in Project B." That's the moat.
|
|
135
143
|
|
|
136
144
|
---
|
|
137
145
|
|
|
@@ -143,7 +151,7 @@ Honest limits, stated upfront so the skeptic has nothing to chase:
|
|
|
143
151
|
- **Not a replacement for reading docs.** It's the shortest path to a memory you already wrote. If the memory isn't there, the feature does nothing.
|
|
144
152
|
- **Not fully local by default.** Tier 2+ reaches out to Supabase for storage and OpenAI for embeddings. Tier 1 is fully local. A fully-local Tier 2 (local Postgres + local embeddings) is on the Sprint 3 roadmap.
|
|
145
153
|
- **Not free forever.** Tier 2+ pays OpenAI fractions of a cent per memory for embeddings. Self-hosted embeddings via Ollama are on the roadmap.
|
|
146
|
-
- **Not proven at scale.** v0.2, validated against 3,
|
|
154
|
+
- **Not proven at scale.** v0.2.5, validated against 3,527 memories in one developer's production store. First full Rumen kickstart on 2026-04-15 processed 111 sessions into 111 insights in one pass. No multi-user data yet. Bug reports and issues welcome.
|
|
147
155
|
|
|
148
156
|
---
|
|
149
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Browser-based terminal multiplexer with metadata overlays, panel flashback memory recall, and AI-aware session management",
|
|
5
5
|
"bin": {
|
|
6
6
|
"termdeck": "./packages/cli/src/index.js"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"dev": "node packages/server/src/index.js",
|
|
25
25
|
"server": "node packages/server/src/index.js",
|
|
26
26
|
"start": "NODE_ENV=production node packages/cli/src/index.js",
|
|
27
|
+
"test": "node --test packages/server/tests/**/*.test.js",
|
|
27
28
|
"install:app": "bash install.sh"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
// - `deno` on PATH
|
|
10
10
|
// - `~/.termdeck/secrets.env` with SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY +
|
|
11
11
|
// DATABASE_URL + ANTHROPIC_API_KEY populated (run `termdeck init --mnestra` first)
|
|
12
|
+
// - OPENAI_API_KEY in secrets.env is OPTIONAL: when present, Rumen's Relate
|
|
13
|
+
// phase uses semantic+keyword hybrid search via OpenAI text-embedding-3-large.
|
|
14
|
+
// When absent, Rumen falls back to keyword-only matching.
|
|
12
15
|
//
|
|
13
16
|
// Steps:
|
|
14
17
|
// 1. Preflight: which supabase, which deno, read secrets.env
|
|
@@ -16,7 +19,7 @@
|
|
|
16
19
|
// 3. supabase link --project-ref <ref>
|
|
17
20
|
// 4. Apply rumen migration 001 via pg
|
|
18
21
|
// 5. supabase functions deploy rumen-tick --no-verify-jwt
|
|
19
|
-
// 6. supabase secrets set DATABASE_URL=... ANTHROPIC_API_KEY=...
|
|
22
|
+
// 6. supabase secrets set DATABASE_URL=... ANTHROPIC_API_KEY=... [OPENAI_API_KEY=...]
|
|
20
23
|
// 7. Test the function with a manual POST (fetch)
|
|
21
24
|
// 8. Apply pg_cron schedule migration (002) with project ref substituted
|
|
22
25
|
|
|
@@ -34,6 +37,44 @@ const {
|
|
|
34
37
|
pgRunner
|
|
35
38
|
} = require(SETUP_DIR);
|
|
36
39
|
|
|
40
|
+
// Pinned fallback used only when the npm registry is unreachable. Bump this
|
|
41
|
+
// when you republish @jhizzard/rumen and can't (or won't) rely on `npm view`
|
|
42
|
+
// at deploy time. The wizard prefers the live registry answer — this value
|
|
43
|
+
// exists so a fully offline machine can still ship a working Edge Function.
|
|
44
|
+
const FALLBACK_RUMEN_VERSION = '0.3.4';
|
|
45
|
+
|
|
46
|
+
// Resolve the current published version of @jhizzard/rumen. Source of truth
|
|
47
|
+
// is the npm registry (because the Edge Function runtime pulls from there,
|
|
48
|
+
// not from local node_modules). On network failure we fall back to
|
|
49
|
+
// FALLBACK_RUMEN_VERSION and tell the user what happened — we don't silently
|
|
50
|
+
// deploy a bogus version.
|
|
51
|
+
function resolveRumenVersion() {
|
|
52
|
+
const r = spawnSync('npm', ['view', '@jhizzard/rumen', 'version'], {
|
|
53
|
+
encoding: 'utf-8',
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
|
+
timeout: 15000
|
|
56
|
+
});
|
|
57
|
+
if (r.status === 0) {
|
|
58
|
+
const version = (r.stdout || '').trim();
|
|
59
|
+
if (/^\d+\.\d+\.\d+/.test(version)) {
|
|
60
|
+
return { version, source: 'npm registry' };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
version: FALLBACK_RUMEN_VERSION,
|
|
64
|
+
source: 'pinned fallback',
|
|
65
|
+
warning: `npm view returned unexpected output: ${JSON.stringify(version)}`
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const stderr = (r.stderr || '').trim();
|
|
69
|
+
return {
|
|
70
|
+
version: FALLBACK_RUMEN_VERSION,
|
|
71
|
+
source: 'pinned fallback',
|
|
72
|
+
warning: stderr
|
|
73
|
+
? `npm view failed: ${stderr.split('\n').pop()}`
|
|
74
|
+
: `npm view failed (exit ${r.status === null ? 'timeout' : r.status}) — offline?`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
37
78
|
const HELP = [
|
|
38
79
|
'',
|
|
39
80
|
'TermDeck Rumen Setup',
|
|
@@ -125,6 +166,18 @@ function preflight() {
|
|
|
125
166
|
}
|
|
126
167
|
ok();
|
|
127
168
|
|
|
169
|
+
// OPENAI_API_KEY is optional: when present, Rumen's Relate phase generates
|
|
170
|
+
// real embeddings for semantic+keyword hybrid search. When absent, Rumen
|
|
171
|
+
// falls back to keyword-only matching (still works, but loses cross-project
|
|
172
|
+
// conceptual retrieval).
|
|
173
|
+
if (!secrets.OPENAI_API_KEY) {
|
|
174
|
+
process.stderr.write(
|
|
175
|
+
'\n⚠ OPENAI_API_KEY is not set in ~/.termdeck/secrets.env.\n' +
|
|
176
|
+
' Rumen will run in keyword-only mode — for full cross-project conceptual\n' +
|
|
177
|
+
' retrieval, add OPENAI_API_KEY to secrets.env and re-run `termdeck init --rumen`.\n\n'
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
128
181
|
return { supabaseBin: sb, denoBin: deno, secrets };
|
|
129
182
|
}
|
|
130
183
|
|
|
@@ -196,7 +249,7 @@ async function applyRumenTables(secrets, dryRun) {
|
|
|
196
249
|
}
|
|
197
250
|
}
|
|
198
251
|
|
|
199
|
-
function deployFunction(dryRun) {
|
|
252
|
+
function deployFunction(rumenVersion, dryRun) {
|
|
200
253
|
step('Running: supabase functions deploy rumen-tick --no-verify-jwt...');
|
|
201
254
|
if (dryRun) { ok('(dry-run)'); return true; }
|
|
202
255
|
|
|
@@ -204,7 +257,7 @@ function deployFunction(dryRun) {
|
|
|
204
257
|
// `supabase/functions/rumen-tick/`. The TermDeck install does NOT include
|
|
205
258
|
// a `supabase/` directory at the project root, so we stage a tiny working
|
|
206
259
|
// directory under `os.tmpdir()` that mirrors what the CLI expects.
|
|
207
|
-
const stage = stageRumenFunction();
|
|
260
|
+
const stage = stageRumenFunction(rumenVersion);
|
|
208
261
|
if (!stage) {
|
|
209
262
|
fail('could not stage rumen-tick function source');
|
|
210
263
|
return false;
|
|
@@ -222,7 +275,10 @@ function deployFunction(dryRun) {
|
|
|
222
275
|
// <stage>/supabase/functions/rumen-tick/{index.ts, tsconfig.json}
|
|
223
276
|
// Also write a minimal `supabase/config.toml` so `supabase functions deploy`
|
|
224
277
|
// doesn't complain about a missing project root.
|
|
225
|
-
function stageRumenFunction() {
|
|
278
|
+
function stageRumenFunction(rumenVersion) {
|
|
279
|
+
if (!rumenVersion || !/^\d+\.\d+\.\d+/.test(rumenVersion)) {
|
|
280
|
+
throw new Error(`stageRumenFunction: invalid rumenVersion ${JSON.stringify(rumenVersion)}`);
|
|
281
|
+
}
|
|
226
282
|
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'termdeck-rumen-stage-'));
|
|
227
283
|
const functionSrc = migrations.rumenFunctionDir();
|
|
228
284
|
if (!fs.existsSync(functionSrc)) return null;
|
|
@@ -230,7 +286,22 @@ function stageRumenFunction() {
|
|
|
230
286
|
const dest = path.join(stage, 'supabase', 'functions', 'rumen-tick');
|
|
231
287
|
fs.mkdirSync(dest, { recursive: true });
|
|
232
288
|
for (const f of fs.readdirSync(functionSrc)) {
|
|
233
|
-
|
|
289
|
+
const srcPath = path.join(functionSrc, f);
|
|
290
|
+
const destPath = path.join(dest, f);
|
|
291
|
+
// Substitute the version placeholder in the Deno entry point. Other files
|
|
292
|
+
// in the directory (tsconfig.json, etc.) are copied verbatim.
|
|
293
|
+
if (f === 'index.ts') {
|
|
294
|
+
const raw = fs.readFileSync(srcPath, 'utf-8');
|
|
295
|
+
if (!raw.includes('__RUMEN_VERSION__')) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`rumen-tick/index.ts is missing the __RUMEN_VERSION__ placeholder — ` +
|
|
298
|
+
`has someone reintroduced a hardcoded version?`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
fs.writeFileSync(destPath, raw.replace(/__RUMEN_VERSION__/g, rumenVersion));
|
|
302
|
+
} else {
|
|
303
|
+
fs.copyFileSync(srcPath, destPath);
|
|
304
|
+
}
|
|
234
305
|
}
|
|
235
306
|
|
|
236
307
|
const configToml = `# staged by termdeck init --rumen
|
|
@@ -245,19 +316,27 @@ verify_jwt = false
|
|
|
245
316
|
}
|
|
246
317
|
|
|
247
318
|
function setFunctionSecrets(secrets, dryRun) {
|
|
248
|
-
|
|
319
|
+
const haveOpenAI = Boolean(secrets.OPENAI_API_KEY);
|
|
320
|
+
const label = haveOpenAI
|
|
321
|
+
? 'DATABASE_URL, ANTHROPIC_API_KEY, OPENAI_API_KEY'
|
|
322
|
+
: 'DATABASE_URL, ANTHROPIC_API_KEY';
|
|
323
|
+
step(`Setting function secrets (${label})...`);
|
|
249
324
|
if (dryRun) { ok('(dry-run)'); return true; }
|
|
250
|
-
const
|
|
325
|
+
const args = [
|
|
251
326
|
'secrets', 'set',
|
|
252
327
|
`DATABASE_URL=${secrets.DATABASE_URL}`,
|
|
253
328
|
`ANTHROPIC_API_KEY=${secrets.ANTHROPIC_API_KEY}`
|
|
254
|
-
]
|
|
329
|
+
];
|
|
330
|
+
if (haveOpenAI) {
|
|
331
|
+
args.push(`OPENAI_API_KEY=${secrets.OPENAI_API_KEY}`);
|
|
332
|
+
}
|
|
333
|
+
const r = runShellCaptured('supabase', args);
|
|
255
334
|
if (!r.ok) {
|
|
256
335
|
fail(`secrets set failed (exit ${r.code})`);
|
|
257
336
|
if (r.stderr) process.stderr.write(r.stderr + '\n');
|
|
258
337
|
return false;
|
|
259
338
|
}
|
|
260
|
-
ok();
|
|
339
|
+
ok(haveOpenAI ? '(hybrid mode)' : '(keyword-only mode — OPENAI_API_KEY not set)');
|
|
261
340
|
return true;
|
|
262
341
|
}
|
|
263
342
|
|
|
@@ -400,7 +479,17 @@ async function main(argv) {
|
|
|
400
479
|
|
|
401
480
|
if (!(await link(projectRef, flags.dryRun))) return 4;
|
|
402
481
|
if (!(await applyRumenTables(secrets, flags.dryRun))) return 5;
|
|
403
|
-
|
|
482
|
+
|
|
483
|
+
step('Resolving @jhizzard/rumen version from npm registry...');
|
|
484
|
+
const resolved = resolveRumenVersion();
|
|
485
|
+
ok();
|
|
486
|
+
process.stdout.write(`→ Using rumen version: ${resolved.version} (from ${resolved.source})\n`);
|
|
487
|
+
if (resolved.warning) {
|
|
488
|
+
process.stderr.write(` ! ${resolved.warning}\n`);
|
|
489
|
+
process.stderr.write(` ! falling back to pinned FALLBACK_RUMEN_VERSION=${FALLBACK_RUMEN_VERSION}\n`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (!deployFunction(resolved.version, flags.dryRun)) return 6;
|
|
404
493
|
if (!setFunctionSecrets(secrets, flags.dryRun)) return 7;
|
|
405
494
|
if (!(await testFunction(projectRef, secrets, flags.dryRun))) return 8;
|
|
406
495
|
if (!flags.skipSchedule) {
|