@jhizzard/termdeck 1.6.0 → 1.7.0

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.6.0",
3
+ "version": "1.7.0",
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"
@@ -256,6 +256,7 @@ function parseArgv(argv) {
256
256
  json: args.includes('--json'),
257
257
  noColor: args.includes('--no-color'),
258
258
  noSchema: args.includes('--no-schema'),
259
+ noAgents: args.includes('--no-agents'),
259
260
  };
260
261
  }
261
262
 
@@ -547,6 +548,83 @@ function renderSchemaResult(result, c) {
547
548
  return out.join('\n');
548
549
  }
549
550
 
551
+ // ── Sprint 70 T2: agent-CLI auth-probe section ─────────────────────────────
552
+ //
553
+ // Surfaces each agent adapter's `checkAuth()` verdict in `termdeck doctor` so a
554
+ // misconfigured agent CLI is caught here instead of failing silently at panel
555
+ // spawn. The motivating case: Google ends Gemini's OAuth serving path on
556
+ // 2026-06-18, after which a Gemini CLI still set to `oauth-personal` stops
557
+ // working — `checkAuth()` reports that as `wrong-mode` and this section turns
558
+ // it into a doctor RED (exit 1) with a remediation hint.
559
+ //
560
+ // Only adapters that expose a `checkAuth` function participate (Gemini today;
561
+ // forward-compatible as Codex/Grok/agy add probes). Static-only (`live:false`)
562
+ // — no spawn / network, so the section never hangs or hits an API. The whole
563
+ // registry require is wrapped: on any load failure the section is skipped
564
+ // (never a crash, never a false RED). Monkey-patchable as
565
+ // `module.exports._runAgentAuthCheck`, the same seam pattern as
566
+ // `_runSchemaCheck`.
567
+ async function _runAgentAuthCheck(opts = {}) {
568
+ let registry;
569
+ try {
570
+ registry = require(path.join(__dirname, '..', '..', 'server', 'src', 'agent-adapters'));
571
+ } catch (err) {
572
+ return {
573
+ skipped: true,
574
+ reason: `agent adapters unavailable: ${err && err.message || err}`,
575
+ agents: [], passed: 0, total: 0, hasGaps: false,
576
+ };
577
+ }
578
+ const adapters = Object.values((registry && registry.AGENT_ADAPTERS) || {})
579
+ .filter((a) => a && typeof a.checkAuth === 'function');
580
+ const agents = [];
581
+ for (const a of adapters) {
582
+ let v;
583
+ try {
584
+ // live:false → static checks only (env + settings.json); never spawns.
585
+ v = await a.checkAuth({ live: false, ...opts });
586
+ } catch (err) {
587
+ v = { ok: false, state: 'error', detail: `probe threw: ${err && err.message || err}`, hint: '' };
588
+ }
589
+ agents.push({
590
+ name: a.displayName || a.name,
591
+ state: v.state,
592
+ ok: v.ok === true,
593
+ detail: v.detail || '',
594
+ hint: v.hint || '',
595
+ });
596
+ }
597
+ const passed = agents.filter((x) => x.ok).length;
598
+ return { skipped: false, agents, passed, total: agents.length, hasGaps: passed < agents.length };
599
+ }
600
+
601
+ function renderAgentAuthResult(result, c) {
602
+ const out = [];
603
+ out.push('');
604
+ out.push(c.bold('Agent CLI auth'));
605
+ out.push('');
606
+ if (result.skipped) {
607
+ out.push(` ${c.dim(`(skipped) ${result.reason}`)}`);
608
+ return out.join('\n');
609
+ }
610
+ if (!result.agents || result.agents.length === 0) {
611
+ out.push(` ${c.dim('(no agent adapters expose an auth probe)')}`);
612
+ return out.join('\n');
613
+ }
614
+ for (const a of result.agents) {
615
+ if (a.ok) {
616
+ out.push(` ${c.green('✓')} ${a.name}: ${a.state}`);
617
+ } else {
618
+ out.push(` ${c.yellow('✗')} ${a.name}: ${a.state}`);
619
+ if (a.detail) out.push(` ${c.dim(a.detail)}`);
620
+ if (a.hint) out.push(` ${c.dim(a.hint)}`);
621
+ }
622
+ }
623
+ out.push('');
624
+ out.push(` ${result.passed}/${result.total} agent auth checks passed`);
625
+ return out.join('\n');
626
+ }
627
+
550
628
  async function doctor(argv) {
551
629
  const opts = parseArgv(argv);
552
630
 
@@ -586,6 +664,22 @@ async function doctor(argv) {
586
664
  }
587
665
  }
