@jhizzard/termdeck 1.0.5 → 1.0.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": "1.0.5",
3
+ "version": "1.0.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"
@@ -41,6 +41,8 @@
41
41
  'use strict';
42
42
 
43
43
  const path = require('path');
44
+ const fs = require('fs');
45
+ const { spawnSync } = require('child_process');
44
46
 
45
47
  const migrations = require('./migrations');
46
48
  const { applyTemplating } = require('./migration-templating');
@@ -183,6 +185,47 @@ const PROBES = Object.freeze([
183
185
  functionSlug: 'graph-inference',
184
186
  requiredMarker: "Deno.env.get('SUPABASE_DB_URL')",
185
187
  presentWhen: 'sourceMatch'
188
+ },
189
+ // Sprint 52 — Class O: deployed-state pin drift between npm-published
190
+ // packages and Supabase-deployed Edge Functions. `npm publish` doesn't
191
+ // touch Supabase; `init --rumen` redeploys. If a user upgraded the npm
192
+ // package but didn't re-run init --rumen, the Edge Function is pinned to
193
+ // whatever rumen version was current at last deploy.
194
+ //
195
+ // probeKind 'edgeFunctionPin':
196
+ // - Downloads deployed Edge Function body via Management API.
197
+ // - Greps the npm:<pkg>@<version> import line.
198
+ // - Compares against the EXPECTED version. Two resolution shapes:
199
+ // - 'npmRegistry': run `npm view <pkg> version` (used when bundled
200
+ // source has a __RUMEN_VERSION__-style placeholder substituted at
201
+ // deploy time).
202
+ // - 'bundledSource': read bundled file, grep same npm:<pkg>@<version>
203
+ // (used when bundled source pins a static version verbatim).
204
+ // - On drift: returns absent → goes to skipped[] with a recommendation
205
+ // pointing at `termdeck init --rumen --yes`.
206
+ // - On unreachable Management API / npm view failure: skipped with the
207
+ // fail-soft reason (mirrors functionSource probe degradation pattern).
208
+ //
209
+ // YELLOW (skipped[]) is the right severity — pin drift is non-blocking
210
+ // for the wizard and non-blocking for any single Rumen tick. It just
211
+ // means stale runtime. The user-actionable fix is `init --rumen --yes`.
212
+ {
213
+ name: 'rumen-tick deployed pin matches current @jhizzard/rumen',
214
+ kind: 'rumen',
215
+ probeKind: 'edgeFunctionPin',
216
+ functionSlug: 'rumen-tick',
217
+ importPattern: /npm:@jhizzard\/rumen@(\d+\.\d+\.\d+(?:-[a-z0-9.]+)?)/,
218
+ expectedFrom: 'npmRegistry',
219
+ npmRegistryPkg: '@jhizzard/rumen'
220
+ },
221
+ {
222
+ name: 'graph-inference deployed pin matches bundled postgres',
223
+ kind: 'rumen',
224
+ probeKind: 'edgeFunctionPin',
225
+ functionSlug: 'graph-inference',
226
+ importPattern: /npm:postgres@(\d+\.\d+\.\d+(?:-[a-z0-9.]+)?)/,
227
+ expectedFrom: 'bundledSource',
228
+ bundledPath: 'packages/server/src/setup/rumen/functions/graph-inference/index.ts'
186
229
  }
187
230
  ]);
188
231
 
@@ -251,13 +294,158 @@ async function probeFunctionSource(target, { projectRef, fetchImpl }) {
251
294
  };
252
295
  }
253
296
 
