@dmsdc-ai/aigentry-telepty 0.1.98 → 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 +436 -0
- package/CLAUDE.md +5 -1
- package/README.md +70 -1
- package/cli.js +232 -53
- package/cross-machine.js +132 -0
- package/daemon.js +399 -39
- 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/docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md +447 -0
- package/docs/superpowers/specs/2026-04-26-prompt-symbol-render-gate.md +571 -0
- package/docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md +608 -0
- package/docs/superpowers/specs/2026-05-02-submit-force-and-retry.md +139 -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/specs/enforce-report-spec.md +237 -0
- 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/src/prompt-symbol-registry.js +97 -0
- package/src/report-enforcement.js +86 -0
- package/src/submit-gate.js +269 -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/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
|
|
|
@@ -1550,6 +1588,32 @@ async function main() {
|
|
|
1550
1588
|
const useSubmit = submitIndex !== -1;
|
|
1551
1589
|
if (useSubmit) args.splice(submitIndex, 1);
|
|
1552
1590
|
|
|
1591
|
+
// Extract --submit-force flag (gate bypass; opt-in escape hatch).
|
|
1592
|
+
// Mirrors `telepty send-key`'s force semantics: skip both Layer 3 and
|
|
1593
|
+
// Layer 1 gates and dispatch Enter immediately. Safe only when the
|
|
1594
|
+
// caller is confident the target REPL is ready (e.g., orchestrator is
|
|
1595
|
+
// visibly idle). See specs/2026-05-02-submit-force-and-retry.md
|
|
1596
|
+
const submitForceIndex = args.indexOf('--submit-force');
|
|
1597
|
+
const submitForce = submitForceIndex !== -1;
|
|
1598
|
+
if (submitForce) args.splice(submitForceIndex, 1);
|
|
1599
|
+
|
|
1600
|
+
// Extract --submit-retry N flag (default 1, clamp [0, 3]). On a 504
|
|
1601
|
+
// gated-failure with a retry-safe reason (gate timed out and body is
|
|
1602
|
+
// still in the input box → idempotent), wait 300ms and retry. Hard-fail
|
|
1603
|
+
// reasons (session_dead/error/restarting/no_state) do NOT retry —
|
|
1604
|
+
// re-firing won't recover and would be a wasted round-trip.
|
|
1605
|
+
let submitRetries = 1;
|
|
1606
|
+
const submitRetryIndex = args.indexOf('--submit-retry');
|
|
1607
|
+
if (submitRetryIndex !== -1) {
|
|
1608
|
+
const raw = Number(args[submitRetryIndex + 1]);
|
|
1609
|
+
if (Number.isFinite(raw)) {
|
|
1610
|
+
submitRetries = Math.min(Math.max(Math.floor(raw), 0), 3);
|
|
1611
|
+
args.splice(submitRetryIndex, 2);
|
|
1612
|
+
} else {
|
|
1613
|
+
args.splice(submitRetryIndex, 1);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1553
1617
|
// Extract --from flag
|
|
1554
1618
|
let fromId;
|
|
1555
1619
|
const fromIndex = args.indexOf('--from');
|
|
@@ -1630,7 +1694,7 @@ async function main() {
|
|
|
1630
1694
|
noEnter: useSubmit
|
|
1631
1695
|
});
|
|
1632
1696
|
|
|
1633
|
-
const res = await fetchWithAuth(
|
|
1697
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
1634
1698
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
1635
1699
|
});
|
|
1636
1700
|
const data = await res.json();
|
|
@@ -1638,23 +1702,84 @@ async function main() {
|
|
|
1638
1702
|
const refSuffix = referencePath ? ` (ref: ${referencePath})` : '';
|
|
1639
1703
|
console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m'.${refSuffix}`);
|
|
1640
1704
|
|
|
1641
|
-
// Terminal-level submit: POST /submit after text injection
|
|
1705
|
+
// Terminal-level submit: POST /submit after text injection.
|
|
1706
|
+
// Daemon-side render-gate handles timing (waits for REPL readiness),
|
|
1707
|
+
// so the CLI no longer needs the legacy 500ms blind sleep. Pass the
|
|
1708
|
+
// injected body so the daemon can verify it was consumed by the input
|
|
1709
|
+
// box and bounded-retry once if not.
|
|
1710
|
+
//
|
|
1711
|
+
// 0.3.3: opt-in --submit-force (gate bypass) and idempotent client-side
|
|
1712
|
+
// retry on retry-safe 504s. The retry guard is gate timeout + body
|
|
1713
|
+
// still visible in the input box (verify.consumed=false) — re-firing
|
|
1714
|
+
// an Enter that genuinely never landed cannot double-submit.
|
|
1715
|
+
// See docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md
|
|
1642
1716
|
if (useSubmit) {
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1717
|
+
const submitBody = {
|
|
1718
|
+
injected_body: injectPrompt || '',
|
|
1719
|
+
retries: 1,
|
|
1720
|
+
retry_delay_ms: 500,
|
|
1721
|
+
...(submitForce ? { force: true } : {}),
|
|
1722
|
+
};
|
|
1723
|
+
const RETRY_DELAY_MS = 300;
|
|
1724
|
+
const RETRY_SAFE_REASONS = new Set([
|
|
1725
|
+
'gated_dispatch_unconsumed',
|
|
1726
|
+
'gate_timeout',
|
|
1727
|
+
'no_prompt_symbol_seen',
|
|
1728
|
+
]);
|
|
1729
|
+
const maxAttempts = 1 + submitRetries;
|
|
1730
|
+
let submitRes = null;
|
|
1731
|
+
let submitData = null;
|
|
1732
|
+
let attemptsMade = 0;
|
|
1733
|
+
let lastError = null;
|
|
1734
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1735
|
+
if (attempt > 0) {
|
|
1736
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
1655
1737
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1738
|
+
attemptsMade = attempt + 1;
|
|
1739
|
+
try {
|
|
1740
|
+
submitRes = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/submit`, {
|
|
1741
|
+
method: 'POST',
|
|
1742
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1743
|
+
body: JSON.stringify(submitBody),
|
|
1744
|
+
});
|
|
1745
|
+
submitData = await submitRes.json();
|
|
1746
|
+
} catch (submitErr) {
|
|
1747
|
+
lastError = submitErr;
|
|
1748
|
+
submitRes = null;
|
|
1749
|
+
submitData = null;
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
if (submitRes.ok) break;
|
|
1753
|
+
if (submitRes.status !== 504) break;
|
|
1754
|
+
const retryReason = submitData && typeof submitData.reason === 'string' ? submitData.reason : null;
|
|
1755
|
+
if (!RETRY_SAFE_REASONS.has(retryReason)) break;
|
|
1756
|
+
}
|
|
1757
|
+
if (lastError) {
|
|
1758
|
+
console.error(`⚠️ Submit failed: ${lastError.message}`);
|
|
1759
|
+
} else if (submitRes && submitRes.ok) {
|
|
1760
|
+
const gateNote = submitData.gated && submitData.gate_wait_ms > 0
|
|
1761
|
+
? ` [gate ${submitData.gate_wait_ms}ms]`
|
|
1762
|
+
: '';
|
|
1763
|
+
const lateNote = submitData.gated_dispatch_after_timeout
|
|
1764
|
+
? ' (dispatched-after-gate-timeout)'
|
|
1765
|
+
: '';
|
|
1766
|
+
const attemptsNote = submitData.attempts > 1 ? ` (${submitData.attempts} attempts)` : '';
|
|
1767
|
+
const retryNote = attemptsMade > 1 ? ` [retry ${attemptsMade - 1}/${submitRetries}]` : '';
|
|
1768
|
+
const forcedNote = submitData.forced ? ' [forced]' : '';
|
|
1769
|
+
console.log(`✅ Submitted via ${submitData.strategy}${attemptsNote}${gateNote}${lateNote}${retryNote}${forcedNote}.`);
|
|
1770
|
+
} else if (submitRes && submitRes.status === 504) {
|
|
1771
|
+
// Soft failure: REPL never readied. Orchestrator scripts depend on
|
|
1772
|
+
// exit 0 here — surface a clear remediation hint but do not exit
|
|
1773
|
+
// non-zero.
|
|
1774
|
+
const reason = (submitData && submitData.reason) || 'gate_timeout';
|
|
1775
|
+
const lastState = (submitData && submitData.last_state) || 'unknown';
|
|
1776
|
+
const retriesNote = attemptsMade > 1 ? ` after ${attemptsMade} attempts` : '';
|
|
1777
|
+
const hint = submitForce
|
|
1778
|
+
? ''
|
|
1779
|
+
: ` Try \`telepty inject --submit --submit-force ${target.id} ...\` or manual \`telepty send-key ${target.id} enter\`.`;
|
|
1780
|
+
console.log(`⚠️ Submit gated-timeout (${reason}, last_state=${lastState})${retriesNote}.${hint}`);
|
|
1781
|
+
} else {
|
|
1782
|
+
console.error(`⚠️ Submit failed: ${formatApiError(submitData)}`);
|
|
1658
1783
|
}
|
|
1659
1784
|
}
|
|
1660
1785
|
} catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
|
|
@@ -1689,7 +1814,7 @@ async function main() {
|
|
|
1689
1814
|
return;
|
|
1690
1815
|
}
|
|
1691
1816
|
|
|
1692
|
-
const res = await fetchWithAuth(
|
|
1817
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
1693
1818
|
method: 'POST',
|
|
1694
1819
|
headers: { 'Content-Type': 'application/json' },
|
|
1695
1820
|
body: JSON.stringify(buildInjectRequestBody('', {}))
|
|
@@ -1719,7 +1844,13 @@ async function main() {
|
|
|
1719
1844
|
process.exit(1);
|
|
1720
1845
|
}
|
|
1721
1846
|
|
|
1722
|
-
|
|
1847
|
+
// send-key is a manual override — bypass the render gate via force=true.
|
|
1848
|
+
// See: docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md §3.1
|
|
1849
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/submit`, {
|
|
1850
|
+
method: 'POST',
|
|
1851
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1852
|
+
body: JSON.stringify({ force: true }),
|
|
1853
|
+
});
|
|
1723
1854
|
const data = await res.json();
|
|
1724
1855
|
if (!res.ok) { console.error(`❌ ${formatApiError(data)}`); return; }
|
|
1725
1856
|
console.log(`✅ Key '${key}' sent to '\x1b[36m${target.id}\x1b[0m'. (strategy: ${data.strategy})`);
|
|
@@ -1743,7 +1874,7 @@ async function main() {
|
|
|
1743
1874
|
const target = await resolveSessionTarget(replyTo);
|
|
1744
1875
|
if (!target) { console.error(`❌ Session '${replyTo}' was not found on any discovered host.`); process.exit(1); }
|
|
1745
1876
|
const body = { prompt: replyText, from: mySessionId, reply_to: mySessionId };
|
|
1746
|
-
const res = await fetchWithAuth(
|
|
1877
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
1747
1878
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
1748
1879
|
});
|
|
1749
1880
|
const data = await res.json();
|
|
@@ -1767,7 +1898,7 @@ async function main() {
|
|
|
1767
1898
|
process.exit(1);
|
|
1768
1899
|
}
|
|
1769
1900
|
|
|
1770
|
-
const res = await fetchWithAuth(
|
|
1901
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/state`);
|
|
1771
1902
|
const data = await res.json();
|
|
1772
1903
|
if (!res.ok) {
|
|
1773
1904
|
console.error(`❌ ${formatApiError(data)}`);
|
|
@@ -1884,7 +2015,7 @@ async function main() {
|
|
|
1884
2015
|
process.exit(1);
|
|
1885
2016
|
}
|
|
1886
2017
|
|
|
1887
|
-
const res = await fetchWithAuth(
|
|
2018
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}/state`, {
|
|
1888
2019
|
method: 'POST',
|
|
1889
2020
|
headers: { 'Content-Type': 'application/json' },
|
|
1890
2021
|
body: JSON.stringify(buildSessionStateReportBody({
|
|
@@ -1929,7 +2060,7 @@ async function main() {
|
|
|
1929
2060
|
|
|
1930
2061
|
const aggregate = { successful: [], failed: [] };
|
|
1931
2062
|
for (const [host, ids] of groupedTargets.entries()) {
|
|
1932
|
-
const res = await fetchWithAuth(
|
|
2063
|
+
const res = await fetchWithAuth(`${daemonUrl(host)}/api/sessions/multicast/inject`, {
|
|
1933
2064
|
method: 'POST',
|
|
1934
2065
|
headers: { 'Content-Type': 'application/json' },
|
|
1935
2066
|
body: JSON.stringify({ session_ids: ids, prompt })
|
|
@@ -1972,7 +2103,7 @@ async function main() {
|
|
|
1972
2103
|
}
|
|
1973
2104
|
|
|
1974
2105
|
for (const host of groupSessionsByHost(local).keys()) {
|
|
1975
|
-
const res = await fetchWithAuth(
|
|
2106
|
+
const res = await fetchWithAuth(`${daemonUrl(host)}/api/sessions/broadcast/inject`, {
|
|
1976
2107
|
method: 'POST',
|
|
1977
2108
|
headers: { 'Content-Type': 'application/json' },
|
|
1978
2109
|
body: JSON.stringify({ prompt: localPrompt })
|
|
@@ -2019,7 +2150,7 @@ async function main() {
|
|
|
2019
2150
|
try {
|
|
2020
2151
|
const target = await resolveSessionTarget(sessionRef);
|
|
2021
2152
|
if (!target) { console.error(`❌ Session '${sessionRef}' not found.`); process.exit(1); }
|
|
2022
|
-
const res = await fetchWithAuth(
|
|
2153
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}`, { method: 'DELETE' });
|
|
2023
2154
|
const data = await res.json();
|
|
2024
2155
|
if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
|
|
2025
2156
|
console.log(`✅ Session '\x1b[36m${target.id}\x1b[0m' deleted.`);
|
|
@@ -2036,7 +2167,7 @@ async function main() {
|
|
|
2036
2167
|
if (s.healthStatus === 'STALE' || s.healthStatus === 'DISCONNECTED') {
|
|
2037
2168
|
try {
|
|
2038
2169
|
const host = s.host || '127.0.0.1';
|
|
2039
|
-
const res = await fetchWithAuth(
|
|
2170
|
+
const res = await fetchWithAuth(`${daemonUrl(host)}/api/sessions/${encodeURIComponent(s.id)}`, { method: 'DELETE' });
|
|
2040
2171
|
if (res.ok) { console.log(` 🗑 Removed ghost: \x1b[36m${s.id}\x1b[0m (${s.healthStatus})`); cleaned++; }
|
|
2041
2172
|
} catch (_) {}
|
|
2042
2173
|
}
|
|
@@ -2056,7 +2187,7 @@ async function main() {
|
|
|
2056
2187
|
process.exit(1);
|
|
2057
2188
|
}
|
|
2058
2189
|
|
|
2059
|
-
const res = await fetchWithAuth(
|
|
2190
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}`, {
|
|
2060
2191
|
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ new_id: newId })
|
|
2061
2192
|
});
|
|
2062
2193
|
const data = await res.json();
|
|
@@ -2088,7 +2219,7 @@ async function main() {
|
|
|
2088
2219
|
return;
|
|
2089
2220
|
}
|
|
2090
2221
|
|
|
2091
|
-
const res = await fetchWithAuth(
|
|
2222
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}`);
|
|
2092
2223
|
const data = await res.json();
|
|
2093
2224
|
if (!res.ok) {
|
|
2094
2225
|
console.error(`❌ Error: ${data.error}`);
|
|
@@ -2103,7 +2234,7 @@ async function main() {
|
|
|
2103
2234
|
return;
|
|
2104
2235
|
}
|
|
2105
2236
|
|
|
2106
|
-
const res = await fetchWithAuth(
|
|
2237
|
+
const res = await fetchWithAuth(`${daemonUrl(target.host)}/api/sessions/${encodeURIComponent(target.id)}`);
|
|
2107
2238
|
const data = await res.json();
|
|
2108
2239
|
if (!res.ok) {
|
|
2109
2240
|
console.error(`❌ Error: ${data.error}`);
|
|
@@ -2556,7 +2687,7 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2556
2687
|
reply_to: orchestratorId,
|
|
2557
2688
|
thread_id: threadId
|
|
2558
2689
|
};
|
|
2559
|
-
const resp = await fetchWithAuth(
|
|
2690
|
+
const resp = await fetchWithAuth(`${daemonUrl(host)}/api/sessions/${encodeURIComponent(session.id)}/inject`, {
|
|
2560
2691
|
method: 'POST',
|
|
2561
2692
|
headers: { 'Content-Type': 'application/json' },
|
|
2562
2693
|
body: JSON.stringify(body)
|
|
@@ -2565,7 +2696,7 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2565
2696
|
// Submit after text injection (300ms delay handled by daemon)
|
|
2566
2697
|
setTimeout(async () => {
|
|
2567
2698
|
try {
|
|
2568
|
-
await fetchWithAuth(
|
|
2699
|
+
await fetchWithAuth(`${daemonUrl(host)}/api/sessions/${encodeURIComponent(session.id)}/submit`, { method: 'POST' });
|
|
2569
2700
|
} catch {}
|
|
2570
2701
|
}, 500);
|
|
2571
2702
|
console.log(` ✅ Injected to ${session.id}`);
|
|
@@ -2815,6 +2946,43 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2815
2946
|
return;
|
|
2816
2947
|
}
|
|
2817
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
|
+
|
|
2818
2986
|
// telepty disconnect [<name> | --all]
|
|
2819
2987
|
if (cmd === 'disconnect') {
|
|
2820
2988
|
if (args[1] === '--all') {
|
|
@@ -2844,8 +3012,9 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2844
3012
|
|
|
2845
3013
|
const active = crossMachine.listActivePeers();
|
|
2846
3014
|
const known = crossMachine.listKnownPeers();
|
|
3015
|
+
const httpPeers = crossMachine.listHttpPeers();
|
|
2847
3016
|
|
|
2848
|
-
console.log('\x1b[1mConnected Peers:\x1b[0m');
|
|
3017
|
+
console.log('\x1b[1mConnected Peers (SSH ControlMaster):\x1b[0m');
|
|
2849
3018
|
if (active.length === 0) {
|
|
2850
3019
|
console.log(' (none)');
|
|
2851
3020
|
} else {
|
|
@@ -2855,7 +3024,8 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2855
3024
|
}
|
|
2856
3025
|
|
|
2857
3026
|
const knownNames = Object.keys(known);
|
|
2858
|
-
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));
|
|
2859
3029
|
if (disconnected.length > 0) {
|
|
2860
3030
|
console.log('\n\x1b[1mKnown Peers (disconnected):\x1b[0m');
|
|
2861
3031
|
for (const name of disconnected) {
|
|
@@ -2863,6 +3033,14 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2863
3033
|
console.log(` \x1b[90m○\x1b[0m ${name} (${p.target}) — last: ${p.lastConnected || 'never'}`);
|
|
2864
3034
|
}
|
|
2865
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
|
+
}
|
|
2866
3044
|
return;
|
|
2867
3045
|
}
|
|
2868
3046
|
|
|
@@ -2880,7 +3058,7 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2880
3058
|
let connectedHosts = 0;
|
|
2881
3059
|
|
|
2882
3060
|
hosts.forEach((host) => {
|
|
2883
|
-
const wsUrl =
|
|
3061
|
+
const wsUrl = `${daemonWsUrl(host)}/api/bus?token=${encodeURIComponent(getAuthToken())}`;
|
|
2884
3062
|
const ws = new WebSocket(wsUrl);
|
|
2885
3063
|
|
|
2886
3064
|
ws.on('open', () => {
|
|
@@ -2958,7 +3136,8 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
2958
3136
|
telepty read-screen <id[@host]> [--lines N] Read session screen buffer
|
|
2959
3137
|
|
|
2960
3138
|
\x1b[1mCross-Machine:\x1b[0m
|
|
2961
|
-
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)
|
|
2962
3141
|
telepty disconnect <name> | --all Disconnect remote host
|
|
2963
3142
|
telepty peers [--remove <name>] List connected peers
|
|
2964
3143
|
|