588
666
 
667
+ // Sprint 70 T2: agent-CLI auth probe (skippable for tests / hosts without
668
+ // agent CLIs). Static-only by default — no spawn / network.
669
+ let agents = null;
670
+ if (!opts.noAgents) {
671
+ try {
672
+ agents = await module.exports._runAgentAuthCheck();
673
+ } catch (err) {
674
+ agents = {
675
+ skipped: false,
676
+ agents: [{ name: 'agent auth', state: 'error', ok: false,
677
+ detail: `unexpected error: ${err && err.message || err}`, hint: '' }],
678
+ passed: 0, total: 1, hasGaps: true,
679
+ };
680
+ }
681
+ }
682
+
589
683
  // Exit-code priority: any network failure → 2; any update available OR
590
684
  // schema gap → 1; else 0. Computed after all rows resolve so a single
591
685
  // transient failure doesn't mask real updates in stdout. A schema connect
@@ -600,10 +694,12 @@ async function doctor(argv) {
600
694
  }
601
695
  if (schema && schema.connectError && exitCode < 2) exitCode = 2;
602
696
  if (schema && !schema.skipped && schema.hasGaps && exitCode < 1) exitCode = 1;
697
+ if (agents && !agents.skipped && agents.hasGaps && exitCode < 1) exitCode = 1;
603
698
 
604
699
  if (opts.json) {
605
700
  const payload = { exitCode, rows };
606
701
  if (schema) payload.schema = schema;
702
+ if (agents) payload.agents = agents;
607
703
  process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
608
704
  return exitCode;
609
705
  }
@@ -615,6 +711,9 @@ async function doctor(argv) {
615
711
  if (schema) {
616
712
  process.stdout.write(renderSchemaResult(schema, c) + '\n');
617
713
  }
714
+ if (agents) {
715
+ process.stdout.write(renderAgentAuthResult(agents, c) + '\n');
716
+ }
618
717
  return exitCode;
619
718
  }
620
719
 
@@ -625,5 +724,6 @@ module.exports._compareSemver = _compareSemver;
625
724
  module.exports._detectMnestraVersion = _detectMnestraVersion;
626
725
  module.exports._selectHybridSearchRpcNames = _selectHybridSearchRpcNames;
627
726
  module.exports._runSchemaCheck = _runSchemaCheck;
727
+ module.exports._runAgentAuthCheck = _runAgentAuthCheck;
628
728
  module.exports.STACK_PACKAGES = STACK_PACKAGES;
629
729
  module.exports.STATUS = STATUS;
@@ -597,7 +597,39 @@ function refreshBundledHookIfNewer(opts = {}) {
597
597
  }
598
598
  const installed = readVersion(HOOK_DEST);
