@jhizzard/termdeck 0.6.3 → 0.6.5

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 CHANGED
@@ -171,7 +171,7 @@ Honest limits, stated upfront so the skeptic has nothing to chase:
171
171
  - **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.
172
172
  - **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 roadmap.
173
173
  - **Not free forever.** Tier 2+ pays OpenAI fractions of a cent per memory for embeddings. Self-hosted embeddings via Ollama are on the roadmap.
174
- - **Not proven at scale.** v0.6.1, validated against 4,590 memories in one developer's production store. The Rumen 2026-04-19 re-kickstart processed 166 sessions into 166 insights in ~5.5 minutes. No multi-user data yet. Bug reports and issues welcome.
174
+ - **Not proven at scale.** v0.6.4, validated against 4,669 memories in one developer's production store. The Rumen 2026-04-19 re-kickstart processed 166 sessions into 166 insights in ~5.5 minutes. No multi-user data yet. Bug reports and issues welcome.
175
175
 
176
176
  ---
177
177
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jhizzard/termdeck",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
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"
@@ -22,6 +22,11 @@
22
22
  // set is already on disk (otherwise the wizard asks
23
23
  // interactively before reusing)
24
24
  // --reset Ignore saved secrets and re-prompt for everything
25
+ // --from-env Skip every prompt; read all five secrets from the
26
+ // process environment (SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY,
27
+ // DATABASE_URL, OPENAI_API_KEY, ANTHROPIC_API_KEY).
28
+ // Required for terminals that fight with our raw-mode
29
+ // secret prompt and for CI / scripted installs.
25
30
  // --dry-run Print what the wizard would do; don't touch the DB or filesystem
26
31
  // --skip-verify Skip the final memory_status_aggregation() check
27
32
  //
