@jhizzard/termdeck 1.0.0 → 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 +2 -1
- package/packages/cli/src/init-mnestra.js +149 -1
- package/packages/cli/src/init-rumen.js +370 -35
- package/packages/server/src/setup/audit-upgrade.js +425 -0
- package/packages/server/src/setup/index.js +2 -1
- package/packages/server/src/setup/mnestra-migrations/013_reclassify_uncertain.sql +39 -0
- package/packages/server/src/setup/mnestra-migrations/014_explicit_grants.sql +46 -0
- package/packages/server/src/setup/mnestra-migrations/015_source_agent.sql +51 -0
- package/packages/server/src/setup/mnestra-migrations/016_mnestra_doctor_probes.sql +117 -0
- package/packages/server/src/setup/mnestra-migrations/017_memory_sessions_session_metadata.sql +94 -0
- package/packages/server/src/setup/preconditions.js +36 -4
- package/packages/server/src/setup/rumen/functions/graph-inference/index.ts +6 -2
- package/packages/server/src/setup/rumen/functions/rumen-tick/index.ts +6 -2
- 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 +740 -0
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
// 4. Apply rumen migration 001 via pg
|
|
21
21
|
// 5. supabase functions deploy rumen-tick AND graph-inference (Sprint 43 T3)
|
|
22
22
|
// from a single staging dir with multi-function supabase/config.toml
|
|
23
|
-
// 6. supabase secrets set DATABASE_URL
|
|
23
|
+
// 6. supabase secrets set, ONE call per key (DATABASE_URL, ANTHROPIC_API_KEY,
|
|
24
|
+
// [OPENAI_API_KEY], [GRAPH_LLM_CLASSIFY=1]) — per-secret to avoid the
|
|
25
|
+
// v2.90.0 multi-arg drop documented in INSTALLER-PITFALLS.md Class J.
|
|
24
26
|
// 7. Test rumen-tick with a manual POST (graph-inference is cron-only)
|
|
25
27
|
// 8. Apply pg_cron schedule migrations 002 (rumen-tick) AND 003 (graph-inference)
|
|
26
28
|
// with project ref substituted
|
|
@@ -38,7 +40,8 @@ const {
|
|
|
38
40
|
migrations,
|
|
39
41
|
migrationTemplating,
|
|
40
42
|
pgRunner,
|
|
41
|
-
preconditions
|
|
43
|
+
preconditions,
|
|
44
|
+
auditUpgrade: auditUpgradeMod
|
|
42
45
|
} = require(SETUP_DIR);
|
|
43
46
|
|
|
44
47
|
const {
|
|
@@ -303,13 +306,84 @@ async function applyRumenTables(secrets, dryRun) {
|
|
|
303
306
|
}
|
|
304
307
|
}
|
|
305
308
|
|
|
309
|
+
// Sprint 51.5 T1 — schema-introspection audit-upgrade. Probes for missing
|
|
310
|
+
// mnestra schema artifacts AND missing rumen cron schedules. Runs before
|
|
311
|
+
// the existing init-rumen flow so the user sees what's about to be applied
|
|
312
|
+
// up front. Idempotent: a re-run on an up-to-date project reports
|
|
313
|
+
// "install up to date" and applies nothing.
|
|
314
|
+
//
|
|
315
|
+
// Brad's 2026-05-02 jizzard-brain report (INSTALLER-PITFALLS.md ledger #13)
|
|
316
|
+
// is the originating motivation: he upgraded npm packages but his database
|
|
317
|
+
// stayed frozen at first-kickstart because no installer code path diffed an
|
|
318
|
+
// existing install against the bundled migration set. After v1.0.1 ships,
|
|
319
|
+
// `npm install -g @jhizzard/termdeck@1.0.1 && termdeck init --rumen` will
|
|
320
|
+
// surface and apply every missing artifact in one pass.
|
|
321
|
+
//
|
|
322
|
+
// Errors are surfaced inline but do NOT abort the wizard — a single
|
|
323
|
+
// failing probe (e.g., pg_cron not enabled when probing for cron.job)
|
|
324
|
+
// shouldn't block the rest of the audit or the rest of the init flow.
|
|
325
|
+
async function runRumenAudit(projectRef, secrets, dryRun) {
|
|
326
|
+
step('Audit-upgrade: probing for missing schema + cron artifacts...');
|
|
327
|
+
if (dryRun) { ok('(dry-run)'); return true; }
|
|
328
|
+
let client;
|
|
329
|
+
try {
|
|
330
|
+
client = await pgRunner.connect(secrets.DATABASE_URL);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
fail(err.message);
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
const result = await auditUpgradeMod.auditUpgrade({
|
|
337
|
+
pgClient: client,
|
|
338
|
+
projectRef
|
|
339
|
+
});
|
|
340
|
+
if (result.applied.length === 0 && result.errors.length === 0 && result.skipped.length === 0) {
|
|
341
|
+
ok(`(install up to date — ${result.probed.length} probes all present)`);
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
ok(`(probed ${result.probed.length}, applied ${result.applied.length}, skipped ${result.skipped.length})`);
|
|
345
|
+
for (const name of result.applied) {
|
|
346
|
+
process.stdout.write(` ✓ applied ${name}\n`);
|
|
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
|
+
}
|
|
355
|
+
for (const e of result.errors) {
|
|
356
|
+
process.stdout.write(` ! ${e.name}: ${e.error}\n`);
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
} catch (err) {
|
|
360
|
+
fail(err.message);
|
|
361
|
+
return true; // non-blocking
|
|
362
|
+
} finally {
|
|
363
|
+
try { await client.end(); } catch (_err) { /* ignore */ }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
306
367
|
// Sprint 43 T3: rumen-tick is the only function with a `__RUMEN_VERSION__`
|
|
307
368
|
// placeholder (its `npm:@jhizzard/rumen@<ver>` import is rewritten at deploy
|
|
308
369
|
// time). graph-inference pins its own deps (`npm:postgres@3.4.4`) and is
|
|
309
370
|
// copied verbatim. If a future function adds a placeholder, list it here.
|
|
310
371
|
const FUNCTIONS_WITH_VERSION_PLACEHOLDER = new Set(['rumen-tick']);
|
|
311
372
|
|
|
312
|
-
|
|
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
|
+
}
|
|
313
387
|
const fnNames = migrations.listRumenFunctions();
|
|
314
388
|
if (fnNames.length === 0) {
|
|
315
389
|
fail('no Rumen Edge Function source found in bundled setup or @jhizzard/rumen package');
|
|
@@ -336,10 +410,14 @@ function deployFunctions(rumenVersion, dryRun) {
|
|
|
336
410
|
ok();
|
|
337
411
|
|
|
338
412
|
for (const name of fnNames) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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 });
|
|
343
421
|
if (!r.ok) {
|
|
344
422
|
fail(`deploy of ${name} failed (exit ${r.code})`);
|
|
345
423
|
return false;
|
|
@@ -400,29 +478,238 @@ ${fnBlocks}`;
|
|
|
400
478
|
return stage;
|
|
401
479
|
}
|
|
402
480
|
|
|
403
|
-
|
|
481
|
+
// Sprint 51.5 T3: per-secret CLI loop. Pre-Sprint-51.5 this issued a single
|
|
482
|
+
// `supabase secrets set KEY1=VAL1 KEY2=VAL2 ...` call with all keys as
|
|
483
|
+
// positional args. Brad's 2026-05-03 4-project install pass observed
|
|
484
|
+
// supabase CLI v2.90.0 silently dropping some args from a multi-arg call —
|
|
485
|
+
// even materializing stray entries from misparsed argv (his email landed as
|
|
486
|
+
// a secret name). Documented as INSTALLER-PITFALLS.md Class J. The
|
|
487
|
+
// deterministic fix is one CLI invocation per secret, exit code checked per
|
|
488
|
+
// call, stderr surfaced with the failing key name. Order is preserved for
|
|
489
|
+
// log readability (DATABASE_URL → ANTHROPIC_API_KEY → optional → optional).
|
|
490
|
+
function setFunctionSecrets(secrets, dryRun, opts = {}) {
|
|
491
|
+
const orderedKeys = ['DATABASE_URL', 'ANTHROPIC_API_KEY'];
|
|
492
|
+
if (secrets.OPENAI_API_KEY) orderedKeys.push('OPENAI_API_KEY');
|
|
493
|
+
if (secrets.GRAPH_LLM_CLASSIFY === '1') orderedKeys.push('GRAPH_LLM_CLASSIFY');
|
|
494
|
+
|
|
404
495
|
const haveOpenAI = Boolean(secrets.OPENAI_API_KEY);
|
|
405
|
-
|
|
406
|
-
? 'DATABASE_URL, ANTHROPIC_API_KEY, OPENAI_API_KEY'
|
|
407
|
-
: 'DATABASE_URL, ANTHROPIC_API_KEY';
|
|
408
|
-
step(`Setting function secrets (${label})...`);
|
|
496
|
+
step(`Setting function secrets per-call (${orderedKeys.join(', ')})...`);
|
|
409
497
|
if (dryRun) { ok('(dry-run)'); return true; }
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
498
|
+
|
|
499
|
+
// Test surface: opts.runner is an optional injected function that mimics
|
|
500
|
+
// runShellCaptured((bin, args) => { ok, code, stdout, stderr }). Production
|
|
501
|
+
// path passes nothing and shells out to the real supabase CLI.
|
|
502
|
+
const runner = (typeof opts.runner === 'function') ? opts.runner : runShellCaptured;
|
|
503
|
+
|
|
504
|
+
for (const key of orderedKeys) {
|
|
505
|
+
const value = secrets[key];
|
|
506
|
+
if (value === undefined || value === null || value === '') {
|
|
507
|
+
fail(`secret ${key} missing from in-memory secrets map — wizard wiring bug`);
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
const r = runner('supabase', ['secrets', 'set', `${key}=${value}`]);
|
|
511
|
+
if (!r || !r.ok) {
|
|
512
|
+
fail(`supabase secrets set ${key} failed (exit ${r ? r.code : 'no-result'})`);
|
|
513
|
+
if (r && r.stderr) process.stderr.write(r.stderr + '\n');
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const llmTag = secrets.GRAPH_LLM_CLASSIFY === '1' ? ', graph LLM classify on' : '';
|
|
518
|
+
ok(`${haveOpenAI ? '(hybrid mode' : '(keyword-only mode — OPENAI_API_KEY not set'}${llmTag})`);
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Sprint 51.5 T3: Build a Supabase SQL-Editor deeplink that pre-fills the
|
|
523
|
+
// vault.create_secret() call for one secret. Used as the fallback when
|
|
524
|
+
// auto-apply via pgRunner can't write to vault.secrets (permission denied,
|
|
525
|
+
// missing extension, etc.) AND as the manual-fix surface in any wizard text
|
|
526
|
+
// that previously instructed users to "click Vault in the dashboard" — the
|
|
527
|
+
// Vault dashboard panel was quietly removed/relocated in current Supabase
|
|
528
|
+
// UIs (Brad 2026-05-03 takeaway #2; INSTALLER-PITFALLS.md Class B).
|
|
529
|
+
//
|
|
530
|
+
// vault.create_secret signature is `(secret text, name text [, description text])`
|
|
531
|
+
// — value-then-name. Both arguments are escaped as Postgres string literals
|
|
532
|
+
// (single-quote doubling). The full URL is roughly:
|
|
533
|
+
// https://supabase.com/dashboard/project/<ref>/sql/new?content=<encoded SQL>
|
|
534
|
+
// Click → SQL Editor opens with the call pre-filled → user clicks Run.
|
|
535
|
+
function vaultSqlEditorUrl(projectRef, secretName, secretValue) {
|
|
536
|
+
if (!projectRef || typeof projectRef !== 'string') {
|
|
537
|
+
throw new Error('vaultSqlEditorUrl: projectRef is required');
|
|
538
|
+
}
|
|
539
|
+
const value = String(secretValue == null ? '' : secretValue).replace(/'/g, "''");
|
|
540
|
+
const name = String(secretName == null ? '' : secretName).replace(/'/g, "''");
|
|
541
|
+
const sql = `select vault.create_secret('${value}', '${name}');`;
|
|
542
|
+
return `https://supabase.com/dashboard/project/${projectRef}/sql/new?content=${encodeURIComponent(sql)}`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Sprint 51.5 T3: Ensure the two Vault secrets the cron schedules need are
|
|
546
|
+
// present, auto-creating them via the user's pg connection when possible.
|
|
547
|
+
//
|
|
548
|
+
// Required:
|
|
549
|
+
// - rumen_service_role_key (used by 002_pg_cron_schedule.sql)
|
|
550
|
+
// - graph_inference_service_role_key (used by 003_graph_inference_schedule.sql)
|
|
551
|
+
//
|
|
552
|
+
// Both keys hold the same value (`secrets.SUPABASE_SERVICE_ROLE_KEY`). Brad's
|
|
553
|
+
// 2026-05-02 recovery on jizzard-brain literally cloned rumen → graph_inference
|
|
554
|
+
// in vault.
|
|
555
|
+
//
|
|
556
|
+
// Strategy:
|
|
557
|
+
// 1. Open a pg connection to DATABASE_URL (same path as applyRumenTables).
|
|
558
|
+
// 2. Probe vault.secrets for both names. (Reads vault.secrets, NOT
|
|
559
|
+
// vault.decrypted_secrets — we don't need the decrypted value, just
|
|
560
|
+
// presence.)
|
|
561
|
+
// 3. For any missing name, call `vault.create_secret($value, $name)`.
|
|
562
|
+
// 4. On per-secret failure (permission denied, etc.), surface a SQL-Editor
|
|
563
|
+
// deeplink the user can click; do not fail the wizard hard — the
|
|
564
|
+
// preconditions audit will catch a still-missing rumen_service_role_key
|
|
565
|
+
// with its own hint, and the user has the actionable URL in front of
|
|
566
|
+
// them either way.
|
|
567
|
+
//
|
|
568
|
+
// Returns `{ ok, created: [...], deeplinks: [{ name, url, error }] }`.
|
|
569
|
+
async function ensureVaultSecrets({ projectRef, secrets, dryRun, _pgClient }) {
|
|
570
|
+
const required = [
|
|
571
|
+
{ name: 'rumen_service_role_key', value: secrets.SUPABASE_SERVICE_ROLE_KEY },
|
|
572
|
+
{ name: 'graph_inference_service_role_key', value: secrets.SUPABASE_SERVICE_ROLE_KEY }
|
|
414
573
|
];
|
|
415
|
-
|
|
416
|
-
|
|
574
|
+
|
|
575
|
+
step('Ensuring Vault secrets (rumen_service_role_key, graph_inference_service_role_key)...');
|
|
576
|
+
if (dryRun) { ok('(dry-run)'); return { ok: true, created: [], deeplinks: [] }; }
|
|
577
|
+
|
|
578
|
+
if (!secrets.SUPABASE_SERVICE_ROLE_KEY) {
|
|
579
|
+
fail('SUPABASE_SERVICE_ROLE_KEY missing from in-memory secrets — preflight should have rejected');
|
|
580
|
+
return { ok: false, created: [], deeplinks: [], error: 'service-role-key-missing' };
|
|
417
581
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
582
|
+
|
|
583
|
+
let client = _pgClient;
|
|
584
|
+
let ownsClient = false;
|
|
585
|
+
if (!client) {
|
|
586
|
+
try {
|
|
587
|
+
client = await pgRunner.connect(secrets.DATABASE_URL);
|
|
588
|
+
ownsClient = true;
|
|
589
|
+
} catch (err) {
|
|
590
|
+
fail(err.message);
|
|
591
|
+
return { ok: false, created: [], deeplinks: [], error: 'pg-connect-failed' };
|
|
592
|
+
}
|
|
423
593
|
}
|
|
424
|
-
|
|
425
|
-
|
|
594
|
+
|
|
595
|
+
let existing = new Set();
|
|
596
|
+
try {
|
|
597
|
+
const probe = await client.query(
|
|
598
|
+
'SELECT name FROM vault.secrets WHERE name = ANY($1::text[])',
|
|
599
|
+
[required.map((x) => x.name)]
|
|
600
|
+
);
|
|
601
|
+
existing = new Set((probe.rows || []).map((r) => r.name));
|
|
602
|
+
} catch (err) {
|
|
603
|
+
fail(`vault.secrets probe failed: ${err.message}`);
|
|
604
|
+
if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
|
|
605
|
+
// Emit deeplinks for both — user can click through manually.
|
|
606
|
+
const deeplinks = required.map(({ name, value }) => ({
|
|
607
|
+
name,
|
|
608
|
+
url: vaultSqlEditorUrl(projectRef, name, value),
|
|
609
|
+
error: err.message
|
|
610
|
+
}));
|
|
611
|
+
printVaultDeeplinks(deeplinks);
|
|
612
|
+
return { ok: false, created: [], deeplinks, error: 'vault-probe-failed' };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const missing = required.filter((x) => !existing.has(x.name));
|
|
616
|
+
if (missing.length === 0) {
|
|
617
|
+
if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
|
|
618
|
+
ok('(both already present)');
|
|
619
|
+
return { ok: true, created: [], deeplinks: [] };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const created = [];
|
|
623
|
+
const deeplinks = [];
|
|
624
|
+
for (const { name, value } of missing) {
|
|
625
|
+
try {
|
|
626
|
+
await client.query('SELECT vault.create_secret($1, $2)', [value, name]);
|
|
627
|
+
created.push(name);
|
|
628
|
+
} catch (err) {
|
|
629
|
+
deeplinks.push({
|
|
630
|
+
name,
|
|
631
|
+
url: vaultSqlEditorUrl(projectRef, name, value),
|
|
632
|
+
error: err && err.message ? err.message : String(err)
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (ownsClient) { try { await client.end(); } catch (_e) { /* ignore */ } }
|
|
637
|
+
|
|
638
|
+
if (deeplinks.length > 0) {
|
|
639
|
+
fail(`auto-created ${created.length} of ${missing.length}; ${deeplinks.length} need a manual SQL Editor click`);
|
|
640
|
+
printVaultDeeplinks(deeplinks);
|
|
641
|
+
return { ok: false, created, deeplinks };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
ok(`(created ${created.length}: ${created.map((n) => n).join(', ')})`);
|
|
645
|
+
return { ok: true, created, deeplinks: [] };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function printVaultDeeplinks(deeplinks) {
|
|
649
|
+
process.stderr.write(
|
|
650
|
+
'\nThe Supabase Vault dashboard panel has been removed in current Supabase UIs.\n' +
|
|
651
|
+
'Open each link below and click Run in SQL Editor to create the secret:\n\n'
|
|
652
|
+
);
|
|
653
|
+
for (const d of deeplinks) {
|
|
654
|
+
process.stderr.write(` ${d.name}\n ${d.url}\n`);
|
|
655
|
+
if (d.error) process.stderr.write(` (auto-apply error: ${d.error})\n`);
|
|
656
|
+
process.stderr.write('\n');
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Sprint 51.5 T3: Install-time prompt for the GRAPH_LLM_CLASSIFY toggle.
|
|
661
|
+
//
|
|
662
|
+
// graph-inference (the daily cron Edge Function) defaults every new edge to
|
|
663
|
+
// `relates_to` unless GRAPH_LLM_CLASSIFY=1 AND ANTHROPIC_API_KEY are set as
|
|
664
|
+
// Edge Function secrets. Pre-Sprint-51.5, no install path covered this — the
|
|
665
|
+
// wizard set ANTHROPIC_API_KEY (already required) but never set
|
|
666
|
+
// GRAPH_LLM_CLASSIFY, leaving the LLM classifier off by default. This is
|
|
667
|
+
// INSTALLER-PITFALLS.md Class F (default-vs-runtime asymmetry — Joshua may
|
|
668
|
+
// or may not have set the flag manually; new installs definitely don't).
|
|
669
|
+
//
|
|
670
|
+
// Side-effect: mutates `secrets.GRAPH_LLM_CLASSIFY` to '1' on Y-path. The
|
|
671
|
+
// per-secret loop in setFunctionSecrets reads that key and pushes it into
|
|
672
|
+
// the supabase secrets set sequence. On N-path the key is left undefined
|
|
673
|
+
// and the loop skips it; the wizard prints the manual flip command so the
|
|
674
|
+
// user can opt in later without re-running the whole wizard.
|
|
675
|
+
//
|
|
676
|
+
// --yes accepts the default (Y). --dry-run reports the plan and assumes Y.
|
|
677
|
+
async function promptGraphLlmClassify({ secrets, flags }) {
|
|
678
|
+
const explainer =
|
|
679
|
+
'\nGraph edge classification\n' +
|
|
680
|
+
'─────────────────────────\n' +
|
|
681
|
+
'When enabled, the daily graph-inference cron uses Claude Haiku 4.5 to label\n' +
|
|
682
|
+
'each new edge with a relationship type (supersedes, contradicts, elaborates,\n' +
|
|
683
|
+
'caused_by, blocks, inspired_by, cross_project_link, relates_to).\n' +
|
|
684
|
+
'\n' +
|
|
685
|
+
'Cost: ~$0.003 per 1k edges classified (a typical project sees a few hundred\n' +
|
|
686
|
+
'new edges per day). Disabled = every edge is typed "relates_to".\n\n';
|
|
687
|
+
|
|
688
|
+
if (flags.dryRun) {
|
|
689
|
+
process.stdout.write(explainer);
|
|
690
|
+
process.stdout.write('? Enable AI-classified graph edges? [Y/n] (dry-run, defaulting Y)\n');
|
|
691
|
+
secrets.GRAPH_LLM_CLASSIFY = '1';
|
|
692
|
+
return { enabled: true, source: 'dry-run' };
|
|
693
|
+
}
|
|
694
|
+
if (flags.yes) {
|
|
695
|
+
process.stdout.write(explainer);
|
|
696
|
+
process.stdout.write('? Enable AI-classified graph edges? [Y/n] (--yes, defaulting Y)\n');
|
|
697
|
+
secrets.GRAPH_LLM_CLASSIFY = '1';
|
|
698
|
+
return { enabled: true, source: '--yes' };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
process.stdout.write(explainer);
|
|
702
|
+
const yes = await prompts.confirm('? Enable AI-classified graph edges?', { defaultYes: true });
|
|
703
|
+
if (yes) {
|
|
704
|
+
secrets.GRAPH_LLM_CLASSIFY = '1';
|
|
705
|
+
process.stdout.write(' → Will set GRAPH_LLM_CLASSIFY=1 + ANTHROPIC_API_KEY in Edge Function secrets.\n\n');
|
|
706
|
+
return { enabled: true, source: 'prompt' };
|
|
707
|
+
}
|
|
708
|
+
process.stdout.write(
|
|
709
|
+
' → GRAPH_LLM_CLASSIFY left unset. Edges will default to "relates_to".\n' +
|
|
710
|
+
' To enable later: supabase secrets set GRAPH_LLM_CLASSIFY=1\n\n'
|
|
711
|
+
);
|
|
712
|
+
return { enabled: false, source: 'prompt' };
|
|
426
713
|
}
|
|
427
714
|
|
|
428
715
|
async function testFunction(projectRef, secrets, dryRun) {
|
|
@@ -601,13 +888,22 @@ function wireAccessTokenInMcpJson({ token, mcpJsonPath, _testFs } = {}) {
|
|
|
601
888
|
return { status: 'updated', path: targetPath };
|
|
602
889
|
}
|
|
603
890
|
|
|
604
|
-
function printNextSteps(projectRef) {
|
|
891
|
+
function printNextSteps(projectRef, vaultResult, llmResult) {
|
|
605
892
|
const rumenTickUrl = `https://${projectRef}.supabase.co/functions/v1/rumen-tick`;
|
|
606
893
|
const graphInferenceUrl = `https://${projectRef}.supabase.co/functions/v1/graph-inference`;
|
|
607
894
|
const now = new Date();
|
|
608
895
|
// Round up to the next 15-minute mark so the hint is accurate.
|
|
609
896
|
const next = new Date(now.getTime());
|
|
610
897
|
next.setUTCMinutes(Math.ceil((now.getUTCMinutes() + 1) / 15) * 15, 0, 0);
|
|
898
|
+
|
|
899
|
+
const vaultLine = (vaultResult && vaultResult.ok)
|
|
900
|
+
? ' Vault secrets: rumen_service_role_key + graph_inference_service_role_key in place.'
|
|
901
|
+
: ' Vault secrets: open the SQL Editor URLs above and click Run for any deeplinks shown.';
|
|
902
|
+
|
|
903
|
+
const llmLine = llmResult && llmResult.enabled
|
|
904
|
+
? ' Graph edges: classified by Claude Haiku 4.5 (GRAPH_LLM_CLASSIFY=1).'
|
|
905
|
+
: ' Graph edges: untyped (relates_to). To enable: supabase secrets set GRAPH_LLM_CLASSIFY=1';
|
|
906
|
+
|
|
611
907
|
process.stdout.write(`
|
|
612
908
|
Rumen is deployed.
|
|
613
909
|
|
|
@@ -618,13 +914,12 @@ Edge Functions:
|
|
|
618
914
|
${graphInferenceUrl}
|
|
619
915
|
|
|
620
916
|
Next steps:
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
5. TermDeck's Flashback will surface cross-project patterns automatically.
|
|
917
|
+
${vaultLine}
|
|
918
|
+
${llmLine}
|
|
919
|
+
Monitor rumen jobs: psql "$DATABASE_URL" -c "SELECT * FROM rumen_jobs ORDER BY started_at DESC LIMIT 5"
|
|
920
|
+
Rumen insights flow back into Mnestra's memory_items via rumen_insights.
|
|
921
|
+
graph-inference fills memory_relationships edges nightly (cosine similarity ≥ 0.85).
|
|
922
|
+
TermDeck's Flashback will surface cross-project patterns automatically.
|
|
628
923
|
`);
|
|
629
924
|
}
|
|
630
925
|
|
|
@@ -658,6 +953,22 @@ async function main(argv) {
|
|
|
658
953
|
}
|
|
659
954
|
}
|
|
660
955
|
|
|
956
|
+
// Sprint 51.5 T3: ensure both Vault secrets are present BEFORE the
|
|
957
|
+
// precondition audit (which checks vault.decrypted_secrets for
|
|
958
|
+
// rumen_service_role_key). Auto-applies via pgRunner; on permission
|
|
959
|
+
// failure prints SQL-Editor deeplinks and lets the audit's own hint
|
|
960
|
+
// catch the still-missing secret. The Vault dashboard panel was quietly
|
|
961
|
+
// removed in current Supabase UIs (Brad 2026-05-03 takeaway #2;
|
|
962
|
+
// INSTALLER-PITFALLS.md Class B), which is why we no longer instruct
|
|
963
|
+
// users to "click Vault."
|
|
964
|
+
let vaultResult = { ok: true, created: [], deeplinks: [] };
|
|
965
|
+
if (!flags.dryRun) {
|
|
966
|
+
vaultResult = await ensureVaultSecrets({ projectRef, secrets, dryRun: false });
|
|
967
|
+
// Continue regardless — preconditions audit will catch a still-missing
|
|
968
|
+
// secret with its own hint, and the user already has the deeplinks if
|
|
969
|
+
// any auto-apply failed.
|
|
970
|
+
}
|
|
971
|
+
|
|
661
972
|
// v0.6.9: front-loaded precondition audit. Runs BEFORE link so we don't
|
|
662
973
|
// create state (function deploy, function secrets, schedule SQL) that the
|
|
663
974
|
// user would have to manually clean up if a precondition is missing. Every
|
|
@@ -691,6 +1002,14 @@ async function main(argv) {
|
|
|
691
1002
|
// already-set) are silent — they're all expected paths.
|
|
692
1003
|
}
|
|
693
1004
|
|
|
1005
|
+
// Sprint 51.5 T1 — audit-upgrade BEFORE the rest of the flow. Surfaces
|
|
1006
|
+
// and applies any drift between the bundled artifact set and what's
|
|
1007
|
+
// actually live on the user's project. Non-blocking on failure (probe
|
|
1008
|
+
// errors get logged inline; main flow continues).
|
|
1009
|
+
if (!flags.dryRun) {
|
|
1010
|
+
await runRumenAudit(projectRef, secrets, flags.dryRun);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
694
1013
|
if (!(await applyRumenTables(secrets, flags.dryRun))) return 5;
|
|
695
1014
|
|
|
696
1015
|
step('Resolving @jhizzard/rumen version from npm registry...');
|
|
@@ -702,7 +1021,13 @@ async function main(argv) {
|
|
|
702
1021
|
process.stderr.write(` ! falling back to pinned FALLBACK_RUMEN_VERSION=${FALLBACK_RUMEN_VERSION}\n`);
|
|
703
1022
|
}
|
|
704
1023
|
|
|
705
|
-
if (!deployFunctions(resolved.version, flags.dryRun)) return 6;
|
|
1024
|
+
if (!deployFunctions(resolved.version, projectRef, flags.dryRun)) return 6;
|
|
1025
|
+
|
|
1026
|
+
// Sprint 51.5 T3: install-time prompt for AI edge classification. Sets
|
|
1027
|
+
// secrets.GRAPH_LLM_CLASSIFY in-memory; the per-secret loop below picks
|
|
1028
|
+
// it up. On --yes / --dry-run defaults to enabled (Y).
|
|
1029
|
+
const llmResult = await promptGraphLlmClassify({ secrets, flags });
|
|
1030
|
+
|
|
706
1031
|
if (!setFunctionSecrets(secrets, flags.dryRun)) return 7;
|
|
707
1032
|
if (!(await testFunction(projectRef, secrets, flags.dryRun))) return 8;
|
|
708
1033
|
if (!flags.skipSchedule) {
|
|
@@ -720,7 +1045,7 @@ async function main(argv) {
|
|
|
720
1045
|
process.stdout.write('→ Skipping pg_cron schedule (per --skip-schedule) ✓\n');
|
|
721
1046
|
}
|
|
722
1047
|
|
|
723
|
-
printNextSteps(projectRef);
|
|
1048
|
+
printNextSteps(projectRef, vaultResult, llmResult);
|
|
724
1049
|
return 0;
|
|
725
1050
|
}
|
|
726
1051
|
|
|
@@ -741,3 +1066,13 @@ module.exports._wireAccessTokenInMcpJson = wireAccessTokenInMcpJson;
|
|
|
741
1066
|
// Sprint 43 T3: stage helper exposed so init-rumen-deploy.test.js can pin
|
|
742
1067
|
// the multi-function staging contract without shelling out to `supabase`.
|
|
743
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;
|
|
1072
|
+
// Sprint 51.5 T3: per-secret CLI loop, Vault SQL-Editor URL builder, and
|
|
1073
|
+
// Vault-secret ensure helper exposed for tests/init-rumen-secrets-per-call,
|
|
1074
|
+
// init-rumen-graph-llm, and init-rumen-vault-deeplinks.
|
|
1075
|
+
module.exports._setFunctionSecrets = setFunctionSecrets;
|
|
1076
|
+
module.exports._vaultSqlEditorUrl = vaultSqlEditorUrl;
|
|
1077
|
+
module.exports._ensureVaultSecrets = ensureVaultSecrets;
|
|
1078
|
+
module.exports._promptGraphLlmClassify = promptGraphLlmClassify;
|