@jhizzard/termdeck 1.8.1 → 1.10.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.8.1",
3
+ "version": "1.10.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
@@ -346,6 +346,16 @@ async function _runSchemaCheck(opts = {}) {
346
346
  let client = optsObj._pgClient || null;
347
347
  let ownsClient = false;
348
348
  if (!client) {
349
+ // Sprint 75 T2 (part C): classify + warn BEFORE the connect attempt —
350
+ // a direct-endpoint URL on an IPv4-only host doesn't fail fast, it
351
+ // hangs until a pool timeout, so the warning must print first.
352
+ // Warn-only; fail-soft if the helper is unavailable.
353
+ try {
354
+ const urlHelper = require(path.join(SETUP_DIR, 'supabase-url'));
355
+ for (const line of urlHelper.directEndpointWarningLines(urlHelper.classifyDbEndpoint(secrets.DATABASE_URL))) {
356
+ process.stdout.write(` ${line}\n`);
357
+ }
358
+ } catch (_e) { /* warn-only — never block the doctor pass */ }
349
359
  try {
350
360
  client = await pgRunner.connect(secrets.DATABASE_URL);
351
361
  ownsClient = true;
@@ -524,6 +534,7 @@ function renderSchemaResult(result, c) {
524
534
  if (result.connectError) {
525
535
  out.push(` ${c.yellow('✗')} could not connect: ${result.connectError}`);
526
536
  out.push(` ${c.dim('Check DATABASE_URL in ~/.termdeck/secrets.env, then re-run.')}`);
537
+ out.push(` ${c.dim('If this host is IPv4-only and the URL is the db.<project-ref> direct endpoint, that is the cause — switch to the Shared Pooler.')}`);
527
538
  return out.join('\n');
528
539
  }
529
540
  for (const section of result.sections) {
@@ -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)