@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.
|
|
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 —
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
1651
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
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
|
|