@dmsdc-ai/aigentry-telepty 0.3.3 → 0.3.5
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/AGENTS.md +23 -0
- package/CHANGELOG.md +110 -0
- package/README.md +67 -1
- package/cli.js +125 -39
- package/cross-machine.js +132 -0
- package/docs/reports/2026-05-05-issue-8-claude-review.md +194 -0
- package/docs/specs/2026-05-05-issue-8-telepty-init.md +477 -0
- package/host-spec.js +60 -0
- package/mcp-server/index.mjs +24 -3
- package/package.json +6 -5
- package/scripts/regen-snippet-fixtures.js +42 -0
- package/skill-installer.js +42 -6
- package/skills/telepty/SKILL.md +1 -1
- package/skills/telepty-allow/SKILL.md +1 -1
- package/skills/telepty-attach/SKILL.md +1 -1
- package/skills/telepty-broadcast/SKILL.md +1 -1
- package/skills/telepty-daemon/SKILL.md +1 -1
- package/skills/telepty-inject/SKILL.md +76 -4
- package/skills/telepty-list/SKILL.md +1 -1
- package/skills/telepty-listen/SKILL.md +1 -1
- package/skills/telepty-rename/SKILL.md +1 -1
- package/skills/telepty-session/SKILL.md +1 -1
- package/src/init/print-snippet.js +114 -0
- package/src/init/snippets/agents.md +15 -0
- package/src/init/snippets/claude.md +15 -0
- package/src/init/snippets/gemini.md +15 -0
- package/tests/snippet-protocol/v1/golden-agents.json +1 -0
- package/tests/snippet-protocol/v1/golden-agents.md +17 -0
- package/tests/snippet-protocol/v1/golden-all.json +3 -0
- package/tests/snippet-protocol/v1/golden-all.md +53 -0
- package/tests/snippet-protocol/v1/golden-claude.json +1 -0
- package/tests/snippet-protocol/v1/golden-claude.md +17 -0
- package/tests/snippet-protocol/v1/golden-gemini.json +1 -0
- package/tests/snippet-protocol/v1/golden-gemini.md +17 -0
package/AGENTS.md
CHANGED
|
@@ -72,3 +72,26 @@ telepty inject --ref --from aigentry-telepty-{cli} aigentry-orchestrator-claude
|
|
|
72
72
|
- **Evidence-Based**: 추측 금지. 데이터/로그/테스트 결과 기반 판단.
|
|
73
73
|
- **Fail Fast**: 에러 즉시 보고. 숨기지 않음.
|
|
74
74
|
- **Constitution**: ~/projects/aigentry/docs/CONSTITUTION.md 준수.
|
|
75
|
+
|
|
76
|
+
## Legacy exception — `skill-installer.js`
|
|
77
|
+
|
|
78
|
+
`skill-installer.js` is a **named legacy exception** grandfathered by
|
|
79
|
+
**ADR 2026-05-05-telepty-devkit-boundary §6.2.1**. It is the single
|
|
80
|
+
exception to the telepty/devkit boundary rule and is NOT precedent for
|
|
81
|
+
new placements.
|
|
82
|
+
|
|
83
|
+
- **Scope**: bugfixes, security patches, dependency upgrades only.
|
|
84
|
+
- **No new feature expansion**: net-new functionality (new CLI detection,
|
|
85
|
+
new skill types, new install paths, new flags) MUST land in
|
|
86
|
+
`@dmsdc-ai/aigentry-devkit`. At migration time devkit will introduce
|
|
87
|
+
`aigentry scaffold install-skills`.
|
|
88
|
+
- **Reviewer enforcement**: PR reviewers cite ADR 2026-05-05 §6.2.1 when
|
|
89
|
+
rejecting feature-expansion PRs against `skill-installer.js`.
|
|
90
|
+
- **Migration triggers** (per ADR §6.2.1): ≥2 PRs in 60 days attempting
|
|
91
|
+
net-new features; devkit feature requires functionality only in
|
|
92
|
+
`skill-installer.js`; breaking change to its interface.
|
|
93
|
+
|
|
94
|
+
Per-CLI hook installation, `CLAUDE.md`/`AGENTS.md`/`GEMINI.md` scaffolding,
|
|
95
|
+
project-file generation, and cross-cutting installable skills are
|
|
96
|
+
**devkit-owned** per ADR 2026-05-05 §3.3 — not telepty's responsibility.
|
|
97
|
+
See `@dmsdc-ai/aigentry-devkit` (`aigentry scaffold …`).
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,116 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.3.5] — 2026-05-05
|
|
6
|
+
|
|
7
|
+
### Added — `telepty init --print-snippet` (Issue #8)
|
|
8
|
+
|
|
9
|
+
New subcommand that emits the canonical telepty-baseline snippet to stdout for
|
|
10
|
+
graceful integration into per-CLI agent files. **Mechanism only** — telepty
|
|
11
|
+
emits the versioned snippet text; downstream tooling (`aigentry-devkit
|
|
12
|
+
scaffold --integrate-telepty`) owns idempotent insertion into
|
|
13
|
+
`~/CLAUDE.md` / `~/AGENTS.md` / `~/GEMINI.md`. Boundary contract per ADR
|
|
14
|
+
`2026-05-05-telepty-devkit-boundary` (commit `e4b072b`).
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
telepty init --print-snippet [--target {claude|agents|gemini|all}] [--format {markdown|json}]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **argv-only**: never consumes stdin (safe in scripted pipelines).
|
|
21
|
+
- **zero file I/O**: pure stdout emission; nothing read from or written to disk.
|
|
22
|
+
- **deterministic**: byte-identical output for a given (target, format) pair —
|
|
23
|
+
fixtures can be hashed for verification.
|
|
24
|
+
- **LF-only bodies**: no CRLF leakage on cross-platform consumers.
|
|
25
|
+
- **stderr clean**: success path emits no warnings.
|
|
26
|
+
|
|
27
|
+
Spec: `docs/specs/2026-05-05-issue-8-telepty-init.md` (commit `8d2dc94`).
|
|
28
|
+
Implementation: `f5c6bad`. Protocol SSOT: `aigentry-ssot/contracts/telepty-snippet-v1.md`
|
|
29
|
+
(commit `f4ff0cd`). 15 conformance fixtures shipped at `tests/snippet-protocol/v1/`
|
|
30
|
+
covering markdown envelopes (claude, agents, gemini, all), JSON records,
|
|
31
|
+
shell-hazard guards, deterministic LF output, default targeting, unsupported-target
|
|
32
|
+
rejection, internal-failure exit codes, stdin-pipe ignore, devkit-free invocation,
|
|
33
|
+
and the snippet golden fixtures themselves.
|
|
34
|
+
|
|
35
|
+
### Docs — G7/G8/G9 M0 audit gate closure (commit `d7b8b21`)
|
|
36
|
+
|
|
37
|
+
Per ADR `2026-05-05-telepty-devkit-boundary` §3.1.2 (devkit owns content
|
|
38
|
+
placement; telepty owns mechanism), three gates closed:
|
|
39
|
+
|
|
40
|
+
- **G7 — `README.md`**: removed reference to the rejected `telepty install
|
|
41
|
+
hooks` subcommand. Per ADR §3.1.2, that responsibility lives in devkit.
|
|
42
|
+
- **G8 — `AGENTS.md`**: added Legacy exception subsection documenting the
|
|
43
|
+
remaining devkit-shaped legacy surface.
|
|
44
|
+
- **G9 — `skill-installer.js`**: top-of-file LEGACY header per ADR §6.2.1
|
|
45
|
+
marking the module as legacy-track (devkit migration pending).
|
|
46
|
+
|
|
47
|
+
### Internal
|
|
48
|
+
|
|
49
|
+
- Cross-LLM review pattern applied: Codex implemented the `init` subcommand
|
|
50
|
+
+ fixtures; Claude reviewed and ACCEPTed (commit `d06e1e9`).
|
|
51
|
+
- `test/enforce-report.test.js` version assertion bumped to track release
|
|
52
|
+
(commit `d0f4495`).
|
|
53
|
+
|
|
54
|
+
### Tests
|
|
55
|
+
|
|
56
|
+
- `test/init.test.js` — full coverage of the new subcommand (snippet
|
|
57
|
+
emission, target/format permutations, stdin-ignore, error exits, devkit-free
|
|
58
|
+
invocation).
|
|
59
|
+
- `tests/snippet-protocol/v1/` — golden fixtures for protocol conformance;
|
|
60
|
+
`npm test` runs `git diff --exit-code` against them so any drift fails CI.
|
|
61
|
+
|
|
62
|
+
### Invariants preserved
|
|
63
|
+
|
|
64
|
+
- Daemon code unchanged. No new dependencies. No `bin` field changes.
|
|
65
|
+
- Existing CLI subcommands (`allow`, `inject`, `list`, `tui`, `daemon`, …)
|
|
66
|
+
unchanged.
|
|
67
|
+
- Cross-host inject path (0.3.4) unchanged.
|
|
68
|
+
|
|
69
|
+
## [0.3.4] — 2026-05-05
|
|
70
|
+
|
|
71
|
+
### Added — Cross-host inject (`<id>@<host>` syntax)
|
|
72
|
+
|
|
73
|
+
Enables `telepty inject <id>@<host> "msg"` to deliver to a remote daemon
|
|
74
|
+
without SSH wrapping, by resolving `<host>` against the peer registry and
|
|
75
|
+
issuing direct HTTP `POST /api/sessions/<id>/inject`. Closes the gap that
|
|
76
|
+
forced operators to either pre-shell into the host or pipe through SSH.
|
|
77
|
+
|
|
78
|
+
- **`connect-http` peer mode** (commit `a92cacc`) — new HTTP-only peer
|
|
79
|
+
registration path that does not require a reverse PTY tunnel; suitable
|
|
80
|
+
for daemons reachable via Tailscale / private DNS.
|
|
81
|
+
- **`TELEPTY_HOST` env parser fix** (commit `a92cacc`) — `<id>@<host>` now
|
|
82
|
+
parses correctly when the host segment contains a port or non-default
|
|
83
|
+
scheme; prior parser dropped the host portion silently.
|
|
84
|
+
- **Peer registry HTTP-only mode** — registry entries can be marked
|
|
85
|
+
HTTP-only so the daemon does not attempt PTY fan-out for them.
|
|
86
|
+
|
|
87
|
+
### Added — Skill installer auto-detect (`486bc1e`)
|
|
88
|
+
|
|
89
|
+
`telepty install` now auto-detects which AI CLIs are present
|
|
90
|
+
(`claude`, `codex`, `gemini`) and only installs the corresponding skill
|
|
91
|
+
files. Reduces noisy "skipped" log lines and prevents stub installs
|
|
92
|
+
on machines that don't have the target CLI yet.
|
|
93
|
+
|
|
94
|
+
### Fixed — Node 18 ESM regression (`fc7ff9a`)
|
|
95
|
+
|
|
96
|
+
Pinned `uuid@9` (was floating to v10, which is ESM-only and caused
|
|
97
|
+
`ERR_REQUIRE_ESM` under Node 18 CommonJS consumers).
|
|
98
|
+
|
|
99
|
+
### Docs
|
|
100
|
+
|
|
101
|
+
- Cross-host inject `<id>@<host>` syntax documented (commit `c8b9bbb`).
|
|
102
|
+
- `[context-ref]` inject protocol standardized across docs (commit `8986a96`).
|
|
103
|
+
- REPORT pattern + orchestrator-id runtime resolution documented in skills
|
|
104
|
+
(commit `658f712`).
|
|
105
|
+
- Korean trigger keywords added to skill `SKILL.md` descriptions for
|
|
106
|
+
cross-locale activation (commit `57f46e1`).
|
|
107
|
+
|
|
108
|
+
### Note — never published to npm
|
|
109
|
+
|
|
110
|
+
`0.3.4` was version-bumped locally but never reached the registry; this
|
|
111
|
+
entry is added retrospectively alongside the `0.3.5` publish so the
|
|
112
|
+
changelog history matches the git log. Registry consumers go directly
|
|
113
|
+
from `0.3.3` → `0.3.5`.
|
|
114
|
+
|
|
5
115
|
## [0.3.3] — 2026-05-02
|
|
6
116
|
|
|
7
117
|
### Added — `inject --submit-force` + idempotent client retry (spec: `docs/superpowers/specs/2026-05-02-submit-force-and-retry.md`)
|
package/README.md
CHANGED
|
@@ -72,13 +72,31 @@ telepty broadcast "status report"
|
|
|
72
72
|
|
|
73
73
|
telepty auto-discovers sessions across your Tailnet. All commands (`list`, `attach`, `inject`, `rename`, `multicast`, `broadcast`) work seamlessly across machines.
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
### `<id>@<host>` syntax
|
|
76
|
+
|
|
77
|
+
To target a specific host (when the same session ID exists on multiple hosts,
|
|
78
|
+
or when there is no Tailnet auto-discovery), append `@<host>` to the session
|
|
79
|
+
ID. `<host>` can be a hostname, LAN IP, or Tailnet name.
|
|
76
80
|
|
|
77
81
|
```bash
|
|
82
|
+
# Hostname / Tailnet name
|
|
78
83
|
telepty inject my-session@macbook "hello"
|
|
79
84
|
telepty attach worker@server-01
|
|
85
|
+
|
|
86
|
+
# LAN IP — useful when no Tailnet is configured
|
|
87
|
+
telepty inject orchestrator-claude@172.28.4.165 "ping"
|
|
88
|
+
telepty read-screen build-runner@10.0.0.42 --lines 50
|
|
80
89
|
```
|
|
81
90
|
|
|
91
|
+
**Requirements**:
|
|
92
|
+
- The remote daemon must be reachable on port **3848** from the calling host
|
|
93
|
+
(LAN routing, firewall rules, or Tailscale).
|
|
94
|
+
- No SSH or `sshd` is required on either side — the call hits the remote
|
|
95
|
+
daemon's HTTP API directly. This is the recommended path for laptop
|
|
96
|
+
daemons that don't run sshd.
|
|
97
|
+
- The `@<host>` qualifier works for `inject`, `attach`, `read-screen`,
|
|
98
|
+
`enter`, `multicast`, and `rename`.
|
|
99
|
+
|
|
82
100
|
## How It Works
|
|
83
101
|
|
|
84
102
|
```
|
|
@@ -92,6 +110,54 @@ CLI (telepty) ──> HTTP/WS ──> Daemon (:3848)
|
|
|
92
110
|
- **`inject`** delivers text via the fastest available path: kitty terminal API, WebSocket, or UDS (Unix Domain Socket for embedded integrations)
|
|
93
111
|
- **`submit`** is handled separately from text injection for reliability across all AI CLIs
|
|
94
112
|
|
|
113
|
+
## `[context-ref]` Protocol — long payloads via shared file
|
|
114
|
+
|
|
115
|
+
When a sender uses `telepty inject --ref <file> <target> "<message>"`, telepty
|
|
116
|
+
stores the payload in a shared file under `~/.telepty/shared/<sha256>.md` and
|
|
117
|
+
injects only a short pointer prompt of the form:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
[context-ref] Read ~/.telepty/shared/<sha256>.md and use it as the source of truth for this task.
|
|
121
|
+
<inline message>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This avoids prompt rot in the receiving session (and in the orchestrator's
|
|
125
|
+
window when the reply is small).
|
|
126
|
+
|
|
127
|
+
### Receiver contract
|
|
128
|
+
|
|
129
|
+
The receiving AI session is expected to:
|
|
130
|
+
1. Detect the `[context-ref]` prefix on the first line.
|
|
131
|
+
2. Read the file at the absolute path.
|
|
132
|
+
3. Treat the file contents as the **authoritative payload** for the task — the
|
|
133
|
+
inline message is supplementary (topic / hint), not the source of truth.
|
|
134
|
+
|
|
135
|
+
### Storage location
|
|
136
|
+
|
|
137
|
+
- File path: `~/.telepty/shared/<sha256>.md` (sha256 of payload body)
|
|
138
|
+
- Created with mode `0600`; readable only by the local user
|
|
139
|
+
- Persists across sessions; not garbage-collected automatically (run
|
|
140
|
+
`telepty clean --shared` to prune)
|
|
141
|
+
|
|
142
|
+
### When to use `--ref`
|
|
143
|
+
|
|
144
|
+
- Payload exceeds ~1KB or contains structured content (code, logs, tables).
|
|
145
|
+
- You want the receiver to load the payload deterministically rather than
|
|
146
|
+
paraphrase it from the inject prompt.
|
|
147
|
+
- You're orchestrating a multi-hop conversation where the orchestrator should
|
|
148
|
+
not see the full payload in its own context window.
|
|
149
|
+
|
|
150
|
+
### Integration scope
|
|
151
|
+
|
|
152
|
+
Per-agent receiver integrations (auto-loading the file via Claude Code
|
|
153
|
+
`UserPromptSubmit` hooks, Codex `AGENTS.md` directives, etc.) are **out of
|
|
154
|
+
scope for telepty core** — they live in the agent's own configuration.
|
|
155
|
+
Per-CLI hook installation lives in devkit: run `aigentry scaffold
|
|
156
|
+
install-hooks {claude|codex|gemini}` after installing
|
|
157
|
+
`@dmsdc-ai/aigentry-devkit`. (Older drafts proposed a receiver-side
|
|
158
|
+
`telepty install` subcommand for this; that direction is rejected per ADR
|
|
159
|
+
2026-05-05-telepty-devkit-boundary §3.1.2 / §3.4 row 2.)
|
|
160
|
+
|
|
95
161
|
## Inject Delivery Paths
|
|
96
162
|
|
|
97
163
|
| Priority | Method | When |
|
package/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ const { formatHostLabel, groupSessionsByHost, pickSessionTarget } = require('./s
|
|
|
18
18
|
const { buildSharedContextPrompt, createSharedContextDescriptor, ensureSharedContextFile } = require('./shared-context');
|
|
19
19
|
const { runInteractiveSkillInstaller } = require('./skill-installer');
|
|
20
20
|
const crossMachine = require('./cross-machine');
|
|
21
|
+
const { parseHostSpec, buildDaemonUrl, buildDaemonWsUrl } = require('./host-spec');
|
|
21
22
|
const { FileMailbox } = require('./src/mailbox/index');
|
|
22
23
|
const args = process.argv.slice(2);
|
|
23
24
|
let pendingTerminalInputError = null;
|
|
@@ -118,23 +119,43 @@ if (!process.env.NO_UPDATE_NOTIFIER && !process.env.TELEPTY_DISABLE_UPDATE_NOTIF
|
|
|
118
119
|
updateNotifier({pkg}).notify({ isGlobal: true });
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
// Support remote host via environment variable or default to localhost
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
// Support remote host via environment variable or default to localhost.
|
|
123
|
+
// TELEPTY_HOST accepts: `host`, `host:port`, or `http://host:port`. Embedded
|
|
124
|
+
// port from TELEPTY_HOST is used unless TELEPTY_PORT is set explicitly.
|
|
125
|
+
const _explicitPort = process.env.TELEPTY_PORT ? Number(process.env.TELEPTY_PORT) : null;
|
|
126
|
+
const _hostSpec = parseHostSpec(process.env.TELEPTY_HOST, _explicitPort || 3848);
|
|
127
|
+
let REMOTE_HOST = _hostSpec.host;
|
|
128
|
+
const PORT = _explicitPort != null ? _explicitPort : _hostSpec.port;
|
|
129
|
+
let DAEMON_URL = buildDaemonUrl(REMOTE_HOST, PORT);
|
|
130
|
+
let WS_URL = buildDaemonWsUrl(REMOTE_HOST, PORT);
|
|
131
|
+
|
|
132
|
+
function daemonUrl(host) {
|
|
133
|
+
if (host == null || host === '') return DAEMON_URL;
|
|
134
|
+
return buildDaemonUrl(host, PORT);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function daemonWsUrl(host) {
|
|
138
|
+
if (host == null || host === '') return WS_URL;
|
|
139
|
+
return buildDaemonWsUrl(host, PORT);
|
|
140
|
+
}
|
|
126
141
|
|
|
127
|
-
|
|
128
|
-
|
|
142
|
+
let cachedAuthToken = null;
|
|
143
|
+
|
|
144
|
+
function getAuthToken() {
|
|
145
|
+
if (cachedAuthToken == null) {
|
|
146
|
+
cachedAuthToken = getConfig().authToken;
|
|
147
|
+
}
|
|
148
|
+
return cachedAuthToken;
|
|
149
|
+
}
|
|
129
150
|
|
|
130
151
|
const fetchWithAuth = (url, options = {}) => {
|
|
131
|
-
const headers = { ...options.headers, 'x-telepty-token':
|
|
152
|
+
const headers = { ...options.headers, 'x-telepty-token': getAuthToken() };
|
|
132
153
|
return fetch(url, { ...options, headers });
|
|
133
154
|
};
|
|
134
155
|
|
|
135
156
|
async function getDaemonMeta(host = REMOTE_HOST) {
|
|
136
157
|
try {
|
|
137
|
-
const res = await fetchWithAuth(
|
|
158
|
+
const res = await fetchWithAuth(`${daemonUrl(host)}/api/meta`, {
|
|
138
159
|
signal: AbortSignal.timeout(1500)
|
|
139
160
|
});
|
|
140
161
|
if (!res.ok) {
|
|
@@ -487,7 +508,7 @@ async function discoverSessions(options = {}) {
|
|
|
487
508
|
|
|
488
509
|
// Local daemon sessions
|
|
489
510
|
try {
|
|
490
|
-
const res = await fetchWithAuth(
|
|
511
|
+
const res = await fetchWithAuth(`${daemonUrl('127.0.0.1')}/api/sessions`, {
|
|
491
512
|
signal: AbortSignal.timeout(1500)
|
|
492
513
|
});
|
|
493
514
|
if (res.ok) {
|
|
@@ -502,6 +523,14 @@ async function discoverSessions(options = {}) {
|
|
|
502
523
|
const remoteSessions = crossMachine.discoverAllRemoteSessions();
|
|
503
524
|
allSessions.push(...remoteSessions);
|
|
504
525
|
|
|
526
|
+
// Remote peer sessions via HTTP (no SSH)
|
|
527
|
+
try {
|
|
528
|
+
const httpSessions = await crossMachine.discoverHttpRemoteSessions();
|
|
529
|
+
allSessions.push(...httpSessions);
|
|
530
|
+
} catch {
|
|
531
|
+
// HTTP peer discovery is best-effort.
|
|
532
|
+
}
|
|
533
|
+
|
|
505
534
|
return allSessions;
|
|
506
535
|
}
|
|
507
536
|
|
|
@@ -550,7 +579,7 @@ async function ensureDaemonRunning(options = {}) {
|
|
|
550
579
|
}
|
|
551
580
|
|
|
552
581
|
async function manageInteractiveAttach(sessionId, targetHost) {
|
|
553
|
-
const wsUrl =
|
|
582
|
+
const wsUrl = `${daemonWsUrl(targetHost)}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(getAuthToken())}`;
|
|
554
583
|
const ws = new WebSocket(wsUrl);
|
|
555
584
|
let cleanupTerminal = null;
|
|
556
585
|
return new Promise((resolve) => {
|
|
@@ -576,7 +605,7 @@ async function manageInteractiveAttach(sessionId, targetHost) {
|
|
|
576
605
|
|
|
577
606
|
// Check if other clients are still attached before destroying
|
|
578
607
|
try {
|
|
579
|
-
const res = await fetchWithAuth(
|
|
608
|
+
const res = await fetchWithAuth(`${daemonUrl(targetHost)}/api/sessions`);
|
|
580
609
|
if (res.ok) {
|
|
581
610
|
const sessions = await res.json();
|
|
582
611
|
const session = sessions.find(s => s.id === sessionId);
|
|
@@ -584,7 +613,7 @@ async function manageInteractiveAttach(sessionId, targetHost) {
|
|
|
584
613
|
console.log(`\n\x1b[33mLeft room '${sessionId}'. Other clients still attached — session kept alive.\x1b[0m\n`);
|
|
585
614
|
} else {
|
|
586
615
|
console.log(`\n\x1b[33mLeft room '${sessionId}'. No other clients — destroying session.\x1b[0m\n`);
|
|
587
|
-
await fetchWithAuth(
|
|
616
|
+
await fetchWithAuth(`${daemonUrl(targetHost)}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
|
|
588
617
|
}
|
|
589
618
|
}
|
|
590
619
|
} catch(e) {
|
|
@@ -787,7 +816,7 @@ async function manageInteractive() {
|
|
|
787
816
|
const { promptText } = injectPromptResponse;
|
|
788
817
|
if (!promptText) continue;
|
|
789
818
|
try {
|
|
790
|
-
const res = await fetchWithAuth(
|
|
819
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
791
820
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: promptText })
|
|
792
821
|
});
|
|
793
822
|
const data = await res.json();
|
|
@@ -812,6 +841,15 @@ async function main() {
|
|
|
812
841
|
return;
|
|
813
842
|
}
|
|
814
843
|
|
|
844
|
+
if (cmd === 'init') {
|
|
845
|
+
const { main: runInit } = require('./src/init/print-snippet');
|
|
846
|
+
const exitCode = runInit(args.slice(1));
|
|
847
|
+
if (exitCode) {
|
|
848
|
+
process.exitCode = exitCode;
|
|
849
|
+
}
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
815
853
|
if (cmd === 'update') {
|
|
816
854
|
console.log('\x1b[36m🔄 Updating telepty to the latest version...\x1b[0m');
|
|
817
855
|
try {
|
|
@@ -1140,7 +1178,7 @@ async function main() {
|
|
|
1140
1178
|
// Connect to daemon WebSocket with auto-reconnect
|
|
1141
1179
|
// owner=1 tells daemon this is the allow bridge (owner), not an attach viewer.
|
|
1142
1180
|
// Daemon uses this to reclaim ownership even if a stale ownerWs is still registered.
|
|
1143
|
-
const wsUrl =
|
|
1181
|
+
const wsUrl = `${daemonWsUrl(REMOTE_HOST)}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(getAuthToken())}&owner=1`;
|
|
1144
1182
|
let daemonWs = null;
|
|
1145
1183
|
let wsReady = false;
|
|
1146
1184
|
let reconnectAttempts = 0;
|
|
@@ -1448,7 +1486,7 @@ async function main() {
|
|
|
1448
1486
|
}
|
|
1449
1487
|
}
|
|
1450
1488
|
|
|
1451
|
-
const wsUrl =
|
|
1489
|
+
const wsUrl = `${daemonWsUrl(targetHost)}/api/sessions/${encodeURIComponent(sessionId)}?token=${encodeURIComponent(getAuthToken())}`;
|
|
1452
1490
|
const ws = new WebSocket(wsUrl);
|
|
1453
1491
|
let cleanupTerminal = null;
|
|
1454
1492
|
|
|
@@ -1486,7 +1524,7 @@ async function main() {
|
|
|
1486
1524
|
|
|
1487
1525
|
// Check if other clients are still attached before destroying
|
|
1488
1526
|
try {
|
|
1489
|
-
const res = await fetchWithAuth(
|
|
1527
|
+
const res = await fetchWithAuth(`${daemonUrl(targetHost)}/api/sessions`);
|
|
1490
1528
|
if (res.ok) {
|
|
1491
1529
|
const allSessions = await res.json();
|
|
1492
1530
|
const session = allSessions.find(s => s.id === sessionId);
|
|
@@ -1494,7 +1532,7 @@ async function main() {
|
|
|
1494
1532
|
console.log(`\n\x1b[33mLeft room '${sessionId}'. Other clients still attached — session kept alive.\x1b[0m`);
|
|
1495
1533
|
} else {
|
|
1496
1534
|
console.log(`\n\x1b[33mLeft room '${sessionId}'. No other clients — destroying session.\x1b[0m`);
|
|
1497
|
-
await fetchWithAuth(
|
|
1535
|
+
await fetchWithAuth(`${daemonUrl(targetHost)}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
|
|
1498
1536
|
}
|
|
1499
1537
|
}
|
|
1500
1538
|
} catch(e) {}
|
|
@@ -1524,7 +1562,7 @@ async function main() {
|
|
|
1524
1562
|
process.exit(1);
|
|
1525
1563
|
}
|
|
1526
1564
|
|
|
1527
|
-
const res = await fetchWithAuth(
|
|
1565
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/screen?lines=${lines}${raw ? '&raw=1' : ''}`);
|
|
1528
1566
|
const data = await res.json();
|
|
1529
1567
|
if (!res.ok) { console.error(`❌ Error: ${data.error}`); process.exit(1); }
|
|
1530
1568
|
|
|
@@ -1656,7 +1694,7 @@ async function main() {
|
|
|
1656
1694
|
noEnter: useSubmit
|
|
1657
1695
|
});
|
|
1658
1696
|
|
|
1659
|
-
const res = await fetchWithAuth(
|
|
1697
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
1660
1698
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
1661
1699
|
});
|
|
1662
1700
|
const data = await res.json();
|
|
@@ -1699,7 +1737,7 @@ async function main() {
|
|
|
1699
1737
|
}
|
|
1700
1738
|
attemptsMade = attempt + 1;
|
|
1701
1739
|
try {
|
|
1702
|
-
submitRes = await fetchWithAuth(
|
|
1740
|
+
submitRes = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/submit`, {
|
|
1703
1741
|
method: 'POST',
|
|
1704
1742
|
headers: { 'Content-Type': 'application/json' },
|
|
1705
1743
|
body: JSON.stringify(submitBody),
|
|
@@ -1776,7 +1814,7 @@ async function main() {
|
|
|
1776
1814
|
return;
|
|
1777
1815
|
}
|
|
1778
1816
|
|
|
1779
|
-
const res = await fetchWithAuth(
|
|
1817
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
1780
1818
|
method: 'POST',
|
|
1781
1819
|
headers: { 'Content-Type': 'application/json' },
|
|
1782
1820
|
body: JSON.stringify(buildInjectRequestBody('', {}))
|
|
@@ -1808,7 +1846,7 @@ async function main() {
|
|
|
1808
1846
|
|
|
1809
1847
|
// send-key is a manual override — bypass the render gate via force=true.
|
|
1810
1848
|
// See: docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md §3.1
|
|
1811
|
-
const res = await fetchWithAuth(
|
|
1849
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/submit`, {
|
|
1812
1850
|
method: 'POST',
|
|
1813
1851
|
headers: { 'Content-Type': 'application/json' },
|
|
1814
1852
|
body: JSON.stringify({ force: true }),
|
|
@@ -1836,7 +1874,7 @@ async function main() {
|
|
|
1836
1874
|
const target = await resolveSessionTarget(replyTo);
|
|
1837
1875
|
if (!target) { console.error(`❌ Session '${replyTo}' was not found on any discovered host.`); process.exit(1); }
|
|
1838
1876
|
const body = { prompt: replyText, from: mySessionId, reply_to: mySessionId };
|
|
1839
|
-
const res = await fetchWithAuth(
|
|
1877
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
1840
1878
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
1841
1879
|
});
|
|
1842
1880
|
const data = await res.json();
|
|
@@ -1860,7 +1898,7 @@ async function main() {
|
|
|
1860
1898
|
process.exit(1);
|
|
1861
1899
|
}
|
|
1862
1900
|
|
|
1863
|
-
const res = await fetchWithAuth(
|
|
1901
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/state`);
|
|
1864
1902
|
const data = await res.json();
|
|
1865
1903
|
if (!res.ok) {
|
|
1866
1904
|
console.error(`❌ ${formatApiError(data)}`);
|
|
@@ -1977,7 +2015,7 @@ async function main() {
|
|
|
1977
2015
|
process.exit(1);
|
|
1978
2016
|
}
|
|
1979
2017
|
|
|
1980
|
-
const res = await fetchWithAuth(
|
|
2018
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/state`, {
|
|
1981
2019
|
method: 'POST',
|
|
1982
2020
|
headers: { 'Content-Type': 'application/json' },
|
|
1983
2021
|
body: JSON.stringify(buildSessionStateReportBody({
|
|
@@ -2022,7 +2060,7 @@ async function main() {
|
|
|
2022
2060
|
|
|
2023
2061
|
const aggregate = { successful: [], failed: [] };
|
|
2024
2062
|
for (const [host, ids] of groupedTargets.entries()) {
|
|
2025
|
-
const res = await fetchWithAuth(
|
|
2063
|
+
const res = await fetchWithAuth(`${daemonUrl(host)}/api/sessions/multicast/inject`, {
|
|
2026
2064
|
method: 'POST',
|
|
2027
2065
|
headers: { 'Content-Type': 'application/json' },
|
|
2028
2066
|
body: JSON.stringify({ session_ids: ids, prompt })
|
|
@@ -2065,7 +2103,7 @@ async function main() {
|
|
|
2065
2103
|
}
|
|
2066
2104
|
|
|
2067
2105
|
for (const host of groupSessionsByHost(local).keys()) {
|
|
2068
|
-
const res = await fetchWithAuth(
|
|
2106
|
+
const res = await fetchWithAuth(`${daemonUrl(host)}/api/sessions/broadcast/inject`, {
|
|
2069
2107
|
method: 'POST',
|
|
2070
2108
|
headers: { 'Content-Type': 'application/json' },
|
|
2071
2109
|
body: JSON.stringify({ prompt: localPrompt })
|
|
@@ -2112,7 +2150,7 @@ async function main() {
|
|
|
2112
2150
|
try {
|
|
2113
2151
|
const target = await resolveSessionTarget(sessionRef);
|
|
2114
2152
|
if (!target) { console.error(`❌ Session '${sessionRef}' not found.`); process.exit(1); }
|
|
2115
|
-
const res = await fetchWithAuth(
|
|
2153
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}`, { method: 'DELETE' });
|
|
2116
2154
|
const data = await res.json();
|
|
2117
2155
|
if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
|
|
2118
2156
|
console.log(`✅ Session '\x1b[36m${target.id}\x1b[0m' deleted.`);
|
|
@@ -2129,7 +2167,7 @@ async function main() {
|
|
|
2129
2167
|
if (s.healthStatus === 'STALE' || s.healthStatus === 'DISCONNECTED') {
|
|
2130
2168
|
try {
|
|
2131
2169
|
const host = s.host || '127.0.0.1';
|
|
2132
|
-
const res = await fetchWithAuth(
|
|
2170
|
+
const res = await fetchWithAuth(`${daemonUrl(host)}/api/sessions/${encodeURIComponent(s.id)}`, { method: 'DELETE' });
|
|
2133
2171
|
if (res.ok) { console.log(` 🗑 Removed ghost: \x1b[36m${s.id}\x1b[0m (${s.healthStatus})`); cleaned++; }
|
|
2134
2172
|
} catch (_) {}
|
|
2135
2173
|
}
|
|
@@ -2149,7 +2187,7 @@ async function main() {
|
|
|
2149
2187
|
process.exit(1);
|
|
2150
2188
|
}
|
|
2151
2189
|
|
|
2152
|
-
const res = await fetchWithAuth(
|
|
2190
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}`, {
|
|
2153
2191
|
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ new_id: newId })
|
|
2154
2192
|
});
|
|
2155
2193
|
const data = await res.json();
|
|
@@ -2181,7 +2219,7 @@ async function main() {
|
|
|
2181
2219
|
return;
|
|
2182
2220
|
}
|
|
2183
2221
|
|
|
2184
|
-
const res = await fetchWithAuth(
|
|
2222
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}`);
|
|
2185
2223
|
const data = await res.json();
|
|
2186
2224
|
if (!res.ok) {
|
|
2187
2225
|
console.error(`❌ Error: ${data.error}`);
|
|
@@ -2196,7 +2234,7 @@ async function main() {
|
|
|
2196
2234
|
return;
|
|
2197
2235
|
}
|
|
2198
2236
|
|
|
2199
|
-
const res = await fetchWithAuth(
|
|
2237
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}`);
|
|
2200
2238
|
const data = await res.json();
|
|
2201
2239
|
if (!res.ok) {
|
|
2202
2240
|
console.error(`❌ Error: ${data.error}`);
|
|
@@ -2649,7 +2687,7 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2649
2687
|
reply_to: orchestratorId,
|
|
2650
2688
|
thread_id: threadId
|
|
2651
2689
|
};
|
|
2652
|
-
const resp = await fetchWithAuth(
|
|
2690
|
+
const resp = await fetchWithAuth(`${daemonUrl(host)}/api/sessions/${encodeURIComponent(session.id)}/inject`, {
|
|
2653
2691
|
method: 'POST',
|
|
2654
2692
|
headers: { 'Content-Type': 'application/json' },
|
|
2655
2693
|
body: JSON.stringify(body)
|
|
@@ -2658,7 +2696,7 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2658
2696
|
// Submit after text injection (300ms delay handled by daemon)
|
|
2659
2697
|
setTimeout(async () => {
|
|
2660
2698
|
try {
|
|
2661
|
-
await fetchWithAuth(
|
|
2699
|
+
await fetchWithAuth(`${daemonUrl(host)}/api/sessions/${encodeURIComponent(session.id)}/submit`, { method: 'POST' });
|
|
2662
2700
|
} catch {}
|
|
2663
2701
|
}, 500);
|
|
2664
2702
|
console.log(` ✅ Injected to ${session.id}`);
|
|
@@ -2908,6 +2946,43 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2908
2946
|
return;
|
|
2909
2947
|
}
|
|
2910
2948
|
|
|
2949
|
+
// telepty connect-http <host>[:port] [--name <name>] [--token <token>]
|
|
2950
|
+
// HTTP-only remote daemon registration (no SSH/sshd required).
|
|
2951
|
+
// Records peer in ~/.telepty/peers.json with transport='http' so subsequent
|
|
2952
|
+
// `telepty list`/`inject`/etc. discover sessions on the remote daemon via
|
|
2953
|
+
// its HTTP API. Designed for laptop daemons where running sshd is not
|
|
2954
|
+
// viable. See GitHub issue #13.
|
|
2955
|
+
if (cmd === 'connect-http') {
|
|
2956
|
+
const target = args[1];
|
|
2957
|
+
if (!target) {
|
|
2958
|
+
console.error('❌ Usage: telepty connect-http <host>[:port] [--name <name>] [--token <token>]');
|
|
2959
|
+
process.exit(1);
|
|
2960
|
+
}
|
|
2961
|
+
const nameFlag = args.indexOf('--name');
|
|
2962
|
+
const tokenFlag = args.indexOf('--token');
|
|
2963
|
+
const options = {};
|
|
2964
|
+
if (nameFlag !== -1 && args[nameFlag + 1]) options.name = args[nameFlag + 1];
|
|
2965
|
+
if (tokenFlag !== -1 && args[tokenFlag + 1]) options.token = args[tokenFlag + 1];
|
|
2966
|
+
|
|
2967
|
+
process.stdout.write(`\x1b[36m🔗 Connecting to ${target} via HTTP...\x1b[0m\n`);
|
|
2968
|
+
try {
|
|
2969
|
+
const result = await crossMachine.connectHttp(target, options);
|
|
2970
|
+
if (result.success) {
|
|
2971
|
+
console.log(`\x1b[32m✅ Connected to ${result.name}\x1b[0m`);
|
|
2972
|
+
console.log(` Host: ${result.host}:${result.port}`);
|
|
2973
|
+
console.log(` Machine ID: ${result.machineId}`);
|
|
2974
|
+
console.log(`\nSessions on ${result.name} are now discoverable via \x1b[36mtelepty list\x1b[0m`);
|
|
2975
|
+
} else {
|
|
2976
|
+
console.error(`\x1b[31m❌ ${result.error}\x1b[0m`);
|
|
2977
|
+
process.exit(1);
|
|
2978
|
+
}
|
|
2979
|
+
} catch (err) {
|
|
2980
|
+
console.error(`\x1b[31m❌ ${err.message}\x1b[0m`);
|
|
2981
|
+
process.exit(1);
|
|
2982
|
+
}
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2911
2986
|
// telepty disconnect [<name> | --all]
|
|
2912
2987
|
if (cmd === 'disconnect') {
|
|
2913
2988
|
if (args[1] === '--all') {
|
|
@@ -2937,8 +3012,9 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2937
3012
|
|
|
2938
3013
|
const active = crossMachine.listActivePeers();
|
|
2939
3014
|
const known = crossMachine.listKnownPeers();
|
|
3015
|
+
const httpPeers = crossMachine.listHttpPeers();
|
|
2940
3016
|
|
|
2941
|
-
console.log('\x1b[1mConnected Peers:\x1b[0m');
|
|
3017
|
+
console.log('\x1b[1mConnected Peers (SSH ControlMaster):\x1b[0m');
|
|
2942
3018
|
if (active.length === 0) {
|
|
2943
3019
|
console.log(' (none)');
|
|
2944
3020
|
} else {
|
|
@@ -2948,7 +3024,8 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2948
3024
|
}
|
|
2949
3025
|
|
|
2950
3026
|
const knownNames = Object.keys(known);
|
|
2951
|
-
const
|
|
3027
|
+
const httpNames = new Set(httpPeers.map((p) => p.name));
|
|
3028
|
+
const disconnected = knownNames.filter(n => !active.find(a => a.name === n) && !httpNames.has(n));
|
|
2952
3029
|
if (disconnected.length > 0) {
|
|
2953
3030
|
console.log('\n\x1b[1mKnown Peers (disconnected):\x1b[0m');
|
|
2954
3031
|
for (const name of disconnected) {
|
|
@@ -2956,6 +3033,14 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2956
3033
|
console.log(` \x1b[90m○\x1b[0m ${name} (${p.target}) — last: ${p.lastConnected || 'never'}`);
|
|
2957
3034
|
}
|
|
2958
3035
|
}
|
|
3036
|
+
|
|
3037
|
+
if (httpPeers.length > 0) {
|
|
3038
|
+
console.log('\n\x1b[1mHTTP Peers (no SSH):\x1b[0m');
|
|
3039
|
+
for (const peer of httpPeers) {
|
|
3040
|
+
const tokenNote = peer.hasToken ? ' [token]' : '';
|
|
3041
|
+
console.log(` \x1b[36m◆\x1b[0m ${peer.name} (${peer.host}:${peer.port}) [${peer.machineId}]${tokenNote}`);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
2959
3044
|
return;
|
|
2960
3045
|
}
|
|
2961
3046
|
|
|
@@ -2973,7 +3058,7 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2973
3058
|
let connectedHosts = 0;
|
|
2974
3059
|
|
|
2975
3060
|
hosts.forEach((host) => {
|
|
2976
|
-
const wsUrl =
|
|
3061
|
+
const wsUrl = `${daemonWsUrl(host)}/api/bus?token=${encodeURIComponent(getAuthToken())}`;
|
|
2977
3062
|
const ws = new WebSocket(wsUrl);
|
|
2978
3063
|
|
|
2979
3064
|
ws.on('open', () => {
|
|
@@ -3051,7 +3136,8 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
3051
3136
|
telepty read-screen <id[@host]> [--lines N] Read session screen buffer
|
|
3052
3137
|
|
|
3053
3138
|
\x1b[1mCross-Machine:\x1b[0m
|
|
3054
|
-
telepty connect <user@host> [--name N] [--port P]
|
|
3139
|
+
telepty connect <user@host> [--name N] [--port P] SSH tunnel to remote host
|
|
3140
|
+
telepty connect-http <host>[:port] [--name N] [--token T] Register remote daemon via HTTP (no SSH)
|
|
3055
3141
|
telepty disconnect <name> | --all Disconnect remote host
|
|
3056
3142
|
telepty peers [--remove <name>] List connected peers
|
|
3057
3143
|
|