@@ -52,6 +57,12 @@ const HELP = [
52
57
  ' --yes Reuse saved secrets without prompting (if a complete',
53
58
  ' set is already on disk in ~/.termdeck/secrets.env)',
54
59
  ' --reset Ignore saved secrets and re-prompt for everything',
60
+ ' --from-env Skip every prompt; read SUPABASE_URL,',
61
+ ' SUPABASE_SERVICE_ROLE_KEY, DATABASE_URL, OPENAI_API_KEY,',
62
+ ' ANTHROPIC_API_KEY from environment variables instead.',
63
+ ' Useful for terminals that fight with raw-mode secret',
64
+ ' prompts (MobaXterm SSH, some Windows shells) and for',
65
+ ' CI / scripted installs.',
55
66
  ' --dry-run Print the plan without touching the database or filesystem',
56
67
  ' --skip-verify Skip the final memory_status_aggregation() sanity call',
57
68
  '',
@@ -70,17 +81,91 @@ const HELP = [
70
81
  ].join('\n');
71
82
 
72
83
  function parseFlags(argv) {
73
- const out = { help: false, yes: false, reset: false, dryRun: false, skipVerify: false };
84
+ const out = {
85
+ help: false,
86
+ yes: false,
87
+ reset: false,
88
+ fromEnv: false,
89
+ dryRun: false,
90
+ skipVerify: false
91
+ };
74
92
  for (const a of argv) {
75
93
  if (a === '--help' || a === '-h') out.help = true;
76
94
  else if (a === '--yes' || a === '-y') out.yes = true;
77
95
  else if (a === '--reset') out.reset = true;
96
+ else if (a === '--from-env') out.fromEnv = true;
78
97
  else if (a === '--dry-run') out.dryRun = true;
79
98
  else if (a === '--skip-verify') out.skipVerify = true;
80
99
  }
81
100
  return out;
82
101
  }
83
102
 
103
+ // Build inputs from process.env directly, skipping every askSecret prompt.
104
+ // Used by --from-env so callers on terminals that fight with our raw-mode
105
+ // secret prompt (Brad's MobaXterm SSH report 2026-04-25, fourth occurrence)
106
+ // can hand the wizard their secrets via env vars instead. Also makes the
107
+ // wizard scriptable for CI / one-shot installers. Returns the same shape
108
+ // as collectInputs() so the rest of main() doesn't care which path filled
109
+ // it. Throws with an actionable message when a required env var is missing
110
+ // or fails its shape check — `--from-env` is strict by design (no fallback
111
+ // to prompts), since callers using it have explicitly opted into the
112
+ // non-interactive path.
113
+ function inputsFromEnv() {
114
+ const env = process.env;
115
+ const missing = [];
116
+ const required = {
117
+ SUPABASE_URL: env.SUPABASE_URL,
118
+ SUPABASE_SERVICE_ROLE_KEY: env.SUPABASE_SERVICE_ROLE_KEY,
119
+ DATABASE_URL: env.DATABASE_URL,
120
+ OPENAI_API_KEY: env.OPENAI_API_KEY
121
+ };
122
+ for (const [k, v] of Object.entries(required)) {
123
+ if (!v || !v.trim()) missing.push(k);
124
+ }
125
+ if (missing.length > 0) {
126
+ throw new Error(
127
+ `--from-env is missing required environment variable(s): ${missing.join(', ')}.\n` +
128
+ 'Set every required key in your shell or pass them on the command line, e.g.:\n' +
129
+ ' SUPABASE_URL=https://xyz.supabase.co \\\n' +
130
+ ' SUPABASE_SERVICE_ROLE_KEY=sb_secret_... \\\n' +
131
+ ' DATABASE_URL=postgres://postgres.<ref>:<pw>@<pooler-host>:6543/postgres \\\n' +
132
+ ' OPENAI_API_KEY=sk-proj-... \\\n' +
133
+ ' ANTHROPIC_API_KEY=sk-ant-... \\\n' +
134
+ ' termdeck init --mnestra --from-env'
135
+ );
136
+ }
137
+
138
+ const projectUrl = urlHelper.parseProjectUrl(required.SUPABASE_URL);
139
+ if (!projectUrl.ok) {
140
+ throw new Error(`SUPABASE_URL is malformed: ${projectUrl.error}`);
141
+ }
142
+
143
+ const dbErr = urlHelper.looksLikePostgresUrl(required.DATABASE_URL);
144
+ if (dbErr) throw new Error(`DATABASE_URL: ${dbErr}`);
145
+
146
+ const srErr = urlHelper.looksLikeServiceRole(required.SUPABASE_SERVICE_ROLE_KEY);
147
+ if (srErr) throw new Error(`SUPABASE_SERVICE_ROLE_KEY: ${srErr}`);
148
+
149
+ const oaErr = urlHelper.looksLikeOpenAiKey(required.OPENAI_API_KEY);
150
+ if (oaErr) throw new Error(`OPENAI_API_KEY: ${oaErr}`);
151
+
152
+ const anthropicKey = (env.ANTHROPIC_API_KEY || '').trim() || null;
153
+ if (anthropicKey) {
154
+ const aErr = urlHelper.looksLikeAnthropicKey(anthropicKey);
155
+ if (aErr) {
156
+ process.stdout.write(` (warning: ANTHROPIC_API_KEY ${aErr} — storing anyway)\n`);
157
+ }
158
+ }
159
+
160
+ return {
161
+ projectUrl,
162
+ serviceRoleKey: required.SUPABASE_SERVICE_ROLE_KEY,
163
+ databaseUrl: required.DATABASE_URL,
164
+ openaiKey: required.OPENAI_API_KEY,
165
+ anthropicKey
166
+ };
167
+ }
168
+
84
169
  function printBanner() {
85
170
  process.stdout.write(`
86
171
  TermDeck Mnestra Setup
@@ -100,6 +185,8 @@ This wizard configures TermDeck's Tier 2 memory layer (Mnestra) by:
100
185
 
101
186
  If you already have a complete ~/.termdeck/secrets.env, the wizard will
102
187
  offer to reuse it (or pass --yes to skip the prompt and resume directly).
188
+ If your terminal fights with the secret prompt, set the five env vars and
189
+ pass --from-env to skip every prompt entirely.
103
190
 
104
191
  Press Ctrl+C at any time to cancel.
105
192
 
@@ -354,11 +441,25 @@ async function main(argv) {
354
441
  printBanner();
355
442
 
356
443
  let inputs;
357
- try {
358
- inputs = await collectInputs({ yes: flags.yes, reset: flags.reset });
359
- } catch (err) {
360
- process.stderr.write(`\n[init --mnestra] ${err.message}\n`);
361
- return 2;
444
+ if (flags.fromEnv) {
445
+ // Skip every interactive prompt secrets come from process.env. Used
446
+ // when the user's terminal fights with the raw-mode secret prompt
447
+ // (Brad/MobaXterm SSH, 2026-04-25 fourth report) or when the wizard
448
+ // is being driven from a CI/install script.
449
+ process.stdout.write('Reading secrets from environment variables (--from-env).\n\n');
450
+ try {
451
+ inputs = inputsFromEnv();
452
+ } catch (err) {
453
+ process.stderr.write(`\n[init --mnestra] ${err.message}\n`);
454
+ return 2;
455
+ }
456
+ } else {
457
+ try {
458
+ inputs = await collectInputs({ yes: flags.yes, reset: flags.reset });
459
+ } catch (err) {
460
+ process.stderr.write(`\n[init --mnestra] ${err.message}\n`);
461
+ return 2;
462
+ }
362
463
  }
363
464
 
364
465
  // Persist secrets BEFORE pg work. If the wizard dies past this point
@@ -210,6 +210,35 @@ function runShellCaptured(command, args, opts = {}) {
210
210
  return { ok: r.status === 0, code: r.status, stdout: r.stdout || '', stderr: r.stderr || '' };
211
211
  }
212
212
 
213
+ // Detect the "no access token" signature in `supabase link` stderr so the
214
+ // wizard can surface a path-aware hint instead of dumping raw CLI output at
215
+ // the user. Brad hit this on 2026-04-26 00:25 UTC on MobaXterm SSH after
216
+ // v0.6.3 unblocked init --mnestra: he had no SUPABASE_ACCESS_TOKEN env var
217
+ // and `supabase login` requires a browser, which his SSH session doesn't
218
+ // have. The actionable path on a non-desktop install is always a PAT from
219
+ // the dashboard — link() now points users straight at it.
220
+ function looksLikeMissingAccessToken(stderr) {
221
+ if (!stderr) return false;
222
+ return /Access token not provided/i.test(stderr) ||
223
+ /SUPABASE_ACCESS_TOKEN environment variable/i.test(stderr);
224
+ }
225
+
226
+ function printAccessTokenHint() {
227
+ process.stderr.write(
228
+ '\nThe Supabase CLI needs a Personal Access Token to link your project.\n' +
229
+ 'On a desktop install you can run `supabase login`, but that opens a\n' +
230
+ 'browser, so SSH/headless users should use the env-var path instead:\n' +
231
+ '\n' +
232
+ ' 1. Generate a token: https://supabase.com/dashboard/account/tokens\n' +
233
+ ' 2. Export it in your shell:\n' +
234
+ ' export SUPABASE_ACCESS_TOKEN=sbp_...\n' +
235
+ ' 3. Re-run: termdeck init --rumen\n' +
236
+ '\n' +
237
+ 'TermDeck does not store this token — it only lives in your shell\n' +
238
+ 'environment for the duration of the install.\n'
239
+ );
240
+ }
241
+
213
242
  async function link(projectRef, dryRun) {
214
243
  step(`Running: supabase link --project-ref ${projectRef}...`);
215
244
  if (dryRun) { ok('(dry-run)'); return true; }
@@ -217,6 +246,7 @@ async function link(projectRef, dryRun) {
217
246
  if (!r.ok) {
218
247
  fail(`supabase link failed (exit ${r.code})`);
219
248
  if (r.stderr) process.stderr.write(r.stderr + '\n');
249
+ if (looksLikeMissingAccessToken(r.stderr)) printAccessTokenHint();
220
250
  return false;
221
251
  }
222
252
  ok();
@@ -512,3 +542,6 @@ if (require.main === module) {
512
542
  }
513
543
 
514
544
  module.exports = main;
545
+ // Test surface — kept on the same export object so the regression suite can
546
+ // pin the access-token detection without spawning a real `supabase` binary.
547
+ module.exports._looksLikeMissingAccessToken = looksLikeMissingAccessToken;
@@ -0,0 +1,35 @@
1
+ -- 007_add_source_session_id.sql
2
+ --
3
+ -- Adds memory_items.source_session_id back to fresh Mnestra installs.
4
+ --
5
+ -- The column existed in the original rag-system schema (TEXT) and is still
6
+ -- present on stores that were upgraded from rag-system → Engram → Mnestra.
7
+ -- It was dropped from the published Mnestra migration set during the rebrand,
8
+ -- which created a silent contract break with Rumen.
9
+ --
10
+ -- Rumen's Extract phase groups memory_items by source_session_id to find
11
+ -- eligible sessions for synthesis (see rumen/src/extract.ts:61). Without this
12
+ -- column, every Rumen cron tick fails with:
13
+ -- ERROR: column m.source_session_id does not exist (SQLSTATE 42703)
14
+ --
15
+ -- Reported 2026-04-26 by a tester (Brad) whose fresh `termdeck init --mnestra`
16
+ -- on v0.6.3 left him with a Mnestra schema that worked for TermDeck/Flashback
17
+ -- but couldn't host Rumen. Surfaced after v0.6.4's access-token hint unblocked
18
+ -- his Rumen install — the Edge Function deployed cleanly, the manual POST test
19
+ -- returned 500, and the pg_cron tick keeps failing with the same query.
20
+ --
21
+ -- TEXT matches the rag-system-era type and Josh's production schema. Rumen's
22
+ -- UUID[] cast handles values that look like UUIDs; non-UUID values would fail
23
+ -- the cast (the column has only ever held UUID-shaped strings historically).
24
+ --
25
+ -- Idempotent. Safe to re-run via `termdeck init --mnestra --yes`. NULL on every
26
+ -- existing row is the correct default — old memories were never tagged with a
27
+ -- session, and Rumen's WHERE source_session_id IS NOT NULL filter excludes
28
+ -- them naturally.
29
+
30
+ ALTER TABLE memory_items
31
+ ADD COLUMN IF NOT EXISTS source_session_id TEXT;
32
+
33
+ CREATE INDEX IF NOT EXISTS idx_memory_items_source_session_id
34
+ ON memory_items (source_session_id)
35
+ WHERE source_session_id IS NOT NULL;