@jhizzard/termdeck 0.6.4 → 0.6.7
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": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
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"
|
|
@@ -385,12 +385,24 @@ async function verifyStatus(client) {
|
|
|
385
385
|
// on disk first; config.yaml only flips to rag.enabled=true once the
|
|
386
386
|
// schema is actually in place.
|
|
387
387
|
function writeSecretsFile(inputs, dryRun) {
|
|
388
|
+
// Normalize transaction-pooler URLs by appending pgbouncer=true&
|
|
389
|
+
// connection_limit=1 when missing. Brad's Rumen logs (2026-04-26)
|
|
390
|
+
// surfaced the warning Supabase recommends: transaction-mode pooling
|
|
391
|
+
// (port 6543) needs those params or PgBouncer can return prepared-
|
|
392
|
+
// statement errors under load. Direct connections and session-mode
|
|
393
|
+
// pooler URLs are returned unchanged. See setup/supabase-url.js.
|
|
394
|
+
const normalized = urlHelper.normalizeDatabaseUrl(inputs.databaseUrl);
|
|
395
|
+
if (normalized.modified) {
|
|
396
|
+
step('Detected transaction pooler URL — appending ?pgbouncer=true&connection_limit=1...');
|
|
397
|
+
ok();
|
|
398
|
+
}
|
|
399
|
+
|
|
388
400
|
step('Writing ~/.termdeck/secrets.env...');
|
|
389
401
|
if (dryRun) { ok('(dry-run)'); return; }
|
|
390
402
|
dotenv.writeSecrets({
|
|
391
403
|
SUPABASE_URL: inputs.projectUrl.url,
|
|
392
404
|
SUPABASE_SERVICE_ROLE_KEY: inputs.serviceRoleKey,
|
|
393
|
-
DATABASE_URL:
|
|
405
|
+
DATABASE_URL: normalized.url,
|
|
394
406
|
OPENAI_API_KEY: inputs.openaiKey,
|
|
395
407
|
...(inputs.anthropicKey ? { ANTHROPIC_API_KEY: inputs.anthropicKey } : {})
|
|
396
408
|
});
|
|
@@ -166,6 +166,20 @@ function preflight() {
|
|
|
166
166
|
}
|
|
167
167
|
ok();
|
|
168
168
|
|
|
169
|
+
// Normalize DATABASE_URL for transaction-pooler usage (v0.6.6+). Users
|
|
170
|
+
// whose ~/.termdeck/secrets.env was written by an earlier wizard version
|
|
171
|
+
// may have a Shared Pooler URL without ?pgbouncer=true. The Edge Function
|
|
172
|
+
// logs a warning when that's the case (Brad's 2026-04-26 report). Fix it
|
|
173
|
+
// here in-memory before forwarding to `supabase secrets set` so the
|
|
174
|
+
// Function gets a clean URL even on partial-upgrade installs. Direct
|
|
175
|
+
// connections and session-mode pooler URLs are returned unchanged.
|
|
176
|
+
const normalized = urlHelper.normalizeDatabaseUrl(secrets.DATABASE_URL);
|
|
177
|
+
if (normalized.modified) {
|
|
178
|
+
step('Detected transaction pooler URL — appending ?pgbouncer=true&connection_limit=1 for the Edge Function...');
|
|
179
|
+
secrets.DATABASE_URL = normalized.url;
|
|
180
|
+
ok();
|
|
181
|
+
}
|
|
182
|
+
|
|
169
183
|
// OPENAI_API_KEY is optional: when present, Rumen's Relate phase generates
|
|
170
184
|
// real embeddings for semantic+keyword hybrid search. When absent, Rumen
|
|
171
185
|
// falls back to keyword-only matching (still works, but loses cross-project
|
|
@@ -455,6 +469,78 @@ async function applySchedule(projectRef, secrets, dryRun) {
|
|
|
455
469
|
}
|
|
456
470
|
}
|
|
457
471
|
|
|
472
|
+
// Backfill SUPABASE_ACCESS_TOKEN into ~/.claude/mcp.json's Supabase MCP
|
|
473
|
+
// server entry. Background: the meta-installer (`@jhizzard/termdeck-stack`)
|
|
474
|
+
// writes `SUPABASE_ACCESS_TOKEN: 'SUPABASE_PAT_HERE'` as a literal
|
|
475
|
+
// placeholder when it wires the Supabase MCP entry. The user is expected
|
|
476
|
+
// to replace it after install. v0.6.4 unblocked the Rumen install path by
|
|
477
|
+
// telling users to `export SUPABASE_ACCESS_TOKEN=sbp_...` in their shell —
|
|
478
|
+
// but that token only got used for `supabase link`, never propagated into
|
|
479
|
+
// `~/.claude/mcp.json`. So Brad's Claude Code was talking to a Supabase
|
|
480
|
+
// MCP server with a placeholder token. He had to update the JSON file
|
|
481
|
+
// manually. Reported 2026-04-26 — Brad's quote: "the token hadn't been
|
|
482
|
+
// written to the Json file which we updated manually, but you may want
|
|
483
|
+
// to put that in the patch at some point."
|
|
484
|
+
//
|
|
485
|
+
// This helper closes the loop. Idempotent and conservative:
|
|
486
|
+
// - Only runs if process.env.SUPABASE_ACCESS_TOKEN is set
|
|
487
|
+
// - Only updates when the existing value is the literal placeholder
|
|
488
|
+
// 'SUPABASE_PAT_HERE' — preserves any real token the user already set
|
|
489
|
+
// - No-op when ~/.claude/mcp.json doesn't exist (user never ran the
|
|
490
|
+
// meta-installer's Tier 4) or when there's no `supabase` MCP entry
|
|
491
|
+
// - No-op (with a soft warning) when the JSON is malformed
|
|
492
|
+
// - Atomic write via tmp-and-rename; mode 0600 to match the file's
|
|
493
|
+
// existing permissions (it already holds the placeholder)
|
|
494
|
+
// - All other mcpServers entries preserved verbatim
|
|
495
|
+
//
|
|
496
|
+
// Returns one of: { status: 'updated', path }, { status: 'already-set', path },
|
|
497
|
+
// { status: 'no-file' }, { status: 'no-supabase-entry', path },
|
|
498
|
+
// { status: 'no-token-in-env' }, { status: 'malformed', path, error }.
|
|
499
|
+
function wireAccessTokenInMcpJson({ token, mcpJsonPath, _testFs } = {}) {
|
|
500
|
+
const fsImpl = _testFs || fs;
|
|
501
|
+
const tokenValue = token || process.env.SUPABASE_ACCESS_TOKEN;
|
|
502
|
+
if (!tokenValue) return { status: 'no-token-in-env' };
|
|
503
|
+
|
|
504
|
+
const targetPath = mcpJsonPath || path.join(os.homedir(), '.claude', 'mcp.json');
|
|
505
|
+
if (!fsImpl.existsSync(targetPath)) return { status: 'no-file' };
|
|
506
|
+
|
|
507
|
+
let raw;
|
|
508
|
+
try {
|
|
509
|
+
raw = fsImpl.readFileSync(targetPath, 'utf-8');
|
|
510
|
+
} catch (err) {
|
|
511
|
+
return { status: 'malformed', path: targetPath, error: err.message };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
let cfg;
|
|
515
|
+
try {
|
|
516
|
+
cfg = JSON.parse(raw);
|
|
517
|
+
} catch (err) {
|
|
518
|
+
return { status: 'malformed', path: targetPath, error: err.message };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const supabaseEntry = cfg && cfg.mcpServers && cfg.mcpServers.supabase;
|
|
522
|
+
if (!supabaseEntry || typeof supabaseEntry !== 'object') {
|
|
523
|
+
return { status: 'no-supabase-entry', path: targetPath };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
supabaseEntry.env = supabaseEntry.env || {};
|
|
527
|
+
const current = supabaseEntry.env.SUPABASE_ACCESS_TOKEN;
|
|
528
|
+
if (current === tokenValue) return { status: 'already-set', path: targetPath };
|
|
529
|
+
if (current && current !== 'SUPABASE_PAT_HERE') {
|
|
530
|
+
// User has set a real token already — don't touch it.
|
|
531
|
+
return { status: 'already-set', path: targetPath };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
supabaseEntry.env.SUPABASE_ACCESS_TOKEN = tokenValue;
|
|
535
|
+
|
|
536
|
+
const tmpPath = `${targetPath}.tmp.${process.pid}`;
|
|
537
|
+
fsImpl.writeFileSync(tmpPath, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
|
|
538
|
+
fsImpl.renameSync(tmpPath, targetPath);
|
|
539
|
+
try { fsImpl.chmodSync(targetPath, 0o600); } catch (_e) { /* best-effort */ }
|
|
540
|
+
|
|
541
|
+
return { status: 'updated', path: targetPath };
|
|
542
|
+
}
|
|
543
|
+
|
|
458
544
|
function printNextSteps(projectRef) {
|
|
459
545
|
const functionUrl = `https://${projectRef}.supabase.co/functions/v1/rumen-tick`;
|
|
460
546
|
const now = new Date();
|
|
@@ -508,6 +594,26 @@ async function main(argv) {
|
|
|
508
594
|
}
|
|
509
595
|
|
|
510
596
|
if (!(await link(projectRef, flags.dryRun))) return 4;
|
|
597
|
+
|
|
598
|
+
// Backfill SUPABASE_ACCESS_TOKEN into ~/.claude/mcp.json now that
|
|
599
|
+
// `supabase link` succeeded (the token is verified-real). The
|
|
600
|
+
// meta-installer wrote a literal 'SUPABASE_PAT_HERE' placeholder
|
|
601
|
+
// there during Tier 4 install — this closes that loop.
|
|
602
|
+
if (!flags.dryRun) {
|
|
603
|
+
const r = wireAccessTokenInMcpJson();
|
|
604
|
+
if (r.status === 'updated') {
|
|
605
|
+
step('Backfilled SUPABASE_ACCESS_TOKEN into ~/.claude/mcp.json...');
|
|
606
|
+
ok();
|
|
607
|
+
} else if (r.status === 'malformed') {
|
|
608
|
+
process.stderr.write(
|
|
609
|
+
`\n ! ${r.path} is not valid JSON — skipping token backfill (${r.error}).\n` +
|
|
610
|
+
` Update the supabase mcpServers entry manually if Claude Code's Supabase MCP is misbehaving.\n\n`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
// Other statuses (no-file, no-supabase-entry, no-token-in-env,
|
|
614
|
+
// already-set) are silent — they're all expected paths.
|
|
615
|
+
}
|
|
616
|
+
|
|
511
617
|
if (!(await applyRumenTables(secrets, flags.dryRun))) return 5;
|
|
512
618
|
|
|
513
619
|
step('Resolving @jhizzard/rumen version from npm registry...');
|
|
@@ -545,3 +651,4 @@ module.exports = main;
|
|
|
545
651
|
// Test surface — kept on the same export object so the regression suite can
|
|
546
652
|
// pin the access-token detection without spawning a real `supabase` binary.
|
|
547
653
|
module.exports._looksLikeMissingAccessToken = looksLikeMissingAccessToken;
|
|
654
|
+
module.exports._wireAccessTokenInMcpJson = wireAccessTokenInMcpJson;
|
|
@@ -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;
|
|
@@ -97,6 +97,65 @@ function looksLikePostgresUrl(url) {
|
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// Detect a Supabase Shared Pooler URL in TRANSACTION mode. Pattern:
|
|
101
|
+
// postgres://postgres.<ref>:<pw>@aws-0-<region>.pooler.supabase.com:6543/postgres
|
|
102
|
+
// Session mode (port 5432 on the pooler) and direct connections
|
|
103
|
+
// (db.<ref>.supabase.co:5432) do NOT need pgbouncer params and must be
|
|
104
|
+
// left alone. Returns true only for the transaction-pooler shape.
|
|
105
|
+
function isTransactionPoolerUrl(parsedUrl) {
|
|
106
|
+
if (!parsedUrl) return false;
|
|
107
|
+
const host = (parsedUrl.hostname || '').toLowerCase();
|
|
108
|
+
// pooler hosts end in `.pooler.supabase.com`. Be lenient on the regional
|
|
109
|
+
// prefix — Supabase has used `aws-0-` historically and may add others.
|
|
110
|
+
if (!host.endsWith('.pooler.supabase.com')) return false;
|
|
111
|
+
// Transaction mode is port 6543. Session mode on the same host is 5432
|
|
112
|
+
// and doesn't want pgbouncer flags.
|
|
113
|
+
return parsedUrl.port === '6543';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Normalize a DATABASE_URL by appending the pgbouncer transaction-mode
|
|
117
|
+
// params Supabase requires when connecting through a transaction pooler.
|
|
118
|
+
//
|
|
119
|
+
// Brad's Rumen logs (2026-04-26) warned:
|
|
120
|
+
// "DATABASE_URL is a Shared Pooler URL but does not have ?pgbouncer=true.
|
|
121
|
+
// Append ?pgbouncer=true&connection_limit=1 for transaction-mode
|
|
122
|
+
// compatibility."
|
|
123
|
+
//
|
|
124
|
+
// The warning is harmless on its own — Rumen's runtime didn't fail because
|
|
125
|
+
// of it — but missing the params can manifest as prepared-statement errors
|
|
126
|
+
// or stuck connections under load with PgBouncer in transaction mode. The
|
|
127
|
+
// safest path is for the wizard to add them on the user's behalf when the
|
|
128
|
+
// URL shape clearly indicates they're needed.
|
|
129
|
+
//
|
|
130
|
+
// Returns `{ url, modified }`. `modified` is true when params were added.
|
|
131
|
+
// Idempotent: a URL that already has pgbouncer=true is returned unchanged.
|
|
132
|
+
// Only touches transaction-pooler URLs (port 6543 on *.pooler.supabase.com).
|
|
133
|
+
// Direct connections (port 5432, db.* hostname) and session-pooler URLs
|
|
134
|
+
// (port 5432 on pooler hostname) are returned unchanged.
|
|
135
|
+
//
|
|
136
|
+
// Errors are swallowed — a malformed URL returns `{ url: original, modified: false }`
|
|
137
|
+
// because validation is the caller's job (looksLikePostgresUrl handles that).
|
|
138
|
+
function normalizeDatabaseUrl(url) {
|
|
139
|
+
if (!url || typeof url !== 'string') return { url, modified: false };
|
|
140
|
+
let u;
|
|
141
|
+
try {
|
|
142
|
+
u = new URL(url);
|
|
143
|
+
} catch (_err) {
|
|
144
|
+
return { url, modified: false };
|
|
145
|
+
}
|
|
146
|
+
if (!isTransactionPoolerUrl(u)) return { url, modified: false };
|
|
147
|
+
|
|
148
|
+
// Already has pgbouncer set? Don't touch.
|
|
149
|
+
if (u.searchParams.has('pgbouncer')) return { url, modified: false };
|
|
150
|
+
|
|
151
|
+
u.searchParams.set('pgbouncer', 'true');
|
|
152
|
+
// Set connection_limit only if not already set — preserve user intent.
|
|
153
|
+
if (!u.searchParams.has('connection_limit')) {
|
|
154
|
+
u.searchParams.set('connection_limit', '1');
|
|
155
|
+
}
|
|
156
|
+
return { url: u.toString(), modified: true };
|
|
157
|
+
}
|
|
158
|
+
|
|
100
159
|
// Mask all but the last 4 chars of a secret for logging.
|
|
101
160
|
function maskSecret(value) {
|
|
102
161
|
if (!value || typeof value !== 'string') return '';
|
|
@@ -110,5 +169,7 @@ module.exports = {
|
|
|
110
169
|
looksLikeOpenAiKey,
|
|
111
170
|
looksLikeAnthropicKey,
|
|
112
171
|
looksLikePostgresUrl,
|
|
172
|
+
isTransactionPoolerUrl,
|
|
173
|
+
normalizeDatabaseUrl,
|
|
113
174
|
maskSecret
|
|
114
175
|
};
|