@0xmaxma/claude-gateway 1.3.25 → 1.3.31

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.
Files changed (63) hide show
  1. package/README.md +47 -1
  2. package/config.template.json +6 -2
  3. package/dist/agent/incident-store.d.ts +89 -0
  4. package/dist/agent/incident-store.d.ts.map +1 -0
  5. package/dist/agent/incident-store.js +299 -0
  6. package/dist/agent/incident-store.js.map +1 -0
  7. package/dist/agent/incident.d.ts +156 -0
  8. package/dist/agent/incident.d.ts.map +1 -0
  9. package/dist/agent/incident.js +177 -0
  10. package/dist/agent/incident.js.map +1 -0
  11. package/dist/agent/recovery-executor.d.ts +117 -0
  12. package/dist/agent/recovery-executor.d.ts.map +1 -0
  13. package/dist/agent/recovery-executor.js +168 -0
  14. package/dist/agent/recovery-executor.js.map +1 -0
  15. package/dist/agent/recovery-policy.d.ts +97 -0
  16. package/dist/agent/recovery-policy.d.ts.map +1 -0
  17. package/dist/agent/recovery-policy.js +164 -0
  18. package/dist/agent/recovery-policy.js.map +1 -0
  19. package/dist/agent/runner.d.ts +44 -0
  20. package/dist/agent/runner.d.ts.map +1 -1
  21. package/dist/agent/runner.js +272 -2
  22. package/dist/agent/runner.js.map +1 -1
  23. package/dist/agent/safe-mode.d.ts +61 -0
  24. package/dist/agent/safe-mode.d.ts.map +1 -0
  25. package/dist/agent/safe-mode.js +102 -0
  26. package/dist/agent/safe-mode.js.map +1 -0
  27. package/dist/agent/triage.d.ts +94 -0
  28. package/dist/agent/triage.d.ts.map +1 -0
  29. package/dist/agent/triage.js +209 -0
  30. package/dist/agent/triage.js.map +1 -0
  31. package/dist/agent/turn-trace.d.ts +120 -0
  32. package/dist/agent/turn-trace.d.ts.map +1 -0
  33. package/dist/agent/turn-trace.js +122 -0
  34. package/dist/agent/turn-trace.js.map +1 -0
  35. package/dist/api/gateway-router.d.ts +21 -0
  36. package/dist/api/gateway-router.d.ts.map +1 -1
  37. package/dist/api/gateway-router.js +58 -17
  38. package/dist/api/gateway-router.js.map +1 -1
  39. package/dist/config/migrator.d.ts +4 -0
  40. package/dist/config/migrator.d.ts.map +1 -1
  41. package/dist/config/migrator.js +60 -3
  42. package/dist/config/migrator.js.map +1 -1
  43. package/dist/index.js +3 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/session/process.d.ts +18 -0
  46. package/dist/session/process.d.ts.map +1 -1
  47. package/dist/session/process.js +61 -1
  48. package/dist/session/process.js.map +1 -1
  49. package/dist/shell/claude-pty-shell.js +59 -0
  50. package/dist/shell/claude-pty-shell.js.map +1 -1
  51. package/dist/shell/control-channel.d.ts +74 -0
  52. package/dist/shell/control-channel.d.ts.map +1 -0
  53. package/dist/shell/control-channel.js +114 -0
  54. package/dist/shell/control-channel.js.map +1 -0
  55. package/dist/types.d.ts +21 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/ui/web-ui.d.ts.map +1 -1
  58. package/dist/ui/web-ui.js +103 -2
  59. package/dist/ui/web-ui.js.map +1 -1
  60. package/mcp/tools/skills/handlers.ts +4 -2
  61. package/mcp/tools/telegram/receiver-server.ts +163 -0
  62. package/mcp/tools/telegram/typing.ts +124 -0
  63. package/package.json +3 -1
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /**
3
+ * Control channel (Epic #195, Phase 3b — item B2).
4
+ *
5
+ * The gateway's recovery executor can ask the PTY wrapper to press a specific
6
+ * key into the interactive TUI — the raw keystrokes the wrapper otherwise only
7
+ * generates internally (a single Enter, a menu digit, an arrow). These reach the
8
+ * wrapper as a NEW stdin message type `{"type":"control","key":...}`, kept
9
+ * distinct from a `type:"user"` message turn so control keystrokes are never
10
+ * confused with prompt text.
11
+ *
12
+ * The vocabulary is CLOSED and validated here, for the same reason the triage
13
+ * schema is closed: the only keystrokes the wrapper will ever emit on behalf of
14
+ * the gateway are the ones in `CONTROL_KEYS`. A malformed or unknown control
15
+ * message is rejected, so no arbitrary byte sequence can be injected into the
16
+ * PTY via this path. This module is pure (parse + map to keystrokes); the
17
+ * wrapper performs the actual `host.writeRaw` and the screen-gated Enter.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.MAX_PTY_INPUT_BYTES = exports.MAX_CONTROL_OPTION = exports.KEY_DOWN = exports.KEY_UP = exports.KEY_ENTER = exports.KEY_ESC = exports.CONTROL_KEYS = void 0;
21
+ exports.parseControlCommand = parseControlCommand;
22
+ exports.keystrokesFor = keystrokesFor;
23
+ exports.isAcceptablePtyInput = isAcceptablePtyInput;
24
+ exports.shouldRoutePtyInput = shouldRoutePtyInput;
25
+ /** Closed set of control keys the gateway may request. */
26
+ exports.CONTROL_KEYS = ['esc', 'esc-esc', 'enter', 'up', 'down', 'select-option'];
27
+ // VT100 keystroke sequences — identical to the ones the wrapper already emits
28
+ // internally (menu-probe arrows, handleMenuReply digit/Enter), so a control key
29
+ // is indistinguishable from a user pressing it.
30
+ exports.KEY_ESC = '\x1b';
31
+ exports.KEY_ENTER = '\r';
32
+ exports.KEY_UP = '\x1b[A';
33
+ exports.KEY_DOWN = '\x1b[B';
34
+ /**
35
+ * Upper bound for a select-option index. Capped at 9 to match the triage schema:
36
+ * the wrapper selects by typing the digit, which is only unambiguous single-char,
37
+ * so a two-digit index could type a wrong immediate selection. Menus are short.
38
+ */
39
+ exports.MAX_CONTROL_OPTION = 9;
40
+ const KEY_SET = new Set(exports.CONTROL_KEYS);
41
+ /**
42
+ * Parse and validate a raw stdin control object into a closed ControlCommand,
43
+ * or null if it cannot be trusted. Never throws. `select-option` requires a
44
+ * bounded positive-integer `option`; anything else is rejected.
45
+ */
46
+ function parseControlCommand(obj) {
47
+ const key = obj['key'];
48
+ if (typeof key !== 'string' || !KEY_SET.has(key))
49
+ return null;
50
+ if (key === 'select-option') {
51
+ const opt = obj['option'];
52
+ if (typeof opt !== 'number' ||
53
+ !Number.isInteger(opt) ||
54
+ opt < 1 ||
55
+ opt > exports.MAX_CONTROL_OPTION) {
56
+ return null;
57
+ }
58
+ return { key: 'select-option', option: opt };
59
+ }
60
+ return { key: key };
61
+ }
62
+ /**
63
+ * The raw keystroke sequence(s) a control command maps to. For select-option
64
+ * this is just the digit — the wrapper sends Enter separately, gated on the
65
+ * menu still being on screen (a TUI may confirm on the digit alone), mirroring
66
+ * the existing menu-selection flow.
67
+ */
68
+ function keystrokesFor(cmd) {
69
+ switch (cmd.key) {
70
+ case 'esc':
71
+ return [exports.KEY_ESC];
72
+ case 'esc-esc':
73
+ return [exports.KEY_ESC, exports.KEY_ESC];
74
+ case 'enter':
75
+ return [exports.KEY_ENTER];
76
+ case 'up':
77
+ return [exports.KEY_UP];
78
+ case 'down':
79
+ return [exports.KEY_DOWN];
80
+ case 'select-option':
81
+ return [String(cmd.option)];
82
+ }
83
+ }
84
+ /**
85
+ * Interactive terminal input (Issue #201). Unlike the closed control vocabulary,
86
+ * input-mode carries arbitrary raw keystroke bytes so a viewer can type any key.
87
+ * Access is gated at the gateway (auth + the localhost-default `gateway.bind`);
88
+ * these pure helpers bound and validate a single frame, and are reused on both
89
+ * sides of the pipe (the gateway WS handler and the PTY wrapper) so the two never
90
+ * drift on what counts as an acceptable frame.
91
+ */
92
+ exports.MAX_PTY_INPUT_BYTES = 8192;
93
+ /**
94
+ * True when `data` is a non-empty string within the per-frame byte bound. The
95
+ * bound is measured in real UTF-8 bytes (not UTF-16 code units), so multi-byte
96
+ * input (emoji, non-Latin scripts) is capped at the same byte budget that will
97
+ * actually be written to the PTY.
98
+ */
99
+ function isAcceptablePtyInput(data, maxBytes = exports.MAX_PTY_INPUT_BYTES) {
100
+ return (typeof data === 'string' &&
101
+ data.length > 0 &&
102
+ Buffer.byteLength(data, 'utf8') <= maxBytes);
103
+ }
104
+ /**
105
+ * Whether one inbound WebSocket frame should be routed into the live PTY.
106
+ * Requires a text (non-binary) frame and an acceptable payload; binary frames
107
+ * and oversized/empty payloads are dropped. Pure — no IO. (Whether a browser
108
+ * sends input at all is a client-side choice via the viewer's mode toggle;
109
+ * access to the socket is gated upstream by auth + `gateway.bind`.)
110
+ */
111
+ function shouldRoutePtyInput(isBinary, data, maxBytes = exports.MAX_PTY_INPUT_BYTES) {
112
+ return !isBinary && isAcceptablePtyInput(data, maxBytes);
113
+ }
114
+ //# sourceMappingURL=control-channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control-channel.js","sourceRoot":"","sources":["../../src/shell/control-channel.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;AAmCH,kDAgBC;AAQD,sCAeC;AAkBD,oDASC;AASD,kDAMC;AAlHD,0DAA0D;AAC7C,QAAA,YAAY,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,CAAU,CAAA;AAU/F,8EAA8E;AAC9E,gFAAgF;AAChF,gDAAgD;AACnC,QAAA,OAAO,GAAG,MAAM,CAAA;AAChB,QAAA,SAAS,GAAG,IAAI,CAAA;AAChB,QAAA,MAAM,GAAG,QAAQ,CAAA;AACjB,QAAA,QAAQ,GAAG,QAAQ,CAAA;AAEhC;;;;GAIG;AACU,QAAA,kBAAkB,GAAG,CAAC,CAAA;AAEnC,MAAM,OAAO,GAAwB,IAAI,GAAG,CAAC,oBAAY,CAAC,CAAA;AAE1D;;;;GAIG;AACH,SAAgB,mBAAmB,CAAC,GAA4B;IAC9D,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;IACtB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7D,IAAI,GAAG,KAAK,eAAe,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAA;QACzB,IACE,OAAO,GAAG,KAAK,QAAQ;YACvB,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC;YACtB,GAAG,GAAG,CAAC;YACP,GAAG,GAAG,0BAAkB,EACxB,CAAC;YACD,OAAO,IAAI,CAAA;QACb,CAAC;QACD,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,GAAG,EAAE,CAAA;IAC9C,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,GAAiB,EAAE,CAAA;AACnC,CAAC;AAED;;;;;GAKG;AACH,SAAgB,aAAa,CAAC,GAAmB;IAC/C,QAAQ,GAAG,CAAC,GAAG,EAAE,CAAC;QAChB,KAAK,KAAK;YACR,OAAO,CAAC,eAAO,CAAC,CAAA;QAClB,KAAK,SAAS;YACZ,OAAO,CAAC,eAAO,EAAE,eAAO,CAAC,CAAA;QAC3B,KAAK,OAAO;YACV,OAAO,CAAC,iBAAS,CAAC,CAAA;QACpB,KAAK,IAAI;YACP,OAAO,CAAC,cAAM,CAAC,CAAA;QACjB,KAAK,MAAM;YACT,OAAO,CAAC,gBAAQ,CAAC,CAAA;QACnB,KAAK,eAAe;YAClB,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;IAC/B,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACU,QAAA,mBAAmB,GAAG,IAAI,CAAA;AAEvC;;;;;GAKG;AACH,SAAgB,oBAAoB,CAClC,IAAa,EACb,WAAmB,2BAAmB;IAEtC,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,CAAC,MAAM,GAAG,CAAC;QACf,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,QAAQ,CAC5C,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,mBAAmB,CACjC,QAAiB,EACjB,IAAa,EACb,WAAmB,2BAAmB;IAEtC,OAAO,CAAC,QAAQ,IAAI,oBAAoB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;AAC1D,CAAC"}
package/dist/types.d.ts CHANGED
@@ -136,6 +136,14 @@ export interface GatewayConfig {
136
136
  gateway: {
137
137
  logDir: string;
138
138
  timezone: string;
139
+ /**
140
+ * Network interface the HTTP/WebSocket server binds to. Defaults to
141
+ * "127.0.0.1" (localhost-only) so the dashboard and API are not exposed to
142
+ * the local network out of the box. Set to "0.0.0.0" to expose all
143
+ * interfaces (e.g. behind a trusted reverse proxy). The `GATEWAY_BIND` env
144
+ * var, when set, takes precedence over this field.
145
+ */
146
+ bind?: string;
139
147
  models?: ModelConfig[];
140
148
  api?: {
141
149
  keys: ApiKey[];
@@ -145,6 +153,19 @@ export interface GatewayConfig {
145
153
  * false = interactive backend: claude TUI under the claude-pty-shell PTY wrapper.
146
154
  */
147
155
  headless?: boolean;
156
+ /**
157
+ * Self-healing recovery (Epic #195, Phase 3). When `autoRecover` is true the
158
+ * turn-trace watchdog may auto-execute a whitelisted recovery action
159
+ * (keystroke, session/receiver restart, safe-mode fallback, guarded resend)
160
+ * for a stalled turn. Default false — detection, incident logging, and
161
+ * notification still run when disabled; only live action execution is gated,
162
+ * so the feature ships dark and the operator opts in when ready. Safe-mode
163
+ * auto-fallback on hard PTY failure is independent of this flag (it is always
164
+ * reversible and never presses keys).
165
+ */
166
+ selfHealing?: {
167
+ autoRecover?: boolean;
168
+ };
148
169
  /** Global history retention/cleanup defaults */
149
170
  history?: HistoryConfig & {
150
171
  cleanupHour?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"web-ui.d.ts","sourceRoot":"","sources":["../../src/ui/web-ui.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,SAAK,GAAG,MAAM,CAkxB5D"}
1
+ {"version":3,"file":"web-ui.d.ts","sourceRoot":"","sources":["../../src/ui/web-ui.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,SAAK,GAAG,MAAM,CAu3B5D"}
package/dist/ui/web-ui.js CHANGED
@@ -119,6 +119,22 @@ function generateDashboardHtml(dashToken = '') {
119
119
  }
120
120
  .pty-close:hover { color: #fc8181; }
121
121
  .pty-refresh:hover { color: #63b3ed; }
122
+ /* Keyboard-focus outline on the viewer (it is tabindex focusable so PageUp/
123
+ PageDown reach the terminal); keep it subtle rather than the default ring. */
124
+ #pty-terminal:focus, #pty-terminal:focus-visible { outline: 1px solid #2d3748; }
125
+ .pty-mode-toggle {
126
+ background: none;
127
+ border: 1px solid #2d3748;
128
+ border-radius: 4px;
129
+ color: #718096;
130
+ cursor: pointer;
131
+ font-size: 0.72rem;
132
+ padding: 1px 7px;
133
+ margin-right: 6px;
134
+ }
135
+ .pty-mode-toggle:hover { color: #63b3ed; border-color: #3d4a5f; }
136
+ /* Active (input) mode — clearly signals the viewer now types into the PTY. */
137
+ .pty-mode-toggle.input-active { color: #0d1117; background: #63b3ed; border-color: #63b3ed; font-weight: 600; }
122
138
  /* Fixed-size terminal viewport — the server PTY runs at 200x50, so the
123
139
  viewer must NOT resize to the panel (that mismatch is what garbles the
124
140
  output). We render at the native size and pan horizontally if the 200-col
@@ -233,13 +249,16 @@ function generateDashboardHtml(dashToken = '') {
233
249
  in view when streaming. -->
234
250
  <div class="pty-viewer" id="pty-viewer">
235
251
  <div class="pty-viewer-header">
236
- <span>Shell Process Viewer &mdash; <span class="agent-label" id="pty-agent-label"></span><span class="session-label" id="pty-session-label"></span></span>
252
+ <span><span id="pty-title-text">Terminal Viewer</span> &mdash; <span class="agent-label" id="pty-agent-label"></span><span class="session-label" id="pty-session-label"></span></span>
237
253
  <span>
254
+ <button class="pty-mode-toggle" id="pty-mode-toggle-btn" title="Toggle input mode (type into the live terminal)">&#x2328; View</button>
238
255
  <button class="pty-refresh" id="pty-refresh-btn" title="Refresh (reconnect &amp; redraw)">&#x21ba;</button>
239
256
  <button class="pty-close" id="pty-close-btn" title="Close">&#x2715;</button>
240
257
  </span>
241
258
  </div>
242
- <div id="pty-terminal"></div>
259
+ <!-- tabindex makes the viewer keyboard-focusable so PageUp/PageDown reach it
260
+ even in read-only view mode (xterm stdin is disabled there). -->
261
+ <div id="pty-terminal" tabindex="0"></div>
243
262
  </div>
244
263
 
245
264
  <!-- Sessions — full width (session-centric, flat list) -->
@@ -350,6 +369,11 @@ function generateDashboardHtml(dashToken = '') {
350
369
  // multi-byte char into noise — decode as UTF-8 with {stream:true} so sequences
351
370
  // split across WebSocket frames are reassembled instead of corrupted.
352
371
  let utf8Decoder = null;
372
+ // Interactive input mode (Issue #201). When on, keystrokes typed into the
373
+ // terminal are streamed to the live PTY over the same WebSocket. Off by
374
+ // default (read-only mirror); the viewer flips it on via the mode toggle.
375
+ let ptyInputMode = false;
376
+ let ptyOnDataDisposable = null;
353
377
 
354
378
  // The agent's TUI enables mouse tracking (DECSET 1000/1002/1003/1006...).
355
379
  // While those modes are active, xterm.js forwards wheel/click events to the
@@ -373,6 +397,9 @@ function generateDashboardHtml(dashToken = '') {
373
397
  // Append the session id after the agent name, e.g. "claude-founder · 3c01897c…".
374
398
  document.getElementById('pty-session-label').textContent = sessionId ? ' \\u00b7 ' + sessionId : '';
375
399
  document.getElementById('pty-viewer').style.display = 'block';
400
+ // Always (re)open a fresh session in read-only view mode; the mode toggle
401
+ // lets the viewer switch into interactive input on demand.
402
+ setPtyInputMode(false);
376
403
 
377
404
  if (!term) {
378
405
  term = new Terminal({
@@ -488,12 +515,53 @@ function generateDashboardHtml(dashToken = '') {
488
515
  // are about to close intentionally, so it does not schedule a new one.
489
516
  if (ptyReconnectTimer) { clearTimeout(ptyReconnectTimer); ptyReconnectTimer = null; }
490
517
  ptyReconnectAttempts = 0;
518
+ setPtyInputMode(false); // always leave input mode when closing/switching
491
519
  if (ptyWs) { ptyWs.onclose = null; ptyWs.onerror = null; ptyWs.close(); ptyWs = null; }
492
520
  currentPtyAgent = null;
493
521
  currentPtySession = null;
494
522
  document.getElementById('pty-viewer').style.display = 'none';
495
523
  }
496
524
 
525
+ // Switch the viewer between read-only mirror and interactive input. In input
526
+ // mode the terminal accepts stdin and every keystroke (printable chars,
527
+ // Enter, arrows, Ctrl-combos, Esc) is streamed to the live PTY over the WS,
528
+ // like a real terminal; the title and toggle reflect the active mode.
529
+ function setPtyInputMode(on) {
530
+ if (!term) on = false;
531
+ ptyInputMode = !!on;
532
+ const toggleBtn = document.getElementById('pty-mode-toggle-btn');
533
+ const titleEl = document.getElementById('pty-title-text');
534
+ if (term) term.options.disableStdin = !ptyInputMode;
535
+ // Detach any previous onData listener before (re)attaching, so toggling
536
+ // off — or toggling on twice — never leaves a duplicate keystroke stream.
537
+ if (ptyOnDataDisposable) { ptyOnDataDisposable.dispose(); ptyOnDataDisposable = null; }
538
+ if (ptyInputMode && term) {
539
+ ptyOnDataDisposable = term.onData(function(data) {
540
+ if (ptyWs && ptyWs.readyState === WebSocket.OPEN) ptyWs.send(data);
541
+ });
542
+ }
543
+ if (titleEl) titleEl.textContent = ptyInputMode ? 'Interactive Terminal' : 'Terminal Viewer';
544
+ if (toggleBtn) {
545
+ toggleBtn.innerHTML = ptyInputMode ? '\\u2328 Input' : '\\u2328 View';
546
+ toggleBtn.classList.toggle('input-active', ptyInputMode);
547
+ toggleBtn.title = ptyInputMode
548
+ ? 'Input mode — keystrokes go to the live terminal (click for read-only)'
549
+ : 'Toggle input mode (type into the live terminal)';
550
+ }
551
+ if (ptyInputMode && term) {
552
+ term.focus(); // typing goes to xterm's textarea
553
+ } else {
554
+ // View mode: focus the container (tabindex) so PageUp/PageDown reach our
555
+ // key handler instead of scrolling the browser page.
556
+ const host = document.getElementById('pty-terminal');
557
+ if (host) host.focus();
558
+ }
559
+ }
560
+
561
+ function togglePtyInputMode() {
562
+ setPtyInputMode(!ptyInputMode);
563
+ }
564
+
497
565
  // Refresh: force a clean reconnect of the CURRENT session. The server replays
498
566
  // a freshly-serialized screen frame on subscribe, so this redraws from a clean
499
567
  // xterm state — a manual escape hatch if the live stream ever drifts.
@@ -505,8 +573,41 @@ function generateDashboardHtml(dashToken = '') {
505
573
  await openPtyViewer(agentId, sessionId);
506
574
  }
507
575
 
576
+ // Page Up / Page Down keys (Issue #201): the alt-screen mirror keeps no
577
+ // scrollback of its own, so a physical PageUp/PageDown key is forwarded to the
578
+ // live TUI — which holds the history and scrolls itself. In read-only view mode
579
+ // xterm's stdin is disabled, so without this those keys would just scroll the
580
+ // browser page; here we intercept ONLY these two keys (never printable input)
581
+ // and send them to the PTY, so the user can page through earlier output without
582
+ // enabling the interactive keyboard. In input mode xterm already forwards them
583
+ // via onData, so we skip to avoid double-send.
584
+ //
585
+ // The listener is attached to the document, NOT the viewer container: xterm renders
586
+ // its own focusable helpers inside #pty-terminal, and with disableStdin the
587
+ // container rarely holds focus reliably (a reconnect or stray click drops it),
588
+ // so a container-scoped keydown silently missed the keys. Gating on the viewer
589
+ // being open + view mode keeps it from hijacking PageUp elsewhere on the page,
590
+ // and we bail when focus is in a real text field so form paging still works.
591
+ // \\x1b[5~ = PageUp, \\x1b[6~ = PageDown.
592
+ function forwardPageKey(e) {
593
+ if (e.key !== 'PageUp' && e.key !== 'PageDown') return;
594
+ if (ptyInputMode) return; // input mode: xterm.onData already sends it
595
+ // Only while THIS viewer is actually open with a live socket.
596
+ const viewer = document.getElementById('pty-viewer');
597
+ if (!viewer || viewer.style.display === 'none') return;
598
+ if (!ptyWs || ptyWs.readyState !== WebSocket.OPEN) return;
599
+ // Don't steal paging from a genuine editable field (none in view mode today,
600
+ // but keep the guard so the document-level listener stays well-behaved).
601
+ const t = e.target;
602
+ if (t && (t.tagName === 'INPUT' || (t.tagName === 'TEXTAREA' && !t.disabled) || t.isContentEditable)) return;
603
+ e.preventDefault();
604
+ ptyWs.send(e.key === 'PageUp' ? '\\x1b[5~' : '\\x1b[6~');
605
+ }
606
+ document.addEventListener('keydown', forwardPageKey);
607
+
508
608
  document.getElementById('pty-close-btn').addEventListener('click', closePtyViewer);
509
609
  document.getElementById('pty-refresh-btn').addEventListener('click', function() { void refreshPtyViewer(); });
610
+ document.getElementById('pty-mode-toggle-btn').addEventListener('click', togglePtyInputMode);
510
611
 
511
612
  // Event delegation for Live buttons (avoids inline onclick + HTML injection)
512
613
  document.getElementById('sessions-tbody').addEventListener('click', function(e) {
@@ -1 +1 @@
1
- {"version":3,"file":"web-ui.js","sourceRoot":"","sources":["../../src/ui/web-ui.ts"],"names":[],"mappings":";;AAIA,sDAkxBC;AAtxBD;;;GAGG;AACH,SAAgB,qBAAqB,CAAC,SAAS,GAAG,EAAE;IAClD,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3E,OAAO;;;;;qCAK4B,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA0wBtC,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"web-ui.js","sourceRoot":"","sources":["../../src/ui/web-ui.ts"],"names":[],"mappings":";;AAIA,sDAu3BC;AA33BD;;;GAGG;AACH,SAAgB,qBAAqB,CAAC,SAAS,GAAG,EAAE;IAClD,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3E,OAAO;;;;;qCAK4B,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA+2BtC,CAAC;AACT,CAAC"}
@@ -161,8 +161,10 @@ export async function installSkill(params: InstallSkillParams): Promise<string>
161
161
  throw new Error(`SKILL.md exceeds ${MAX_SKILL_SIZE / 1024}KB limit (${(content.length / 1024).toFixed(1)}KB)`);
162
162
  }
163
163
 
164
- // Parse frontmatter to extract name
165
- const { extractFrontmatter } = await import('../../../src/skills/parser');
164
+ // Parse frontmatter to extract name. Import compiled dist/, not raw src/ —
165
+ // src/ is not published (files: ["mcp/"]), so a src/ import throws "Cannot find
166
+ // module" on installed packages. Enforced by mcp-no-src-imports.test.ts.
167
+ const { extractFrontmatter } = await import('../../../dist/skills/parser.js');
166
168
  const extracted = extractFrontmatter(content);
167
169
  if (!extracted) {
168
170
  throw new Error('Invalid SKILL.md: no valid YAML frontmatter found.');
@@ -28,7 +28,15 @@ import { randomBytes } from 'crypto'
28
28
  import { readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, statSync, renameSync, realpathSync, chmodSync, existsSync } from 'fs'
29
29
  import { homedir } from 'os'
30
30
  import { join, extname, sep } from 'path'
31
+ import { execFileSync } from 'child_process'
31
32
  import { createWorkingStateManager, drainOrphanForwards } from './typing'
33
+ // Import compiled dist/, not raw src/ — src/ is not published (files: ["mcp/"]),
34
+ // so a src/ import crashes this bun-run receiver on installed packages (the bug
35
+ // that silenced every bot on systemd installs). Enforced by
36
+ // tests/unit/mcp-no-src-imports.test.ts.
37
+ import { formatTurnIncident, type TurnIncident } from '../../../dist/agent/turn-trace.js'
38
+ import { createIncidentStore } from '../../../dist/agent/incident-store.js'
39
+ import type { RecoveryOutcome } from '../../../dist/agent/incident.js'
32
40
  import { initDedupDir, isDuplicate as _isDuplicate, pruneDedup as _pruneDedup } from './dedup'
33
41
  import { hasMarkdown, toTelegramHtml, migrateAccess } from './pure'
34
42
 
@@ -104,6 +112,70 @@ let botUsername = ''
104
112
 
105
113
  const TYPING_DIR = join(STATE_DIR, 'typing')
106
114
 
115
+ // ─── Incident store (Epic #195, Phase 2) ────────────────────────────────────
116
+ // Persist turn-trace stalls as scrubbed on-disk bundles, deduped by fingerprint
117
+ // (stage + failure class + CLI version) with escalation. The store owns all
118
+ // disk IO; the pure decision logic lives in src/agent/incident.ts.
119
+ const INCIDENTS_DIR = join(homedir(), '.claude-gateway', 'incidents')
120
+
121
+ // The Claude CLI version is part of the fingerprint. Resolve it once and cache
122
+ // it — a new version is a new fingerprint, and the receiver is restarted when
123
+ // the CLI is updated, so a per-process cache is safe.
124
+ let cachedCliVersion: string | null = null
125
+ function getCliVersion(): string {
126
+ if (cachedCliVersion !== null) return cachedCliVersion
127
+ try {
128
+ const out = execFileSync('claude', ['--version'], {
129
+ encoding: 'utf8',
130
+ timeout: 5000,
131
+ stdio: ['ignore', 'pipe', 'ignore'],
132
+ })
133
+ const m = out.trim().match(/^(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/)
134
+ cachedCliVersion = m ? m[1] : 'unknown'
135
+ } catch {
136
+ cachedCliVersion = 'unknown'
137
+ }
138
+ return cachedCliVersion
139
+ }
140
+
141
+ // Gateway version, best-effort, for manifest context only (not fingerprinted).
142
+ function readGatewayVersion(): string {
143
+ for (const rel of ['../../../package.json', '../../package.json']) {
144
+ try {
145
+ const raw = readFileSync(join(__dirname, rel), 'utf8')
146
+ const v = (JSON.parse(raw) as { version?: string }).version
147
+ if (typeof v === 'string' && v.length > 0) return v
148
+ } catch {
149
+ // try next candidate
150
+ }
151
+ }
152
+ return 'unknown'
153
+ }
154
+
155
+ const incidentStore = createIncidentStore({
156
+ dir: INCIDENTS_DIR,
157
+ fs: { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, rmSync },
158
+ now: () => Date.now(),
159
+ channel: 'telegram',
160
+ gatewayVersion: readGatewayVersion(),
161
+ getCliVersion,
162
+ })
163
+
164
+ // Prune old bundles at startup and daily thereafter (retention is enforced in
165
+ // the store; this just triggers it periodically).
166
+ try {
167
+ incidentStore.prune()
168
+ } catch {
169
+ // Non-fatal: a prune failure must not stop the receiver from serving.
170
+ }
171
+ setInterval(() => {
172
+ try {
173
+ incidentStore.prune()
174
+ } catch {
175
+ /* non-fatal */
176
+ }
177
+ }, 24 * 60 * 60 * 1000).unref()
178
+
107
179
  const typingManager = createWorkingStateManager(
108
180
  TYPING_DIR,
109
181
  {
@@ -117,8 +189,99 @@ const typingManager = createWorkingStateManager(
117
189
  ]),
118
190
  },
119
191
  { mkdirSync, writeFileSync, existsSync, rmSync, readFileSync, statSync },
192
+ // Turn-trace watchdog sink (Epic #195, Phase 2): persist a scrubbed incident
193
+ // bundle, then notify the affected chat only when the escalation rules say so
194
+ // (quiet on the first stall, louder once a fingerprint repeats). Any failure
195
+ // here is swallowed — incident bookkeeping must never break the live channel.
196
+ (incident, evidence) => {
197
+ process.stderr.write(`telegram channel: ${formatTurnIncident(incident)}\n`)
198
+ try {
199
+ const result = incidentStore.record(incident, evidence)
200
+ if (result.escalation.notify) {
201
+ void notifyIncident(incident.chatId, result)
202
+ incidentStore.markNotified(result.id, result.escalation.level)
203
+ }
204
+ // Cross-process recovery bridge (Epic #195, Phase 3b): for stages whose
205
+ // recovery lives in the runner (session/CLI), ask it to attempt recovery
206
+ // and persist the outcome. Fire-and-forget — recovery must never block the
207
+ // channel, and it is a no-op unless the operator enabled autoRecover.
208
+ void attemptRecover(incident, result.id)
209
+ } catch (err) {
210
+ process.stderr.write(
211
+ `telegram channel: incident persist failed: ${String(err)}\n`,
212
+ )
213
+ }
214
+ },
120
215
  )
121
216
 
217
+ // Stages whose recovery must run in the runner process (live session control):
218
+ // session injection, CLI startup, and no-output progress wedges. Inbound
219
+ // (channel ingress) and delivery/dispatch (transport) are receiver-side and are
220
+ // not bridged here.
221
+ const RUNNER_RECOVERABLE_STAGES: ReadonlySet<string> = new Set(['inject', 'startup', 'progress'])
222
+
223
+ // A stable-per-turn key for the runner's intervention budget. Uses the stalled
224
+ // stage's start second (at − sinceMs), so repeated detections of the SAME wedged
225
+ // turn share a budget while a genuinely new turn gets a fresh one.
226
+ function recoveryTurnKey(incident: TurnIncident): string {
227
+ const startSec = Math.round((incident.at - incident.sinceMs) / 1000)
228
+ return `${incident.chatId}:${startSec}`
229
+ }
230
+
231
+ // Ask the runner to attempt recovery for a session/CLI-stage stall, then persist
232
+ // the returned outcome to the incident bundle. Best-effort and non-blocking.
233
+ async function attemptRecover(incident: TurnIncident, incidentId: string): Promise<void> {
234
+ if (!CALLBACK_URL_BASE) return
235
+ if (!RUNNER_RECOVERABLE_STAGES.has(incident.stage)) return
236
+ try {
237
+ const res = await fetch(CALLBACK_URL_BASE + '/recover', {
238
+ method: 'POST',
239
+ headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify({
241
+ incidentId,
242
+ chatId: incident.chatId,
243
+ stage: incident.stage,
244
+ failureClass: incident.failureClass,
245
+ turnKey: recoveryTurnKey(incident),
246
+ }),
247
+ })
248
+ if (!res.ok) return
249
+ const data = (await res.json()) as { ok?: boolean; outcome?: RecoveryOutcome }
250
+ if (data.ok && data.outcome) {
251
+ incidentStore.appendRecovery(incidentId, data.outcome)
252
+ }
253
+ } catch (err) {
254
+ process.stderr.write(`telegram channel: recover POST failed: ${String(err)}\n`)
255
+ }
256
+ }
257
+
258
+ // Short, content-free notice for a stalled turn. No message text or chat id is
259
+ // echoed back — only the pipeline stage and (on repeat) the occurrence count.
260
+ async function notifyIncident(
261
+ chatId: string,
262
+ result: { escalation: { level: string }; occurrences: number; manifest: { stage: string } },
263
+ ): Promise<void> {
264
+ const stage = result.manifest.stage
265
+ let text: string
266
+ if (result.escalation.level === 'investigate') {
267
+ text =
268
+ `⚠️ Recurring stall detected at the "${stage}" stage ` +
269
+ `(${result.occurrences}× recently). This looks like a persistent issue — ` +
270
+ `it has been logged for investigation.`
271
+ } else {
272
+ // Neutral wording: automatic recovery only runs when the operator has
273
+ // enabled gateway.selfHealing.autoRecover (off by default), and the receiver
274
+ // cannot see that runner-side flag — so do not promise it here.
275
+ text =
276
+ `⚠️ A turn stalled at the "${stage}" stage and has been logged.`
277
+ }
278
+ try {
279
+ await bot.api.sendMessage(chatId, text)
280
+ } catch {
281
+ // Best-effort notify; the incident is already persisted regardless.
282
+ }
283
+ }
284
+
122
285
  type PendingEntry = {
123
286
  senderId: string
124
287
  chatId: string