@jhizzard/termdeck 0.3.8 → 0.4.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/README.md +1 -1
- package/config/config.example.yaml +8 -0
- package/package.json +2 -2
- package/packages/client/public/app.js +7 -5
- package/packages/client/public/index.html +1 -0
- package/packages/client/public/style.css +23 -0
- package/packages/server/src/index.js +5 -0
- package/packages/server/src/rag.js +34 -8
- package/packages/server/src/transcripts.js +10 -1
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ Honest limits, stated upfront so the skeptic has nothing to chase:
|
|
|
161
161
|
- **Not a replacement for reading docs.** It's the shortest path to a memory you already wrote. If the memory isn't there, the feature does nothing.
|
|
162
162
|
- **Not fully local by default.** Tier 2+ reaches out to Supabase for storage and OpenAI for embeddings. Tier 1 is fully local. A fully-local Tier 2 (local Postgres + local embeddings) is on the roadmap.
|
|
163
163
|
- **Not free forever.** Tier 2+ pays OpenAI fractions of a cent per memory for embeddings. Self-hosted embeddings via Ollama are on the roadmap.
|
|
164
|
-
- **Not proven at scale.** v0.
|
|
164
|
+
- **Not proven at scale.** v0.4.0, validated against 3,527 memories in one developer's production store. First full Rumen kickstart on 2026-04-15 processed 111 sessions into 111 insights in one pass. No multi-user data yet. Bug reports and issues welcome.
|
|
165
165
|
|
|
166
166
|
---
|
|
167
167
|
|
|
@@ -7,6 +7,14 @@ shell: /bin/zsh # or /bin/bash
|
|
|
7
7
|
|
|
8
8
|
defaultTheme: tokyo-night
|
|
9
9
|
|
|
10
|
+
# Mnestra (pgvector memory store) integration
|
|
11
|
+
# Controls whether scripts/start.sh launches `mnestra serve` automatically.
|
|
12
|
+
# autoStart: true — start mnestra serve automatically when TermDeck boots
|
|
13
|
+
# autoStart: false — never auto-start (leave Mnestra management to the user)
|
|
14
|
+
# (unset) — start.sh prints a hint but does not launch
|
|
15
|
+
mnestra:
|
|
16
|
+
autoStart: true
|
|
17
|
+
|
|
10
18
|
# Project definitions
|
|
11
19
|
# Each project maps a name to a directory + defaults
|
|
12
20
|
# These appear in the prompt bar dropdown and enable auto-cd + default themes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"express": "^4.18.2",
|
|
35
35
|
"open": "^10.0.0",
|
|
36
36
|
"pg": "^8.20.0",
|
|
37
|
-
"uuid": "^
|
|
37
|
+
"uuid": "^13.0.0",
|
|
38
38
|
"ws": "^8.16.0",
|
|
39
39
|
"yaml": "^2.3.4"
|
|
40
40
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* Extracted from index.html 2026-04-15 — see git blame on index.html prior to commit UNCOMMITTED for history */
|
|
2
2
|
// ===== TermDeck Client =====
|
|
3
3
|
const API = window.location.origin;
|
|
4
|
-
const
|
|
4
|
+
const WS_PROTOCOL = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
5
|
+
const WS_BASE = `${WS_PROTOCOL}//${window.location.host}/ws`;
|
|
5
6
|
|
|
6
7
|
// State
|
|
7
8
|
const state = {
|
|
@@ -122,6 +123,7 @@
|
|
|
122
123
|
<span class="panel-type">${getTypeLabel(meta.type)}</span>
|
|
123
124
|
${meta.project ? `<span class="panel-project ${projClass}">${meta.project}</span>` : ''}
|
|
124
125
|
<span class="panel-index" id="idx-${id}"></span>
|
|
126
|
+
<span class="panel-sid" title="Session ID: ${id}">${id.slice(0, 8)}</span>
|
|
125
127
|
<span class="panel-status" id="status-${id}">${meta.statusDetail || meta.status}</span>
|
|
126
128
|
</div>
|
|
127
129
|
<div class="panel-header-right">
|
|
@@ -2032,7 +2034,7 @@
|
|
|
2032
2034
|
{
|
|
2033
2035
|
target: '.topbar-center',
|
|
2034
2036
|
title: 'Layout modes',
|
|
2035
|
-
body: `
|
|
2037
|
+
body: `Eight preset grid layouts — <kbd>1x1</kbd> through <kbd>4x2</kbd>, <strong>orch</strong> (1 large + stacked, 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.`,
|
|
2036
2038
|
},
|
|
2037
2039
|
{
|
|
2038
2040
|
target: '#termSwitcher',
|
|
@@ -2508,10 +2510,10 @@
|
|
|
2508
2510
|
document.getElementById('promptInput').focus();
|
|
2509
2511
|
}
|
|
2510
2512
|
}
|
|
2511
|
-
// Ctrl+Shift+1-
|
|
2512
|
-
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key >= '1' && e.key <= '
|
|
2513
|
+
// Ctrl+Shift+1-7 OR Cmd+Shift+1-7 → layout switch (Mac friendly)
|
|
2514
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key >= '1' && e.key <= '7') {
|
|
2513
2515
|
e.preventDefault();
|
|
2514
|
-
const layouts = ['1x1', '2x1', '2x2', '3x2', '2x4', '4x2'];
|
|
2516
|
+
const layouts = ['1x1', '2x1', '2x2', '3x2', '2x4', '4x2', 'orch'];
|
|
2515
2517
|
setLayout(layouts[parseInt(e.key) - 1]);
|
|
2516
2518
|
}
|
|
2517
2519
|
// Ctrl+Shift+] / [ → cycle between terminals
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
<button class="layout-btn" data-layout="3x2">3x2</button>
|
|
42
42
|
<button class="layout-btn" data-layout="2x4">2x4</button>
|
|
43
43
|
<button class="layout-btn" data-layout="4x2">4x2</button>
|
|
44
|
+
<button class="layout-btn" data-layout="orch" title="Orchestrator: 1 large left + stacked right">orch</button>
|
|
44
45
|
<button class="layout-btn control-btn" data-layout="control" title="Aggregate activity feed">control</button>
|
|
45
46
|
</div>
|
|
46
47
|
</div>
|
|
@@ -312,6 +312,16 @@
|
|
|
312
312
|
.grid-container.layout-4x2 { grid-template-columns: 1fr 1fr 1fr 1fr; grid-template-rows: 1fr 1fr; }
|
|
313
313
|
.grid-container.layout-2x4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr 1fr 1fr; }
|
|
314
314
|
|
|
315
|
+
/* Orchestrator: 1 large left panel (60%), remaining stack on the right (40%). */
|
|
316
|
+
.grid-container.layout-orch {
|
|
317
|
+
grid-template-columns: 3fr 2fr;
|
|
318
|
+
grid-auto-rows: 1fr;
|
|
319
|
+
}
|
|
320
|
+
.grid-container.layout-orch .term-panel:first-child {
|
|
321
|
+
grid-row: 1 / -1;
|
|
322
|
+
grid-column: 1;
|
|
323
|
+
}
|
|
324
|
+
|
|
315
325
|
/* Focus mode: single terminal fills the grid */
|
|
316
326
|
.grid-container.layout-focus { grid-template-columns: 1fr; grid-template-rows: 1fr; }
|
|
317
327
|
.grid-container.layout-focus .term-panel:not(.focused) { display: none; }
|
|
@@ -393,6 +403,19 @@
|
|
|
393
403
|
}
|
|
394
404
|
.panel-index:empty { display: none; }
|
|
395
405
|
|
|
406
|
+
/* Short session ID (first 8 chars) — orchestrator-friendly: lets the
|
|
407
|
+
overseer reference a terminal without hitting the API. */
|
|
408
|
+
.panel-sid {
|
|
409
|
+
font-family: 'SF Mono', 'JetBrains Mono', Consolas, monospace;
|
|
410
|
+
font-size: 10px;
|
|
411
|
+
color: var(--tg-text-dim);
|
|
412
|
+
background: var(--tg-surface-hover);
|
|
413
|
+
padding: 1px 5px;
|
|
414
|
+
border-radius: 3px;
|
|
415
|
+
letter-spacing: 0.3px;
|
|
416
|
+
white-space: nowrap;
|
|
417
|
+
}
|
|
418
|
+
|
|
396
419
|
.panel-project {
|
|
397
420
|
font-size: 10px;
|
|
398
421
|
padding: 1px 7px;
|
|
@@ -133,6 +133,11 @@ function createServer(config) {
|
|
|
133
133
|
// ==================== REST API ====================
|
|
134
134
|
|
|
135
135
|
// GET /api/health - preflight health checks (Sprint 6 T1, wired by T3)
|
|
136
|
+
// SECURITY NOTE: Returns operational detail (memory counts, DB latency, project paths,
|
|
137
|
+
// RAG breaker state). Intentional for local-first use — TermDeck binds to 127.0.0.1 by
|
|
138
|
+
// default and the CLI guardrail blocks beyond-localhost binds without explicit opt-in.
|
|
139
|
+
// For any non-loopback deployment (Sprint 18+ remote story), gate this route behind auth
|
|
140
|
+
// or scope the response to a minimal {status, version} payload.
|
|
136
141
|
app.get('/api/health', createHealthHandler(config));
|
|
137
142
|
|
|
138
143
|
// GET /api/sessions - list all active sessions
|
|
@@ -59,8 +59,12 @@ class RAGIntegration {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
// Circuit breaker: track consecutive 404s per table name.
|
|
62
|
-
// After 3 consecutive 404s,
|
|
63
|
-
|
|
62
|
+
// After 3 consecutive 404s, open the breaker. The breaker auto-transitions
|
|
63
|
+
// to half-open after 5 minutes, allowing one retry attempt. A successful
|
|
64
|
+
// retry fully resets the breaker; a failed retry re-opens it for another
|
|
65
|
+
// 5-minute backoff window.
|
|
66
|
+
this._circuitBreaker = new Map(); // table -> { count, open, openedAt, halfOpen }
|
|
67
|
+
this._halfOpenDelayMs = 5 * 60 * 1000;
|
|
64
68
|
|
|
65
69
|
if (this.enabled) {
|
|
66
70
|
this._startSync();
|
|
@@ -142,23 +146,35 @@ class RAGIntegration {
|
|
|
142
146
|
}, this._projectFor(session));
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
// Circuit breaker check — returns true if pushes to this table are disabled
|
|
149
|
+
// Circuit breaker check — returns true if pushes to this table are disabled.
|
|
150
|
+
// Has a side effect: when the 5-minute half-open window has elapsed, flips
|
|
151
|
+
// the breaker to half-open and permits one retry attempt through.
|
|
146
152
|
_isCircuitOpen(table) {
|
|
147
153
|
const state = this._circuitBreaker.get(table);
|
|
148
|
-
|
|
154
|
+
if (!state || !state.open) return false;
|
|
155
|
+
if (state.halfOpen) return true; // retry already in flight — block concurrent pushes
|
|
156
|
+
|
|
157
|
+
const elapsed = Date.now() - (state.openedAt || 0);
|
|
158
|
+
if (elapsed >= this._halfOpenDelayMs) {
|
|
159
|
+
state.halfOpen = true;
|
|
160
|
+
console.log(`[rag] circuit breaker half-open for ${table}, retrying`);
|
|
161
|
+
return false; // allow one attempt through
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
149
164
|
}
|
|
150
165
|
|
|
151
166
|
// Record a 404 for a table; opens the breaker after 3 consecutive hits
|
|
152
167
|
_record404(table) {
|
|
153
168
|
let state = this._circuitBreaker.get(table);
|
|
154
169
|
if (!state) {
|
|
155
|
-
state = { count: 0, open: false };
|
|
170
|
+
state = { count: 0, open: false, openedAt: null, halfOpen: false };
|
|
156
171
|
this._circuitBreaker.set(table, state);
|
|
157
172
|
}
|
|
158
173
|
state.count += 1;
|
|
159
174
|
if (state.count >= 3 && !state.open) {
|
|
160
175
|
state.open = true;
|
|
161
|
-
|
|
176
|
+
state.openedAt = Date.now();
|
|
177
|
+
console.warn(`[rag] circuit breaker open for ${table} — disabling pushes (table may not exist in Supabase)`);
|
|
162
178
|
}
|
|
163
179
|
}
|
|
164
180
|
|
|
@@ -208,7 +224,16 @@ class RAGIntegration {
|
|
|
208
224
|
// Success — reset any accumulated 404 count for this table
|
|
209
225
|
this._resetCircuit(table);
|
|
210
226
|
} catch (err) {
|
|
211
|
-
|
|
227
|
+
const state = this._circuitBreaker.get(table);
|
|
228
|
+
if (state && state.halfOpen) {
|
|
229
|
+
// Half-open retry failed — re-open for another 5-minute backoff window
|
|
230
|
+
state.halfOpen = false;
|
|
231
|
+
state.openedAt = Date.now();
|
|
232
|
+
console.warn(`[rag] circuit breaker re-opened for ${table} after half-open retry failed`);
|
|
233
|
+
} else if (!state || !state.open) {
|
|
234
|
+
// Log at warn (not error) to reduce noise — the circuit breaker handles persistence
|
|
235
|
+
console.warn('[rag] push to', table, 'failed:', err.message);
|
|
236
|
+
}
|
|
212
237
|
throw err; // Propagate to caller so sync loop knows this event failed
|
|
213
238
|
}
|
|
214
239
|
}
|
|
@@ -243,7 +268,8 @@ class RAGIntegration {
|
|
|
243
268
|
});
|
|
244
269
|
synced.push(event.id);
|
|
245
270
|
} catch (err) {
|
|
246
|
-
|
|
271
|
+
// Don't print full stack traces for expected 404s (missing tables)
|
|
272
|
+
console.debug('[rag] sync push failed for event', event.id + ':', err.message);
|
|
247
273
|
break; // Stop on first failure, retry next cycle
|
|
248
274
|
}
|
|
249
275
|
}
|
|
@@ -46,6 +46,8 @@ class TranscriptWriter {
|
|
|
46
46
|
// Lazy pool
|
|
47
47
|
this._pool = null;
|
|
48
48
|
this._poolFailed = false;
|
|
49
|
+
this._poolFailedAt = 0;
|
|
50
|
+
this._poolRetryMs = 30_000;
|
|
49
51
|
|
|
50
52
|
// Start flush timer
|
|
51
53
|
this._timer = null;
|
|
@@ -56,7 +58,13 @@ class TranscriptWriter {
|
|
|
56
58
|
|
|
57
59
|
// Lazy-init pg.Pool (same pattern as getRumenPool in index.js)
|
|
58
60
|
_getPool() {
|
|
59
|
-
if (this._pool
|
|
61
|
+
if (this._pool) return this._pool;
|
|
62
|
+
if (this._poolFailed) {
|
|
63
|
+
if (Date.now() - this._poolFailedAt < this._poolRetryMs) return null;
|
|
64
|
+
console.warn('[transcript] retrying pool creation after 30s cooldown');
|
|
65
|
+
this._poolFailed = false;
|
|
66
|
+
this._poolFailedAt = 0;
|
|
67
|
+
}
|
|
60
68
|
if (!pg || !this._databaseUrl) return null;
|
|
61
69
|
try {
|
|
62
70
|
this._pool = new pg.Pool({
|
|
@@ -72,6 +80,7 @@ class TranscriptWriter {
|
|
|
72
80
|
} catch (err) {
|
|
73
81
|
console.error('[transcript] pool creation failed:', err.message);
|
|
74
82
|
this._poolFailed = true;
|
|
83
|
+
this._poolFailedAt = Date.now();
|
|
75
84
|
return null;
|
|
76
85
|
}
|
|
77
86
|
}
|