@jhizzard/termdeck 1.8.0 → 1.8.1

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.8.0",
3
+ "version": "1.8.1",
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"
@@ -25,13 +25,14 @@
25
25
  "packages/server",
26
26
  "packages/client",
27
27
  "packages/cli",
28
- "packages/stack-installer"
28
+ "packages/stack-installer",
29
+ "packages/mcp-bridge"
29
30
  ],
30
31
  "scripts": {
31
32
  "dev": "node packages/server/src/index.js",
32
33
  "server": "node packages/server/src/index.js",
33
34
  "start": "NODE_ENV=production node packages/cli/src/index.js",
34
- "test": "node --test packages/server/tests/**/*.test.js packages/cli/tests/**/*.test.js packages/stack-installer/tests/**/*.test.js",
35
+ "test": "WEB_CHAT_DRIVER_NO_BROWSER=1 node --test packages/server/tests/**/*.test.js packages/cli/tests/**/*.test.js packages/stack-installer/tests/**/*.test.js packages/mcp-bridge/test/*.test.js packages/web-chat-driver/tests/*.test.js",
35
36
  "install:app": "bash install.sh",
36
37
  "sync-rumen-functions": "bash scripts/sync-rumen-functions.sh",
37
38
  "sync:agents": "node scripts/sync-agent-instructions.js"
@@ -309,32 +309,25 @@ function bootPromptTemplate(lane = {}, sprint = {}) {
309
309
  }
310
310
 
311
311
  // ──────────────────────────────────────────────────────────────────────────
312
- // mcpConfig — UNVERIFIED at lane time. The brief specifies the path
313
- // `~/.gemini/antigravity-cli/mcp_config.json`, which does NOT exist on disk yet,
314
- // and agy's `settings.json` carries no `mcpServers` key so agy's actual
315
- // MCP-registry read path could not be confirmed against the binary. Modeled on
316
- // the Gemini-family record shape (`mcpServers.NAME = {command,args,env}`, the
317
- // same schema gemini.js uses) since Antigravity is a Gemini-family CLI. The
318
- // shared mcp-autowire helper would CREATE this file on panel spawn. This is a
319
- // non-load-bearing nicety (auto-wiring Mnestra into agy panels); if a future
320
- // probe shows agy reads MCP from settings.json or another path/shape, correct
321
- // here. Env-key omission discipline matches gemini.js (concrete-or-omit; agy,
322
- // like Claude/Gemini, does not shell-expand `${VAR}` in MCP env).
312
+ // mcpConfig — null (Mnestra MCP auto-wire intentionally OFF for agy).
313
+ // VERIFIED 2026-06-08 (live 4-CLI 360): Antigravity's MCP is NOT file-config-
314
+ // driven agy's MCP servers are managed by its embedded "exa" language-server
315
+ // (RPCs `RefreshMcpServers` / `GetMcpServerStates`; type
316
+ // `gemini.GeminiMCPServerConfig`), not a readable `mcp_config.json`. Ruled out
317
+ // empirically against a LIVE agy panel: a de-secreted mnestra block written to
318
+ // BOTH `~/.gemini/config/mcp_config.json` AND the appDataDir
319
+ // `~/.gemini/antigravity-cli/mcp_config.json` left agy reporting
320
+ // `NO-MNESTRA-TOOL`; `~/.gemini/settings.json` already carries mnestra (gemini
321
+ // reads it) yet agy ignores it; `agy --help` exposes no `mcp` subcommand and
322
+ // `agy plugin list` is empty. A file-based mcpConfig here only targets a dead
323
+ // path, so it is `null` → the shared mcp-autowire helper cleanly skips (exactly
324
+ // the Claude case). Wiring Mnestra into agy is a deferred follow-up via the
325
+ // Antigravity language-server registration mechanism (likely IDE- /
326
+ // `RefreshMcpServers`-driven). This was always a non-load-bearing nicety: agy's
327
+ // PTY panel + the memory CAPTURE path (source_agent=antigravity, Sprint 70) both
328
+ // work; only the agy-side memory READ is deferred.
323
329
  // ──────────────────────────────────────────────────────────────────────────
324
330
 
325
- const MNESTRA_ENV_KEYS = ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'OPENAI_API_KEY'];
326
-
327
- function buildMnestraBlock({ secrets } = {}) {
328
- const env = {};
329
- for (const key of MNESTRA_ENV_KEYS) {
330
- const value = secrets && secrets[key];
331
- if (typeof value === 'string' && value.length > 0 && !/^\$\{[^}]*\}$/.test(value)) {
332
- env[key] = value;
333
- }
334
- }
335
- return { mnestra: { command: 'mnestra', args: [], env } };
336
- }
337
-
338
331
  const antigravityAdapter = {
339
332
  name: 'antigravity',
340
333
  sessionType: 'antigravity',
@@ -385,12 +378,10 @@ const antigravityAdapter = {
385
378
  // true (bracketed-paste fast path), flip to false if a lane-time test shows
386
379
  // the TUI input box eats the paste markers.
387
380
  acceptsPaste: true,
388
- mcpConfig: {
389
- path: '~/.gemini/antigravity-cli/mcp_config.json',
390
- format: 'json',
391
- mcpServersKey: 'mcpServers',
392
- mnestraBlock: buildMnestraBlock,
393
- },
381
+ // See the mcpConfig note above — Antigravity MCP is language-server-mediated,
382
+ // not file-config; null so mcp-autowire skips (Claude-style) instead of writing
383
+ // a dead-path file. agy memory READ is a deferred follow-up; CAPTURE works.
384
+ mcpConfig: null,
394
385
  };
395
386
 
396
387
  module.exports = antigravityAdapter;
@@ -1590,6 +1590,57 @@ function createServer(config) {
1590
1590
  // sync), wire screencast→WS, completion→capture, disconnect→close. Fail-soft
1591
1591
  // at every step — a missing/partial/throwing driver degrades the panel to
1592
1592
  // 'errored', never crashes the server.
1593
+ // Render-watchdog: self-heal a wedged web-chat cold-start. On a brand-new
1594
+ // browser profile the first load very occasionally paints nothing (empty
1595
+ // body.innerText) even though attach + screencast are healthy; a full
1596
+ // re-navigation clears it (a reload does NOT). Polls briefly for paint, then
1597
+ // re-navigates up to `attempts` times. Returns true if the page painted (or
1598
+ // we cannot measure — never block readiness on the watchdog itself), false if
1599
+ // it stayed blank. Provider-neutral: "painted" == the body has any visible
1600
+ // text, which empirically separates the white cold-start wedge (innerText
1601
+ // length 0) from a rendered SPA (>0). (Sprint-72 hardening — 2026-06-09.)
1602
+ async function ensureWebChatRendered(session, handle, startUrl, opts = {}) {
1603
+ const settleMs = opts.settleMs || Number(process.env.TERMDECK_WEBCHAT_RENDER_SETTLE_MS) || 8000;
1604
+ const attempts = opts.attempts != null ? opts.attempts
1605
+ : (Number(process.env.TERMDECK_WEBCHAT_RENDER_ATTEMPTS) || 2);
1606
+ const stepMs = opts.stepMs || Number(process.env.TERMDECK_WEBCHAT_RENDER_STEP_MS) || 500;
1607
+ if (!handle || !handle.page || typeof handle.page.evaluate !== 'function') return true;
1608
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1609
+ const painted = async () => {
1610
+ try {
1611
+ return await handle.page.evaluate(
1612
+ () => !!(document && document.body && (document.body.innerText || '').trim().length > 0),
1613
+ );
1614
+ } catch (_e) {
1615
+ return false;
1616
+ }
1617
+ };
1618
+ const settle = async () => {
1619
+ for (let waited = 0; waited < settleMs; waited += stepMs) {
1620
+ if (session._webChatClosed) return true;
1621
+ if (await painted()) return true;
1622
+ await sleep(stepMs);
1623
+ }
1624
+ return painted();
1625
+ };
1626
+ if (await settle()) return true;
1627
+ for (let tries = 1; tries <= attempts; tries++) {
1628
+ if (session._webChatClosed) return true;
1629
+ applyWebChatStatus(session, { status: 'starting', statusDetail: `Recovering blank page (try ${tries}/${attempts})…` });
1630
+ try {
1631
+ if (typeof handle.navigate === 'function') {
1632
+ await handle.navigate(startUrl, { waitUntil: 'domcontentloaded' });
1633
+ } else {
1634
+ await handle.page.goto(startUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
1635
+ }
1636
+ } catch (_e) {
1637
+ /* navigation hiccup — re-check paint anyway */
1638
+ }
1639
+ if (await settle()) return true;
1640
+ }
1641
+ return false;
1642
+ }
1643
+
1593
1644
  function setupWebChatSession(session) {
1594
1645
  session.pty = null;
1595
1646
  session.pid = null;
@@ -1647,8 +1698,18 @@ function createServer(config) {
1647
1698
  catch (_e) { /* never disrupt */ }
1648
1699
  }
1649
1700
  };
1650
- if (handle && typeof handle.screencast === 'function') handle.screencast(onFrame);
1651
- else if (driver.cdp && typeof driver.cdp.screencast === 'function') driver.cdp.screencast(handle, onFrame);
1701
+ // Render quality (Sprint-72 hardening, 2026-06-09): pass crisp, Retina-friendly
1702
+ // screencast opts. The driver's bare default was a blurry 1280x800 @ jpeg-q60 — fine
1703
+ // on a 1x display, soft on a 2x Mac (the HiDPI canvas then upscales it). Env-tunable
1704
+ // down for slow links: TERMDECK_WEBCHAT_QUALITY / _MAXW / _MAXH / _FORMAT.
1705
+ const scOpts = {
1706
+ format: process.env.TERMDECK_WEBCHAT_FORMAT || 'jpeg',
1707
+ quality: Number(process.env.TERMDECK_WEBCHAT_QUALITY) || 85,
1708
+ maxWidth: Number(process.env.TERMDECK_WEBCHAT_MAXW) || 2560,
1709
+ maxHeight: Number(process.env.TERMDECK_WEBCHAT_MAXH) || 1600,
1710
+ };
1711
+ if (handle && typeof handle.screencast === 'function') handle.screencast(onFrame, scOpts);
1712
+ else if (driver.cdp && typeof driver.cdp.screencast === 'function') driver.cdp.screencast(handle, onFrame, scOpts);
1652
1713
  } catch (err) {
1653
1714
  console.error('[web-chat] screencast wiring failed:', err && err.message ? err.message : err);
1654
1715
  }
@@ -1673,7 +1734,17 @@ function createServer(config) {
1673
1734
  }
1674
1735
  } catch (_e) { /* optional hook — absence is fine */ }
1675
1736
 
1676
- applyWebChatStatus(session, { status: 'idle', statusDetail: 'Ready' });
1737
+ // Self-heal a flaky blank cold-start before declaring the panel Ready.
1738
+ let rendered = true;
1739
+ try {
1740
+ rendered = await ensureWebChatRendered(session, handle, startUrl);
1741
+ } catch (err) {
1742
+ console.error('[web-chat] render-watchdog error:', err && err.message ? err.message : err);
1743
+ }
1744
+ if (session._webChatClosed) return;
1745
+ applyWebChatStatus(session, rendered
1746
+ ? { status: 'idle', statusDetail: 'Ready' }
1747
+ : { status: 'errored', statusDetail: 'page did not render (blank after retries)' });
1677
1748
  })();
