@dmsdc-ai/aigentry-telepty 0.1.68 → 0.1.70
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 +129 -52
- package/cross-machine.js +174 -128
- package/daemon.js +230 -48
- package/interactive-terminal.js +46 -1
- package/package.json +1 -1
- package/terminal-backend.js +137 -0
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));
|
|
@@ -138,6 +139,21 @@ const sessions = {};
|
|
|
138
139
|
const handoffs = {};
|
|
139
140
|
const threads = {};
|
|
140
141
|
|
|
142
|
+
function appendToOutputRing(session, data) {
|
|
143
|
+
if (!session.outputRing) session.outputRing = [];
|
|
144
|
+
session.outputRing.push(data);
|
|
145
|
+
// Keep total data under ~200KB limit by trimming old entries
|
|
146
|
+
let totalLen = session.outputRing.reduce((sum, d) => sum + d.length, 0);
|
|
147
|
+
while (totalLen > 200000 && session.outputRing.length > 1) {
|
|
148
|
+
totalLen -= session.outputRing[0].length;
|
|
149
|
+
session.outputRing.shift();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Detect terminal environment at daemon startup
|
|
154
|
+
const DETECTED_TERMINAL = terminalBackend.detectTerminal();
|
|
155
|
+
console.log(`[DAEMON] Terminal backend: ${DETECTED_TERMINAL}`);
|
|
156
|
+
|
|
141
157
|
// Restore persisted session metadata (wrapped sessions await reconnect)
|
|
142
158
|
const _persisted = loadPersistedSessions();
|
|
143
159
|
for (const [id, meta] of Object.entries(_persisted)) {
|
|
@@ -147,8 +163,7 @@ for (const [id, meta] of Object.entries(_persisted)) {
|
|
|
147
163
|
command: meta.command || 'wrapped', cwd: meta.cwd || process.cwd(),
|
|
148
164
|
createdAt: meta.createdAt || new Date().toISOString(),
|
|
149
165
|
lastActivityAt: meta.lastActivityAt || new Date().toISOString(),
|
|
150
|
-
clients: new Set(), isClosing: false
|
|
151
|
-
};
|
|
166
|
+
clients: new Set(), isClosing: false, outputRing: [], ready: false, };
|
|
152
167
|
console.log(`[PERSIST] Restored session ${id} (awaiting reconnect)`);
|
|
153
168
|
}
|
|
154
169
|
}
|
|
@@ -256,8 +271,10 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
256
271
|
createdAt: new Date().toISOString(),
|
|
257
272
|
lastActivityAt: new Date().toISOString(),
|
|
258
273
|
clients: new Set(),
|
|
259
|
-
isClosing: false
|
|
260
|
-
|
|
274
|
+
isClosing: false,
|
|
275
|
+
outputRing: [],
|
|
276
|
+
ready: true,
|
|
277
|
+
};
|
|
261
278
|
sessions[session_id] = sessionRecord;
|
|
262
279
|
|
|
263
280
|
// Broadcast session creation to bus
|
|
@@ -279,6 +296,8 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
279
296
|
return;
|
|
280
297
|
}
|
|
281
298
|
|
|
299
|
+
appendToOutputRing(currentSession, data);
|
|
300
|
+
|
|
282
301
|
// Send to direct WS clients
|
|
283
302
|
currentSession.clients.forEach(ws => {
|
|
284
303
|
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'output', data }));
|
|
@@ -304,7 +323,7 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
304
323
|
});
|
|
305
324
|
|
|
306
325
|
app.post('/api/sessions/register', (req, res) => {
|
|
307
|
-
const { session_id, command, cwd = process.cwd(), backend, cmux_workspace_id } = req.body;
|
|
326
|
+
const { session_id, command, cwd = process.cwd(), backend, cmux_workspace_id, cmux_surface_id } = req.body;
|
|
308
327
|
if (!session_id) return res.status(400).json({ error: 'session_id is required' });
|
|
309
328
|
// Entitlement: check session limit for new registrations
|
|
310
329
|
if (!sessions[session_id]) {
|
|
@@ -322,6 +341,7 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
322
341
|
if (cwd) existing.cwd = cwd;
|
|
323
342
|
if (backend) existing.backend = backend;
|
|
324
343
|
if (cmux_workspace_id) existing.cmuxWorkspaceId = cmux_workspace_id;
|
|
344
|
+
if (cmux_surface_id) existing.cmuxSurfaceId = cmux_surface_id;
|
|
325
345
|
console.log(`[REGISTER] Re-registered session ${session_id} (updated metadata)`);
|
|
326
346
|
return res.status(200).json({ session_id, type: 'wrapped', command: existing.command, cwd: existing.cwd, reregistered: true });
|
|
327
347
|
}
|
|
@@ -335,11 +355,14 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
335
355
|
cwd,
|
|
336
356
|
backend: backend || 'kitty',
|
|
337
357
|
cmuxWorkspaceId: cmux_workspace_id || null,
|
|
358
|
+
cmuxSurfaceId: cmux_surface_id || null,
|
|
338
359
|
createdAt: new Date().toISOString(),
|
|
339
360
|
lastActivityAt: new Date().toISOString(),
|
|
340
361
|
clients: new Set(),
|
|
341
|
-
isClosing: false
|
|
342
|
-
|
|
362
|
+
isClosing: false,
|
|
363
|
+
outputRing: [],
|
|
364
|
+
ready: false,
|
|
365
|
+
};
|
|
343
366
|
// Check for existing session with same base alias and emit replaced event
|
|
344
367
|
const baseAlias = session_id.replace(/-\d+$/, '');
|
|
345
368
|
const replaced = Object.keys(sessions).find(id => {
|
|
@@ -393,10 +416,12 @@ app.get('/api/sessions', (req, res) => {
|
|
|
393
416
|
cwd: session.cwd,
|
|
394
417
|
backend: session.backend || 'kitty',
|
|
395
418
|
cmuxWorkspaceId: session.cmuxWorkspaceId || null,
|
|
419
|
+
cmuxSurfaceId: session.cmuxSurfaceId || null,
|
|
396
420
|
createdAt: session.createdAt,
|
|
397
421
|
lastActivityAt: session.lastActivityAt || null,
|
|
398
422
|
idleSeconds,
|
|
399
|
-
active_clients: session.clients.size
|
|
423
|
+
active_clients: session.clients.size,
|
|
424
|
+
ready: session.ready || false
|
|
400
425
|
};
|
|
401
426
|
});
|
|
402
427
|
if (idleGt !== null) {
|
|
@@ -436,6 +461,7 @@ app.get('/api/meta', (req, res) => {
|
|
|
436
461
|
host: HOST,
|
|
437
462
|
port: Number(PORT),
|
|
438
463
|
machine_id: MACHINE_ID,
|
|
464
|
+
terminal: DETECTED_TERMINAL,
|
|
439
465
|
capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon', 'handoff-inbox', 'deliberation-threads', 'cross-machine']
|
|
440
466
|
});
|
|
441
467
|
});
|
|
@@ -464,6 +490,27 @@ app.post('/api/sessions/multicast/inject', (req, res) => {
|
|
|
464
490
|
const session = sessions[id];
|
|
465
491
|
if (session) {
|
|
466
492
|
try {
|
|
493
|
+
// cmux per-session backend (text + enter)
|
|
494
|
+
if (session.backend === 'cmux') {
|
|
495
|
+
const ok = terminalBackend.cmuxSendText(id, prompt);
|
|
496
|
+
if (ok) {
|
|
497
|
+
setTimeout(() => terminalBackend.cmuxSendEnter(id), 300);
|
|
498
|
+
results.successful.push({ id, strategy: 'cmux_auto' });
|
|
499
|
+
// Broadcast injection to bus
|
|
500
|
+
const busMsg = JSON.stringify({
|
|
501
|
+
type: 'injection',
|
|
502
|
+
sender: 'cli',
|
|
503
|
+
target_agent: id,
|
|
504
|
+
content: prompt,
|
|
505
|
+
timestamp: new Date().toISOString()
|
|
506
|
+
});
|
|
507
|
+
busClients.forEach(client => {
|
|
508
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
509
|
+
});
|
|
510
|
+
return; // skip WS path for this session
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
467
514
|
// Inject text first, then \r separately after delay
|
|
468
515
|
if (session.type === 'wrapped') {
|
|
469
516
|
if (session.ownerWs && session.ownerWs.readyState === 1) {
|
|
@@ -516,6 +563,16 @@ app.post('/api/sessions/broadcast/inject', (req, res) => {
|
|
|
516
563
|
Object.keys(sessions).forEach(id => {
|
|
517
564
|
const session = sessions[id];
|
|
518
565
|
try {
|
|
566
|
+
// cmux per-session backend (text + enter)
|
|
567
|
+
if (session.backend === 'cmux') {
|
|
568
|
+
const ok = terminalBackend.cmuxSendText(id, prompt);
|
|
569
|
+
if (ok) {
|
|
570
|
+
setTimeout(() => terminalBackend.cmuxSendEnter(id), 300);
|
|
571
|
+
results.successful.push({ id, strategy: 'cmux_auto' });
|
|
572
|
+
return; // skip WS path for this session
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
519
576
|
// Inject text first, then \r separately after delay
|
|
520
577
|
if (session.type === 'wrapped') {
|
|
521
578
|
if (session.ownerWs && session.ownerWs.readyState === 1) {
|
|
@@ -729,8 +786,11 @@ app.post('/api/sessions/:id/submit', (req, res) => {
|
|
|
729
786
|
console.log(`[SUBMIT] Session ${id} (${session.command}) using strategy: ${strategy}`);
|
|
730
787
|
|
|
731
788
|
let success = false;
|
|
732
|
-
// cmux backend
|
|
733
|
-
if (session.backend === 'cmux'
|
|
789
|
+
// cmux per-session backend
|
|
790
|
+
if (session.backend === 'cmux') {
|
|
791
|
+
success = terminalBackend.cmuxSendEnter(id);
|
|
792
|
+
}
|
|
793
|
+
if (!success && session.backend === 'cmux' && session.cmuxWorkspaceId) {
|
|
734
794
|
success = submitViaCmux(id);
|
|
735
795
|
}
|
|
736
796
|
if (!success) {
|
|
@@ -768,8 +828,11 @@ app.post('/api/sessions/submit-all', (req, res) => {
|
|
|
768
828
|
const strategy = getSubmitStrategy(session.command);
|
|
769
829
|
let success = false;
|
|
770
830
|
|
|
771
|
-
// cmux backend
|
|
772
|
-
if (session.backend === 'cmux'
|
|
831
|
+
// cmux per-session backend
|
|
832
|
+
if (session.backend === 'cmux') {
|
|
833
|
+
success = terminalBackend.cmuxSendEnter(id);
|
|
834
|
+
}
|
|
835
|
+
if (!success && session.backend === 'cmux' && session.cmuxWorkspaceId) {
|
|
773
836
|
success = submitViaCmux(id);
|
|
774
837
|
}
|
|
775
838
|
if (!success) {
|
|
@@ -835,68 +898,115 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
835
898
|
|
|
836
899
|
let submitResult = null;
|
|
837
900
|
if (session.type === 'wrapped') {
|
|
838
|
-
// For wrapped sessions: try
|
|
839
|
-
// then
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
901
|
+
// For wrapped sessions: try cmux send (daemon-level auto-detect),
|
|
902
|
+
// then kitty send-text (bypasses allow bridge queue),
|
|
903
|
+
// then WS as fallback, then submit via consistent path for CR.
|
|
904
|
+
//
|
|
905
|
+
// When session is NOT ready (CLI hasn't shown prompt yet), skip cmux/kitty
|
|
906
|
+
// because they bypass the allow-bridge's prompt-ready queue.
|
|
907
|
+
// The WS path sends to the allow-bridge which queues until CLI is ready.
|
|
908
|
+
const sock = session.ready ? findKittySocket() : null;
|
|
909
|
+
if (sock && !session.kittyWindowId) session.kittyWindowId = findKittyWindowId(sock, id);
|
|
910
|
+
const wid = session.ready ? session.kittyWindowId : null;
|
|
843
911
|
|
|
844
912
|
let kittyOk = false;
|
|
845
|
-
|
|
846
|
-
|
|
913
|
+
let cmuxOk = false;
|
|
914
|
+
let deliveryPath = null; // 'cmux', 'kitty', 'ws'
|
|
915
|
+
|
|
916
|
+
// cmux per-session backend: send text directly to surface (only when ready)
|
|
917
|
+
if (session.ready && session.backend === 'cmux') {
|
|
918
|
+
cmuxOk = terminalBackend.cmuxSendText(id, finalPrompt);
|
|
919
|
+
if (cmuxOk) {
|
|
920
|
+
deliveryPath = 'cmux';
|
|
921
|
+
console.log(`[INJECT] cmux send for ${id}`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (!cmuxOk && wid && sock) {
|
|
926
|
+
// Kitty send-text primary (only when ready — bypasses allow bridge queue)
|
|
847
927
|
try {
|
|
848
928
|
const escaped = finalPrompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
|
|
849
929
|
require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} '${escaped}'`, {
|
|
850
930
|
timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
851
931
|
});
|
|
852
932
|
kittyOk = true;
|
|
933
|
+
deliveryPath = 'kitty';
|
|
853
934
|
console.log(`[INJECT] Kitty send-text for ${id} (window ${wid})`);
|
|
854
935
|
} catch {
|
|
855
936
|
// Invalidate cached window ID — window may have changed or been closed
|
|
856
937
|
session.kittyWindowId = null;
|
|
857
938
|
}
|
|
858
939
|
}
|
|
859
|
-
if (!kittyOk) {
|
|
860
|
-
//
|
|
940
|
+
if (!cmuxOk && !kittyOk) {
|
|
941
|
+
// WS path: allow-bridge has its own prompt-ready queue
|
|
861
942
|
const wsOk = writeToSession(finalPrompt);
|
|
862
943
|
if (!wsOk) {
|
|
863
944
|
return res.status(503).json({ error: 'Process not connected' });
|
|
864
945
|
}
|
|
865
|
-
|
|
946
|
+
deliveryPath = 'ws';
|
|
947
|
+
if (!session.ready) {
|
|
948
|
+
console.log(`[INJECT] WS (not ready, allow-bridge will queue) for ${id}`);
|
|
949
|
+
} else {
|
|
950
|
+
console.log(`[INJECT] WS fallback for ${id}`);
|
|
951
|
+
}
|
|
866
952
|
}
|
|
867
953
|
|
|
868
954
|
if (!no_enter) {
|
|
869
955
|
setTimeout(() => {
|
|
870
956
|
let submitted = false;
|
|
871
957
|
|
|
872
|
-
//
|
|
873
|
-
if (
|
|
874
|
-
|
|
875
|
-
if (
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// 3. Fallback: kitty send-text → WS
|
|
886
|
-
if (!submitted) {
|
|
887
|
-
console.log(`[INJECT] submit fallback for ${id}`);
|
|
958
|
+
// Use the SAME path that delivered text for CR to guarantee ordering
|
|
959
|
+
if (deliveryPath === 'cmux') {
|
|
960
|
+
// cmux: send-key return via same surface
|
|
961
|
+
if (session.backend === 'cmux') {
|
|
962
|
+
submitted = terminalBackend.cmuxSendEnter(id);
|
|
963
|
+
if (submitted) console.log(`[INJECT] cmux submit for ${id}`);
|
|
964
|
+
}
|
|
965
|
+
if (!submitted && session.backend === 'cmux' && session.cmuxWorkspaceId) {
|
|
966
|
+
submitted = submitViaCmux(id);
|
|
967
|
+
if (submitted) console.log(`[INJECT] cmux session-level submit for ${id}`);
|
|
968
|
+
}
|
|
969
|
+
} else if (deliveryPath === 'kitty') {
|
|
970
|
+
// kitty: send-text CR via same window (not osascript!)
|
|
888
971
|
if (wid && sock) {
|
|
889
972
|
try {
|
|
890
973
|
require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\r'`, {
|
|
891
974
|
timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
892
975
|
});
|
|
976
|
+
submitted = true;
|
|
977
|
+
console.log(`[INJECT] kitty submit for ${id} (window ${wid})`);
|
|
893
978
|
} catch {
|
|
894
|
-
|
|
979
|
+
session.kittyWindowId = null;
|
|
895
980
|
}
|
|
896
|
-
} else {
|
|
897
|
-
writeToSession('\r');
|
|
898
981
|
}
|
|
899
982
|
}
|
|
983
|
+
// deliveryPath === 'ws' or any fallback:
|
|
984
|
+
// Try terminal-level submit first (bypasses PTY ICRNL which converts CR→LF)
|
|
985
|
+
// This matters for cmux/kitty sessions where text went via WS but
|
|
986
|
+
// the application expects CR(13) not LF(10) from Enter.
|
|
987
|
+
if (!submitted && session.backend === 'cmux') {
|
|
988
|
+
submitted = terminalBackend.cmuxSendEnter(id);
|
|
989
|
+
if (submitted) console.log(`[INJECT] cmux submit (fallback) for ${id}`);
|
|
990
|
+
}
|
|
991
|
+
if (!submitted && session.backend === 'cmux' && session.cmuxWorkspaceId) {
|
|
992
|
+
submitted = submitViaCmux(id);
|
|
993
|
+
if (submitted) console.log(`[INJECT] cmux session-level submit (fallback) for ${id}`);
|
|
994
|
+
}
|
|
995
|
+
if (!submitted && wid && sock) {
|
|
996
|
+
try {
|
|
997
|
+
require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} $'\\r'`, {
|
|
998
|
+
timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
999
|
+
});
|
|
1000
|
+
submitted = true;
|
|
1001
|
+
console.log(`[INJECT] kitty submit (fallback) for ${id}`);
|
|
1002
|
+
} catch {
|
|
1003
|
+
session.kittyWindowId = null;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (!submitted) {
|
|
1007
|
+
writeToSession('\r');
|
|
1008
|
+
console.log(`[INJECT] WS submit for ${id}`);
|
|
1009
|
+
}
|
|
900
1010
|
|
|
901
1011
|
// Update tab title (kitty-specific, safe to fail)
|
|
902
1012
|
if (wid && sock) {
|
|
@@ -907,7 +1017,7 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
907
1017
|
} catch {}
|
|
908
1018
|
}
|
|
909
1019
|
}, 500);
|
|
910
|
-
submitResult = { deferred: true, strategy:
|
|
1020
|
+
submitResult = { deferred: true, strategy: deliveryPath || 'ws' };
|
|
911
1021
|
}
|
|
912
1022
|
} else {
|
|
913
1023
|
// Spawned sessions: direct PTY write
|
|
@@ -979,6 +1089,53 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
979
1089
|
}
|
|
980
1090
|
});
|
|
981
1091
|
|
|
1092
|
+
// GET /api/sessions/:id/screen — read current screen buffer
|
|
1093
|
+
app.get('/api/sessions/:id/screen', (req, res) => {
|
|
1094
|
+
const requestedId = req.params.id;
|
|
1095
|
+
const resolvedId = resolveSessionAlias(requestedId);
|
|
1096
|
+
if (!resolvedId) return res.status(404).json({ error: 'Session not found', requested: requestedId });
|
|
1097
|
+
const session = sessions[resolvedId];
|
|
1098
|
+
|
|
1099
|
+
const lines = parseInt(req.query.lines) || 50;
|
|
1100
|
+
const raw = req.query.raw === '1' || req.query.raw === 'true';
|
|
1101
|
+
|
|
1102
|
+
if (!session.outputRing || session.outputRing.length === 0) {
|
|
1103
|
+
return res.json({ session_id: resolvedId, screen: '', lines: 0, raw: false });
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Join all buffered output
|
|
1107
|
+
const fullOutput = session.outputRing.join('');
|
|
1108
|
+
|
|
1109
|
+
// Strip ANSI escape sequences for clean text
|
|
1110
|
+
function stripAnsi(str) {
|
|
1111
|
+
return str
|
|
1112
|
+
.replace(/[[0-9;]*[a-zA-Z]/g, '') // CSI sequences
|
|
1113
|
+
.replace(/][^]*/g, '') // OSC sequences (BEL terminated)
|
|
1114
|
+
.replace(/][^]*\\/g, '') // OSC sequences (ST terminated)
|
|
1115
|
+
.replace(/[()][AB012]/g, '') // Character set selection
|
|
1116
|
+
.replace(/[>=<]/g, '') // Keypad mode
|
|
1117
|
+
.replace(/[[?]?[0-9;]*[hlsurm]/g, '') // Mode set/reset
|
|
1118
|
+
.replace(/[[0-9;]*[ABCDHJ]/g, '') // Cursor movement
|
|
1119
|
+
.replace(/[[0-9;]*[KG]/g, '') // Line clearing
|
|
1120
|
+
.replace(/\r/g, ''); // Carriage returns
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const cleaned = raw ? fullOutput : stripAnsi(fullOutput);
|
|
1124
|
+
|
|
1125
|
+
// Take last N lines
|
|
1126
|
+
const allLines = cleaned.split('\n');
|
|
1127
|
+
const lastLines = allLines.slice(-lines);
|
|
1128
|
+
const screen = lastLines.join('\n').trim();
|
|
1129
|
+
|
|
1130
|
+
res.json({
|
|
1131
|
+
session_id: resolvedId,
|
|
1132
|
+
screen,
|
|
1133
|
+
lines: lastLines.length,
|
|
1134
|
+
total_lines: allLines.length,
|
|
1135
|
+
raw: !!raw
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
1138
|
+
|
|
982
1139
|
app.patch('/api/sessions/:id', (req, res) => {
|
|
983
1140
|
const requestedId = req.params.id;
|
|
984
1141
|
const resolvedId = resolveSessionAlias(requestedId);
|
|
@@ -1060,13 +1217,22 @@ function busAutoRoute(msg) {
|
|
|
1060
1217
|
const prompt = (msg.payload && msg.payload.prompt) || msg.content || msg.prompt || JSON.stringify(msg);
|
|
1061
1218
|
const inject_id = crypto.randomUUID();
|
|
1062
1219
|
|
|
1063
|
-
// Write to session (kitty
|
|
1220
|
+
// Write to session (cmux auto-detect > kitty > session-level cmux > WS fallback)
|
|
1064
1221
|
const sock = findKittySocket();
|
|
1065
1222
|
if (!targetSession.kittyWindowId && sock) targetSession.kittyWindowId = findKittyWindowId(sock, targetId);
|
|
1066
1223
|
const wid = targetSession.kittyWindowId;
|
|
1067
1224
|
let delivered = false;
|
|
1068
1225
|
|
|
1069
|
-
|
|
1226
|
+
// cmux per-session backend: send text + enter to surface
|
|
1227
|
+
if (!delivered && targetSession.backend === 'cmux') {
|
|
1228
|
+
const textOk = terminalBackend.cmuxSendText(targetId, prompt);
|
|
1229
|
+
if (textOk) {
|
|
1230
|
+
setTimeout(() => terminalBackend.cmuxSendEnter(targetId), 500);
|
|
1231
|
+
delivered = true;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (!delivered && wid && sock && targetSession.type === 'wrapped') {
|
|
1070
1236
|
try {
|
|
1071
1237
|
const escaped = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
|
|
1072
1238
|
require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} '${escaped}'`, {
|
|
@@ -1082,7 +1248,7 @@ function busAutoRoute(msg) {
|
|
|
1082
1248
|
delivered = true;
|
|
1083
1249
|
} catch {}
|
|
1084
1250
|
}
|
|
1085
|
-
// cmux backend: use WS for text, cmux send-key for enter
|
|
1251
|
+
// Session-level cmux backend: use WS for text, cmux send-key for enter
|
|
1086
1252
|
if (!delivered && targetSession.backend === 'cmux' && targetSession.cmuxWorkspaceId) {
|
|
1087
1253
|
if (targetSession.type === 'wrapped' && targetSession.ownerWs && targetSession.ownerWs.readyState === 1) {
|
|
1088
1254
|
targetSession.ownerWs.send(JSON.stringify({ type: 'inject', data: prompt }));
|
|
@@ -1469,8 +1635,10 @@ wss.on('connection', (ws, req) => {
|
|
|
1469
1635
|
createdAt: new Date().toISOString(),
|
|
1470
1636
|
lastActivityAt: new Date().toISOString(),
|
|
1471
1637
|
clients: new Set([ws]),
|
|
1472
|
-
isClosing: false
|
|
1473
|
-
|
|
1638
|
+
isClosing: false,
|
|
1639
|
+
outputRing: [],
|
|
1640
|
+
ready: false,
|
|
1641
|
+
};
|
|
1474
1642
|
sessions[sessionId] = autoSession;
|
|
1475
1643
|
console.log(`[WS] Auto-registered wrapped session ${sessionId} on reconnect`);
|
|
1476
1644
|
// Set tab title via kitty (no \x0c redraw — it causes flickering on multi-session reconnect)
|
|
@@ -1515,11 +1683,25 @@ wss.on('connection', (ws, req) => {
|
|
|
1515
1683
|
// Owner sending output -> broadcast to other clients + update activity
|
|
1516
1684
|
if (type === 'output') {
|
|
1517
1685
|
activeSession.lastActivityAt = new Date().toISOString();
|
|
1686
|
+
appendToOutputRing(activeSession, data);
|
|
1518
1687
|
activeSession.clients.forEach(client => {
|
|
1519
1688
|
if (client !== ws && client.readyState === 1) {
|
|
1520
1689
|
client.send(JSON.stringify({ type: 'output', data }));
|
|
1521
1690
|
}
|
|
1522
1691
|
});
|
|
1692
|
+
} else if (type === 'ready') {
|
|
1693
|
+
activeSession.ready = true;
|
|
1694
|
+
activeSession.lastActivityAt = new Date().toISOString();
|
|
1695
|
+
console.log(`[READY] Session ${sessionId} CLI is ready for inject`);
|
|
1696
|
+
// Broadcast readiness to bus (cmux/kitty paths now enabled for this session)
|
|
1697
|
+
const readyMsg = JSON.stringify({
|
|
1698
|
+
type: 'session_ready',
|
|
1699
|
+
session_id: sessionId,
|
|
1700
|
+
timestamp: new Date().toISOString()
|
|
1701
|
+
});
|
|
1702
|
+
busClients.forEach(client => {
|
|
1703
|
+
if (client.readyState === 1) client.send(readyMsg);
|
|
1704
|
+
});
|
|
1523
1705
|
}
|
|
1524
1706
|
} else {
|
|
1525
1707
|
// Non-owner client input -> forward to owner as inject
|
package/interactive-terminal.js
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const TERMINAL_CLEANUP_SEQUENCE = [
|
|
6
|
+
'\x1b[?1049l', // Leave the alternate screen if the child died before cleanup.
|
|
7
|
+
'\x1b[?25h', // Ensure the cursor is visible again.
|
|
8
|
+
'\x1b[?1l', // Disable application cursor keys.
|
|
9
|
+
'\x1b>', // Disable application keypad mode.
|
|
10
|
+
'\x1b[?1000l',
|
|
11
|
+
'\x1b[?1002l',
|
|
12
|
+
'\x1b[?1003l',
|
|
13
|
+
'\x1b[?1004l',
|
|
14
|
+
'\x1b[?1005l',
|
|
15
|
+
'\x1b[?1006l',
|
|
16
|
+
'\x1b[?1007l',
|
|
17
|
+
'\x1b[?1015l',
|
|
18
|
+
'\x1b[<u', // Disable kitty keyboard protocol.
|
|
19
|
+
'\x1b[>4;0m', // Disable modifyOtherKeys.
|
|
20
|
+
'\x1b[?2004l' // Disable bracketed paste.
|
|
21
|
+
].join('');
|
|
22
|
+
|
|
3
23
|
function getTerminalSize(output, fallback = {}) {
|
|
4
24
|
const envCols = Number.parseInt(process.env.COLUMNS || '', 10);
|
|
5
25
|
const envRows = Number.parseInt(process.env.LINES || '', 10);
|
|
@@ -31,10 +51,30 @@ function removeListener(stream, eventName, handler) {
|
|
|
31
51
|
}
|
|
32
52
|
}
|
|
33
53
|
|
|
54
|
+
function restoreTerminalModes(output) {
|
|
55
|
+
if (!output) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
if (typeof output.fd === 'number') {
|
|
61
|
+
fs.writeSync(output.fd, TERMINAL_CLEANUP_SEQUENCE);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof output.write === 'function') {
|
|
66
|
+
output.write(TERMINAL_CLEANUP_SEQUENCE);
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// Ignore cleanup failures when the TTY is already gone.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
34
73
|
function attachInteractiveTerminal(input, output, handlers = {}) {
|
|
35
74
|
const { onData = null, onResize = null } = handlers;
|
|
36
75
|
|
|
37
76
|
if (input && input.isTTY && typeof input.setRawMode === 'function') {
|
|
77
|
+
input.__teleptyRawModeActive = true;
|
|
38
78
|
input.setRawMode(true);
|
|
39
79
|
}
|
|
40
80
|
|
|
@@ -57,15 +97,20 @@ function attachInteractiveTerminal(input, output, handlers = {}) {
|
|
|
57
97
|
|
|
58
98
|
if (input && input.isTTY && typeof input.setRawMode === 'function') {
|
|
59
99
|
input.setRawMode(false);
|
|
100
|
+
input.__teleptyRawModeActive = false;
|
|
60
101
|
}
|
|
61
102
|
|
|
62
103
|
if (input && typeof input.pause === 'function') {
|
|
63
104
|
input.pause();
|
|
64
105
|
}
|
|
106
|
+
|
|
107
|
+
restoreTerminalModes(output);
|
|
65
108
|
};
|
|
66
109
|
}
|
|
67
110
|
|
|
68
111
|
module.exports = {
|
|
69
112
|
attachInteractiveTerminal,
|
|
70
|
-
getTerminalSize
|
|
113
|
+
getTerminalSize,
|
|
114
|
+
restoreTerminalModes,
|
|
115
|
+
TERMINAL_CLEANUP_SEQUENCE
|
|
71
116
|
};
|