@dmsdc-ai/aigentry-telepty 0.6.11 → 0.6.12

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
4
4
 
5
+ ## 0.6.12 — 2026-07-07
6
+
7
+ ### Fixed
8
+ - **#715** `read-screen` leaked modern escape sequences (kitty keyboard protocol, DECSCUSR, colon sub-parameter SGR, DECRQM) as literal text — codex/claude screens showed `0 q`/`<u>1u`/`4:2m` garbage. The screen ANSI stripper now consumes the full ECMA-48 CSI grammar (params `0x30–0x3F`, intermediates `0x20–0x2F`, final `0x40–0x7E`), extracted to `src/screen-ansi.js` with a regression suite.
9
+ - **#716** `inject --submit` into codex never registered: codex's composer paste-burst detection swallowed a CR coalesced with the injected text. Paste-capable CLIs (detected via `ESC[?2004h`) now receive the text wrapped in bracketed paste (`ESC[200~ … ESC[201~`) with the submit CR written separately outside the envelope — submission is timing-independent. Non-paste CLIs are byte-identical. Live-validated on codex 0.142.5.
10
+ - **#713 (partial)** The same ECMA-48 gap in `session-state.js` / `prompt-symbol-registry.js` broke prompt/consumption detection for claude v2.1.198 (which emits kitty-protocol sequences every render). Both matchers aligned to the corrected CSI form. Note: fresh claude v2.1.198 sessions still gate on bridge `promptReady` prompt-symbol detection over the raw stream, which the new absolute-positioning composer defeats — full fix tracked separately.
11
+
12
+ ### Rollout
13
+ - #715/#716 are daemon-side (daemon restart only). #713's bridge-side detection improvement additionally applies to newly started `telepty allow` bridges.
14
+
5
15
  ## [Unreleased]
6
16
 
7
17
  ## [0.6.11] - 2026-07-05
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  **Connect any terminal to any terminal, any machine.**
4
4
 
5
+ ![telepty demo — inject a prompt into a live AI CLI session and read its screen](docs/demo.gif)
6
+
5
7
  telepty is a PTY orchestration daemon and session bridge for AI CLI workflows. It lets you spawn, attach to, and inject commands into terminal sessions — locally or across machines via Tailscale.
6
8
 
7
9
  Built for AI CLI workflows (Claude Code, Codex, Gemini CLI), but works with any interactive terminal program.
@@ -297,6 +299,21 @@ npm test # 70 tests (node:test)
297
299
  npm run test:watch # Watch mode