1678
1749
  }
1679
1750
 
@@ -3589,6 +3660,24 @@ if (require.main === module) {
3589
3660
  process.on('SIGINT', () => handleShutdown('SIGINT'));
3590
3661
  process.on('SIGTERM', () => handleShutdown('SIGTERM'));
3591
3662
 
3663
+ // Fail-soft crash guards (Sprint-72 hardening, 2026-06-09). One bad async
3664
+ // rejection or uncaught error anywhere — a panel handler, a request, a hook —
3665
+ // must NOT crash the whole server and take every live terminal panel (and the
3666
+ // user's work) down with it. We LOG prominently (per-event ISO timestamp, like
3667
+ // the boot banner, so crash boundaries stay greppable) and keep running. This
3668
+ // trades the small risk of continuing in a degraded state for the much larger
3669
+ // cost of losing every panel; a process supervisor is the backstop if the
3670
+ // process ever truly wedges. Shutdown is exempt — let handleShutdown finish.
3671
+ process.on('unhandledRejection', (reason) => {
3672
+ if (shutdownInProgress) return;
3673
+ const msg = (reason && reason.stack) || (reason && reason.message) || String(reason);
3674
+ console.error(`[server] unhandledRejection (kept alive · ${new Date().toISOString()}):\n${msg}`);
3675
+ });
3676
+ process.on('uncaughtException', (err) => {
3677
+ if (shutdownInProgress) return;
3678
+ console.error(`[server] uncaughtException (kept alive · ${new Date().toISOString()}):\n${(err && err.stack) || err}`);
3679
+ });
3680
+
3592
3681
  server.listen(port, host, () => {
3593
3682
  // Sprint 60 v1.0.14 (Item 5) — per-boot banner with ISO timestamp + PID.
3594
3683
  // Brad's 2026-05-07 forensic: a single 260KB termdeck.log spanned Apr 25
@@ -21,9 +21,19 @@ function parseStatusMd(filePath) {
21
21
  const content = fs.readFileSync(filePath, 'utf8');
22
22
  const lines = content.split('\n');
23
23
 
24
- // Regex per BRIEF (loosened for multiple suffixes):
25
- // ^### \[(T\d+(?:-[A-Z0-9-]+)?|ORCH)\] (FINDING|PROPOSE|LANDED|DONE|AUDIT-RED|AUDIT-CONCERN|CHECKPOINT|FINAL-VERDICT|STATUS|RULING) (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}) ET — (.*?)$
26
- const POST_RE = /^### \[(T\d+(?:-[A-Z0-9-]+)?|ORCH)\] (FINDING|PROPOSE|LANDED|DONE|AUDIT-RED|AUDIT-CONCERN|CHECKPOINT|FINAL-VERDICT|STATUS|RULING) (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}) ET — (.*?)$/;
24
+ // Canonical post header (global CLAUDE.md § lane post-shape uniformity):
25
+ // ### [T<n>] VERB[ (qualifier)] YYYY-MM-DD HH:MM ET — <gist>
26
+ // The verb is HARD-ANCHORED to the header position (immediately after the
27
+ // lane tag), so a verb word appearing in a gist/prose — e.g. "DONE" inside a
28
+ // CHECKPOINT line's gist — is never mis-counted as that verb; it survives
29
+ // only as gist (capture group 5). Verb vocabulary tracks the REAL lane
30
+ // vocabulary, incl. FIX-PROPOSED / FIX-LANDED / AUDIT-PASS / AUDIT-FAIL, plus
31
+ // an optional parenthetical qualifier after the verb (e.g. the real shape
32
+ // `AUDIT-PASS (cdp/render)`). Order longer compounds before their shorter
33
+ // prefixes is unnecessary here (all alternatives are anchored + whitespace-
34
+ // delimited), but FIX-*/AUDIT-* are listed before bare PROPOSE/LANDED for
35
+ // readability.
36
+ const POST_RE = /^### \[(T\d+(?:-[A-Z0-9-]+)?|ORCH)\] (FINDING|FIX-PROPOSED|PROPOSE|FIX-LANDED|LANDED|DONE|AUDIT-RED|AUDIT-CONCERN|AUDIT-PASS|AUDIT-FAIL|CHECKPOINT|FINAL-VERDICT|STATUS|RULING)(?:\s+\([^)]*\))? (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}) ET — (.*?)$/;
27
37
 
28
38
  const laneLandeds = {}; // laneTag -> latest LANDED timestamp
29
39
  const openReds = []; // List of {tag, timestamp, gist}
@@ -57,7 +67,7 @@ function parseStatusMd(filePath) {
57
67
  gist
58
68
  };
59
69
 
60
- if (verb === 'LANDED') {
70
+ if (verb === 'LANDED' || verb === 'FIX-LANDED') {
61
71
  laneLandeds[tag] = timestamp;
62
72
  }
63
73