297
+ // Sprint 52 — Class O: deployed-state pin drift between npm-published
298
+ // packages and Supabase-deployed Edge Functions.
299
+ //
300
+ // Probes one Edge Function for npm:<pkg>@<version> drift between the
301
+ // deployed body and the EXPECTED version. Returns:
302
+ // - { present: true } when deployed pin == expected pin (no drift)
303
+ // - { present: false, probeError: '<recommendation>' } when drift is
304
+ // detected — caller routes to skipped[] (YELLOW, non-blocking).
305
+ // - { present: false, probeError: '<reason>' } when probe can't run
306
+ // (no fetch impl, no token, Management API HTTP error, npm view
307
+ // failure, deployed body doesn't match the importPattern).
308
+ //
309
+ // Required ctx:
310
+ // - projectRef: passed through from auditUpgrade()
311
+ // - SUPABASE_ACCESS_TOKEN in env (sbp_*) — same as functionSource probe
312
+ // Optional ctx:
313
+ // - fetchImpl: test injection for HTTP. Defaults to globalThis.fetch.
314
+ // - npmViewImpl: test injection for `npm view <pkg> version`. Defaults
315
+ // to a real spawnSync('npm', ['view', pkg, 'version']). Returns
316
+ // { ok, version, error }.
317
+ // - readFileImpl: test injection for bundled-source reads. Defaults to
318
+ // fs.readFileSync(absPath, 'utf-8').
319
+ // - repoRoot: optional — used to resolve target.bundledPath. Defaults to
320
+ // packages/server/src/setup/.. relative resolution (3 levels up from
321
+ // this file is the repo root).
322
+ async function probeEdgeFunctionPin(target, ctx = {}) {
323
+ const fn = ctx.fetchImpl || (typeof globalThis !== 'undefined' ? globalThis.fetch : undefined);
324
+ if (typeof fn !== 'function') {
325
+ return { present: false, probeError: 'no fetch implementation available' };
326
+ }
327
+ if (!ctx.projectRef) {
328
+ return { present: false, probeError: 'projectRef required for edgeFunctionPin probe' };
329
+ }
330
+ const accessToken = process.env.SUPABASE_ACCESS_TOKEN;
331
+ if (!accessToken) {
332
+ return {
333
+ present: false,
334
+ 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 Edge Function pin-drift detection.'
335
+ };
336
+ }
337
+
338
+ // Resolve EXPECTED version first — if this fails we can short-circuit
339
+ // before the Management API round trip.
340
+ let expected;
341
+ if (target.expectedFrom === 'npmRegistry') {
342
+ if (!target.npmRegistryPkg) {
343
+ return { present: false, probeError: `edgeFunctionPin probe ${target.name} missing npmRegistryPkg` };
344
+ }
345
+ const npmView = ctx.npmViewImpl || defaultNpmViewVersion;
346
+ let r;
347
+ try { r = await npmView(target.npmRegistryPkg); }
348
+ catch (err) { return { present: false, probeError: `npm view ${target.npmRegistryPkg} failed: ${err.message}` }; }
349
+ if (!r || !r.ok) {
350
+ return {
351
+ present: false,
352
+ probeError: `npm view ${target.npmRegistryPkg} version failed: ${r && r.error ? r.error : 'unknown error'}`
353
+ };
354
+ }
355
+ expected = r.version;
356
+ } else if (target.expectedFrom === 'bundledSource') {
357
+ if (!target.bundledPath || !target.importPattern) {
358
+ return { present: false, probeError: `edgeFunctionPin probe ${target.name} missing bundledPath or importPattern` };
359
+ }
360
+ const repoRoot = ctx.repoRoot || path.resolve(__dirname, '..', '..', '..', '..', '..');
361
+ const bundledAbs = path.isAbsolute(target.bundledPath)
362
+ ? target.bundledPath
363
+ : path.join(repoRoot, target.bundledPath);
364
+ const readImpl = ctx.readFileImpl || ((p) => fs.readFileSync(p, 'utf-8'));
365
+ let bundledBody;
366
+ try { bundledBody = readImpl(bundledAbs); }
367
+ catch (err) { return { present: false, probeError: `bundled source read failed at ${bundledAbs}: ${err.message}` }; }
368
+ const m = bundledBody.match(target.importPattern);
369
+ if (!m) {
370
+ return { present: false, probeError: `bundled source at ${target.bundledPath} does not contain ${target.importPattern} — has the import been removed or renamed?` };
371
+ }
372
+ expected = m[1];
373
+ } else {
374
+ return { present: false, probeError: `edgeFunctionPin probe ${target.name} has unknown expectedFrom: ${target.expectedFrom}` };
375
+ }
376
+
377
+ // Fetch deployed body via Management API.
378
+ let res;
379
+ try {
380
+ res = await fn(
381
+ `https://api.supabase.com/v1/projects/${ctx.projectRef}/functions/${target.functionSlug}/body`,
382
+ { headers: { 'Authorization': `Bearer ${accessToken}` } }
383
+ );
384
+ } catch (err) {
385
+ return { present: false, probeError: `Management API fetch failed: ${err.message}` };
386
+ }
387
+ if (!res.ok) {
388
+ return {
389
+ present: false,
390
+ probeError: `Management API returned HTTP ${res.status} for ${target.functionSlug}/body — function may not be deployed yet, or access token lacks permission.`
391
+ };
392
+ }
393
+ let body;
394
+ try { body = await res.text(); }
395
+ catch (err) { return { present: false, probeError: `body decode failed: ${err.message}` }; }
396
+
397
+ const m = body.match(target.importPattern);
398
+ if (!m) {
399
+ return {
400
+ present: false,
401
+ probeError: `deployed ${target.functionSlug} body does not match ${target.importPattern} — function may be at an unexpected source revision; re-run \`termdeck init --rumen --yes\` to redeploy from bundled.`
402
+ };
403
+ }
404
+ const deployed = m[1];
405
+ if (deployed === expected) {
406
+ return { present: true };
407
+ }
408
+ return {
409
+ present: false,
410
+ probeError: `pin drift on ${target.functionSlug}: deployed=${deployed}, expected=${expected}. Run \`termdeck init --rumen --yes\` to redeploy from current.`
411
+ };
412
+ }
413
+
414
+ // Default `npm view <pkg> version` shellout. Returns { ok, version, error }.
415
+ // Synchronous spawnSync with 15s timeout — same shape as init-rumen.js
416
+ // resolveRumenVersion helper. Wrapped in a thenable so the probe can await.
417
+ function defaultNpmViewVersion(pkg) {
418
+ return Promise.resolve().then(() => {
419
+ const r = spawnSync('npm', ['view', pkg, 'version'], {
420
+ encoding: 'utf-8',
421
+ stdio: ['ignore', 'pipe', 'pipe'],
422
+ timeout: 15000
423
+ });
424
+ if (r.status === 0) {
425
+ const v = (r.stdout || '').trim();
426
+ if (/^\d+\.\d+\.\d+/.test(v)) return { ok: true, version: v };
427
+ return { ok: false, error: `unexpected output: ${JSON.stringify(v)}` };
428
+ }
429
+ const stderr = (r.stderr || '').trim();
430
+ return {
431
+ ok: false,
432
+ error: stderr ? stderr.split('\n').pop() : `exit ${r.status === null ? 'timeout' : r.status} — offline?`
433
+ };
434
+ });
435
+ }
436
+
254
437
  // Run a probe and decide present/absent based on the probe's contract.