298
300
  ```
299
301
 
302
+ ## Ecosystem
303
+
304
+ telepty runs **standalone** — it needs none of the other aigentry modules and installs with the single command above. It is also the transport layer of the broader **aigentry** ecosystem, whose modules are each independently published and independently useful:
305
+
306
+ | Module | Package | Version | Role | Maturity |
307
+ | --- | --- | --- | --- | --- |
308
+ | **telepty** | `@dmsdc-ai/aigentry-telepty` | 0.6.12 | Cross-terminal / cross-machine prompt transport (PTY daemon) | Shipping |
309
+ | **brain** | `@dmsdc-ai/aigentry-brain` | 0.2.8 | Persistent cross-session memory (MCP server) | Early |
310
+ | **deliberation** | `@dmsdc-ai/aigentry-deliberation` | 0.0.47 | Multi-AI structured debate + synthesis (MCP server) | Early |
311
+ | **devkit** | `@dmsdc-ai/aigentry-devkit` | 0.0.22 | Installer/scaffold for the AI dev environment | Early |
312
+ | **aterm** | `@dmsdc-ai/aterm` | 0.2.14 | Terminal launcher with native session IPC | Early |
313
+ | **orchestrator** | *(unpublished)* | — | Control tower that drives sessions via telepty | Internal |
314
+
315
+ > Licenses: all MIT except `@dmsdc-ai/aterm` (UNLICENSED).
316
+
300
317
  ## License
301
318
 
302
319
  MIT
package/README.tmpl.md ADDED
@@ -0,0 +1,310 @@
1
+ # telepty
2
+
3
+ **Connect any terminal to any terminal, any machine.**
4
+
5
+ ![telepty demo — inject a prompt into a live AI CLI session and read its screen](docs/demo.gif)
6
+
7
+ telepty is a PTY orchestration daemon and session bridge for AI CLI workflows. It lets you spawn, attach to, and inject commands into terminal sessions — locally or across machines via Tailscale.
8
+
9
+ Built for AI CLI workflows (Claude Code, Codex, Gemini CLI), but works with any interactive terminal program.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ # macOS / Linux
15
+ curl -fsSL https://raw.githubusercontent.com/dmsdc-ai/aigentry-telepty/main/install.sh | bash
16
+
17
+ # Windows (PowerShell as Admin)
18
+ iwr -useb https://raw.githubusercontent.com/dmsdc-ai/aigentry-telepty/main/install.ps1 | iex
19
+
20
+ # Or via npm
21
+ npm install -g @dmsdc-ai/aigentry-telepty
22
+ ```
23
+
24
+ The installer sets up telepty as a background service (`launchd` on macOS, `systemd` on Linux, detached process on Windows).
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # 1. Start the daemon
30
+ telepty daemon
31
+
32
+ # 2. Wrap an existing CLI session for remote control
33
+ telepty allow --id my-session claude
34
+
35
+ # 3. List active sessions (local + Tailnet)
36
+ telepty list
37
+
38
+ # 4. Inject a prompt into a session
39
+ telepty inject my-session "explain this codebase"
40
+
41
+ # 5. Attach to a session interactively
42
+ telepty attach my-session
43
+
44
+ # 6. Broadcast to all sessions
45
+ telepty broadcast "status report"
46
+ ```
47
+
48
+ ## What telepty is — and what it is not
49
+
50
+ telepty is a **PTY orchestration daemon for AI CLI workflows**.
51
+ It is **not** a terminal multiplexer and does not replace tmux.
52
+
53
+ > **tmux is better at being a terminal. telepty is better at letting
54
+ > software operate many terminals.**
55
+
56
+ tmux owns terminal *fidelity*: panes, windows, full VT emulation, scrollback,
57
+ copy-mode, capture-pane, local Unix-socket operation, zero runtime deps.
58
+ telepty owns *automation*: HTTP/WS APIs, authenticated remote access,
59
+ readiness-aware inject/submit, event streams, cross-machine session control.
60
+
61
+ ## telepty vs tmux
62
+
63
+ | Area | tmux | telepty |
64
+ |---|---|---|
65
+ | Core layer | terminal multiplexer + emulator | PTY orchestration daemon |
66
+ | Primary user | a human at a keyboard | software / an orchestrator |
67
+ | Terminal fidelity | full VT/grid/scrollback/copy-mode | output stream + heuristic state |
68
+ | IPC | local Unix socket | HTTP/WS/REST daemon (:3848) |
69
+ | Input model | open-loop `send-keys` | readiness-gated inject/submit |
70
+ | Multi-session fan-out | `synchronize-panes` (1 window, 1 host) | broadcast/multicast (cross-machine) |
71
+ | Remote | via SSH | native daemon HTTP, no sshd |
72
+ | Dependencies | zero runtime deps (C) | Node daemon + deps |
73
+
74
+ ## When to use which
75
+ - **Use tmux** for panes, scrollback, copy-mode, capture-pane, and local
76
+ human terminal work — telepty does none of this and doesn't try to.
77
+ - **Use telepty** when software needs to spawn, inspect, inject into, and
78
+ track many AI-CLI sessions over an API — across machines.
79
+
80
+ ## Limitations (honest)
81
+ - No terminal emulation: no cell grid, cursor model, or copy-mode. Screen
82
+ reads are buffered bytes + heuristic state, not a ground-truth screen.
83
+ - Requires a background daemon and a network port (:3848, auth-gated).
84
+
85
+ ## Core Commands
86
+
87
+ | Command | Description |
88
+ |---------|-------------|
89
+ | `telepty daemon` | Start the background daemon (port 3848) |
90
+ | `telepty allow --id <name> <cmd>` | Wrap a CLI for inject control |
91
+ | `telepty spawn --id <name> <cmd>` | Spawn a new background session |
92
+ | `telepty list [--json]` | List sessions across all discovered hosts |
93
+ | `telepty attach [id[@host]]` | Attach to a session (interactive picker if no ID) |
94
+ | `telepty inject <id[@host]> "text"` | Inject text into a session |
95
+ | `telepty inject --submit <id> "text"` | Inject text and press Enter (render-gated, retries once on safe gate-timeout) |
96
+ | `telepty inject --submit --submit-force <id> "text"` | As above, but bypass the gate (skip Layer 1/3 detection — opt-in escape hatch) |
97
+ | `telepty inject --submit --submit-retry N <id> "text"` | Override retry count [0–3] on safe 504 (default 1) |
98
+ | `telepty enter <id[@host]>` | Send Enter/Return to a session |
99
+ | `telepty multicast <id1,id2> "text"` | Inject into multiple sessions |
100
+ | `telepty broadcast "text"` | Inject into ALL sessions |
101
+ | `telepty rename <old> <new>` | Rename a session |
102
+ | `telepty read-screen <id> [--lines N]` | Read session screen buffer |
103
+ | `telepty reply "text"` | Reply to the last injector |
104
+ | `telepty monitor` | Real-time event billboard |
105
+ | `telepty listen` | Stream event bus as JSON |
106
+ | `telepty tui` | Full TUI dashboard |
107
+ | `telepty layout [grid\|tall\|stack]` | Arrange kitty windows |
108
+ | `telepty update` | Update to latest version |
109
+
110
+ ## Environment variables
111
+
112
+ | Variable | Values | Default | Description |
113
+ |----------|--------|---------|-------------|
114
+ | `TELEPTY_SUBMIT_FORCE_DEFAULT` | `1`, `true`, `yes`, `on` to enable; unset, `0`, or `off` to disable | unset | Makes `telepty inject --submit <id> "text"` behave as if `--submit-force` was passed. |
115
+
116
+ `TELEPTY_SUBMIT_FORCE_DEFAULT=1` is for orchestrators and automation that
117
+ already know their targets are real, initialized REPLs. It avoids the transient
118
+ 504 `bootstrap_not_ready` path where injected text lands in the target input box
119
+ but the render-gated submit refuses to press Enter while the target session is in
120
+ a temporary working state.
121
+
122
+ This bypasses the safety gate that protects sessions still booting. Set it only
123
+ when you understand that trade-off. Use `--no-submit-force` on a specific
124
+ `telepty inject --submit` call to restore the gated behavior even when the
125
+ environment default is enabled.
126
+
127
+ ## Cross-Machine Sessions
128
+
129
+ telepty auto-discovers sessions across your Tailnet. All commands (`list`, `attach`, `inject`, `rename`, `multicast`, `broadcast`) work seamlessly across machines.
130
+
131
+ ### Zero-config on Tailscale (auto bind + auto trust)
132
+
133
+ On a host that is on a **Tailscale tailnet**, a fresh install is cross-machine-ready with
134
+ **no manual env**. At startup the daemon detects its tailnet interface (a `100.64.0.0/10`
135
+ address) and:
136
+
137
+ - **binds :3848 to the tailnet IP only** (plus loopback) — LAN/public interfaces stay
138
+ closed, so the control API is reachable **only from your Tailnet**, never the flat LAN;
139
+ - **trusts tailnet peers automatically** — Tailscale's own ACLs already gate who is on the
140
+ tailnet, so a tailnet peer needs no token or manual allowlist.
141
+
142
+ > **Trust boundary:** on a Tailscale host, telepty auto-exposes :3848 to your **entire
143
+ > tailnet** (every peer Tailscale's ACLs let onto it). If you share your tailnet with
144
+ > machines you don't fully trust, set `TELEPTY_PEER_ALLOWLIST` to restrict to specific
145
+ > peers/CIDRs, or `TELEPTY_NO_TAILNET_AUTO=1` to stay loopback-only.
146
+
147
+ **Zero-config cross-machine is Tailscale-specific.** On a non-Tailscale host (plain LAN,
148
+ other mesh VPNs) the daemon stays **loopback-only** (the safe default) — set `TELEPTY_BIND`
149
+ + `TELEPTY_PEER_ALLOWLIST` manually to expose it. Manual `TELEPTY_BIND` / `HOST` /
150
+ `TELEPTY_PEER_ALLOWLIST` always win over auto-detect.
151
+
152
+ **Windows:** Defender Firewall blocks inbound on the tailnet interface by default. On the
153
+ auto path the daemon adds the inbound allow-rule automatically when run elevated, otherwise
154
+ it prints the exact one-time `netsh` command in the startup banner.
155
+
156
+ Addressing stays **IP-free**: use MagicDNS / hostnames with `<id>@<host>` (below) — you
157
+ never need to type a `100.x.y.z` address.
158
+
159
+ ### `<id>@<host>` syntax
160
+
161
+ To target a specific host (when the same session ID exists on multiple hosts,
162
+ or when there is no Tailnet auto-discovery), append `@<host>` to the session
163
+ ID. `<host>` can be a hostname, LAN IP, or Tailnet name.
164
+
165
+ ```bash
166
+ # Hostname / Tailnet name
167
+ telepty inject my-session@macbook "hello"
168
+ telepty attach worker@server-01
169
+
170
+ # LAN IP — useful when no Tailnet is configured
171
+ telepty inject orchestrator-claude@172.28.4.165 "ping"
172
+ telepty read-screen build-runner@10.0.0.42 --lines 50
173
+ ```
174
+
175
+ **Requirements**:
176
+ - The remote daemon must be reachable on port **3848** from the calling host
177
+ (LAN routing, firewall rules, or Tailscale).
178
+ - No SSH or `sshd` is required on either side — the call hits the remote
179
+ daemon's HTTP API directly. This is the recommended path for laptop
180
+ daemons that don't run sshd.
181
+ - The `@<host>` qualifier works for `inject`, `attach`, `read-screen`,
182
+ `enter`, `multicast`, and `rename`.
183
+
184
+ ## How It Works
185
+
186
+ ```
187
+ CLI (telepty) ──> HTTP/WS ──> Daemon (:3848)
188
+ ├── Session WebSocket (/api/sessions/:id)
189
+ ├── Event Bus WebSocket (/api/bus)
190
+ └── REST API (/api/sessions/*)
191
+ ```
192
+
193
+ - **`allow`** wraps a CLI process in a PTY bridge, enabling remote inject
194
+ - **`inject`** delivers text via the fastest available path: kitty terminal API, WebSocket, or UDS (Unix Domain Socket for embedded integrations)
195
+ - **`submit`** is handled separately from text injection for reliability across all AI CLIs
196
+
197
+ ## `[context-ref]` Protocol — long payloads via shared file
198
+
199
+ When a sender uses `telepty inject --ref <file> <target> "<message>"`, telepty
200
+ stores the payload in a shared file under `~/.telepty/shared/<sha256>.md` and
201
+ injects only a short pointer prompt of the form:
202
+
203
+ ```
204
+ [context-ref] Read ~/.telepty/shared/<sha256>.md and use it as the source of truth for this task.
205
+ <inline message>
206
+ ```
207
+
208
+ This avoids prompt rot in the receiving session (and in the orchestrator's
209
+ window when the reply is small).
210
+
211
+ ### Receiver contract
212
+
213
+ The receiving AI session is expected to:
214
+ 1. Detect the `[context-ref]` prefix on the first line.
215
+ 2. Read the file at the absolute path.
216
+ 3. Treat the file contents as the **authoritative payload** for the task — the
217
+ inline message is supplementary (topic / hint), not the source of truth.
218
+
219
+ ### Storage location
220
+
221
+ - File path: `~/.telepty/shared/<sha256>.md` (sha256 of payload body)
222
+ - Created with mode `0600`; readable only by the local user
223
+ - Persists across sessions; not garbage-collected automatically (run
224
+ `telepty clean --shared` to prune)
225
+
226
+ ### When to use `--ref`
227
+
228
+ - Payload exceeds ~1KB or contains structured content (code, logs, tables).
229
+ - You want the receiver to load the payload deterministically rather than
230
+ paraphrase it from the inject prompt.
231
+ - You're orchestrating a multi-hop conversation where the orchestrator should
232
+ not see the full payload in its own context window.
233
+
234
+ ### Integration scope
235
+
236
+ Per-agent receiver integrations (auto-loading the file via Claude Code
237
+ `UserPromptSubmit` hooks, Codex `AGENTS.md` directives, etc.) are **out of
238
+ scope for telepty core** — they live in the agent's own configuration.
239
+ Per-CLI hook installation lives in devkit: run `aigentry scaffold
240
+ install-hooks {claude|codex|gemini}` after installing
241
+ `@dmsdc-ai/aigentry-devkit`. (Older drafts proposed a receiver-side
242
+ `telepty install` subcommand for this; that direction is rejected per ADR
243
+ 2026-05-05-telepty-devkit-boundary §3.1.2 / §3.4 row 2.)
244
+
245
+ ## Inject Delivery Paths
246
+
247
+ | Priority | Method | When |
248
+ |----------|--------|------|
249
+ | 1 | `kitty @ send-text` | Terminal supports kitty protocol |
250
+ | 2 | UDS (Unix Domain Socket) | Embedded IPC sessions (e.g. aterm) |
251
+ | 3 | WebSocket PTY write | Wrapped sessions via allow-bridge |
252
+
253
+ ## AI CLI Integration
254
+
255
+ telepty works as a session bridge for AI CLIs. Use `allow` to wrap any CLI:
256
+
257
+ ```bash
258
+ # Claude Code
259
+ telepty allow --id claude-main claude
260
+
261
+ # Codex
262
+ telepty allow --id codex-main codex
263
+
264
+ # Gemini CLI
265
+ telepty allow --id gemini-main gemini
266
+ ```
267
+
268
+ Then inject prompts, read output, or attach from anywhere:
269
+
270
+ ```bash
271
+ telepty inject claude-main "refactor the auth module"
272
+ telepty read-screen claude-main --lines 50
273
+ telepty attach claude-main
274
+ ```
275
+
276
+ ## Deliberation (Multi-Session Discussion)
277
+
278
+ Coordinate structured discussions across multiple AI sessions:
279
+
280
+ ```bash
281
+ telepty deliberate --topic "API design for v2" --sessions claude-1,claude-2,codex-1
282
+ telepty deliberate status
283
+ telepty deliberate end <thread_id>
284
+ ```
285
+
286
+ ## Skill Installation
287
+
288
+ telepty ships with packaged skills for Claude Code, Codex, and Gemini CLI. Run the interactive installer:
289
+
290
+ ```bash
291
+ telepty
292
+ # Choose "Install telepty skills"
293
+ ```
294
+
295
+ ## Testing
296
+
297
+ ```bash
298
+ npm test # 70 tests (node:test)
299
+ npm run test:watch # Watch mode
300
+ ```
301
+
302
+ ## Ecosystem
303
+
304
+ telepty runs **standalone** — it needs none of the other aigentry modules and installs with the single command above. It is also the transport layer of the broader **aigentry** ecosystem, whose modules are each independently published and independently useful:
305
+
306
+ {{ecosystem_table}}
307
+
308
+ ## License
309
+
310
+ MIT
package/daemon.js CHANGED
@@ -19,6 +19,7 @@ const { UnixSocketNotifier } = require('./src/mailbox/notifier');
19
19
  const { SessionStateManager, STATE_DISPLAY, stripAnsi: stripAnsiState } = require('./session-state');
20
20
  const { classifyReportPrompt, buildAutoSummary } = require('./src/report-enforcement');
21
21
  const submitGate = require('./src/submit-gate');
22
+ const { stripAnsiForScreen } = require('./src/screen-ansi'); // #715: read-screen ANSI/VT stripper
22
23
  const { sampleChildCpuSeconds } = require('./src/child-cpu'); // #52: quiet-thinking CPU recheck
23
24
  const readyRegistry = require('./src/prompt-symbol-registry');
24
25
  const lifecycle = require('./src/lifecycle');
@@ -1819,6 +1820,22 @@ function isTerminalGateFailure(gateResult) {
1819
1820
  );
1820
1821
  }
1821
1822
 
1823
+ // #716: bracketed-paste submit envelope. codex/claude composers swallow a submit CR
1824
+ // that arrives coalesced with the injected text burst (paste-burst / coalesced read).
1825
+ // For a session the CLI marked paste-capable (it emitted ESC[?2004h — tracked in
1826
+ // appendToOutputRing), wrap the injected TEXT in bracketed-paste markers so the burst
1827
+ // is an explicit, delimited paste; the submit CR is written SEPARATELY and OUTSIDE
1828
+ // this envelope, so it is an unambiguous keystroke regardless of inter-write timing.
1829
+ // Non-paste-capable sessions (legacy claude/gemini/agy that never advertised ?2004h)
1830
+ // are byte-identical. An empty body is never wrapped.
1831
+ const BRACKETED_PASTE_START = '\x1b[200~';
1832
+ const BRACKETED_PASTE_END = '\x1b[201~';
1833
+ function maybeBracketedPaste(text, session) {
1834
+ if (!text) return text;
1835
+ if (!session || !session.bracketedPasteCapable) return text;
1836
+ return BRACKETED_PASTE_START + text + BRACKETED_PASTE_END;
1837
+ }
1838
+
1822
1839
  async function deliverInjectionToSession(id, session, prompt, options = {}) {
1823
1840
  const now = Date.now();
1824
1841
  if (!options.bypassBootstrapQueue && shouldQueueBootstrapOperation(session)) {
@@ -1869,12 +1886,17 @@ async function deliverInjectionToSession(id, session, prompt, options = {}) {
1869
1886
  origin: options.origin
1870
1887
  }).payload;
1871
1888
 
1889
+ // #716: wrap the delivered text in bracketed paste for paste-capable CLIs so the
1890
+ // deferred/gated submit CR (written separately, outside the 200~/201~ envelope)
1891
+ // reliably fires instead of being swallowed into the paste burst. No-op otherwise.
1892
+ const deliveredBody = maybeBracketedPaste(deliveredPrompt, session);
1893
+
1872
1894
  try {
1873
1895
  const ack = mailbox.enqueue({
1874
1896
  msg_id: msgId,
1875
1897
  from,
1876
1898
  to: id,
1877
- payload: deliveredPrompt,
1899
+ payload: deliveredBody,
1878
1900
  created_at: Math.floor(now / 1000),
1879
1901
  attempt: 0,
1880
1902
  });
@@ -1915,7 +1937,7 @@ async function deliverInjectionToSession(id, session, prompt, options = {}) {
1915
1937
  } catch (err) {
1916
1938
  console.error(`[MAILBOX] Enqueue failed for ${id}: ${err.message}`);
1917
1939
  // Fallback: direct delivery (backward compat during migration)
1918
- const textResult = await writeDataToSession(id, session, deliveredPrompt);
1940
+ const textResult = await writeDataToSession(id, session, deliveredBody);
1919
1941
  if (!textResult.success) return textResult;
1920
1942
 
1921
1943
  if (!options.noEnter && session.type !== 'aterm') {
@@ -1942,6 +1964,12 @@ async function deliverInjectionToSession(id, session, prompt, options = {}) {
1942
1964
 
1943
1965
  function appendToOutputRing(session, data) {
1944
1966
  if (!session.outputRing) session.outputRing = [];
1967
+ // #716: track bracketed-paste capability from the CLI's own mode-set output so
1968
+ // injects are wrapped (maybeBracketedPaste) only for paste-capable composers —
1969
+ // codex/claude emit ESC[?2004h. Last h/l in the chunk wins.
1970
+ const bpOn = data.lastIndexOf('\x1b[?2004h');
1971
+ const bpOff = data.lastIndexOf('\x1b[?2004l');
1972
+ if (bpOn !== -1 || bpOff !== -1) session.bracketedPasteCapable = bpOn > bpOff;
1945
1973
  // #52: monotonic byte counter — the inject-time watermark that scopes echo-evidence
1946
1974
  // matching to frames appended AFTER the inject (survives ring trimming below).
1947
1975
  session.outputRingTotalBytes = (session.outputRingTotalBytes || 0) + data.length;
@@ -3561,30 +3589,7 @@ app.get('/api/sessions/:id/screen', (req, res) => {
3561
3589
  // Join all buffered output
3562
3590
  const fullOutput = session.outputRing.join('');
3563
3591
 
3564
- // Strip ANSI escape sequences for clean text
3565
- function stripAnsi(str) {
3566
- return str
3567
- // Replace cursor-forward (ESC[NC, ESC[C) with N spaces to preserve whitespace
3568
- .replace(/\[(\d*)C/g, (_, n) => ' '.repeat(Number(n) || 1))
3569
- // CSI sequences: ESC [ ? (optional) params final_byte
3570
- .replace(/\[\??[0-9;]*[a-zA-Z@`]/g, '')
3571
- // OSC sequences: ESC ] ... BEL
3572
- .replace(/\][^]*/g, '')
3573
- // OSC sequences: ESC ] ... ST (ESC \)
3574
- .replace(/\][^]*\\/g, '')
3575
- // Character set selection: ESC ( / ) + charset
3576
- .replace(/[()][AB012]/g, '')
3577
- // Keypad and other 2-char ESC sequences
3578
- .replace(/[>=<78DMEHcNOZ~}|]/g, '')
3579
- // DCS / PM / APC sequences
3580
- .replace(/^_][^]*\\/g, '')
3581
- // Any remaining bare ESC + single char
3582
- .replace(/./g, '')
3583
- // Carriage returns
3584
- .replace(/\r/g, '');
3585
- }
3586
-
3587
- const cleaned = raw ? fullOutput : stripAnsi(fullOutput);
3592
+ const cleaned = raw ? fullOutput : stripAnsiForScreen(fullOutput);
3588
3593
 
