@dmsdc-ai/aigentry-telepty 0.1.67 → 0.1.69
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/cli.js +92 -21
- package/cross-machine.js +221 -0
- package/daemon.js +108 -25
- package/package.json +1 -1
- package/terminal-backend.js +137 -0
package/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ const { attachInteractiveTerminal, getTerminalSize } = require('./interactive-te
|
|
|
15
15
|
const { getRuntimeInfo } = require('./runtime-info');
|
|
16
16
|
const { formatHostLabel, groupSessionsByHost, pickSessionTarget } = require('./session-routing');
|
|
17
17
|
const { runInteractiveSkillInstaller } = require('./skill-installer');
|
|
18
|
+
const crossMachine = require('./cross-machine');
|
|
18
19
|
const args = process.argv.slice(2);
|
|
19
20
|
let pendingTerminalInputError = null;
|
|
20
21
|
let simulatedPromptErrorInjected = false;
|
|
@@ -208,30 +209,16 @@ function getDiscoveryHosts() {
|
|
|
208
209
|
.filter(Boolean);
|
|
209
210
|
extraHosts.forEach((host) => hosts.add(host));
|
|
210
211
|
|
|
211
|
-
//
|
|
212
|
+
// Include relay peers for cross-machine session discovery
|
|
212
213
|
const relayPeers = String(process.env.TELEPTY_RELAY_PEERS || '')
|
|
213
214
|
.split(',')
|
|
214
215
|
.map((host) => host.trim())
|
|
215
216
|
.filter(Boolean);
|
|
216
217
|
relayPeers.forEach((host) => hosts.add(host));
|
|
217
218
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
const tsStatus = execSync('tailscale status --json', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
224
|
-
const tsData = JSON.parse(tsStatus);
|
|
225
|
-
if (tsData && tsData.Peer) {
|
|
226
|
-
for (const peer of Object.values(tsData.Peer)) {
|
|
227
|
-
if (peer.Online && peer.TailscaleIPs && peer.TailscaleIPs.length > 0) {
|
|
228
|
-
hosts.add(peer.TailscaleIPs[0]);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
} catch (e) {
|
|
233
|
-
// Tailscale not available or not running, ignore
|
|
234
|
-
}
|
|
219
|
+
// Include SSH tunnel-connected peers
|
|
220
|
+
const connectedHosts = crossMachine.getConnectedHosts();
|
|
221
|
+
connectedHosts.forEach((host) => hosts.add(host));
|
|
235
222
|
|
|
236
223
|
return Array.from(hosts);
|
|
237
224
|
}
|
|
@@ -242,7 +229,7 @@ async function discoverSessions(options = {}) {
|
|
|
242
229
|
const allSessions = [];
|
|
243
230
|
|
|
244
231
|
if (!options.silent) {
|
|
245
|
-
process.stdout.write('\x1b[36m🔍 Discovering active sessions across
|
|
232
|
+
process.stdout.write('\x1b[36m🔍 Discovering active sessions across connected machines...\x1b[0m\n');
|
|
246
233
|
}
|
|
247
234
|
|
|
248
235
|
await Promise.all(hosts.map(async (host) => {
|
|
@@ -710,7 +697,8 @@ async function main() {
|
|
|
710
697
|
command,
|
|
711
698
|
cwd: process.cwd(),
|
|
712
699
|
backend: detectedBackend,
|
|
713
|
-
cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null
|
|
700
|
+
cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
|
|
701
|
+
cmux_surface_id: process.env.CMUX_SURFACE_ID || null
|
|
714
702
|
})
|
|
715
703
|
});
|
|
716
704
|
const data = await res.json();
|
|
@@ -789,7 +777,8 @@ async function main() {
|
|
|
789
777
|
command,
|
|
790
778
|
cwd: process.cwd(),
|
|
791
779
|
backend: detectedBackend,
|
|
792
|
-
cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null
|
|
780
|
+
cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
|
|
781
|
+
cmux_surface_id: process.env.CMUX_SURFACE_ID || null
|
|
793
782
|
})
|
|
794
783
|
});
|
|
795
784
|
} catch (e) {
|
|
@@ -1874,6 +1863,85 @@ Discuss the following topic from your project's perspective. Engage with other s
|
|
|
1874
1863
|
return;
|
|
1875
1864
|
}
|
|
1876
1865
|
|
|
1866
|
+
// telepty connect <target> [--name <name>] [--port <port>]
|
|
1867
|
+
if (cmd === 'connect') {
|
|
1868
|
+
const target = args[1];
|
|
1869
|
+
if (!target) {
|
|
1870
|
+
console.error('❌ Usage: telepty connect <user@host> [--name <name>] [--port <port>]');
|
|
1871
|
+
process.exit(1);
|
|
1872
|
+
}
|
|
1873
|
+
const nameFlag = args.indexOf('--name');
|
|
1874
|
+
const portFlag = args.indexOf('--port');
|
|
1875
|
+
const options = {};
|
|
1876
|
+
if (nameFlag !== -1 && args[nameFlag + 1]) options.name = args[nameFlag + 1];
|
|
1877
|
+
if (portFlag !== -1 && args[portFlag + 1]) options.port = Number(args[portFlag + 1]);
|
|
1878
|
+
|
|
1879
|
+
process.stdout.write(`\x1b[36m🔗 Connecting to ${target}...\x1b[0m\n`);
|
|
1880
|
+
const result = await crossMachine.connect(target, options);
|
|
1881
|
+
if (result.success) {
|
|
1882
|
+
console.log(`\x1b[32m✅ Connected to ${result.name}\x1b[0m`);
|
|
1883
|
+
console.log(` Machine ID: ${result.machineId}`);
|
|
1884
|
+
console.log(` Local port: ${result.localPort}`);
|
|
1885
|
+
console.log(` Version: ${result.version}`);
|
|
1886
|
+
console.log(`\nSessions on ${result.name} are now discoverable via \x1b[36mtelepty list\x1b[0m`);
|
|
1887
|
+
} else {
|
|
1888
|
+
console.error(`\x1b[31m❌ ${result.error}\x1b[0m`);
|
|
1889
|
+
process.exit(1);
|
|
1890
|
+
}
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// telepty disconnect [<name> | --all]
|
|
1895
|
+
if (cmd === 'disconnect') {
|
|
1896
|
+
if (args[1] === '--all') {
|
|
1897
|
+
const result = crossMachine.disconnectAll();
|
|
1898
|
+
console.log(`\x1b[32m✅ Disconnected from ${result.disconnected.length} peer(s)\x1b[0m`);
|
|
1899
|
+
} else if (args[1]) {
|
|
1900
|
+
const result = crossMachine.disconnect(args[1]);
|
|
1901
|
+
if (result.success) {
|
|
1902
|
+
console.log(`\x1b[32m✅ Disconnected from ${result.name}\x1b[0m`);
|
|
1903
|
+
} else {
|
|
1904
|
+
console.error(`\x1b[31m❌ ${result.error}\x1b[0m`);
|
|
1905
|
+
}
|
|
1906
|
+
} else {
|
|
1907
|
+
console.error('❌ Usage: telepty disconnect <name> | --all');
|
|
1908
|
+
process.exit(1);
|
|
1909
|
+
}
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// telepty peers [--remove <name>]
|
|
1914
|
+
if (cmd === 'peers') {
|
|
1915
|
+
if (args[1] === '--remove' && args[2]) {
|
|
1916
|
+
crossMachine.removePeer(args[2]);
|
|
1917
|
+
console.log(`\x1b[32m✅ Removed peer ${args[2]}\x1b[0m`);
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
const active = crossMachine.listActivePeers();
|
|
1922
|
+
const known = crossMachine.listKnownPeers();
|
|
1923
|
+
|
|
1924
|
+
console.log('\x1b[1mConnected Peers:\x1b[0m');
|
|
1925
|
+
if (active.length === 0) {
|
|
1926
|
+
console.log(' (none)');
|
|
1927
|
+
} else {
|
|
1928
|
+
for (const peer of active) {
|
|
1929
|
+
console.log(` \x1b[32m●\x1b[0m ${peer.name} (${peer.target}) → localhost:${peer.localPort} [${peer.machineId}]`);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
const knownNames = Object.keys(known);
|
|
1934
|
+
const disconnected = knownNames.filter(n => !active.find(a => a.name === n));
|
|
1935
|
+
if (disconnected.length > 0) {
|
|
1936
|
+
console.log('\n\x1b[1mKnown Peers (disconnected):\x1b[0m');
|
|
1937
|
+
for (const name of disconnected) {
|
|
1938
|
+
const p = known[name];
|
|
1939
|
+
console.log(` \x1b[90m○\x1b[0m ${name} (${p.target}) — last: ${p.lastConnected || 'never'}`);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1877
1945
|
if (cmd === 'listen' || cmd === 'monitor') {
|
|
1878
1946
|
await ensureDaemonRunning();
|
|
1879
1947
|
|
|
@@ -1959,6 +2027,9 @@ Usage:
|
|
|
1959
2027
|
telepty multicast <id1[@host],id2[@host]> "<prompt>" Inject text into multiple specific sessions
|
|
1960
2028
|
telepty broadcast "<prompt>" Inject text into ALL active sessions
|
|
1961
2029
|
telepty rename <old_id[@host]> <new_id> Rename a session (updates terminal title too)
|
|
2030
|
+
telepty connect <user@host> [--name N] [--port P] Connect to a remote machine via SSH tunnel
|
|
2031
|
+
telepty disconnect <name> | --all Disconnect from a remote machine
|
|
2032
|
+
telepty peers [--remove <name>] List connected and known peers
|
|
1962
2033
|
telepty listen Listen to the event bus and print JSON to stdout
|
|
1963
2034
|
telepty monitor Human-readable real-time billboard of bus events
|
|
1964
2035
|
telepty update Update telepty to the latest version
|
package/cross-machine.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const PEERS_PATH = path.join(os.homedir(), '.telepty', 'peers.json');
|
|
9
|
+
const BASE_LOCAL_PORT = 3849; // tunnels start at this port
|
|
10
|
+
|
|
11
|
+
// In-memory active tunnels
|
|
12
|
+
const activeTunnels = new Map(); // name -> { process, localPort, target, connectedAt, ... }
|
|
13
|
+
|
|
14
|
+
function loadPeers() {
|
|
15
|
+
try {
|
|
16
|
+
if (!fs.existsSync(PEERS_PATH)) return { peers: {} };
|
|
17
|
+
return JSON.parse(fs.readFileSync(PEERS_PATH, 'utf8'));
|
|
18
|
+
} catch { return { peers: {} }; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function savePeers(data) {
|
|
22
|
+
try {
|
|
23
|
+
fs.mkdirSync(path.dirname(PEERS_PATH), { recursive: true });
|
|
24
|
+
fs.writeFileSync(PEERS_PATH, JSON.stringify(data, null, 2));
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getNextLocalPort() {
|
|
29
|
+
const usedPorts = new Set([...activeTunnels.values()].map(t => t.localPort));
|
|
30
|
+
let port = BASE_LOCAL_PORT;
|
|
31
|
+
while (usedPorts.has(port)) port++;
|
|
32
|
+
return port;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Connect to a remote machine via SSH tunnel.
|
|
37
|
+
* @param {string} target - "user@host" or "host" (uses current user)
|
|
38
|
+
* @param {object} options - { name, port }
|
|
39
|
+
* @returns {Promise<object>} - { success, name, localPort, machineId, version } or { success: false, error }
|
|
40
|
+
*/
|
|
41
|
+
async function connect(target, options = {}) {
|
|
42
|
+
const remotePort = options.port || 3848;
|
|
43
|
+
const localPort = getNextLocalPort();
|
|
44
|
+
|
|
45
|
+
// Parse target
|
|
46
|
+
let sshTarget = target;
|
|
47
|
+
if (!target.includes('@')) {
|
|
48
|
+
sshTarget = `${os.userInfo().username}@${target}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const name = options.name || target.split('@').pop().split('.')[0]; // short hostname
|
|
52
|
+
|
|
53
|
+
// Check if already connected
|
|
54
|
+
if (activeTunnels.has(name)) {
|
|
55
|
+
const existing = activeTunnels.get(name);
|
|
56
|
+
return { success: false, error: `Already connected to ${name} on port ${existing.localPort}` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create SSH tunnel
|
|
60
|
+
const tunnel = spawn('ssh', [
|
|
61
|
+
'-N', // No remote command
|
|
62
|
+
'-L', `${localPort}:localhost:${remotePort}`, // Local port forwarding
|
|
63
|
+
'-o', 'ServerAliveInterval=30', // Keep alive
|
|
64
|
+
'-o', 'ServerAliveCountMax=3', // Disconnect after 3 missed keepalives
|
|
65
|
+
'-o', 'ExitOnForwardFailure=yes', // Fail if port forwarding fails
|
|
66
|
+
'-o', 'ConnectTimeout=10', // Connection timeout
|
|
67
|
+
'-o', 'StrictHostKeyChecking=accept-new', // Auto-accept new host keys
|
|
68
|
+
sshTarget
|
|
69
|
+
], {
|
|
70
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
71
|
+
detached: false
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Wait for tunnel to establish or fail
|
|
75
|
+
const result = await new Promise((resolve) => {
|
|
76
|
+
let stderr = '';
|
|
77
|
+
const timeout = setTimeout(() => {
|
|
78
|
+
// If process is still running after 5s, tunnel is up
|
|
79
|
+
if (!tunnel.killed && tunnel.exitCode === null) {
|
|
80
|
+
resolve({ success: true });
|
|
81
|
+
} else {
|
|
82
|
+
resolve({ success: false, error: stderr || 'Connection timeout' });
|
|
83
|
+
}
|
|
84
|
+
}, 5000);
|
|
85
|
+
|
|
86
|
+
tunnel.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
87
|
+
tunnel.on('exit', (code) => {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
if (code !== 0) {
|
|
90
|
+
resolve({ success: false, error: stderr || `SSH exited with code ${code}` });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
tunnel.on('error', (err) => {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
resolve({ success: false, error: err.message });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!result.success) {
|
|
100
|
+
tunnel.kill();
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Verify remote daemon is accessible through tunnel
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch(`http://127.0.0.1:${localPort}/api/meta`, {
|
|
107
|
+
signal: AbortSignal.timeout(3000)
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) throw new Error('Daemon not responding');
|
|
110
|
+
const meta = await res.json();
|
|
111
|
+
|
|
112
|
+
const peerInfo = {
|
|
113
|
+
process: tunnel,
|
|
114
|
+
localPort,
|
|
115
|
+
target: sshTarget,
|
|
116
|
+
name,
|
|
117
|
+
machineId: meta.machine_id || name,
|
|
118
|
+
version: meta.version || 'unknown',
|
|
119
|
+
connectedAt: new Date().toISOString()
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
activeTunnels.set(name, peerInfo);
|
|
123
|
+
|
|
124
|
+
// Persist peer for reconnection
|
|
125
|
+
const peers = loadPeers();
|
|
126
|
+
peers.peers[name] = {
|
|
127
|
+
target: sshTarget,
|
|
128
|
+
remotePort,
|
|
129
|
+
lastConnected: peerInfo.connectedAt,
|
|
130
|
+
machineId: peerInfo.machineId
|
|
131
|
+
};
|
|
132
|
+
savePeers(peers);
|
|
133
|
+
|
|
134
|
+
// Monitor tunnel health
|
|
135
|
+
tunnel.on('exit', () => {
|
|
136
|
+
console.log(`[PEER] SSH tunnel to ${name} disconnected`);
|
|
137
|
+
activeTunnels.delete(name);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
name,
|
|
143
|
+
localPort,
|
|
144
|
+
machineId: peerInfo.machineId,
|
|
145
|
+
version: peerInfo.version
|
|
146
|
+
};
|
|
147
|
+
} catch (err) {
|
|
148
|
+
tunnel.kill();
|
|
149
|
+
return { success: false, error: `Remote daemon not accessible: ${err.message}` };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function disconnect(name) {
|
|
154
|
+
const tunnel = activeTunnels.get(name);
|
|
155
|
+
if (!tunnel) {
|
|
156
|
+
return { success: false, error: `Not connected to ${name}` };
|
|
157
|
+
}
|
|
158
|
+
tunnel.process.kill();
|
|
159
|
+
activeTunnels.delete(name);
|
|
160
|
+
return { success: true, name };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function disconnectAll() {
|
|
164
|
+
const names = [...activeTunnels.keys()];
|
|
165
|
+
names.forEach(name => disconnect(name));
|
|
166
|
+
return { disconnected: names };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function listActivePeers() {
|
|
170
|
+
return [...activeTunnels.entries()].map(([name, info]) => ({
|
|
171
|
+
name,
|
|
172
|
+
target: info.target,
|
|
173
|
+
localPort: info.localPort,
|
|
174
|
+
machineId: info.machineId,
|
|
175
|
+
connectedAt: info.connectedAt,
|
|
176
|
+
host: `127.0.0.1:${info.localPort}`
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function listKnownPeers() {
|
|
181
|
+
return loadPeers().peers;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get all connected peer hosts for discovery.
|
|
186
|
+
* @returns {string[]} Array of "127.0.0.1:PORT" strings
|
|
187
|
+
*/
|
|
188
|
+
function getConnectedHosts() {
|
|
189
|
+
return [...activeTunnels.values()].map(t => `127.0.0.1:${t.localPort}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get connected host for a specific peer.
|
|
194
|
+
* @param {string} name - Peer name
|
|
195
|
+
* @returns {string|null} "127.0.0.1:PORT" or null
|
|
196
|
+
*/
|
|
197
|
+
function getPeerHost(name) {
|
|
198
|
+
const tunnel = activeTunnels.get(name);
|
|
199
|
+
return tunnel ? `127.0.0.1:${tunnel.localPort}` : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function removePeer(name) {
|
|
203
|
+
disconnect(name); // disconnect if active
|
|
204
|
+
const peers = loadPeers();
|
|
205
|
+
delete peers.peers[name];
|
|
206
|
+
savePeers(peers);
|
|
207
|
+
return { success: true };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
connect,
|
|
212
|
+
disconnect,
|
|
213
|
+
disconnectAll,
|
|
214
|
+
listActivePeers,
|
|
215
|
+
listKnownPeers,
|
|
216
|
+
getConnectedHosts,
|
|
217
|
+
getPeerHost,
|
|
218
|
+
removePeer,
|
|
219
|
+
loadPeers,
|
|
220
|
+
PEERS_PATH
|
|
221
|
+
};
|
package/daemon.js
CHANGED
|
@@ -8,6 +8,7 @@ const { getConfig } = require('./auth');
|
|
|
8
8
|
const pkg = require('./package.json');
|
|
9
9
|
const { claimDaemonState, clearDaemonState } = require('./daemon-control');
|
|
10
10
|
const { checkEntitlement } = require('./entitlement');
|
|
11
|
+
const terminalBackend = require('./terminal-backend');
|
|
11
12
|
|
|
12
13
|
const config = getConfig();
|
|
13
14
|
const EXPECTED_TOKEN = config.authToken;
|
|
@@ -19,7 +20,7 @@ function persistSessions() {
|
|
|
19
20
|
try {
|
|
20
21
|
const data = {};
|
|
21
22
|
for (const [id, s] of Object.entries(sessions)) {
|
|
22
|
-
data[id] = { id, type: s.type, command: s.command, cwd: s.cwd, backend: s.backend || null, cmuxWorkspaceId: s.cmuxWorkspaceId || null, createdAt: s.createdAt, lastActivityAt: s.lastActivityAt || null };
|
|
23
|
+
data[id] = { id, type: s.type, command: s.command, cwd: s.cwd, backend: s.backend || null, cmuxWorkspaceId: s.cmuxWorkspaceId || null, cmuxSurfaceId: s.cmuxSurfaceId || null, createdAt: s.createdAt, lastActivityAt: s.lastActivityAt || null };
|
|
23
24
|
}
|
|
24
25
|
fs.mkdirSync(require('path').dirname(SESSION_PERSIST_PATH), { recursive: true });
|
|
25
26
|
fs.writeFileSync(SESSION_PERSIST_PATH, JSON.stringify(data, null, 2));
|
|
@@ -91,14 +92,12 @@ function verifyJwt(token) {
|
|
|
91
92
|
function isAllowedPeer(ip) {
|
|
92
93
|
if (!ip) return false;
|
|
93
94
|
const cleanIp = ip.replace('::ffff:', '');
|
|
94
|
-
// Localhost always allowed
|
|
95
|
+
// Localhost always allowed (includes SSH tunnel traffic)
|
|
95
96
|
if (cleanIp === '127.0.0.1' || ip === '::1') return true;
|
|
96
|
-
// Tailscale range (100.x.y.z)
|
|
97
|
-
if (cleanIp.startsWith('100.')) return true;
|
|
98
97
|
// Peer allowlist
|
|
99
98
|
if (PEER_ALLOWLIST.length > 0) return PEER_ALLOWLIST.includes(cleanIp);
|
|
100
99
|
// No allowlist = allow all authenticated
|
|
101
|
-
return
|
|
100
|
+
return true;
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
// Authentication Middleware
|
|
@@ -106,7 +105,7 @@ app.use((req, res, next) => {
|
|
|
106
105
|
const clientIp = req.ip;
|
|
107
106
|
|
|
108
107
|
if (isAllowedPeer(clientIp)) {
|
|
109
|
-
return next(); // Trust local
|
|
108
|
+
return next(); // Trust local and allowlisted peers (SSH tunnels arrive as localhost)
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
const token = req.headers['x-telepty-token'] || req.query.token;
|
|
@@ -140,6 +139,10 @@ const sessions = {};
|
|
|
140
139
|
const handoffs = {};
|
|
141
140
|
const threads = {};
|
|
142
141
|
|
|
142
|
+
// Detect terminal environment at daemon startup
|
|
143
|
+
const DETECTED_TERMINAL = terminalBackend.detectTerminal();
|
|
144
|
+
console.log(`[DAEMON] Terminal backend: ${DETECTED_TERMINAL}`);
|
|
145
|
+
|
|
143
146
|
// Restore persisted session metadata (wrapped sessions await reconnect)
|
|
144
147
|
const _persisted = loadPersistedSessions();
|
|
145
148
|
for (const [id, meta] of Object.entries(_persisted)) {
|
|
@@ -306,7 +309,7 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
306
309
|
});
|
|
307
310
|
|
|
308
311
|
app.post('/api/sessions/register', (req, res) => {
|
|
309
|
-
const { session_id, command, cwd = process.cwd(), backend, cmux_workspace_id } = req.body;
|
|
312
|
+
const { session_id, command, cwd = process.cwd(), backend, cmux_workspace_id, cmux_surface_id } = req.body;
|
|
310
313
|
if (!session_id) return res.status(400).json({ error: 'session_id is required' });
|
|
311
314
|
// Entitlement: check session limit for new registrations
|
|
312
315
|
if (!sessions[session_id]) {
|
|
@@ -324,6 +327,7 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
324
327
|
if (cwd) existing.cwd = cwd;
|
|
325
328
|
if (backend) existing.backend = backend;
|
|
326
329
|
if (cmux_workspace_id) existing.cmuxWorkspaceId = cmux_workspace_id;
|
|
330
|
+
if (cmux_surface_id) existing.cmuxSurfaceId = cmux_surface_id;
|
|
327
331
|
console.log(`[REGISTER] Re-registered session ${session_id} (updated metadata)`);
|
|
328
332
|
return res.status(200).json({ session_id, type: 'wrapped', command: existing.command, cwd: existing.cwd, reregistered: true });
|
|
329
333
|
}
|
|
@@ -337,6 +341,7 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
337
341
|
cwd,
|
|
338
342
|
backend: backend || 'kitty',
|
|
339
343
|
cmuxWorkspaceId: cmux_workspace_id || null,
|
|
344
|
+
cmuxSurfaceId: cmux_surface_id || null,
|
|
340
345
|
createdAt: new Date().toISOString(),
|
|
341
346
|
lastActivityAt: new Date().toISOString(),
|
|
342
347
|
clients: new Set(),
|
|
@@ -395,6 +400,7 @@ app.get('/api/sessions', (req, res) => {
|
|
|
395
400
|
cwd: session.cwd,
|
|
396
401
|
backend: session.backend || 'kitty',
|
|
397
402
|
cmuxWorkspaceId: session.cmuxWorkspaceId || null,
|
|
403
|
+
cmuxSurfaceId: session.cmuxSurfaceId || null,
|
|
398
404
|
createdAt: session.createdAt,
|
|
399
405
|
lastActivityAt: session.lastActivityAt || null,
|
|
400
406
|
idleSeconds,
|
|
@@ -438,10 +444,24 @@ app.get('/api/meta', (req, res) => {
|
|
|
438
444
|
host: HOST,
|
|
439
445
|
port: Number(PORT),
|
|
440
446
|
machine_id: MACHINE_ID,
|
|
441
|
-
|
|
447
|
+
terminal: DETECTED_TERMINAL,
|
|
448
|
+
capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon', 'handoff-inbox', 'deliberation-threads', 'cross-machine']
|
|
442
449
|
});
|
|
443
450
|
});
|
|
444
451
|
|
|
452
|
+
// Peer management endpoint (for cross-machine module)
|
|
453
|
+
app.get('/api/peers', (req, res) => {
|
|
454
|
+
try {
|
|
455
|
+
const crossMachine = require('./cross-machine');
|
|
456
|
+
res.json({
|
|
457
|
+
active: crossMachine.listActivePeers(),
|
|
458
|
+
known: crossMachine.listKnownPeers()
|
|
459
|
+
});
|
|
460
|
+
} catch {
|
|
461
|
+
res.json({ active: [], known: {} });
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
445
465
|
app.post('/api/sessions/multicast/inject', (req, res) => {
|
|
446
466
|
const { session_ids, prompt } = req.body;
|
|
447
467
|
if (!prompt) return res.status(400).json({ error: 'prompt is required' });
|
|
@@ -453,6 +473,27 @@ app.post('/api/sessions/multicast/inject', (req, res) => {
|
|
|
453
473
|
const session = sessions[id];
|
|
454
474
|
if (session) {
|
|
455
475
|
try {
|
|
476
|
+
// cmux auto-detect at daemon level (text + enter)
|
|
477
|
+
if (DETECTED_TERMINAL === 'cmux') {
|
|
478
|
+
const ok = terminalBackend.cmuxSendText(id, prompt);
|
|
479
|
+
if (ok) {
|
|
480
|
+
setTimeout(() => terminalBackend.cmuxSendEnter(id), 300);
|
|
481
|
+
results.successful.push({ id, strategy: 'cmux_auto' });
|
|
482
|
+
// Broadcast injection to bus
|
|
483
|
+
const busMsg = JSON.stringify({
|
|
484
|
+
type: 'injection',
|
|
485
|
+
sender: 'cli',
|
|
486
|
+
target_agent: id,
|
|
487
|
+
content: prompt,
|
|
488
|
+
timestamp: new Date().toISOString()
|
|
489
|
+
});
|
|
490
|
+
busClients.forEach(client => {
|
|
491
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
492
|
+
});
|
|
493
|
+
return; // skip WS path for this session
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
456
497
|
// Inject text first, then \r separately after delay
|
|
457
498
|
if (session.type === 'wrapped') {
|
|
458
499
|
if (session.ownerWs && session.ownerWs.readyState === 1) {
|
|
@@ -505,6 +546,16 @@ app.post('/api/sessions/broadcast/inject', (req, res) => {
|
|
|
505
546
|
Object.keys(sessions).forEach(id => {
|
|
506
547
|
const session = sessions[id];
|
|
507
548
|
try {
|
|
549
|
+
// cmux auto-detect at daemon level (text + enter)
|
|
550
|
+
if (DETECTED_TERMINAL === 'cmux') {
|
|
551
|
+
const ok = terminalBackend.cmuxSendText(id, prompt);
|
|
552
|
+
if (ok) {
|
|
553
|
+
setTimeout(() => terminalBackend.cmuxSendEnter(id), 300);
|
|
554
|
+
results.successful.push({ id, strategy: 'cmux_auto' });
|
|
555
|
+
return; // skip WS path for this session
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
508
559
|
// Inject text first, then \r separately after delay
|
|
509
560
|
if (session.type === 'wrapped') {
|
|
510
561
|
if (session.ownerWs && session.ownerWs.readyState === 1) {
|
|
@@ -718,8 +769,12 @@ app.post('/api/sessions/:id/submit', (req, res) => {
|
|
|
718
769
|
console.log(`[SUBMIT] Session ${id} (${session.command}) using strategy: ${strategy}`);
|
|
719
770
|
|
|
720
771
|
let success = false;
|
|
721
|
-
// cmux
|
|
722
|
-
if (
|
|
772
|
+
// cmux auto-detect at daemon level
|
|
773
|
+
if (DETECTED_TERMINAL === 'cmux') {
|
|
774
|
+
success = terminalBackend.cmuxSendEnter(id);
|
|
775
|
+
}
|
|
776
|
+
// Session-level cmux backend
|
|
777
|
+
if (!success && session.backend === 'cmux' && session.cmuxWorkspaceId) {
|
|
723
778
|
success = submitViaCmux(id);
|
|
724
779
|
}
|
|
725
780
|
if (!success) {
|
|
@@ -757,8 +812,12 @@ app.post('/api/sessions/submit-all', (req, res) => {
|
|
|
757
812
|
const strategy = getSubmitStrategy(session.command);
|
|
758
813
|
let success = false;
|
|
759
814
|
|
|
760
|
-
// cmux
|
|
761
|
-
if (
|
|
815
|
+
// cmux auto-detect at daemon level
|
|
816
|
+
if (DETECTED_TERMINAL === 'cmux') {
|
|
817
|
+
success = terminalBackend.cmuxSendEnter(id);
|
|
818
|
+
}
|
|
819
|
+
// Session-level cmux backend
|
|
820
|
+
if (!success && session.backend === 'cmux' && session.cmuxWorkspaceId) {
|
|
762
821
|
success = submitViaCmux(id);
|
|
763
822
|
}
|
|
764
823
|
if (!success) {
|
|
@@ -824,14 +883,23 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
824
883
|
|
|
825
884
|
let submitResult = null;
|
|
826
885
|
if (session.type === 'wrapped') {
|
|
827
|
-
// For wrapped sessions: try
|
|
828
|
-
// then
|
|
886
|
+
// For wrapped sessions: try cmux send (daemon-level auto-detect),
|
|
887
|
+
// then kitty send-text (bypasses allow bridge queue),
|
|
888
|
+
// then WS as fallback, then submit via cmux/osascript/kitty/WS
|
|
829
889
|
const sock = findKittySocket();
|
|
830
890
|
if (!session.kittyWindowId && sock) session.kittyWindowId = findKittyWindowId(sock, id);
|
|
831
891
|
const wid = session.kittyWindowId;
|
|
832
892
|
|
|
833
893
|
let kittyOk = false;
|
|
834
|
-
|
|
894
|
+
let cmuxOk = false;
|
|
895
|
+
|
|
896
|
+
// cmux backend: send text directly to surface (daemon-level auto-detect)
|
|
897
|
+
if (DETECTED_TERMINAL === 'cmux') {
|
|
898
|
+
cmuxOk = terminalBackend.cmuxSendText(id, finalPrompt);
|
|
899
|
+
if (cmuxOk) console.log(`[INJECT] cmux send for ${id}`);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (!cmuxOk && wid && sock) {
|
|
835
903
|
// Kitty send-text primary (bypasses allow bridge queue)
|
|
836
904
|
try {
|
|
837
905
|
const escaped = finalPrompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
|
|
@@ -845,7 +913,7 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
845
913
|
session.kittyWindowId = null;
|
|
846
914
|
}
|
|
847
915
|
}
|
|
848
|
-
if (!kittyOk) {
|
|
916
|
+
if (!cmuxOk && !kittyOk) {
|
|
849
917
|
// Fallback: WS (works with new allow bridges that have queue flush)
|
|
850
918
|
const wsOk = writeToSession(finalPrompt);
|
|
851
919
|
if (!wsOk) {
|
|
@@ -858,20 +926,26 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
858
926
|
setTimeout(() => {
|
|
859
927
|
let submitted = false;
|
|
860
928
|
|
|
861
|
-
// 1. cmux backend:
|
|
862
|
-
if (
|
|
863
|
-
submitted =
|
|
929
|
+
// 1. cmux backend: send-key return to surface (daemon-level auto-detect)
|
|
930
|
+
if (DETECTED_TERMINAL === 'cmux') {
|
|
931
|
+
submitted = terminalBackend.cmuxSendEnter(id);
|
|
864
932
|
if (submitted) console.log(`[INJECT] cmux submit for ${id}`);
|
|
865
933
|
}
|
|
866
934
|
|
|
867
|
-
// 2.
|
|
935
|
+
// 2. Session-level cmux (registered backend)
|
|
936
|
+
if (!submitted && session.backend === 'cmux' && session.cmuxWorkspaceId) {
|
|
937
|
+
submitted = submitViaCmux(id);
|
|
938
|
+
if (submitted) console.log(`[INJECT] cmux session-level submit for ${id}`);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// 3. kitty/default: osascript primary
|
|
868
942
|
if (!submitted) {
|
|
869
943
|
const submitStrategy = getSubmitStrategy(session.command);
|
|
870
944
|
const keyCombo = submitStrategy === 'osascript_cmd_enter' ? 'cmd_enter' : 'return_key';
|
|
871
945
|
submitted = submitViaOsascript(id, keyCombo);
|
|
872
946
|
}
|
|
873
947
|
|
|
874
|
-
//
|
|
948
|
+
// 4. Fallback: kitty send-text → WS
|
|
875
949
|
if (!submitted) {
|
|
876
950
|
console.log(`[INJECT] submit fallback for ${id}`);
|
|
877
951
|
if (wid && sock) {
|
|
@@ -896,7 +970,7 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
896
970
|
} catch {}
|
|
897
971
|
}
|
|
898
972
|
}, 500);
|
|
899
|
-
submitResult = { deferred: true, strategy: session.backend === 'cmux' ? 'cmux_with_fallback' : 'osascript_with_fallback' };
|
|
973
|
+
submitResult = { deferred: true, strategy: DETECTED_TERMINAL === 'cmux' ? 'cmux_auto' : (session.backend === 'cmux' ? 'cmux_with_fallback' : 'osascript_with_fallback') };
|
|
900
974
|
}
|
|
901
975
|
} else {
|
|
902
976
|
// Spawned sessions: direct PTY write
|
|
@@ -1049,13 +1123,22 @@ function busAutoRoute(msg) {
|
|
|
1049
1123
|
const prompt = (msg.payload && msg.payload.prompt) || msg.content || msg.prompt || JSON.stringify(msg);
|
|
1050
1124
|
const inject_id = crypto.randomUUID();
|
|
1051
1125
|
|
|
1052
|
-
// Write to session (kitty
|
|
1126
|
+
// Write to session (cmux auto-detect > kitty > session-level cmux > WS fallback)
|
|
1053
1127
|
const sock = findKittySocket();
|
|
1054
1128
|
if (!targetSession.kittyWindowId && sock) targetSession.kittyWindowId = findKittyWindowId(sock, targetId);
|
|
1055
1129
|
const wid = targetSession.kittyWindowId;
|
|
1056
1130
|
let delivered = false;
|
|
1057
1131
|
|
|
1058
|
-
|
|
1132
|
+
// cmux backend: send text + enter to surface (daemon-level auto-detect)
|
|
1133
|
+
if (!delivered && DETECTED_TERMINAL === 'cmux') {
|
|
1134
|
+
const textOk = terminalBackend.cmuxSendText(targetId, prompt);
|
|
1135
|
+
if (textOk) {
|
|
1136
|
+
setTimeout(() => terminalBackend.cmuxSendEnter(targetId), 500);
|
|
1137
|
+
delivered = true;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (!delivered && wid && sock && targetSession.type === 'wrapped') {
|
|
1059
1142
|
try {
|
|
1060
1143
|
const escaped = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
|
|
1061
1144
|
require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} '${escaped}'`, {
|
|
@@ -1071,7 +1154,7 @@ function busAutoRoute(msg) {
|
|
|
1071
1154
|
delivered = true;
|
|
1072
1155
|
} catch {}
|
|
1073
1156
|
}
|
|
1074
|
-
// cmux backend: use WS for text, cmux send-key for enter
|
|
1157
|
+
// Session-level cmux backend: use WS for text, cmux send-key for enter
|
|
1075
1158
|
if (!delivered && targetSession.backend === 'cmux' && targetSession.cmuxWorkspaceId) {
|
|
1076
1159
|
if (targetSession.type === 'wrapped' && targetSession.ownerWs && targetSession.ownerWs.readyState === 1) {
|
|
1077
1160
|
targetSession.ownerWs.send(JSON.stringify({ type: 'inject', data: prompt }));
|
package/package.json
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// Detect terminal environment at daemon level
|
|
6
|
+
function detectTerminal() {
|
|
7
|
+
// 1. cmux: check env var or cmux ping
|
|
8
|
+
if (process.env.CMUX_WORKSPACE_ID) {
|
|
9
|
+
try {
|
|
10
|
+
execSync('cmux ping', { timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
11
|
+
return 'cmux';
|
|
12
|
+
} catch {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 2. kitty: check for socket
|
|
16
|
+
try {
|
|
17
|
+
const files = require('fs').readdirSync('/tmp').filter(f => f.startsWith('kitty-sock'));
|
|
18
|
+
if (files.length > 0) return 'kitty';
|
|
19
|
+
} catch {}
|
|
20
|
+
|
|
21
|
+
// 3. headless fallback
|
|
22
|
+
return 'headless';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Cache: sessionId -> surfaceRef
|
|
26
|
+
const surfaceCache = new Map();
|
|
27
|
+
let lastCacheRefresh = 0;
|
|
28
|
+
const CACHE_TTL = 30000; // 30 seconds
|
|
29
|
+
|
|
30
|
+
// Build session -> cmux surface mapping from tab titles
|
|
31
|
+
function refreshSurfaceCache() {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (now - lastCacheRefresh < CACHE_TTL && surfaceCache.size > 0) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Find number of workspaces from list-windows
|
|
37
|
+
const windowsOutput = execSync('cmux list-windows', { timeout: 5000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
38
|
+
const workspacesMatch = windowsOutput.match(/workspaces=(\d+)/);
|
|
39
|
+
const workspaceCount = workspacesMatch ? parseInt(workspacesMatch[1]) : 10;
|
|
40
|
+
|
|
41
|
+
surfaceCache.clear();
|
|
42
|
+
for (let i = 1; i <= workspaceCount; i++) {
|
|
43
|
+
try {
|
|
44
|
+
const output = execSync(`cmux list-pane-surfaces --workspace workspace:${i}`, {
|
|
45
|
+
timeout: 3000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
|
|
46
|
+
});
|
|
47
|
+
// Parse: "* surface:1 ⚡ telepty :: aigentry-orchestrator-claude [selected]"
|
|
48
|
+
const lines = output.split('\n').filter(l => l.trim());
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const surfaceMatch = line.match(/surface:(\d+)/);
|
|
51
|
+
const sessionMatch = line.match(/telepty\s*::\s*(\S+)/);
|
|
52
|
+
if (surfaceMatch && sessionMatch) {
|
|
53
|
+
surfaceCache.set(sessionMatch[1], `surface:${surfaceMatch[1]}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
lastCacheRefresh = now;
|
|
59
|
+
console.log(`[BACKEND] Refreshed cmux surface cache: ${surfaceCache.size} sessions mapped`);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(`[BACKEND] Failed to refresh surface cache:`, err.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Find cmux surface ref for a session
|
|
66
|
+
function findSurface(sessionId) {
|
|
67
|
+
refreshSurfaceCache();
|
|
68
|
+
|
|
69
|
+
// Direct match
|
|
70
|
+
if (surfaceCache.has(sessionId)) return surfaceCache.get(sessionId);
|
|
71
|
+
|
|
72
|
+
// Prefix match (e.g., "aigentry-orchestrator" matches "aigentry-orchestrator-claude")
|
|
73
|
+
for (const [id, ref] of surfaceCache.entries()) {
|
|
74
|
+
if (id.startsWith(sessionId) || sessionId.startsWith(id)) return ref;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Send text to a cmux surface
|
|
81
|
+
function cmuxSendText(sessionId, text) {
|
|
82
|
+
const surface = findSurface(sessionId);
|
|
83
|
+
if (!surface) return false;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Escape single quotes for shell
|
|
87
|
+
const escaped = text.replace(/'/g, "'\\''");
|
|
88
|
+
execSync(`cmux send --surface ${surface} '${escaped}'`, {
|
|
89
|
+
timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
90
|
+
});
|
|
91
|
+
console.log(`[BACKEND] cmux send text to ${sessionId} (${surface})`);
|
|
92
|
+
return true;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`[BACKEND] cmux send failed for ${sessionId}:`, err.message);
|
|
95
|
+
// Invalidate cache entry
|
|
96
|
+
surfaceCache.delete(sessionId);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Send enter key to a cmux surface
|
|
102
|
+
function cmuxSendEnter(sessionId) {
|
|
103
|
+
const surface = findSurface(sessionId);
|
|
104
|
+
if (!surface) return false;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
execSync(`cmux send-key --surface ${surface} return`, {
|
|
108
|
+
timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
109
|
+
});
|
|
110
|
+
console.log(`[BACKEND] cmux send-key return to ${sessionId} (${surface})`);
|
|
111
|
+
return true;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(`[BACKEND] cmux send-key failed for ${sessionId}:`, err.message);
|
|
114
|
+
surfaceCache.delete(sessionId);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Invalidate cache for a session (e.g., when surface changes)
|
|
120
|
+
function invalidateCache(sessionId) {
|
|
121
|
+
surfaceCache.delete(sessionId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function clearCache() {
|
|
125
|
+
surfaceCache.clear();
|
|
126
|
+
lastCacheRefresh = 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
detectTerminal,
|
|
131
|
+
findSurface,
|
|
132
|
+
cmuxSendText,
|
|
133
|
+
cmuxSendEnter,
|
|
134
|
+
refreshSurfaceCache,
|
|
135
|
+
invalidateCache,
|
|
136
|
+
clearCache
|
|
137
|
+
};
|