255
438
  // Sprint 51.6 T3: dispatches by target.probeKind. Default is the legacy
256
439
  // pgClient.query path; 'functionSource' calls probeFunctionSource (HTTP).
440
+ // Sprint 52 (Class O): 'edgeFunctionPin' calls probeEdgeFunctionPin (HTTP
441
+ // + npm view / bundled-source resolution).
257
442
  async function probeOne(pgClient, target, ctx = {}) {
258
443
  if (target.probeKind === 'functionSource') {
259
444
  return probeFunctionSource(target, ctx);
260
445
  }
446
+ if (target.probeKind === 'edgeFunctionPin') {
447
+ return probeEdgeFunctionPin(target, ctx);
448
+ }
261
449
  let result;
262
450
  try {
263
451
  result = await pgClient.query(target.probeSql);
@@ -343,7 +531,10 @@ async function auditUpgrade({
343
531
  dryRun = false,
344
532
  probes,
345
533
  _migrations,
346
- _fetch
534
+ _fetch,
535
+ _npmView,
536
+ _readFile,
537
+ _repoRoot
347
538
  } = {}) {
348
539
  if (!pgClient || typeof pgClient.query !== 'function') {
349
540
  throw new Error('auditUpgrade: pgClient with .query() is required');
@@ -365,7 +556,13 @@ async function auditUpgrade({
365
556
 
366
557
  for (const target of targets) {
367
558
  probed.push(target.name);
368
- const probeResult = await probeOne(pgClient, target, { projectRef, fetchImpl: _fetch });
559
+ const probeResult = await probeOne(pgClient, target, {
560
+ projectRef,
561
+ fetchImpl: _fetch,
562
+ npmViewImpl: _npmView,
563
+ readFileImpl: _readFile,
564
+ repoRoot: _repoRoot
565
+ });
369
566
  if (probeResult.present) {
370
567
  present.push(target.name);
371
568
  continue;
@@ -374,10 +571,15 @@ async function auditUpgrade({
374
571
  // Sprint 51.6 T3 — Bug D: functionSource probes go to skipped[] (not
375
572
  // missing[]). The corresponding fix is a re-run of `init --rumen` which
376
573
  // calls deployFunctions; audit-upgrade does not auto-redeploy.
377
- if (target.probeKind === 'functionSource') {
574
+ // Sprint 52 Class O: same treatment for edgeFunctionPin probes —
575
+ // pin drift is non-blocking (YELLOW), recommendation in skipped reason.
576
+ if (target.probeKind === 'functionSource' || target.probeKind === 'edgeFunctionPin') {
577
+ const fallbackReason = target.probeKind === 'edgeFunctionPin'
578
+ ? 'pin drift — redeploy via init --rumen'
579
+ : 'function source drift — redeploy via init --rumen';
378
580
  skipped.push({
379
581
  name: target.name,
380
- reason: probeResult.probeError || 'function source drift — redeploy via init --rumen',
582
+ reason: probeResult.probeError || fallbackReason,
381
583
  });
382
584
  continue;
383
585
  }
@@ -420,6 +622,7 @@ module.exports = {
420
622
  // selection / apply pathway behavior without needing a live pg client.
421
623
  _probeOne: probeOne,
422
624
  _probeFunctionSource: probeFunctionSource,
625
+ _probeEdgeFunctionPin: probeEdgeFunctionPin,
423
626
  _applyOne: applyOne,
424
627
  _resolveMigrationFile: resolveMigrationFile
425
628
  };
@@ -12,6 +12,43 @@
12
12
  --
13
13
  -- Fix 5 — Project affinity scoring. Exact project match multiplies the
14
14
  -- score by 1.5x; mismatches are penalised 0.7x.
15
+ --
16
+ -- Sprint 51.9 — signature-drift guard. Same Class A pattern Sprint 52.1
17
+ -- closed for `match_memories` (mig 001:81-95). Codex T4 surfaced the cousin
18
+ -- 2026-05-04 14:42 ET during Sprint 51.5b dogfood: long-lived v0.6.x-era
19
+ -- installs (Joshua's petvetbid, likely Brad's jizzard-brain) ALSO have a
20
+ -- 10-arg drift overload of `memory_hybrid_search` coexisting with the
21
+ -- canonical 8-arg signature. The drift overload carries the never-shipped
22
+ -- `recency_weight`/`decay_days` parameters from a pre-canonical Mnestra
23
+ -- iteration or the rag-system writer's bootstrap. PostgREST + MCP clients
24
+ -- hit ambiguous-overload errors when calling `memory_hybrid_search` with
25
+ -- the canonical 8-arg shape because Postgres can't disambiguate.
26
+ --
27
+ -- The do-block below drops all `public.memory_hybrid_search` overloads
28
+ -- regardless of arg list, so the subsequent CREATE OR REPLACE always
29
+ -- lands cleanly on greenfield AND existing-drift installs. Idempotent —
30
+ -- on a brand-new project the loop iterates zero times. Scoped to schema
31
+ -- `public`. No CASCADE — same reasoning as mig 001's guard: SQL function
32
+ -- bodies that call this function (none currently exist) would resolve
33
+ -- by name at call time.
34
+ --
35
+ -- mig 004 will subsequently `CREATE OR REPLACE` this function with the
36
+ -- match_count cap variant (still 8 args). End state: ONE 8-arg version
37
+ -- of memory_hybrid_search in public schema. Ambiguity gone.
38
+
39
+ do $$
40
+ declare
41
+ r record;
42
+ begin
43
+ for r in
44
+ select p.oid::regprocedure as sig
45
+ from pg_proc p
46
+ join pg_namespace n on n.oid = p.pronamespace
47
+ where p.proname = 'memory_hybrid_search' and n.nspname = 'public'
48
+ loop
49
+ execute 'drop function ' || r.sig::text;
50
+ end loop;
51
+ end $$;
15
52
 
16
53
  create or replace function memory_hybrid_search (
17
54
  query_text text,
@@ -15,38 +15,24 @@
15
15
  --
16
16
  -- Idempotent: every function uses CREATE OR REPLACE; every GRANT is
17
17
  -- safe to re-run. No data mutation. Safe to re-apply on every install.
18
-
19
- -- ── 1. cron.job_run_details lookup ───────────────────────────────────────
20
18
  --
21
- -- Returns the most recent N runs of a named cron job, projecting only
22
- -- the columns the doctor needs (status, start/end timestamps, return
23
- -- message). The doctor parses `return_message` for the rumen-tick /
24
- -- graph-inference all-zeros pattern.
19
+ -- Sprint 51.9 pg_cron-conditional cron probes. T3 caught 2026-05-04
20
+ -- 14:55 ET that fresh Supabase projects (mnestra-only, no rumen) hit
21
+ -- exit-5 on this migration: `cron.job_run_details` and `cron.job` are
22
+ -- parse-time-resolved by `language sql` functions, and a fresh project
23
+ -- does NOT have `pg_cron` enabled by default (only `pg_stat_statements,
24
+ -- pg_trgm, pgcrypto, plpgsql, supabase_vault, uuid-ossp, vector` are in
25
+ -- the default set). The two cron-touching functions
26
+ -- (`mnestra_doctor_cron_runs`, `mnestra_doctor_cron_job_exists`) +
27
+ -- their grants are now wrapped in a do$$ guard that only emits them
28
+ -- when `pg_cron` is enabled. The doctor's cron-related probes return
29
+ -- the existing `unknown` band (Sprint 51.5 T2 already established it)
30
+ -- when the wrappers don't exist — graceful degradation. Petvetbid +
31
+ -- jizzard-brain are unaffected because both have rumen installed,
32
+ -- which enables pg_cron via rumen's mig 002. Closes the
33
+ -- mnestra-only-no-rumen fresh-install path.
25
34
 
26
- create or replace function mnestra_doctor_cron_runs(
27
- p_jobname text,
28
- p_limit int default 10
29
- )
30
- returns table (
31
- jobname text,
32
- status text,
33
- start_time timestamptz,
34
- end_time timestamptz,
35
- return_message text
36
- )
37
- language sql
38
- security definer
39
- set search_path = cron, public
40
- as $$
41
- select j.jobname, d.status, d.start_time, d.end_time, d.return_message
42
- from cron.job_run_details d
43
- join cron.job j on j.jobid = d.jobid
44
- where j.jobname = p_jobname
45
- order by d.start_time desc
46
- limit greatest(coalesce(p_limit, 10), 1);
47
- $$;
48
-
49
- -- ── 2. column existence probe (public schema only) ───────────────────────
35
+ -- ── 1. column existence probe (public schema only — cron-independent) ───
50
36
 
51
37
  create or replace function mnestra_doctor_column_exists(
52
38
  p_table text,
@@ -66,7 +52,7 @@ as $$
66
52
  );
67
53
  $$;
68
54
 
69
- -- ── 3. RPC / function existence probe ────────────────────────────────────
55
+ -- ── 2. RPC / function existence probe (cron-independent) ────────────────
70
56
 
71
57
  create or replace function mnestra_doctor_rpc_exists(p_name text)
72
58
  returns boolean
@@ -83,21 +69,12 @@ as $$
83
69
  );
84
70
  $$;
85
71
 
86
- -- ── 4. cron.job existence probe (does the named job exist at all) ───────
87
-
88
- create or replace function mnestra_doctor_cron_job_exists(p_jobname text)
89
- returns boolean
90
- language sql
91
- security definer
92
- set search_path = cron, public
93
- as $$
94
- select exists (select 1 from cron.job where jobname = p_jobname);
95
- $$;
96
-
97
- -- ── 5. vault.secrets existence probe (no value disclosure) ──────────────
72
+ -- ── 3. vault.secrets existence probe (no value disclosure) ──────────────
98
73
  --
99
74
  -- Existence-only — never returns the secret value. The doctor only needs
100
75
  -- to know whether the named vault entry was created during stack install.
76
+ -- supabase_vault IS in the Supabase default extension set, so this is
77
+ -- safe to emit unconditionally on greenfield projects.
101
78
 
102
79
  create or replace function mnestra_doctor_vault_secret_exists(p_name text)
103
80
  returns boolean
@@ -108,10 +85,68 @@ as $$
108
85
  select exists (select 1 from vault.secrets where name = p_name);
109
86
  $$;
110
87
 
111
- -- ── 6. Grants ────────────────────────────────────────────────────────────
88
+ -- ── 4. Grants for the cron-independent probes ───────────────────────────
112
89
 
113
- grant execute on function mnestra_doctor_cron_runs(text, int) to service_role;
114
90
  grant execute on function mnestra_doctor_column_exists(text, text) to service_role;
115
91
  grant execute on function mnestra_doctor_rpc_exists(text) to service_role;
116
- grant execute on function mnestra_doctor_cron_job_exists(text) to service_role;
117
92
  grant execute on function mnestra_doctor_vault_secret_exists(text) to service_role;
93
+
94
+ -- ── 5. cron.* probes (only emit when pg_cron is enabled) ────────────────
95
+ --
96
+ -- The two SECURITY DEFINER wrappers below reference `cron.job_run_details`
97
+ -- and `cron.job`. Because Postgres parse-time-resolves identifiers in
98
+ -- `language sql` functions, the CREATE itself fails when `cron` schema
99
+ -- doesn't exist — not just function execution. The do$$ guard checks
100
+ -- pg_extension first; if pg_cron isn't enabled, we skip emitting these
101
+ -- two wrappers entirely. The doctor's cron probes return `unknown`
102
+ -- in that case (Sprint 51.5 T2 contract).
103
+ --
104
+ -- Idempotent: do$$ runs every replay; CREATE OR REPLACE keeps the
105
+ -- function definitions in sync if pg_cron later gets enabled and the
106
+ -- migration re-runs. Existing installs (petvetbid, jizzard-brain) have
107
+ -- pg_cron from Rumen's install path and emit these unconditionally.
108
+
109
+ do $cron_guard$
110
+ begin
111
+ if exists (select 1 from pg_extension where extname = 'pg_cron') then
112
+ execute $stmt$
113
+ create or replace function mnestra_doctor_cron_runs(
114
+ p_jobname text,
115
+ p_limit int default 10
116
+ )
117
+ returns table (
118
+ jobname text,
119
+ status text,
120
+ start_time timestamptz,
121
+ end_time timestamptz,
122
+ return_message text
123
+ )
124
+ language sql
125
+ security definer
126
+ set search_path = cron, public
127
+ as $body$
128
+ select j.jobname, d.status, d.start_time, d.end_time, d.return_message
129
+ from cron.job_run_details d
130
+ join cron.job j on j.jobid = d.jobid
131
+ where j.jobname = p_jobname
132
+ order by d.start_time desc
133
+ limit greatest(coalesce(p_limit, 10), 1);
134
+ $body$;
135
+ $stmt$;
136
+
137
+ execute $stmt$
138
+ create or replace function mnestra_doctor_cron_job_exists(p_jobname text)
139
+ returns boolean
140
+ language sql
141
+ security definer
142
+ set search_path = cron, public
143
+ as $body$
144
+ select exists (select 1 from cron.job where jobname = p_jobname);
145
+ $body$;
146
+ $stmt$;
147
+
148
+ execute 'grant execute on function mnestra_doctor_cron_runs(text, int) to service_role';
149
+ execute 'grant execute on function mnestra_doctor_cron_job_exists(text) to service_role';
150
+ end if;
151
+ end
152
+ $cron_guard$;