3589
3594
  // Take last N lines
3590
3595
  const allLines = cleaned.split('\n');
@@ -4611,6 +4616,8 @@ module.exports = {
4611
4616
  loadNodeBrokerConfig, // node-mode: resolve broker.json / env config (or null)
4612
4617
  startNodeBrokerClient, // node-mode: start createBrokerClient (default-OFF; in-process deliver)
4613
4618
  deliverInjectionToSession, // §4.3: the in-process delivery wired into the broker-client
4619
+ appendToOutputRing, // #716: seam that tracks bracketed-paste capability (?2004h)
4620
+ maybeBracketedPaste, // #716: capability-gated bracketed-paste wrap of injected text
4614
4621
  resolveBindHost, // telepty#50 + #672: pure bind-address policy (loopback default, env opt-in, tailnet auto)
4615
4622
  formatBindHint, // telepty#50 + #672: startup bind/exposure banner line
4616
4623
  isTailnetAuto, // #672: pure predicate — is the zero-config tailnet path active
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
@@ -35,11 +35,12 @@
35
35
  "CHANGELOG.md"
36
36
  ],
37
37
  "scripts": {
38
+ "prepublishOnly": "node scripts/gen-readme.mjs",
38
39
  "postinstall": "node scripts/postinstall.js",
39
40
  "preuninstall": "node scripts/preuninstall.js",
40
- "test": "node --require ./test-support/setup-env.js --test test/auth.test.js test/http-auth.test.js test/tailnet-autobind.test.js test/win-firewall.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/dupid-flap-kill-stick-56.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/submit-gate-restore-register-678.test.js test/submit-busy-dispatch-694.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/daemon-lifecycle-55.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js test/inject-notfound-teardown-691.test.js && git diff --exit-code tests/snippet-protocol/v1/",
41
- "test:watch": "node --require ./test-support/setup-env.js --test --watch test/auth.test.js test/http-auth.test.js test/tailnet-autobind.test.js test/win-firewall.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/dupid-flap-kill-stick-56.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/submit-gate-restore-register-678.test.js test/submit-busy-dispatch-694.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/daemon-lifecycle-55.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js test/inject-notfound-teardown-691.test.js",
42
- "test:ci": "node --require ./test-support/setup-env.js --test --test-reporter=spec test/auth.test.js test/http-auth.test.js test/tailnet-autobind.test.js test/win-firewall.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/dupid-flap-kill-stick-56.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/submit-gate-restore-register-678.test.js test/submit-busy-dispatch-694.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/daemon-lifecycle-55.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js test/inject-notfound-teardown-691.test.js && git diff --exit-code tests/snippet-protocol/v1/",
41
+ "test": "node --require ./test-support/setup-env.js --test test/auth.test.js test/http-auth.test.js test/tailnet-autobind.test.js test/win-firewall.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/read-screen-ansi-715.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/dupid-flap-kill-stick-56.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/submit-gate-restore-register-678.test.js test/submit-busy-dispatch-694.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-bracketed-paste-716.test.js test/inject-consumption-evidence.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/daemon-lifecycle-55.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js test/inject-notfound-teardown-691.test.js && git diff --exit-code tests/snippet-protocol/v1/",
42
+ "test:watch": "node --require ./test-support/setup-env.js --test --watch test/auth.test.js test/http-auth.test.js test/tailnet-autobind.test.js test/win-firewall.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/read-screen-ansi-715.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/dupid-flap-kill-stick-56.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/submit-gate-restore-register-678.test.js test/submit-busy-dispatch-694.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-bracketed-paste-716.test.js test/inject-consumption-evidence.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/daemon-lifecycle-55.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js test/inject-notfound-teardown-691.test.js",
43
+ "test:ci": "node --require ./test-support/setup-env.js --test --test-reporter=spec test/auth.test.js test/http-auth.test.js test/tailnet-autobind.test.js test/win-firewall.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/read-screen-ansi-715.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/dupid-flap-kill-stick-56.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/submit-gate-restore-register-678.test.js test/submit-busy-dispatch-694.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-bracketed-paste-716.test.js test/inject-consumption-evidence.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/daemon-lifecycle-55.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js test/inject-notfound-teardown-691.test.js && git diff --exit-code tests/snippet-protocol/v1/",
43
44
  "typecheck": "tsc --noEmit",
44
45
  "regen-fixtures": "node scripts/regen-snippet-fixtures.js"
45
46
  },
