@jhizzard/termdeck 1.0.1 → 1.0.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
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",
@@ -481,6 +481,92 @@ function writeYamlConfig(dryRun) {
481
481
  else ok();
482
482
  }
483
483
 
484
+ // Sprint 51.6 T3 — hook upgrade gap fix.
485
+ //
486
+ // Codex's Sprint 51.6 GAP at 20:11 ET surfaced this: the bundled session-end
487
+ // hook ships in `packages/stack-installer/assets/hooks/memory-session-end.js`,
488
+ // and `npm install -g @jhizzard/termdeck@latest` lands the new bundled file
489
+ // in node_modules — but `termdeck init --mnestra` never touched
490
+ // ~/.claude/hooks/memory-session-end.js. The user's daily-driver kept
491
+ // running the OLD installed copy forever. v1.0.2 closes that gap by adding
492
+ // this refresh step to init --mnestra. The version stamp in the bundled
493
+ // hook (// @termdeck/stack-installer-hook v<N>) gates the overwrite — only
494
+ // strictly-newer bundled stamps trigger a refresh, so a hand-edited
495
+ // installed file with v=current stays put.
496
+ //
497
+ // Backup is best-effort timestamped: `<dest>.bak.<YYYYMMDDhhmmss>`. Matches
498
+ // the pattern Joshua already had on disk from earlier stack-installer runs.
499
+ function refreshBundledHookIfNewer(opts = {}) {
500
+ const dryRun = !!opts.dryRun;
501
+ const HOME = require('os').homedir();
502
+ const HOOK_DEST = opts.destPath || path.join(HOME, '.claude', 'hooks', 'memory-session-end.js');
503
+ // Sprint 51.6 T4-CODEX audit 20:28 ET fix: bundled hook source must be on
504
+ // a path that ships in @jhizzard/termdeck's npm tarball. Root package.json
505
+ // includes `packages/stack-installer/assets/hooks/**` (added 51.6 T3) so
506
+ // this path resolves both in the monorepo and in the published tarball.
507
+ const HOOK_SOURCE = opts.sourcePath
508
+ || path.join(__dirname, '..', '..', 'stack-installer', 'assets', 'hooks', 'memory-session-end.js');
509
+ const SIG_RE = /@termdeck\/stack-installer-hook\s+v(\d+)/;
510
+ const TERMDECK_MARKERS = [
511
+ /TermDeck session-end memory hook/,
512
+ /@jhizzard\/termdeck-stack/,
513
+ /Vendored into ~\/\.claude\/hooks\/memory-session-end\.js by @jhizzard/i,
514
+ ];
515
+
516
+ function readHead(p) {
517
+ try { return fs.readFileSync(p, 'utf8').slice(0, 4096); }
518
+ catch (_) { return null; }
519
+ }
520
+ function readVersion(p) {
521
+ const head = readHead(p);
522
+ if (!head) return null;
523
+ const m = head.match(SIG_RE);
524
+ return m ? parseInt(m[1], 10) : null;
525
+ }
526
+ function looksTermdeckManaged(p) {
527
+ const head = readHead(p);
528
+ if (!head) return false;
529
+ return TERMDECK_MARKERS.some((m) => m.test(head));
530
+ }
531
+
532
+ if (!fs.existsSync(HOOK_SOURCE)) {
533
+ return { status: 'no-bundled', message: 'bundled hook source not found' };
534
+ }
535
+ const bundled = readVersion(HOOK_SOURCE);
536
+ if (bundled === null) {
537
+ return { status: 'bundled-unsigned', message: 'bundled hook missing version stamp; skipping refresh' };
538
+ }
539
+ if (!fs.existsSync(HOOK_DEST)) {
540
+ if (dryRun) return { status: 'would-install', bundled };
541
+ fs.mkdirSync(path.dirname(HOOK_DEST), { recursive: true });
542
+ fs.copyFileSync(HOOK_SOURCE, HOOK_DEST);
543
+ fs.chmodSync(HOOK_DEST, 0o644);
544
+ return { status: 'installed', bundled };
545
+ }
546
+ const installed = readVersion(HOOK_DEST);
547
+ if (installed !== null && installed >= bundled) {
548
+ return { status: 'up-to-date', installed, bundled };
549
+ }
550
+ // Sprint 51.6 T4-CODEX audit 20:23 ET safety gate: an unsigned installed
551
+ // hook gets refreshed ONLY if it looks TermDeck-managed (carries one of
552
+ // the docstring markers from a prior bundled cut). A genuinely custom
553
+ // user hook with no TermDeck fingerprint stays put.
554
+ if (installed === null && !looksTermdeckManaged(HOOK_DEST)) {
555
+ return {
556
+ status: 'custom-hook-preserved',
557
+ message: 'installed hook lacks TermDeck-managed markers; keeping as-is. Re-run with --force-overwrite to bypass.',
558
+ bundled,
559
+ };
560
+ }
561
+ if (dryRun) return { status: 'would-refresh', from: installed, to: bundled };
562
+ const stamp = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14);
563
+ const backup = `${HOOK_DEST}.bak.${stamp}`;
564
+ try { fs.copyFileSync(HOOK_DEST, backup); } catch (_) { /* best-effort */ }
565
+ fs.copyFileSync(HOOK_SOURCE, HOOK_DEST);
566
+ fs.chmodSync(HOOK_DEST, 0o644);
567
+ return { status: 'refreshed', from: installed, to: bundled, backup };
568
+ }
569
+
484
570
  function printNextSteps() {
485
571
  process.stdout.write(`
486
572
  Mnestra is configured.
@@ -578,6 +664,27 @@ async function main(argv) {
578
664
  await applyMigrations(client, false);
579
665
  await runMnestraAudit(client, inputs.projectUrl.projectRef, false);
580
666
  writeYamlConfig(false);
667
+ // Sprint 51.6 T3: refresh ~/.claude/hooks/memory-session-end.js when the
668
+ // bundled hook's version stamp is newer than the installed copy. Closes
669
+ // the upgrade gap where bundled fixes never reached users' machines via
670
+ // the standard `npm install -g @jhizzard/termdeck@latest && termdeck
671
+ // init --mnestra` path. Best-effort, timestamped backup, fail-soft.
672
+ step('Refreshing ~/.claude/hooks/memory-session-end.js (if bundled is newer)...');
673
+ try {
674
+ const r = refreshBundledHookIfNewer({ dryRun: false });
675
+ if (r.status === 'refreshed') {
676
+ ok(`refreshed v${r.from ?? 0} → v${r.to} (backup: ${path.basename(r.backup)})`);
677
+ } else if (r.status === 'installed') {
678
+ ok(`installed v${r.bundled} (no prior copy)`);
679
+ } else if (r.status === 'up-to-date') {
680
+ ok(`up-to-date (v${r.installed})`);
681
+ } else {
682
+ ok(`(${r.status}${r.message ? ': ' + r.message : ''})`);
683
+ }
684
+ } catch (err) {
685
+ // Don't abort init for a hook-refresh failure — log + continue.
686
+ process.stdout.write(` ! hook refresh failed: ${err.message} (continuing)\n`);
687
+ }
581
688
  // v0.6.9: post-write outcome verification. Confirms each migration's
582
689
  // expected schema bits actually landed — including memory_items.
583
690
  // source_session_id (the v0.6.5 column whose absence cascaded into
@@ -623,3 +730,5 @@ if (require.main === module) {
623
730
  }
624
731
 
625
732
  module.exports = main;
733
+ // Sprint 51.6 T3 — exported for tests/init-mnestra-hook-refresh.test.js.
734
+ 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
- function deployFunctions(rumenVersion, dryRun) {
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
- step(`Running: supabase functions deploy ${name} --no-verify-jwt...`);
394
- const r = runShell('supabase', ['functions', 'deploy', name, '--no-verify-jwt'], {
395
- cwd: stage
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
- async function probeOne(pgClient, target) {
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