@jhizzard/termdeck 1.0.1 → 1.0.3
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/package.json +2 -1
- package/packages/cli/src/init-mnestra.js +175 -3
- package/packages/cli/src/init-rumen.js +35 -8
- package/packages/server/src/setup/audit-upgrade.js +126 -3
- package/packages/server/src/setup/mnestra-migrations/017_memory_sessions_session_metadata.sql +94 -0
- package/packages/stack-installer/README.md +73 -0
- package/packages/stack-installer/assets/hooks/README.md +172 -0
- package/packages/stack-installer/assets/hooks/memory-session-end.js +898 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"packages/cli/templates/**",
|
|
12
12
|
"packages/server/src/**",
|
|
13
13
|
"packages/client/public/**",
|
|
14
|
+
"packages/stack-installer/assets/hooks/**",
|
|
14
15
|
"config/config.example.yaml",
|
|
15
16
|
"config/secrets.env.example",
|
|
16
17
|
"config/transcript-migration.sql",
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// existing values. Done BEFORE any database work so a later pg connect
|
|
11
11
|
// or migration failure doesn't lose the user's typed-in keys.
|
|
12
12
|
// 3. Connect via `pg` using the direct URL
|
|
13
|
-
// 4. Apply
|
|
13
|
+
// 4. Apply all bundled Mnestra migrations in order (currently 17 — count
|
|
14
|
+
// grows over time; audit-upgrade probes for any not yet applied and
|
|
15
|
+
// runs them idempotently against existing installs)
|
|
14
16
|
// 5. Update ~/.termdeck/config.yaml — set rag.enabled: false (MCP-only
|
|
15
17
|
// default; opt into TermDeck-side RAG via dashboard toggle) and point
|
|
16
18
|
// at ${VAR} refs (only after migrations apply cleanly — otherwise the
|
|
@@ -75,7 +77,7 @@ const HELP = [
|
|
|
75
77
|
' saved values if a complete set already exists in secrets.env.',
|
|
76
78
|
' 2. Writes ~/.termdeck/secrets.env IMMEDIATELY (merge-aware) so a later',
|
|
77
79
|
' pg connect or migration failure does not lose what you typed in.',
|
|
78
|
-
' 3. Connects to Postgres and applies
|
|
80
|
+
' 3. Connects to Postgres and applies all bundled Mnestra schema + RPC migrations.',
|
|
79
81
|
' 4. Updates ~/.termdeck/config.yaml — sets rag.enabled: false (MCP-only',
|
|
80
82
|
' default) and references ${VAR} keys for credentials.',
|
|
81
83
|
' 5. Verifies the Mnestra store is reachable via memory_status_aggregation().',
|
|
@@ -182,7 +184,8 @@ This wizard configures TermDeck's Tier 2 memory layer (Mnestra) by:
|
|
|
182
184
|
4. Asking for an Anthropic API key (optional, summaries)
|
|
183
185
|
5. Writing ~/.termdeck/secrets.env (before any database work, so a
|
|
184
186
|
pg failure cannot lose what you typed in)
|
|
185
|
-
6. Connecting to Postgres + applying
|
|
187
|
+
6. Connecting to Postgres + applying all bundled SQL migrations
|
|
188
|
+
(audit-upgrade detects + applies any missing on existing installs)
|
|
186
189
|
7. Updating ~/.termdeck/config.yaml — rag.enabled: false (MCP-only
|
|
187
190
|
default; toggle in dashboard later) with \${VAR} refs (only after
|
|
188
191
|
migrations apply cleanly)
|
|
@@ -481,6 +484,156 @@ function writeYamlConfig(dryRun) {
|
|
|
481
484
|
else ok();
|
|
482
485
|
}
|
|
483
486
|
|
|
487
|
+
// Sprint 51.6 T3 — hook upgrade gap fix.
|
|
488
|
+
//
|
|
489
|
+
// Codex's Sprint 51.6 GAP at 20:11 ET surfaced this: the bundled session-end
|
|
490
|
+
// hook ships in `packages/stack-installer/assets/hooks/memory-session-end.js`,
|
|
491
|
+
// and `npm install -g @jhizzard/termdeck@latest` lands the new bundled file
|
|
492
|
+
// in node_modules — but `termdeck init --mnestra` never touched
|
|
493
|
+
// ~/.claude/hooks/memory-session-end.js. The user's daily-driver kept
|
|
494
|
+
// running the OLD installed copy forever. v1.0.2 closes that gap by adding
|
|
495
|
+
// this refresh step to init --mnestra. The version stamp in the bundled
|
|
496
|
+
// hook (// @termdeck/stack-installer-hook v<N>) gates the overwrite — only
|
|
497
|
+
// strictly-newer bundled stamps trigger a refresh, so a hand-edited
|
|
498
|
+
// installed file with v=current stays put.
|
|
499
|
+
//
|
|
500
|
+
// Backup is best-effort timestamped: `<dest>.bak.<YYYYMMDDhhmmss>`. Matches
|
|
501
|
+
// the pattern Joshua already had on disk from earlier stack-installer runs.
|
|
502
|
+
function refreshBundledHookIfNewer(opts = {}) {
|
|
503
|
+
const dryRun = !!opts.dryRun;
|
|
504
|
+
const HOME = require('os').homedir();
|
|
505
|
+
const HOOK_DEST = opts.destPath || path.join(HOME, '.claude', 'hooks', 'memory-session-end.js');
|
|
506
|
+
// Sprint 51.6 T4-CODEX audit 20:28 ET fix: bundled hook source must be on
|
|
507
|
+
// a path that ships in @jhizzard/termdeck's npm tarball. Root package.json
|
|
508
|
+
// includes `packages/stack-installer/assets/hooks/**` (added 51.6 T3) so
|
|
509
|
+
// this path resolves both in the monorepo and in the published tarball.
|
|
510
|
+
const HOOK_SOURCE = opts.sourcePath
|
|
511
|
+
|| path.join(__dirname, '..', '..', 'stack-installer', 'assets', 'hooks', 'memory-session-end.js');
|
|
512
|
+
const SIG_RE = /@termdeck\/stack-installer-hook\s+v(\d+)/;
|
|
513
|
+
const TERMDECK_MARKERS = [
|
|
514
|
+
/TermDeck session-end memory hook/,
|
|
515
|
+
/@jhizzard\/termdeck-stack/,
|
|
516
|
+
/Vendored into ~\/\.claude\/hooks\/memory-session-end\.js by @jhizzard/i,
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
function readHead(p) {
|
|
520
|
+
try { return fs.readFileSync(p, 'utf8').slice(0, 4096); }
|
|
521
|
+
catch (_) { return null; }
|
|
522
|
+
}
|
|
523
|
+
function readVersion(p) {
|
|
524
|
+
const head = readHead(p);
|
|
525
|
+
if (!head) return null;
|
|
526
|
+
const m = head.match(SIG_RE);
|
|
527
|
+
return m ? parseInt(m[1], 10) : null;
|
|
528
|
+
}
|
|
529
|
+
function looksTermdeckManaged(p) {
|
|
530
|
+
const head = readHead(p);
|
|
531
|
+
if (!head) return false;
|
|
532
|
+
return TERMDECK_MARKERS.some((m) => m.test(head));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (!fs.existsSync(HOOK_SOURCE)) {
|
|
536
|
+
return { status: 'no-bundled', message: 'bundled hook source not found' };
|
|
537
|
+
}
|
|
538
|
+
const bundled = readVersion(HOOK_SOURCE);
|
|
539
|
+
if (bundled === null) {
|
|
540
|
+
return { status: 'bundled-unsigned', message: 'bundled hook missing version stamp; skipping refresh' };
|
|
541
|
+
}
|
|
542
|
+
if (!fs.existsSync(HOOK_DEST)) {
|
|
543
|
+
if (dryRun) return { status: 'would-install', bundled };
|
|
544
|
+
fs.mkdirSync(path.dirname(HOOK_DEST), { recursive: true });
|
|
545
|
+
fs.copyFileSync(HOOK_SOURCE, HOOK_DEST);
|
|
546
|
+
fs.chmodSync(HOOK_DEST, 0o644);
|
|
547
|
+
return { status: 'installed', bundled };
|
|
548
|
+
}
|
|
549
|
+
const installed = readVersion(HOOK_DEST);
|
|
550
|
+
if (installed !== null && installed >= bundled) {
|
|
551
|
+
return { status: 'up-to-date', installed, bundled };
|
|
552
|
+
}
|
|
553
|
+
// Sprint 51.6 T4-CODEX audit 20:23 ET safety gate: an unsigned installed
|
|
554
|
+
// hook gets refreshed ONLY if it looks TermDeck-managed (carries one of
|
|
555
|
+
// the docstring markers from a prior bundled cut). A genuinely custom
|
|
556
|
+
// user hook with no TermDeck fingerprint stays put.
|
|
557
|
+
if (installed === null && !looksTermdeckManaged(HOOK_DEST)) {
|
|
558
|
+
return {
|
|
559
|
+
status: 'custom-hook-preserved',
|
|
560
|
+
message: 'installed hook lacks TermDeck-managed markers; keeping as-is. Re-run with --force-overwrite to bypass.',
|
|
561
|
+
bundled,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
if (dryRun) return { status: 'would-refresh', from: installed, to: bundled };
|
|
565
|
+
const stamp = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14);
|
|
566
|
+
const backup = `${HOOK_DEST}.bak.${stamp}`;
|
|
567
|
+
try { fs.copyFileSync(HOOK_DEST, backup); } catch (_) { /* best-effort */ }
|
|
568
|
+
fs.copyFileSync(HOOK_SOURCE, HOOK_DEST);
|
|
569
|
+
fs.chmodSync(HOOK_DEST, 0o644);
|
|
570
|
+
return { status: 'refreshed', from: installed, to: bundled, backup };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Sprint 51.7 T1 — wizard wire-up bug fix.
|
|
574
|
+
//
|
|
575
|
+
// Moved upstream of `pgRunner.connect` and the migration-replay loop so
|
|
576
|
+
// DB-side failures (Class A schema drift, network blips, partial state)
|
|
577
|
+
// cannot strand the hook upgrade. Joshua's 2026-05-03 Phase B run threw at
|
|
578
|
+
// `applyMigrations()` on `001_mnestra_tables.sql` (the `match_memories`
|
|
579
|
+
// CREATE OR REPLACE return-type drift on petvetbid — existing function had
|
|
580
|
+
// columns in a different order, Postgres rejected with "cannot change return
|
|
581
|
+
// type of existing function"). Outer catch at the old call site fired and
|
|
582
|
+
// returned exit 5; the refresh at the old wire-up never ran. Brad's
|
|
583
|
+
// jizzard-brain reproduced the same symptom under v1.0.2.
|
|
584
|
+
//
|
|
585
|
+
// Hook refresh is a LOCAL filesystem operation. It has no dependency on DB
|
|
586
|
+
// success, so it should run as part of the initial local-setup phase next
|
|
587
|
+
// to `writeSecretsFile`, not buried after a 17-migration replay. This also
|
|
588
|
+
// means the wizard ALWAYS lands the bundled hook on disk after a successful
|
|
589
|
+
// `npm install -g @jhizzard/termdeck@latest && termdeck init --mnestra`,
|
|
590
|
+
// even when the DB phase fails — a meaningful upgrade-path improvement
|
|
591
|
+
// because the hook fix is independently valuable.
|
|
592
|
+
//
|
|
593
|
+
// `--dry-run` exercises this path with `dryRun: true` so the wizard
|
|
594
|
+
// truthfully reports what WOULD happen on a live run (Sprint 51.6 Phase B
|
|
595
|
+
// dry-run probe couldn't catch the wire-up bug because dry-run early-
|
|
596
|
+
// returned BEFORE the old refresh location at line 677).
|
|
597
|
+
//
|
|
598
|
+
// Stderr instrumentation is gated behind `TERMDECK_DEBUG_WIREUP=1` so
|
|
599
|
+
// production users never see noise; the gate is broadly useful for future
|
|
600
|
+
// wire-up bisects (any developer can re-run the wizard with the env var
|
|
601
|
+
// set and get a deterministic trace).
|
|
602
|
+
function runHookRefresh({ dryRun = false } = {}) {
|
|
603
|
+
const debug = !!process.env.TERMDECK_DEBUG_WIREUP;
|
|
604
|
+
step('Refreshing ~/.claude/hooks/memory-session-end.js (if bundled is newer)...');
|
|
605
|
+
if (debug) {
|
|
606
|
+
const HOME = require('os').homedir();
|
|
607
|
+
const HOOK_DEST = path.join(HOME, '.claude', 'hooks', 'memory-session-end.js');
|
|
608
|
+
const HOOK_SOURCE = path.join(__dirname, '..', '..', 'stack-installer', 'assets', 'hooks', 'memory-session-end.js');
|
|
609
|
+
process.stderr.write(`[wire-up-debug] runHookRefresh entry: dryRun=${dryRun} HOOK_DEST=${HOOK_DEST} HOOK_SOURCE=${HOOK_SOURCE} HOOK_SOURCE_exists=${fs.existsSync(HOOK_SOURCE)} HOOK_DEST_exists=${fs.existsSync(HOOK_DEST)}\n`);
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const r = refreshBundledHookIfNewer({ dryRun });
|
|
613
|
+
if (debug) process.stderr.write(`[wire-up-debug] runHookRefresh return: ${JSON.stringify(r)}\n`);
|
|
614
|
+
if (r.status === 'refreshed') {
|
|
615
|
+
ok(`refreshed v${r.from ?? 0} → v${r.to} (backup: ${path.basename(r.backup)})`);
|
|
616
|
+
} else if (r.status === 'would-refresh') {
|
|
617
|
+
ok(`would-refresh v${r.from ?? 0} → v${r.to} (dry-run)`);
|
|
618
|
+
} else if (r.status === 'installed') {
|
|
619
|
+
ok(`installed v${r.bundled} (no prior copy)`);
|
|
620
|
+
} else if (r.status === 'would-install') {
|
|
621
|
+
ok(`would-install v${r.bundled} (dry-run, no prior copy)`);
|
|
622
|
+
} else if (r.status === 'up-to-date') {
|
|
623
|
+
ok(`up-to-date (v${r.installed})`);
|
|
624
|
+
} else {
|
|
625
|
+
ok(`(${r.status}${r.message ? ': ' + r.message : ''})`);
|
|
626
|
+
}
|
|
627
|
+
} catch (err) {
|
|
628
|
+
// Don't abort init for a hook-refresh failure — log + continue. The
|
|
629
|
+
// user's wizard goal (DB setup) is independent of hook refresh; even
|
|
630
|
+
// if refresh fails (e.g. permission denied, FS error), the wizard
|
|
631
|
+
// should continue to do the DB work.
|
|
632
|
+
process.stdout.write(` ! hook refresh failed: ${err.message} (continuing)\n`);
|
|
633
|
+
if (debug) process.stderr.write(`[wire-up-debug] runHookRefresh threw: ${err && err.stack || err}\n`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
484
637
|
function printNextSteps() {
|
|
485
638
|
process.stdout.write(`
|
|
486
639
|
Mnestra is configured.
|
|
@@ -551,6 +704,17 @@ async function main(argv) {
|
|
|
551
704
|
return 6;
|
|
552
705
|
}
|
|
553
706
|
|
|
707
|
+
// Sprint 51.7 T1 — refresh ~/.claude/hooks/memory-session-end.js BEFORE the
|
|
708
|
+
// DB phase. Hook refresh is local FS work; coupling it downstream of pg
|
|
709
|
+
// connect + 17-migration replay (the old wire-up at line 677 in v1.0.2)
|
|
710
|
+
// meant ANY DB-side error (Joshua's mig-001 `match_memories` return-type
|
|
711
|
+
// drift, Brad's same on jizzard-brain) silently skipped the upgrade. With
|
|
712
|
+
// refresh here, the user always lands the bundled hook even when the DB
|
|
713
|
+
// phase later fails — decoupled concerns, idempotent re-runs, and the
|
|
714
|
+
// helper handles its own try/catch internally so a refresh failure never
|
|
715
|
+
// strands the wizard.
|
|
716
|
+
runHookRefresh({ dryRun: flags.dryRun });
|
|
717
|
+
|
|
554
718
|
step('Connecting to Supabase...');
|
|
555
719
|
if (flags.dryRun) {
|
|
556
720
|
ok('(dry-run, skipped)');
|
|
@@ -578,6 +742,12 @@ async function main(argv) {
|
|
|
578
742
|
await applyMigrations(client, false);
|
|
579
743
|
await runMnestraAudit(client, inputs.projectUrl.projectRef, false);
|
|
580
744
|
writeYamlConfig(false);
|
|
745
|
+
// Sprint 51.7 T1: hook refresh moved upstream — see runHookRefresh()
|
|
746
|
+
// call site near writeSecretsFile. The old wire-up here was reachable
|
|
747
|
+
// only when every DB step succeeded, which Sprint 51.6 Phase B proved
|
|
748
|
+
// was the bug (mig-001 `match_memories` return-type drift threw and
|
|
749
|
+
// stranded the upgrade for both Joshua and Brad).
|
|
750
|
+
|
|
581
751
|
// v0.6.9: post-write outcome verification. Confirms each migration's
|
|
582
752
|
// expected schema bits actually landed — including memory_items.
|
|
583
753
|
// source_session_id (the v0.6.5 column whose absence cascaded into
|
|
@@ -623,3 +793,5 @@ if (require.main === module) {
|
|
|
623
793
|
}
|
|
624
794
|
|
|
625
795
|
module.exports = main;
|
|
796
|
+
// Sprint 51.6 T3 — exported for tests/init-mnestra-hook-refresh.test.js.
|
|
797
|
+
module.exports.refreshBundledHookIfNewer = refreshBundledHookIfNewer;
|
|
@@ -337,14 +337,21 @@ async function runRumenAudit(projectRef, secrets, dryRun) {
|
|
|
337
337
|
pgClient: client,
|
|
338
338
|
projectRef
|
|
339
339
|
});
|
|
340
|
-
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
340
|
+
if (result.applied.length === 0 && result.errors.length === 0 && result.skipped.length === 0) {
|
|
341
341
|
ok(`(install up to date — ${result.probed.length} probes all present)`);
|
|
342
342
|
return true;
|
|
343
343
|
}
|
|
344
|
-
ok(`(probed ${result.probed.length}, applied ${result.applied.length})`);
|
|
344
|
+
ok(`(probed ${result.probed.length}, applied ${result.applied.length}, skipped ${result.skipped.length})`);
|
|
345
345
|
for (const name of result.applied) {
|
|
346
346
|
process.stdout.write(` ✓ applied ${name}\n`);
|
|
347
347
|
}
|
|
348
|
+
// Sprint 51.6 T3 — Bug D: surface skipped[] entries (functionSource
|
|
349
|
+
// probes that detected drift but can't auto-redeploy from audit). The
|
|
350
|
+
// wizard will redeploy below in the deployFunctions step, which fixes
|
|
351
|
+
// the drift; this print just makes the diagnosis visible.
|
|
352
|
+
for (const s of result.skipped) {
|
|
353
|
+
process.stdout.write(` ⊘ skipped ${s.name}: ${s.reason}\n`);
|
|
354
|
+
}
|
|
348
355
|
for (const e of result.errors) {
|
|
349
356
|
process.stdout.write(` ! ${e.name}: ${e.error}\n`);
|
|
350
357
|
}
|
|
@@ -363,7 +370,20 @@ async function runRumenAudit(projectRef, secrets, dryRun) {
|
|
|
363
370
|
// copied verbatim. If a future function adds a placeholder, list it here.
|
|
364
371
|
const FUNCTIONS_WITH_VERSION_PLACEHOLDER = new Set(['rumen-tick']);
|
|
365
372
|
|
|
366
|
-
|
|
373
|
+
// Sprint 51.6 T3 — `projectRef` is required and passed explicitly to every
|
|
374
|
+
// `supabase functions deploy` invocation as `--project-ref <ref>`. Brad's
|
|
375
|
+
// 2026-05-03 jizzard-brain install hit Bug C: `supabase link --project-ref`
|
|
376
|
+
// runs successfully (audit-upgrade probes confirm the link is live), but a
|
|
377
|
+
// few subprocess calls later `supabase functions deploy` errors with
|
|
378
|
+
// `Cannot find project ref. Have you run supabase link?` because the link
|
|
379
|
+
// state persists per-cwd in supabase/config.toml and the staged-functions
|
|
380
|
+
// directory has none. Threading --project-ref through dodges link-state
|
|
381
|
+
// coupling entirely. The flag is supported by supabase CLI v1.x and v2.x.
|
|
382
|
+
function deployFunctions(rumenVersion, projectRef, dryRun) {
|
|
383
|
+
if (!projectRef || typeof projectRef !== 'string') {
|
|
384
|
+
fail('deployFunctions: projectRef is required (Sprint 51.6 T3 — explicit --project-ref to dodge subprocess link-state isolation)');
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
367
387
|
const fnNames = migrations.listRumenFunctions();
|
|
368
388
|
if (fnNames.length === 0) {
|
|
369
389
|
fail('no Rumen Edge Function source found in bundled setup or @jhizzard/rumen package');
|
|
@@ -390,10 +410,14 @@ function deployFunctions(rumenVersion, dryRun) {
|
|
|
390
410
|
ok();
|
|
391
411
|
|
|
392
412
|
for (const name of fnNames) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
413
|
+
// Sprint 51.6 T3 — `--project-ref <ref>` explicit, dodging supabase
|
|
414
|
+
// link-state subprocess isolation (Bug C, Brad's 2026-05-03 install).
|
|
415
|
+
step(`Running: supabase functions deploy ${name} --project-ref ${projectRef} --no-verify-jwt...`);
|
|
416
|
+
const r = runShell('supabase', [
|
|
417
|
+
'functions', 'deploy', name,
|
|
418
|
+
'--project-ref', projectRef,
|
|
419
|
+
'--no-verify-jwt',
|
|
420
|
+
], { cwd: stage });
|
|
397
421
|
if (!r.ok) {
|
|
398
422
|
fail(`deploy of ${name} failed (exit ${r.code})`);
|
|
399
423
|
return false;
|
|
@@ -997,7 +1021,7 @@ async function main(argv) {
|
|
|
997
1021
|
process.stderr.write(` ! falling back to pinned FALLBACK_RUMEN_VERSION=${FALLBACK_RUMEN_VERSION}\n`);
|
|
998
1022
|
}
|
|
999
1023
|
|
|
1000
|
-
if (!deployFunctions(resolved.version, flags.dryRun)) return 6;
|
|
1024
|
+
if (!deployFunctions(resolved.version, projectRef, flags.dryRun)) return 6;
|
|
1001
1025
|
|
|
1002
1026
|
// Sprint 51.5 T3: install-time prompt for AI edge classification. Sets
|
|
1003
1027
|
// secrets.GRAPH_LLM_CLASSIFY in-memory; the per-secret loop below picks
|
|
@@ -1042,6 +1066,9 @@ module.exports._wireAccessTokenInMcpJson = wireAccessTokenInMcpJson;
|
|
|
1042
1066
|
// Sprint 43 T3: stage helper exposed so init-rumen-deploy.test.js can pin
|
|
1043
1067
|
// the multi-function staging contract without shelling out to `supabase`.
|
|
1044
1068
|
module.exports._stageRumenFunctions = stageRumenFunctions;
|
|
1069
|
+
// Sprint 51.6 T3 — exported for tests/init-rumen-project-ref.test.js so the
|
|
1070
|
+
// --project-ref invariant can be asserted without spawning a real shell.
|
|
1071
|
+
module.exports._deployFunctions = deployFunctions;
|
|
1045
1072
|
// Sprint 51.5 T3: per-secret CLI loop, Vault SQL-Editor URL builder, and
|
|
1046
1073
|
// Vault-secret ensure helper exposed for tests/init-rumen-secrets-per-call,
|
|
1047
1074
|
// init-rumen-graph-llm, and init-rumen-vault-deeplinks.
|
|
@@ -117,6 +117,23 @@ const PROBES = Object.freeze([
|
|
|
117
117
|
" and column_name = 'source_agent' limit 1",
|
|
118
118
|
presentWhen: 'rowReturned'
|
|
119
119
|
},
|
|
120
|
+
{
|
|
121
|
+
// Sprint 51.6 T3 — bundled session-end hook (TermDeck v1.0.2+) writes
|
|
122
|
+
// the rich rag-system column set to memory_sessions; canonical engram
|
|
123
|
+
// mig 001 only ships (id, project, summary, metadata, created_at).
|
|
124
|
+
// Probe for memory_sessions.session_id (the most distinctive of the
|
|
125
|
+
// mig-017 columns) and apply mig 017 if absent. Idempotent on petvetbid
|
|
126
|
+
// where the columns are already present from hand-applied DDL.
|
|
127
|
+
name: 'memory_sessions.session_id',
|
|
128
|
+
kind: 'mnestra',
|
|
129
|
+
migrationFile: '017_memory_sessions_session_metadata.sql',
|
|
130
|
+
probeSql:
|
|
131
|
+
"select 1 as present from information_schema.columns " +
|
|
132
|
+
"where table_schema = 'public' " +
|
|
133
|
+
" and table_name = 'memory_sessions' " +
|
|
134
|
+
" and column_name = 'session_id' limit 1",
|
|
135
|
+
presentWhen: 'rowReturned'
|
|
136
|
+
},
|
|
120
137
|
{
|
|
121
138
|
name: 'rumen-tick cron schedule',
|
|
122
139
|
kind: 'rumen',
|
|
@@ -134,6 +151,38 @@ const PROBES = Object.freeze([
|
|
|
134
151
|
probeSql:
|
|
135
152
|
"select 1 as present from cron.job where jobname = 'graph-inference-tick' limit 1",
|
|
136
153
|
presentWhen: 'rowReturned'
|
|
154
|
+
},
|
|
155
|
+
// Sprint 51.6 T3 — Brad's Bug D: function-existence probes (cron schedule
|
|
156
|
+
// checks for jobname presence) are not enough. The deployed Edge Function
|
|
157
|
+
// SOURCE may be stale even when the cron job and function both exist.
|
|
158
|
+
// jizzard-brain on 2026-05-03: deployed rumen-tick was missing the
|
|
159
|
+
// SUPABASE_DB_URL fallback that Sprint 51.5 T1 added; cron probe said
|
|
160
|
+
// "present", source was old. The marker check below detects that drift.
|
|
161
|
+
//
|
|
162
|
+
// probeKind 'functionSource' triggers a Management API fetch instead of a
|
|
163
|
+
// pgClient.query. Bumps to skipped[] (not missing[]) when drift is
|
|
164
|
+
// detected — the corresponding "apply" is a redeploy via init-rumen's
|
|
165
|
+
// deployFunctions, not an SQL migration. The wizard shows skipped[]
|
|
166
|
+
// entries with their probeError, prompting the user to re-run init.
|
|
167
|
+
//
|
|
168
|
+
// Maintenance: bump `requiredMarker` whenever a new feature is added to
|
|
169
|
+
// the bundled function source that is meaningful enough to gate redeploys
|
|
170
|
+
// on. The marker should be a string unique to the post-change version.
|
|
171
|
+
{
|
|
172
|
+
name: 'rumen-tick deployed source has SUPABASE_DB_URL fallback',
|
|
173
|
+
kind: 'rumen',
|
|
174
|
+
probeKind: 'functionSource',
|
|
175
|
+
functionSlug: 'rumen-tick',
|
|
176
|
+
requiredMarker: "Deno.env.get('SUPABASE_DB_URL')",
|
|
177
|
+
presentWhen: 'sourceMatch'
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'graph-inference deployed source has SUPABASE_DB_URL fallback',
|
|
181
|
+
kind: 'rumen',
|
|
182
|
+
probeKind: 'functionSource',
|
|
183
|
+
functionSlug: 'graph-inference',
|
|
184
|
+
requiredMarker: "Deno.env.get('SUPABASE_DB_URL')",
|
|
185
|
+
presentWhen: 'sourceMatch'
|
|
137
186
|
}
|
|
138
187
|
]);
|
|
139
188
|
|
|
@@ -147,8 +196,68 @@ function resolveMigrationFile(target, files) {
|
|
|
147
196
|
return files.find((f) => path.basename(f) === wanted) || null;
|
|
148
197
|
}
|
|
149
198
|
|
|
199
|
+
// Sprint 51.6 T3 — Bug D: Edge Function source-drift detection.
|
|
200
|
+
//
|
|
201
|
+
// Fetches the deployed Edge Function body from Supabase Management API and
|
|
202
|
+
// looks for a marker string the bundled source contains. Returns absent
|
|
203
|
+
// (with probeError) when the marker is missing — meaning the deployed
|
|
204
|
+
// function is older than the bundle and should be redeployed.
|
|
205
|
+
//
|
|
206
|
+
// Requires:
|
|
207
|
+
// - projectRef passed through from auditUpgrade()
|
|
208
|
+
// - SUPABASE_ACCESS_TOKEN in env (a personal access token, format `sbp_*`)
|
|
209
|
+
// Fail-soft when either is missing — recorded as probeError, treated as
|
|
210
|
+
// absent. The audit caller decides whether to surface to the user.
|
|
211
|
+
async function probeFunctionSource(target, { projectRef, fetchImpl }) {
|
|
212
|
+
const fn = fetchImpl || (typeof globalThis !== 'undefined' ? globalThis.fetch : undefined);
|
|
213
|
+
if (typeof fn !== 'function') {
|
|
214
|
+
return { present: false, probeError: 'no fetch implementation available' };
|
|
215
|
+
}
|
|
216
|
+
if (!projectRef) {
|
|
217
|
+
return { present: false, probeError: 'projectRef required for functionSource probe' };
|
|
218
|
+
}
|
|
219
|
+
const accessToken = process.env.SUPABASE_ACCESS_TOKEN;
|
|
220
|
+
if (!accessToken) {
|
|
221
|
+
return {
|
|
222
|
+
present: false,
|
|
223
|
+
probeError: 'SUPABASE_ACCESS_TOKEN not set; cannot fetch deployed function body. Set the personal access token (`supabase login` writes it to ~/.supabase/access-token) to enable function-source drift detection.',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
let res;
|
|
227
|
+
try {
|
|
228
|
+
res = await fn(
|
|
229
|
+
`https://api.supabase.com/v1/projects/${projectRef}/functions/${target.functionSlug}/body`,
|
|
230
|
+
{ headers: { 'Authorization': `Bearer ${accessToken}` } }
|
|
231
|
+
);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return { present: false, probeError: `Management API fetch failed: ${err.message}` };
|
|
234
|
+
}
|
|
235
|
+
if (!res.ok) {
|
|
236
|
+
return {
|
|
237
|
+
present: false,
|
|
238
|
+
probeError: `Management API returned HTTP ${res.status} for ${target.functionSlug}/body — function may not be deployed yet, or access token lacks permission.`
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
let body;
|
|
242
|
+
try { body = await res.text(); }
|
|
243
|
+
catch (err) { return { present: false, probeError: `body decode failed: ${err.message}` }; }
|
|
244
|
+
|
|
245
|
+
if (target.requiredMarker && body.includes(target.requiredMarker)) {
|
|
246
|
+
return { present: true };
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
present: false,
|
|
250
|
+
probeError: `deployed ${target.functionSlug} source missing marker (${JSON.stringify(target.requiredMarker)}) — re-run \`termdeck init --rumen\` to redeploy from bundled source.`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
150
254
|
// Run a probe and decide present/absent based on the probe's contract.
|
|
151
|
-
|
|
255
|
+
// Sprint 51.6 T3: dispatches by target.probeKind. Default is the legacy
|
|
256
|
+
// pgClient.query path; 'functionSource' calls probeFunctionSource (HTTP).
|
|
257
|
+
async function probeOne(pgClient, target, ctx = {}) {
|
|
258
|
+
if (target.probeKind === 'functionSource') {
|
|
259
|
+
return probeFunctionSource(target, ctx);
|
|
260
|
+
}
|
|
152
261
|
let result;
|
|
153
262
|
try {
|
|
154
263
|
result = await pgClient.query(target.probeSql);
|
|
@@ -233,7 +342,8 @@ async function auditUpgrade({
|
|
|
233
342
|
projectRef,
|
|
234
343
|
dryRun = false,
|
|
235
344
|
probes,
|
|
236
|
-
_migrations
|
|
345
|
+
_migrations,
|
|
346
|
+
_fetch
|
|
237
347
|
} = {}) {
|
|
238
348
|
if (!pgClient || typeof pgClient.query !== 'function') {
|
|
239
349
|
throw new Error('auditUpgrade: pgClient with .query() is required');
|
|
@@ -255,11 +365,23 @@ async function auditUpgrade({
|
|
|
255
365
|
|
|
256
366
|
for (const target of targets) {
|
|
257
367
|
probed.push(target.name);
|
|
258
|
-
const probeResult = await probeOne(pgClient, target);
|
|
368
|
+
const probeResult = await probeOne(pgClient, target, { projectRef, fetchImpl: _fetch });
|
|
259
369
|
if (probeResult.present) {
|
|
260
370
|
present.push(target.name);
|
|
261
371
|
continue;
|
|
262
372
|
}
|
|
373
|
+
|
|
374
|
+
// Sprint 51.6 T3 — Bug D: functionSource probes go to skipped[] (not
|
|
375
|
+
// missing[]). The corresponding fix is a re-run of `init --rumen` which
|
|
376
|
+
// calls deployFunctions; audit-upgrade does not auto-redeploy.
|
|
377
|
+
if (target.probeKind === 'functionSource') {
|
|
378
|
+
skipped.push({
|
|
379
|
+
name: target.name,
|
|
380
|
+
reason: probeResult.probeError || 'function source drift — redeploy via init --rumen',
|
|
381
|
+
});
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
263
385
|
missing.push(target.name);
|
|
264
386
|
|
|
265
387
|
if (dryRun) continue;
|
|
@@ -297,6 +419,7 @@ module.exports = {
|
|
|
297
419
|
// Test surface — kept exported so audit-upgrade.test.js can pin probe
|
|
298
420
|
// selection / apply pathway behavior without needing a live pg client.
|
|
299
421
|
_probeOne: probeOne,
|
|
422
|
+
_probeFunctionSource: probeFunctionSource,
|
|
300
423
|
_applyOne: applyOne,
|
|
301
424
|
_resolveMigrationFile: resolveMigrationFile
|
|
302
425
|
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
-- Migration 017 — memory_sessions session metadata columns.
|
|
2
|
+
--
|
|
3
|
+
-- Sprint 51.6 T3 (TermDeck v1.0.2 hotfix wave). Brings the canonical engram
|
|
4
|
+
-- memory_sessions schema in line with the rag-system writer's column set so
|
|
5
|
+
-- TermDeck's bundled session-end hook can write a uniform shape on both
|
|
6
|
+
-- fresh-canonical installs and Joshua's daily-driver petvetbid (where the
|
|
7
|
+
-- columns were already added by hand when rag-system bootstrap ran).
|
|
8
|
+
--
|
|
9
|
+
-- Why: until v1.0.2 the bundled hook only wrote memory_items. The actual
|
|
10
|
+
-- memory_sessions writer was Joshua's PRIOR personal hook at
|
|
11
|
+
-- ~/Documents/Graciella/rag-system/hooks/memory-session-end.js, which spawned
|
|
12
|
+
-- ~/Documents/Graciella/rag-system/src/scripts/process-session.ts; that script
|
|
13
|
+
-- INSERTed memory_sessions rows with this richer column set. When the
|
|
14
|
+
-- TermDeck stack-installer overwrote the personal hook on 2026-05-02, the
|
|
15
|
+
-- writer disappeared and memory_sessions stopped accumulating. v1.0.2's
|
|
16
|
+
-- bundled hook gains a memory_sessions write path; this migration ensures
|
|
17
|
+
-- the schema it expects exists everywhere.
|
|
18
|
+
--
|
|
19
|
+
-- Idempotent — safe on:
|
|
20
|
+
-- 1. petvetbid (where these columns are already present from hand-applied
|
|
21
|
+
-- DDL Joshua ran when setting up rag-system; the IF NOT EXISTS guards
|
|
22
|
+
-- no-op on every column).
|
|
23
|
+
-- 2. Fresh canonical installs that ran migrations 001-016 only (the canonical
|
|
24
|
+
-- engram set, which left memory_sessions at the minimal mig-001 shape).
|
|
25
|
+
-- 3. Re-runs of this migration — every operation is guarded.
|
|
26
|
+
--
|
|
27
|
+
-- The unique constraint on session_id is wrapped in a do-block because
|
|
28
|
+
-- ADD CONSTRAINT does not support IF NOT EXISTS in PostgreSQL. Joshua's
|
|
29
|
+
-- petvetbid already has the constraint as memory_sessions_session_id_key
|
|
30
|
+
-- (auto-named by the rag-system bootstrap); this block detects that name
|
|
31
|
+
-- and skips re-adding.
|
|
32
|
+
--
|
|
33
|
+
-- session_id is added NULLABLE on canonical installs even though petvetbid's
|
|
34
|
+
-- existing constraint is NOT NULL. Adding NOT NULL via ALTER TABLE on a
|
|
35
|
+
-- table with existing rows would fail; the bundled hook always supplies
|
|
36
|
+
-- session_id at write time, so nullability is non-blocking. A future sprint
|
|
37
|
+
-- may tighten to NOT NULL with a DEFAULT after a backfill pass.
|
|
38
|
+
|
|
39
|
+
-- Defensive: vector extension must already be installed (migration 001
|
|
40
|
+
-- requires it for memory_items.embedding). If it's somehow missing this
|
|
41
|
+
-- ADD COLUMN errors, surfacing the real environment issue rather than
|
|
42
|
+
-- silently skipping the embedding column.
|
|
43
|
+
|
|
44
|
+
alter table public.memory_sessions
|
|
45
|
+
add column if not exists session_id text,
|
|
46
|
+
add column if not exists summary_embedding vector(1536),
|
|
47
|
+
add column if not exists started_at timestamptz,
|
|
48
|
+
add column if not exists ended_at timestamptz,
|
|
49
|
+
add column if not exists duration_minutes integer,
|
|
50
|
+
add column if not exists messages_count integer default 0,
|
|
51
|
+
add column if not exists facts_extracted integer default 0,
|
|
52
|
+
add column if not exists files_changed jsonb default '[]'::jsonb,
|
|
53
|
+
add column if not exists topics jsonb default '[]'::jsonb,
|
|
54
|
+
add column if not exists transcript_path text;
|
|
55
|
+
|
|
56
|
+
-- Unique constraint on session_id. Skip if any unique constraint on
|
|
57
|
+
-- (session_id) is already in place — covers both the canonical name
|
|
58
|
+
-- memory_sessions_session_id_key and any alternate name from a manual
|
|
59
|
+
-- ALTER TABLE Joshua may have run on petvetbid.
|
|
60
|
+
do $$
|
|
61
|
+
declare
|
|
62
|
+
has_unique boolean;
|
|
63
|
+
begin
|
|
64
|
+
select exists (
|
|
65
|
+
select 1
|
|
66
|
+
from pg_constraint c
|
|
67
|
+
join pg_class t on t.oid = c.conrelid
|
|
68
|
+
join pg_namespace n on n.oid = t.relnamespace
|
|
69
|
+
where n.nspname = 'public'
|
|
70
|
+
and t.relname = 'memory_sessions'
|
|
71
|
+
and c.contype = 'u'
|
|
72
|
+
and (
|
|
73
|
+
select array_agg(att.attname order by att.attnum)
|
|
74
|
+
from unnest(c.conkey) as colnum
|
|
75
|
+
join pg_attribute att on att.attrelid = c.conrelid and att.attnum = colnum
|
|
76
|
+
) = ARRAY['session_id']::name[]
|
|
77
|
+
) into has_unique;
|
|
78
|
+
|
|
79
|
+
if not has_unique then
|
|
80
|
+
alter table public.memory_sessions
|
|
81
|
+
add constraint memory_sessions_session_id_key unique (session_id);
|
|
82
|
+
end if;
|
|
83
|
+
end $$;
|
|
84
|
+
|
|
85
|
+
-- HNSW index on summary_embedding for future similarity search. Idempotent.
|
|
86
|
+
-- Cost on insert is negligible; cost on backfill is one-time.
|
|
87
|
+
create index if not exists memory_sessions_summary_embedding_hnsw_idx
|
|
88
|
+
on public.memory_sessions using hnsw (summary_embedding vector_cosine_ops)
|
|
89
|
+
with (m = 16, ef_construction = 64);
|
|
90
|
+
|
|
91
|
+
-- Helpful covering index for time-range scans (used by Flashback / rumen
|
|
92
|
+
-- queries that filter by ended_at). Idempotent.
|
|
93
|
+
create index if not exists memory_sessions_ended_at_idx
|
|
94
|
+
on public.memory_sessions(ended_at desc nulls last);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @jhizzard/termdeck-stack
|
|
2
|
+
|
|
3
|
+
One-command installer for the TermDeck developer memory stack.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx @jhizzard/termdeck-stack
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What gets installed
|
|
10
|
+
|
|
11
|
+
| Layer | Package | What it does |
|
|
12
|
+
|-------|---------|--------------|
|
|
13
|
+
| 1 | `@jhizzard/termdeck` | Browser terminal multiplexer with metadata overlays and Flashback recall toasts |
|
|
14
|
+
| 2 | `@jhizzard/mnestra` | pgvector memory store + MCP server. Lights up Flashback. Provides `memory_*` tools to Claude Code, Cursor, Windsurf |
|
|
15
|
+
| 3 | `@jhizzard/rumen` | Async learning loop on a Supabase Edge Function cron. Synthesizes cross-project insights |
|
|
16
|
+
| 4 | `@supabase/mcp-server-supabase` | MCP that lets the TermDeck setup wizard provision your Supabase project automatically |
|
|
17
|
+
|
|
18
|
+
The wizard:
|
|
19
|
+
|
|
20
|
+
1. Prints the four-layer overview so you see what you're agreeing to.
|
|
21
|
+
2. Detects which pieces are already on your machine.
|
|
22
|
+
3. Asks which tier you want (default: 4 — full stack).
|
|
23
|
+
4. Runs `npm install -g` for the missing pieces.
|
|
24
|
+
5. Merges Mnestra and Supabase MCP entries into `~/.claude/mcp.json` — preserving any existing MCP servers.
|
|
25
|
+
6. Prints the next steps (Supabase PAT, credentials, `termdeck` to start).
|
|
26
|
+
|
|
27
|
+
## Modes
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
npx @jhizzard/termdeck-stack # interactive
|
|
31
|
+
npx @jhizzard/termdeck-stack --tier 4 # unattended
|
|
32
|
+
npx @jhizzard/termdeck-stack --dry-run # print plan, don't install
|
|
33
|
+
npx @jhizzard/termdeck-stack --yes # accept defaults (combine with --tier)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Known limitations
|
|
37
|
+
|
|
38
|
+
Tier 3 (Rumen) currently still requires one manual command after the
|
|
39
|
+
installer finishes:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
termdeck init --rumen
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That command deploys the Rumen Supabase Edge Function, applies the
|
|
46
|
+
migration, and installs the `pg_cron` schedule. Auto-running it from
|
|
47
|
+
the meta-installer is queued — until then the wizard prints it as an
|
|
48
|
+
explicit next step.
|
|
49
|
+
|
|
50
|
+
## Version vs. the rest of the stack
|
|
51
|
+
|
|
52
|
+
This package's version tracks the meta-installer surface, not the
|
|
53
|
+
underlying packages. Each layer ships on its own release cadence:
|
|
54
|
+
|
|
55
|
+
| Package | Where to look |
|
|
56
|
+
|---------|---------------|
|
|
57
|
+
| `@jhizzard/termdeck` | https://www.npmjs.com/package/@jhizzard/termdeck |
|
|
58
|
+
| `@jhizzard/mnestra` | https://www.npmjs.com/package/@jhizzard/mnestra |
|
|
59
|
+
| `@jhizzard/rumen` | https://www.npmjs.com/package/@jhizzard/rumen |
|
|
60
|
+
|
|
61
|
+
The installer always pulls each layer's `latest` dist-tag, so a fresh
|
|
62
|
+
`npx @jhizzard/termdeck-stack` run picks up the most recent published
|
|
63
|
+
version of every layer regardless of this package's own version.
|
|
64
|
+
|
|
65
|
+
## Why this exists
|
|
66
|
+
|
|
67
|
+
The TermDeck stack used to be a 15-step install: provision Supabase, run six SQL migrations, mint API keys, paste them into `secrets.env`, edit `config.yaml`, install Mnestra globally, deploy Rumen, install the Supabase MCP, wire `~/.claude/mcp.json`. Most testers bounced before step 5.
|
|
68
|
+
|
|
69
|
+
This installer collapses every step that's a `npm install -g` into one command, then drops the user at the doorstep of the in-browser setup wizard (which handles credentials).
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|