package/session-state.js CHANGED
@@ -144,7 +144,10 @@ const ERROR_PATTERNS = [
144
144
  ];
145
145
 
146
146
  // ANSI escape stripper (preserves OSC 133 detection by running after OSC check)
147
- const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\x1b\[[\?]?[0-9;]*[hlm]/g;
147
+ // CSI branch = ECMA-48 CSI (see #715): params 0x30-0x3f (incl. < > = : ? used by
148
+ // kitty-keyboard/modifyOtherKeys), intermediates 0x20-0x2f, final 0x40-0x7e. The
149
+ // prior [0-9;] param class leaked claude v2.1.198's ESC[<u/ESC[>1u/ESC[>4;2m (#713).
150
+ const ANSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]|\x1b\][^\x07]*\x07|\x1b[()][AB012]/g;
148
151
 
149
152
  function stripAnsi(str) {
150
153
  return str.replace(ANSI_RE, '');
@@ -143,7 +143,10 @@ function isKnownAiCli(command) {
143
143
  return !!lookup(command);
144
144
  }
145
145
 
146
- const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b[()][AB012]|\x1b[>=<78DMEHcNOZ~}|]/g;
146
+ // CSI branch = ECMA-48 CSI (see #715): params 0x30-0x3f (incl. < > = : used by
147
+ // kitty-keyboard/modifyOtherKeys), intermediates 0x20-0x2f, final 0x40-0x7e. The
148
+ // prior [0-9;?] param class leaked claude v2.1.198's ESC[<u/ESC[>1u/ESC[>4;2m (#713).
149
+ const ANSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]|\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b[()][AB012]|\x1b[>=<78DMEHcNOZ~}|]/g;
147
150
 
