@jhizzard/termdeck 1.8.1 → 1.9.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 +2 -1
- package/packages/cli/assets/supervise/com.jhizzard.termdeck-supervise.plist +38 -0
- package/packages/cli/assets/supervise/termdeck-supervise.service +27 -0
- package/packages/cli/assets/supervise/termdeck-supervise.sh +146 -0
- package/packages/cli/assets/supervise/termdeck-supervise.timer +14 -0
- package/packages/cli/src/index.js +15 -2
- package/packages/cli/src/init-bridge.js +1270 -0
- package/packages/cli/src/init.js +1 -0
- package/packages/client/public/app.js +135 -9
- package/packages/client/public/index.html +1 -0
- package/packages/client/public/input-guard.js +192 -0
- package/packages/client/public/style.css +63 -0
- package/packages/server/src/agent-adapters/web-chat-grok.js +22 -22
- package/packages/server/src/index.js +6 -1
- package/packages/stack-installer/assets/hooks/README.md +25 -15
- package/packages/stack-installer/assets/hooks/memory-pre-compact.js +35 -7
- package/packages/stack-installer/assets/hooks/memory-session-end.js +121 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhizzard/termdeck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"main": "packages/cli/src/index.js",
|
|
9
9
|
"files": [
|
|
10
10
|
"packages/cli/src/**",
|
|
11
|
+
"packages/cli/assets/**",
|
|
11
12
|
"packages/cli/templates/**",
|
|
12
13
|
"packages/server/src/**",
|
|
13
14
|
"packages/server/share/**",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<!--
|
|
4
|
+
TermDeck stack supervisor — runs termdeck-supervise.sh every 60s and at
|
|
5
|
+
login, keeping the server / Mnestra webhook / cloudflared tunnel / MCP
|
|
6
|
+
bridge alive.
|
|
7
|
+
|
|
8
|
+
Staged by `termdeck init --bridge` from the vendored package asset
|
|
9
|
+
(packages/cli/assets/supervise/) with the __TERMDECK_*__ tokens resolved
|
|
10
|
+
for this machine at stage time. Structural source of truth:
|
|
11
|
+
scripts/com.jhizzard.termdeck-supervise.plist in the repo — keep the dict
|
|
12
|
+
keys in lockstep (pinned by packages/cli/tests/init-bridge.test.js).
|
|
13
|
+
|
|
14
|
+
The wizard copies this file into ~/Library/LaunchAgents/ but NEVER loads
|
|
15
|
+
it; the operator runs:
|
|
16
|
+
launchctl load -w ~/Library/LaunchAgents/com.jhizzard.termdeck-supervise.plist
|
|
17
|
+
-->
|
|
18
|
+
<plist version="1.0">
|
|
19
|
+
<dict>
|
|
20
|
+
<key>Label</key>
|
|
21
|
+
<string>com.jhizzard.termdeck-supervise</string>
|
|
22
|
+
<key>ProgramArguments</key>
|
|
23
|
+
<array>
|
|
24
|
+
<string>/bin/bash</string>
|
|
25
|
+
<string>__TERMDECK_SUPERVISE_SCRIPT__</string>
|
|
26
|
+
</array>
|
|
27
|
+
<key>RunAtLoad</key>
|
|
28
|
+
<true/>
|
|
29
|
+
<key>StartInterval</key>
|
|
30
|
+
<integer>60</integer>
|
|
31
|
+
<key>StandardOutPath</key>
|
|
32
|
+
<string>__TERMDECK_HOME__/.termdeck/logs/supervise.out.log</string>
|
|
33
|
+
<key>StandardErrorPath</key>
|
|
34
|
+
<string>__TERMDECK_HOME__/.termdeck/logs/supervise.err.log</string>
|
|
35
|
+
<key>ProcessType</key>
|
|
36
|
+
<string>Background</string>
|
|
37
|
+
</dict>
|
|
38
|
+
</plist>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# TermDeck stack supervisor — one tick of termdeck-supervise.sh.
|
|
2
|
+
# Staged by `termdeck init --bridge` from the vendored package asset
|
|
3
|
+
# (packages/cli/assets/supervise/) with the __TERMDECK_*__ tokens resolved
|
|
4
|
+
# for this machine at stage time. Structural source of truth:
|
|
5
|
+
# docs/examples/termdeck-supervise.service in the repo — keep in lockstep
|
|
6
|
+
# (pinned by packages/cli/tests/init-bridge.test.js).
|
|
7
|
+
#
|
|
8
|
+
# Pair with termdeck-supervise.timer (60s cadence); the timer is what keeps
|
|
9
|
+
# the stack alive, this unit is a single idempotent pass. The wizard copies
|
|
10
|
+
# both files into ~/.config/systemd/user/ but NEVER enables them; the
|
|
11
|
+
# operator runs:
|
|
12
|
+
# systemctl --user daemon-reload
|
|
13
|
+
# systemctl --user enable --now termdeck-supervise.timer
|
|
14
|
+
# loginctl enable-linger "$(whoami)"
|
|
15
|
+
#
|
|
16
|
+
# Logs: journalctl --user -u termdeck-supervise.service -n 50
|
|
17
|
+
# plus the script's own ~/.termdeck/logs/supervise.log
|
|
18
|
+
|
|
19
|
+
[Unit]
|
|
20
|
+
Description=TermDeck stack supervisor (one idempotent tick)
|
|
21
|
+
|
|
22
|
+
[Service]
|
|
23
|
+
Type=oneshot
|
|
24
|
+
# systemd's minimal PATH hides npm-global bins (node, mnestra, termdeck) and
|
|
25
|
+
# often cloudflared — prepend the usual homes. Same pitfall as termdeck.service.
|
|
26
|
+
Environment="PATH=%h/.npm-global/bin:%h/.local/bin:/usr/local/bin:/usr/bin:/bin"
|
|
27
|
+
ExecStart=/bin/bash __TERMDECK_SUPERVISE_SCRIPT__
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# termdeck-supervise.sh — keep the TermDeck stack self-healing.
|
|
3
|
+
#
|
|
4
|
+
# Ensures (and restarts if down) the four daily-driver processes. Every check is
|
|
5
|
+
# by PORT, never by process-arg path — `pgrep -f 'mcp-bridge/src/server.js'` is a
|
|
6
|
+
# known false-negative because the bridge's argv is just `node src/server.js`.
|
|
7
|
+
# 1. TermDeck server :3000
|
|
8
|
+
# 2. Mnestra webhook :37778
|
|
9
|
+
# 3. cloudflared tunnel (public HTTPS → :8870; named = stable URL, quick = ephemeral)
|
|
10
|
+
# 4. MCP bridge :8870 (re-pinned to the CURRENT tunnel URL via /healthz drift check)
|
|
11
|
+
#
|
|
12
|
+
# Idempotent: run once to bring the stack up; run on a timer (launchd/cron) to
|
|
13
|
+
# keep it up. State lives in ~/.termdeck: a STABLE operator secret + the current
|
|
14
|
+
# public URL + per-component logs. A stable secret means a bridge restart never
|
|
15
|
+
# silently changes the consent secret; the URL file means the bridge always
|
|
16
|
+
# re-pins to wherever the tunnel currently is.
|
|
17
|
+
#
|
|
18
|
+
# Config (env, or ~/.termdeck/supervisor.env):
|
|
19
|
+
# TERMDECK_REPO_DIR default: derived from this script's location
|
|
20
|
+
# TERMDECK_SECRETS_ENV default: ~/.termdeck/secrets.env
|
|
21
|
+
# TERMDECK_TUNNEL_NAME a named cloudflared tunnel → STABLE url (recommended once you
|
|
22
|
+
# have a Cloudflare domain). Unset ⇒ ephemeral quick tunnel.
|
|
23
|
+
# TERMDECK_PUBLIC_HOSTNAME the https host routed to the named tunnel (required if NAME set)
|
|
24
|
+
# TERMDECK_BRIDGE_ALLOWLIST_PROJECTS default '*' (panels visible to web chats; still approval-gated)
|
|
25
|
+
# TERMDECK_SUPERVISE_DRY_RUN=1 log intended actions, start/kill NOTHING (safe to test live)
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
STATE_DIR="${HOME}/.termdeck"
|
|
29
|
+
LOG_DIR="${STATE_DIR}/logs"
|
|
30
|
+
mkdir -p "$LOG_DIR"
|
|
31
|
+
# Operator overrides first, then defaults.
|
|
32
|
+
if [ -f "${STATE_DIR}/supervisor.env" ]; then set -a; . "${STATE_DIR}/supervisor.env"; set +a; fi
|
|
33
|
+
|
|
34
|
+
SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
REPO_DIR="${TERMDECK_REPO_DIR:-$(cd "${SELF_DIR}/.." && pwd)}"
|
|
36
|
+
SECRETS_ENV="${TERMDECK_SECRETS_ENV:-${STATE_DIR}/secrets.env}"
|
|
37
|
+
SECRET_FILE="${STATE_DIR}/bridge-operator-secret.txt"
|
|
38
|
+
URL_FILE="${STATE_DIR}/bridge-public-url.txt"
|
|
39
|
+
ALLOWLIST_PROJECTS="${TERMDECK_BRIDGE_ALLOWLIST_PROJECTS:-*}"
|
|
40
|
+
DRY="${TERMDECK_SUPERVISE_DRY_RUN:-0}"
|
|
41
|
+
|
|
42
|
+
ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
|
|
43
|
+
log() { echo "[$(ts)] $*" | tee -a "${LOG_DIR}/supervise.log" >&2; }
|
|
44
|
+
port_up() { lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1; }
|
|
45
|
+
would() { [ "$DRY" = "1" ]; }
|
|
46
|
+
|
|
47
|
+
ensure_secret() {
|
|
48
|
+
[ -s "$SECRET_FILE" ] && return 0
|
|
49
|
+
would && { log "DRY: would generate a stable operator secret at $SECRET_FILE"; return 0; }
|
|
50
|
+
( umask 077; openssl rand -hex 16 > "$SECRET_FILE" )
|
|
51
|
+
log "generated a stable operator secret at $SECRET_FILE"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
start_server() {
|
|
55
|
+
port_up 3000 && return 0
|
|
56
|
+
would && { log "DRY: would START TermDeck server :3000"; return 0; }
|
|
57
|
+
log "TermDeck server :3000 DOWN — starting"
|
|
58
|
+
( cd "$REPO_DIR" || exit 1
|
|
59
|
+
set -a; [ -f "$SECRETS_ENV" ] && . "$SECRETS_ENV"; set +a
|
|
60
|
+
nohup node packages/server/src/index.js >>"${LOG_DIR}/server.log" 2>&1 & )
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
start_webhook() {
|
|
64
|
+
port_up 37778 && return 0
|
|
65
|
+
would && { log "DRY: would START Mnestra webhook :37778"; return 0; }
|
|
66
|
+
log "Mnestra webhook :37778 DOWN — starting"
|
|
67
|
+
( set -a; [ -f "$SECRETS_ENV" ] && . "$SECRETS_ENV"; set +a
|
|
68
|
+
MNESTRA_WEBHOOK_PORT=37778 nohup mnestra serve >>"${LOG_DIR}/mnestra.log" 2>&1 & )
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
tunnel_up() { pgrep -f 'cloudflared tunnel' >/dev/null 2>&1; }
|
|
72
|
+
|
|
73
|
+
start_tunnel() {
|
|
74
|
+
if [ -n "${TERMDECK_TUNNEL_NAME:-}" ]; then
|
|
75
|
+
tunnel_up && return 0
|
|
76
|
+
would && { log "DRY: would START named tunnel '${TERMDECK_TUNNEL_NAME}'"; return 0; }
|
|
77
|
+
log "named cloudflared tunnel '${TERMDECK_TUNNEL_NAME}' DOWN — starting (stable URL)"
|
|
78
|
+
nohup cloudflared tunnel run "${TERMDECK_TUNNEL_NAME}" >>"${LOG_DIR}/cloudflared.log" 2>&1 &
|
|
79
|
+
echo "https://${TERMDECK_PUBLIC_HOSTNAME:?set TERMDECK_PUBLIC_HOSTNAME for a named tunnel}" > "$URL_FILE"
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
tunnel_up && [ -s "$URL_FILE" ] && return 0
|
|
83
|
+
would && { log "DRY: would START quick tunnel + capture trycloudflare URL"; return 0; }
|
|
84
|
+
log "quick cloudflared tunnel DOWN — starting + capturing URL"
|
|
85
|
+
: > "${LOG_DIR}/cloudflared.log"
|
|
86
|
+
nohup cloudflared tunnel --url http://127.0.0.1:8870 >>"${LOG_DIR}/cloudflared.log" 2>&1 &
|
|
87
|
+
for _ in $(seq 1 40); do
|
|
88
|
+
local url; url=$(grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' "${LOG_DIR}/cloudflared.log" 2>/dev/null | head -1)
|
|
89
|
+
[ -n "$url" ] && { echo "$url" > "$URL_FILE"; log "tunnel URL captured: $url"; return 0; }
|
|
90
|
+
sleep 1
|
|
91
|
+
done
|
|
92
|
+
log "WARN: quick tunnel URL did not appear within 40s"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
bridge_resource() {
|
|
96
|
+
curl -s --max-time 5 http://127.0.0.1:8870/healthz 2>/dev/null \
|
|
97
|
+
| sed -n 's/.*"resource":"\([^"]*\)".*/\1/p'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
start_bridge() {
|
|
101
|
+
local pub; pub="$(cat "$URL_FILE" 2>/dev/null || true)"
|
|
102
|
+
[ -z "$pub" ] && { log "no public URL yet — skipping bridge"; return 1; }
|
|
103
|
+
local want="${pub%/}/mcp"
|
|
104
|
+
if port_up 8870; then
|
|
105
|
+
[ "$(bridge_resource)" = "$want" ] && return 0
|
|
106
|
+
would && { log "DRY: would RESTART bridge (URL drift → $want)"; return 0; }
|
|
107
|
+
log "bridge URL drift — restarting to re-pin $want"
|
|
108
|
+
lsof -nP -ti TCP:8870 -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
|
|
109
|
+
sleep 1
|
|
110
|
+
else
|
|
111
|
+
would && { log "DRY: would START MCP bridge :8870 (url=$want)"; return 0; }
|
|
112
|
+
log "MCP bridge :8870 DOWN — starting"
|
|
113
|
+
fi
|
|
114
|
+
ensure_secret
|
|
115
|
+
( cd "${REPO_DIR}/packages/mcp-bridge" || exit 1
|
|
116
|
+
TERMDECK_BRIDGE_PUBLIC_URL="$pub" \
|
|
117
|
+
TERMDECK_BRIDGE_OPERATOR_SECRET="$(cat "$SECRET_FILE")" \
|
|
118
|
+
MNESTRA_WEBHOOK_URL="http://localhost:37778/mnestra" \
|
|
119
|
+
TERMDECK_API_BASE="http://127.0.0.1:3000" \
|
|
120
|
+
TERMDECK_BRIDGE_ALLOWLIST_PROJECTS="$ALLOWLIST_PROJECTS" \
|
|
121
|
+
nohup node src/server.js >>"${LOG_DIR}/bridge.log" 2>&1 & )
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Adopt an already-running stack: if we don't yet know the public URL but the
|
|
125
|
+
# bridge is already serving, learn it from /healthz (so we never spawn a second
|
|
126
|
+
# tunnel over a healthy one). Recording the URL is metadata-only — safe in dry-run.
|
|
127
|
+
adopt_url() {
|
|
128
|
+
[ -s "$URL_FILE" ] && return 0
|
|
129
|
+
port_up 8870 || return 0
|
|
130
|
+
local res; res="$(bridge_resource)"
|
|
131
|
+
[ -z "$res" ] && return 0
|
|
132
|
+
echo "${res%/mcp}" > "$URL_FILE"
|
|
133
|
+
log "adopted existing bridge public URL: ${res%/mcp}"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main() {
|
|
137
|
+
log "supervise tick (dry=$DRY) — repo=$REPO_DIR"
|
|
138
|
+
start_server
|
|
139
|
+
start_webhook
|
|
140
|
+
adopt_url
|
|
141
|
+
start_tunnel
|
|
142
|
+
start_bridge
|
|
143
|
+
log "state: server=$(port_up 3000 && echo up || echo DOWN) webhook=$(port_up 37778 && echo up || echo DOWN) bridge=$(port_up 8870 && echo up || echo DOWN) url=$(cat "$URL_FILE" 2>/dev/null || echo none)"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
main "$@"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 60s cadence for termdeck-supervise.service — see that unit's header for the
|
|
2
|
+
# install recipe. Enable the TIMER, not the service:
|
|
3
|
+
# systemctl --user enable --now termdeck-supervise.timer
|
|
4
|
+
|
|
5
|
+
[Unit]
|
|
6
|
+
Description=Run the TermDeck stack supervisor every 60s
|
|
7
|
+
|
|
8
|
+
[Timer]
|
|
9
|
+
OnBootSec=30
|
|
10
|
+
OnUnitActiveSec=60
|
|
11
|
+
AccuracySec=5
|
|
12
|
+
|
|
13
|
+
[Install]
|
|
14
|
+
WantedBy=timers.target
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// termdeck [--port 3000] [--no-open]
|
|
6
6
|
// termdeck init --mnestra [flags] # Tier 2 memory setup (wired to init-mnestra.js)
|
|
7
7
|
// termdeck init --rumen [flags] # Tier 3 async learning deploy
|
|
8
|
+
// termdeck init --bridge [flags] # Tier 5 web-chat bridge (named tunnel + supervisor)
|
|
8
9
|
//
|
|
9
10
|
// Note (Sprint 3): the `--mnestra` flag name matches the current init-mnestra.js
|
|
10
11
|
// filename. When the main orchestrator completes the Mnestra → Mnestra rename
|
|
@@ -223,7 +224,7 @@ if (args[0] === 'init') {
|
|
|
223
224
|
// silently picking the first. The `--auto` / `--mcp-supabase` flags are
|
|
224
225
|
// NOT in this list — they're handled by init.js (the orchestrator) and
|
|
225
226
|
// can co-exist with init.js's own flag set.
|
|
226
|
-
const MODES = ['--project', '--mnestra', '--rumen'];
|
|
227
|
+
const MODES = ['--project', '--mnestra', '--rumen', '--bridge'];
|
|
227
228
|
const presentModes = MODES.filter((m) => args.slice(1).includes(m));
|
|
228
229
|
if (presentModes.length > 1) {
|
|
229
230
|
console.error(`[cli] init: pass only one of ${MODES.join(' | ')}; got ${presentModes.join(' + ')}`);
|
|
@@ -257,6 +258,16 @@ if (args[0] === 'init') {
|
|
|
257
258
|
});
|
|
258
259
|
return;
|
|
259
260
|
}
|
|
261
|
+
// Sprint 73 T2: Tier 5 web-chat bridge wizard. MUST dispatch here, above
|
|
262
|
+
// the leading-dash catch-all below — otherwise `init --bridge` would
|
|
263
|
+
// silently route into the unified init.js orchestrator.
|
|
264
|
+
if (mode === '--bridge') {
|
|
265
|
+
run(path.join(__dirname, 'init-bridge.js'), rest).catch((err) => {
|
|
266
|
+
console.error('[cli] init --bridge failed:', err && err.stack || err);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
260
271
|
|
|
261
272
|
// Sprint 64 T1: default (no mode) OR `--auto` / `--mcp-supabase` → route to
|
|
262
273
|
// the unified orchestrator at init.js. It runs init-mnestra + init-rumen +
|
|
@@ -282,11 +293,12 @@ if (args[0] === 'init') {
|
|
|
282
293
|
});
|
|
283
294
|
return;
|
|
284
295
|
}
|
|
285
|
-
console.error('Usage: termdeck init [--auto] | --mnestra | --rumen | --project <name>');
|
|
296
|
+
console.error('Usage: termdeck init [--auto] | --mnestra | --rumen | --bridge | --project <name>');
|
|
286
297
|
console.error(' termdeck init Unified setup (Mnestra + Rumen + doctor)');
|
|
287
298
|
console.error(' termdeck init --auto Auto-provision via Supabase MCP (alias: --mcp-supabase)');
|
|
288
299
|
console.error(' termdeck init --mnestra Configure Tier 2 memory (Supabase + Mnestra)');
|
|
289
300
|
console.error(' termdeck init --rumen Deploy Tier 3 async learning (Rumen)');
|
|
301
|
+
console.error(' termdeck init --bridge Scaffold Tier 5 web-chat bridge (named tunnel + supervisor)');
|
|
290
302
|
console.error(' termdeck init --project <name> Scaffold a new project with CLAUDE.md + orchestration docs');
|
|
291
303
|
console.error(' termdeck init --help Show full flag reference');
|
|
292
304
|
process.exit(1);
|
|
@@ -388,6 +400,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
388
400
|
termdeck --session-logs Write per-session markdown logs to ~/.termdeck/sessions/
|
|
389
401
|
termdeck init --mnestra Configure Tier 2 memory (Supabase + Mnestra)
|
|
390
402
|
termdeck init --rumen Deploy Tier 3 async learning (Rumen)
|
|
403
|
+
termdeck init --bridge Scaffold Tier 5 web-chat bridge (named tunnel + supervisor)
|
|
391
404
|
termdeck init --project NAME Scaffold a new project with CLAUDE.md + orchestration docs (--dry-run, --force)
|
|
392
405
|
termdeck forge Generate Claude skills from memories (experimental)
|
|
393
406
|
termdeck doctor Diagnose stack — npm versions + Supabase schema (use --no-schema to skip the DB probe)
|