599
599
  if (installed !== null && installed >= bundled) {
600
- return { status: 'up-to-date', installed, bundled };
600
+ // Sprint 67 T1 — content-drift gate. Stamp-equal does NOT prove content-
601
+ // equal. Sprints 62/63/64 each grew the v2-stamped session-end body
602
+ // without bumping to v3; the daily-driver sat on the Sprint-51.7-era v2
603
+ // body for ~2 weeks (May 4 → May 19, 2026) because this early-return
604
+ // fired before any bytes were compared. Compare bytes; if they differ
605
+ // AND the installed file is TermDeck-managed (same trust signal that
606
+ // gates the unsigned-installed safety branch below), refresh with a
607
+ // backup. Hand-edited user files (no TermDeck markers) are still
608
+ // preserved — drift in that case is the user's intent.
609
+ let identical = false;
610
+ try {
611
+ identical = fs.readFileSync(HOOK_SOURCE).equals(fs.readFileSync(HOOK_DEST));
612
+ } catch (_) {
613
+ // Best-effort: a transient read error shouldn't trigger an overwrite.
614
+ // Treat as identical so we exit through the safe up-to-date path.
615
+ identical = true;
616
+ }
617
+ if (identical) return { status: 'up-to-date', installed, bundled };
618
+ if (!looksTermdeckManaged(HOOK_DEST)) {
619
+ return {
620
+ status: 'custom-hook-preserved-content-drift',
621
+ message: 'installed hook is stamp-equal to bundled but bytes differ and the file lacks TermDeck-managed markers; keeping as-is.',
622
+ installed,
623
+ bundled,
624
+ };
625
+ }
626
+ if (dryRun) return { status: 'would-refresh-content-drift', from: installed, to: bundled };
627
+ const dStamp = new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14);
628
+ const dBackup = `${HOOK_DEST}.bak.${dStamp}`;
629
+ try { fs.copyFileSync(HOOK_DEST, dBackup); } catch (_) { /* best-effort */ }
630
+ fs.copyFileSync(HOOK_SOURCE, HOOK_DEST);
631
+ fs.chmodSync(HOOK_DEST, 0o644);
632
+ return { status: 'refreshed-content-drift', from: installed, to: bundled, backup: dBackup };
601
633
  }
602
634
  // Sprint 51.6 T4-CODEX audit 20:23 ET safety gate: an unsigned installed
603
635
  // hook gets refreshed ONLY if it looks TermDeck-managed (carries one of