148
151
  function stripAnsi(value) {
149
152
  return String(value == null ? '' : value).replace(ANSI_RE, '');
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ // #715 — telepty read-screen rendered codex/claude sessions as garbage. The /screen
4
+ // endpoint has no VT emulator: it concatenates the raw PTY output ring and strips ANSI
5
+ // by regex. The classic CSI matcher only handled `ESC [ params final`, so CSI sequences
6
+ // with intermediate bytes (SPACE, $), the < = > private markers, or : sub-parameters
7
+ // failed to match and the fallback leaked their tail as literal text. This is the
8
+ // screen-render ANSI stripper; kept pure + standalone so it is unit-testable without
9
+ // booting the daemon. (Parser-only fix; the submit/gate path is a separate concern.)
10
+ function stripAnsiForScreen(str) {
11
+ return str
12
+ // Replace cursor-forward (ESC[NC, ESC[C) with N spaces to preserve whitespace
13
+ .replace(/\[(\d*)C/g, (_, n) => ' '.repeat(Number(n) || 1))
14
+ // CSI (ECMA-48): ESC [ params(0x30-3F incl : ; < = > ?) intermediates(0x20-2F) final(0x40-7E).
15
+ // #715: the classic /\x1b[\??[0-9;]*.../ form dropped intermediates, the < = >
16
+ // private markers and colon sub-params, so a partial match leaked the tail as
17
+ // literal text (DECSCUSR ESC[0 q -> `0 q`, kitty ESC[>1u -> `>1u`, etc.).
18
+ .replace(/\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g, '')
19
+ // OSC sequences: ESC ] ... BEL
20
+ .replace(/\][^]*/g, '')
21
+ // OSC sequences: ESC ] ... ST (ESC \)
22
+ .replace(/\][^]*\\/g, '')
23
+ // Character set selection: ESC ( / ) + charset
24
+ .replace(/[()][AB012]/g, '')
25
+ // Keypad and other 2-char ESC sequences
26
+ .replace(/[>=<78DMEHcNOZ~}|]/g, '')
27
+ // DCS / PM / APC sequences
28
+ .replace(/^_][^]*\\/g, '')
29
+ // Any remaining bare ESC + single char
30
+ .replace(/./g, '')
31
+ // Carriage returns
32
+ .replace(/\r/g, '');
33
+ }
34
+
35
+ module.exports = { stripAnsiForScreen };