@@ -625,11 +657,11 @@ function refreshBundledHookIfNewer(opts = {}) {
625
657
  // DB-side failures (Class A schema drift, network blips, partial state)
626
658
  // cannot strand the hook upgrade. Joshua's 2026-05-03 Phase B run threw at
627
659
  // `applyMigrations()` on `001_mnestra_tables.sql` (the `match_memories`
628
- // CREATE OR REPLACE return-type drift on petvetbid — existing function had
660
+ // CREATE OR REPLACE return-type drift on the daily-driver project — existing function had
629
661
  // columns in a different order, Postgres rejected with "cannot change return
630
662
  // type of existing function"). Outer catch at the old call site fired and
631
663
  // returned exit 5; the refresh at the old wire-up never ran. Brad's
632
- // jizzard-brain reproduced the same symptom under v1.0.2.
664
+ // peer install reproduced the same symptom under v1.0.2.
633
665
  //
634
666
  // Hook refresh is a LOCAL filesystem operation. It has no dependency on DB
635
667
  // success, so it should run as part of the initial local-setup phase next
@@ -660,7 +692,7 @@ function refreshBundledHookIfNewer(opts = {}) {
660
692
  // v1.0.0/v1.0.1) gets the v2 hook FILE post-1.0.3, but the file is still
661
693
  // wired under `Stop`. The v2 hook does not gate on event type, so it
662
694
  // fires every assistant turn and writes N `session_summary` rows in
663
- // `memory_items` per session (Brad's 2026-05-04 jizzard-brain repro).
695
+ // `memory_items` per session (Brad's 2026-05-04 peer install repro).
664
696
  //
665
697
  // `_mergeSessionEndHookEntry` is a 1:1 hoist of the same-named function
666
698
  // in `packages/stack-installer/src/index.js:451`. We can't `require()`
@@ -965,6 +997,12 @@ function runHookRefresh({ dryRun = false } = {}) {
965
997
  ok(`installed v${r.bundled} (no prior copy)`);
966
998
  } else if (r.status === 'would-install') {
967
999
  ok(`would-install v${r.bundled} (dry-run, no prior copy)`);
1000
+ } else if (r.status === 'refreshed-content-drift') {
1001
+ ok(`refreshed v${r.from} → v${r.to} (content-drift; backup: ${path.basename(r.backup)})`);
1002
+ } else if (r.status === 'would-refresh-content-drift') {
1003
+ ok(`would-refresh v${r.from} → v${r.to} (content-drift; dry-run)`);
1004
+ } else if (r.status === 'custom-hook-preserved-content-drift') {
1005
+ ok(`custom-hook-preserved (bytes differ from bundled but no TermDeck markers; keeping as-is)`);
968
1006
  } else if (r.status === 'up-to-date') {
969
1007
  ok(`up-to-date (v${r.installed})`);
970
1008
  } else {
@@ -998,6 +1036,12 @@ function runHookRefresh({ dryRun = false } = {}) {
998
1036
  ok(`installed v${r.bundled} (no prior copy)`);
999
1037
  } else if (r.status === 'would-install') {
1000
1038
  ok(`would-install v${r.bundled} (dry-run, no prior copy)`);
1039
+ } else if (r.status === 'refreshed-content-drift') {
1040
+ ok(`refreshed v${r.from} → v${r.to} (content-drift; backup: ${path.basename(r.backup)})`);
1041
+ } else if (r.status === 'would-refresh-content-drift') {
1042
+ ok(`would-refresh v${r.from} → v${r.to} (content-drift; dry-run)`);
1043
+ } else if (r.status === 'custom-hook-preserved-content-drift') {
1044
+ ok(`custom-hook-preserved (bytes differ from bundled but no TermDeck markers; keeping as-is)`);
1001
1045
  } else if (r.status === 'up-to-date') {
1002
1046
  ok(`up-to-date (v${r.installed})`);
1003
1047
  } else {
@@ -1083,7 +1127,7 @@ async function main(argv) {
1083
1127
  // DB phase. Hook refresh is local FS work; coupling it downstream of pg
1084
1128
  // connect + 17-migration replay (the old wire-up at line 677 in v1.0.2)
1085
1129
  // meant ANY DB-side error (Joshua's mig-001 `match_memories` return-type
1086
- // drift, Brad's same on jizzard-brain) silently skipped the upgrade. With
1130
+ // drift, Brad's same on peer install) silently skipped the upgrade. With
1087
1131
  // refresh here, the user always lands the bundled hook even when the DB
1088
1132
  // phase later fails — decoupled concerns, idempotent re-runs, and the
1089
1133
  // helper handles its own try/catch internally so a refresh failure never
@@ -1098,7 +1142,7 @@ async function main(argv) {
1098
1142
  // and writes N session_summary rows in memory_items per session. This
1099
1143
  // migration is idempotent and runs alongside the file refresh so the
1100
1144
  // wire-up + wiring stay in lockstep on every wizard pass. Brad's
1101
- // 2026-05-04 jizzard-brain repro is the canonical fixture for this
1145
+ // 2026-05-04 peer install repro is the canonical fixture for this
1102
1146
  // class of bug (INSTALLER-PITFALLS.md ledger #16).
1103
1147
  runSettingsJsonMigration({ dryRun: flags.dryRun });
1104
1148
 
@@ -331,7 +331,7 @@ async function applyRumenTables(secrets, dryRun) {
331
331
  // up front. Idempotent: a re-run on an up-to-date project reports
332
332
  // "install up to date" and applies nothing.
333
333
  //
334
- // Brad's 2026-05-02 jizzard-brain report (INSTALLER-PITFALLS.md ledger #13)
334
+ // Brad's 2026-05-02 peer install report (INSTALLER-PITFALLS.md ledger #13)
335
335
  // is the originating motivation: he upgraded npm packages but his database
336
336
  // stayed frozen at first-kickstart because no installer code path diffed an
337
337
  // existing install against the bundled migration set. After v1.0.1 ships,
@@ -391,7 +391,7 @@ const FUNCTIONS_WITH_VERSION_PLACEHOLDER = new Set(['rumen-tick']);
391
391
 
392
392
  // Sprint 51.6 T3 — `projectRef` is required and passed explicitly to every
393
393
  // `supabase functions deploy` invocation as `--project-ref <ref>`. Brad's
394
- // 2026-05-03 jizzard-brain install hit Bug C: `supabase link --project-ref`
394
+ // 2026-05-03 peer install install hit Bug C: `supabase link --project-ref`
395
395
  // runs successfully (audit-upgrade probes confirm the link is live), but a
396
396
  // few subprocess calls later `supabase functions deploy` errors with
397
397
  // `Cannot find project ref. Have you run supabase link?` because the link
@@ -577,7 +577,7 @@ function vaultSqlEditorUrl(projectRef, secretName, secretValue) {
577
577
  // - graph_inference_service_role_key (used by 003_graph_inference_schedule.sql)
578
578
  //
579
579
  // Both keys hold the same value (`secrets.SUPABASE_SERVICE_ROLE_KEY`). Brad's
580
- // 2026-05-02 recovery on jizzard-brain literally cloned rumen → graph_inference
580
+ // 2026-05-02 recovery on peer install literally cloned rumen → graph_inference
581
581
  // in vault.
582
582
  //
583
583
  // Strategy:
@@ -3377,6 +3377,12 @@
3377
3377
 
3378
3378
  // ===== Layout =====
3379
3379
  function setLayout(layout) {
3380
+ // Sprint 67 T3: legacy `orch` layout retired (superseded by the role-tagged
3381
+ // ORCH-pin row from Sprint 65). Redirect any stale callers to `4x2` so the
3382
+ // grid still renders cleanly if `orch` arrives from older code paths.
3383
+ if (layout === 'orch') {
3384
+ layout = '4x2';
3385
+ }
3380
3386
  const wasControl = state.layout === 'control';
3381
3387
  // Only persist "real" grid layouts as state.layout; the control view is
3382
3388
  // an overlay, not a target to restore to when the user hits Escape.
@@ -3385,15 +3391,7 @@
3385
3391
  }
3386
3392
  const grid = document.getElementById('termGrid');
3387
3393
  grid.className = `grid-container layout-${layout}`;
3388
-
3389
- // Orchestrator layout: set column count based on worker panels (total - 1)
3390
- if (layout === 'orch') {
3391
- const panelCount = grid.querySelectorAll('.term-panel').length;
3392
- const workerCount = Math.max(0, panelCount - 1);
3393
- grid.setAttribute('data-orch-cols', String(workerCount || panelCount));
3394
- } else {
3395
- grid.removeAttribute('data-orch-cols');
3396
- }
3394
+ grid.removeAttribute('data-orch-cols');
3397
3395
 
3398
3396
  // Remove focus/half states
3399
3397
  document.querySelectorAll('.term-panel').forEach(p => {
@@ -3685,7 +3683,7 @@
3685
3683
  {
3686
3684
  target: '.topbar-center',
3687
3685
  title: 'Layout modes',
3688
- body: `Eight preset grid layouts — <kbd>1x1</kbd> through <kbd>4x2</kbd>, <strong>orch</strong> (4 workers across the top + 1 full-width orchestrator across the bottom, for 4+1 sprints), plus <strong>control</strong> (aggregate activity feed). Click any layout to switch instantly; all terminals re-fit to the new grid. Keyboard shortcuts <kbd>Cmd+Shift+1</kbd>–<kbd>Cmd+Shift+7</kbd> (or <kbd>Ctrl+Shift+1</kbd>–<kbd>7</kbd>) do the same.`,
3686
+ body: `Preset grid layouts — <kbd>1x1</kbd> through <kbd>4x4</kbd> plus <strong>control</strong> (aggregate activity feed). Click any layout to switch instantly; all terminals re-fit to the new grid. Keyboard shortcuts <kbd>Cmd+Shift+1</kbd>–<kbd>Cmd+Shift+9</kbd> (or <kbd>Ctrl+Shift+1</kbd>–<kbd>9</kbd>) cycle through them. Orchestrator panels are pinned to a dedicated row above the grid via <strong>meta.role</strong>; no special "orch" layout needed.`,
3689
3687
  },
3690
3688
  {
3691
3689
  target: '#termSwitcher',
@@ -5001,7 +4999,10 @@
5001
4999
  // the new dense presets. Topbar buttons cover every preset incl. 2x5/5x2/3x4.
5002
5000
  if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key >= '0' && e.key <= '9') {
5003
5001
  e.preventDefault();
5004
- const layouts = ['1x1', '2x1', '2x2', '3x2', '2x4', '4x2', 'orch', '1x2', '4x3', '4x4'];
5002
+ // Sprint 67 T3: index 6 (was `orch`, key 7) is now null — the legacy
5003
+ // orch layout is retired in favor of the role-tagged ORCH-pin row.
5004
+ // Keep the slot to preserve muscle memory on keys 8/9/0.
5005
+ const layouts = ['1x1', '2x1', '2x2', '3x2', '2x4', '4x2', null, '1x2', '4x3', '4x4'];
5005
5006
  const idx = e.key === '0' ? 9 : parseInt(e.key, 10) - 1;
5006
5007
  if (layouts[idx]) setLayout(layouts[idx]);
5007
5008
  }
@@ -47,7 +47,6 @@
47
47
  <button class="layout-btn" data-layout="4x3" title="12 panels — 4 cols × 3 rows">4x3</button>
48
48
  <button class="layout-btn" data-layout="3x4" title="12 panels — 3 cols × 4 rows">3x4</button>
49
49
  <button class="layout-btn" data-layout="4x4" title="16 panels">4x4</button>
50
- <button class="layout-btn" data-layout="orch" title="Orchestrator: 4 workers across top, 1 full-width orchestrator across bottom">orch</button>
51
50
  <button class="layout-btn control-btn" data-layout="control" title="Aggregate activity feed">control</button>
52
51
  </div>
53
52
  </div>
@@ -329,30 +329,10 @@
329
329
  .grid-container.layout-3x4 { grid-template-columns: 1fr 1fr 1fr; grid-template-rows: repeat(4, 1fr); }
330
330
  .grid-container.layout-4x4 { grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); }
331
331
 
332
- /* Orchestrator: workers across the top (60%), one full-width orchestrator
333
- panel across the bottom (40%). The last panel is always the orchestrator.
334
- JS sets a data-orch-cols attribute on the grid to match the worker count. */
335
- /* Orchestrator: 2x2 workers on top (60%), full-width orchestrator bottom (40%) */
336
- .grid-container.layout-orch {
337
- grid-template-columns: repeat(2, 1fr);
338
- grid-template-rows: 2fr 2fr 2fr;
339
- }
340
- .grid-container.layout-orch .term-panel:last-child {
341
- grid-column: 1 / -1;
342
- grid-row: 3;
343
- }
344
- /* Single panel: fill entire grid */
345
- .grid-container.layout-orch[data-orch-cols="0"] {
346
- grid-template-rows: 1fr;
347
- grid-template-columns: 1fr;
348
- }
349
- .grid-container.layout-orch[data-orch-cols="0"] .term-panel:last-child {
350
- grid-row: 1;
351
- }
352
-
353
332
  /* Focus mode: single terminal fills the grid */
354
333
  .grid-container.layout-focus { grid-template-columns: 1fr; grid-template-rows: 1fr; }
355
334
  .grid-container.layout-focus .term-panel:not(.focused) { display: none; }
335
+ .orch-pin-row:has(~ .grid-container.layout-focus) { display: none; }
356
336
 
357
337
  /* Half mode: one big + small stack */
358
338
  .grid-container.layout-half {
@@ -3035,17 +3015,9 @@
3035
3015
  }
3036
3016
  }
3037
3017
 
3038
- /* Very narrow viewports (rare — sub-1024 widths): also collapse 2x4
3039
- and orch to something sane. 2x4 already uses 2 columns, so we just
3040
- let it scroll; orch falls back to a single-column stack. */
3018
+ /* Very narrow viewports (rare — sub-1024 widths): let dense grids scroll
3019
+ rather than truncate panels. */
3041
3020
  @media (max-width: 900px) {
3042
- .grid-container.layout-orch {
3043
- grid-template-columns: 1fr;
3044
- grid-template-rows: repeat(var(--orch-worker-rows, 3), 1fr) 1.2fr;
3045
- }
3046
- .grid-container.layout-orch .term-panel:last-child {
3047
- grid-column: 1;
3048
- }
3049
3021
  .grid-container { overflow: auto; }
3050
3022
  }
3051
3023