@dmsdc-ai/aigentry-telepty 0.1.75 → 0.1.77

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 CHANGED
@@ -15,6 +15,7 @@ const { cleanupDaemonProcesses } = require('./daemon-control');
15
15
  const { attachInteractiveTerminal, getTerminalSize, restoreTerminalModes } = require('./interactive-terminal');
16
16
  const { getRuntimeInfo } = require('./runtime-info');
17
17
  const { formatHostLabel, groupSessionsByHost, pickSessionTarget } = require('./session-routing');
18
+ const { buildSharedContextPrompt, createSharedContextDescriptor, ensureSharedContextFile } = require('./shared-context');
18
19
  const { runInteractiveSkillInstaller } = require('./skill-installer');
19
20
  const crossMachine = require('./cross-machine');
20
21
  const args = process.argv.slice(2);
@@ -144,6 +145,195 @@ async function getDaemonMeta(host = REMOTE_HOST) {
144
145
  }
145
146
  }
146
147
 
148
+ function detectTerminalProgram(env = process.env) {
149
+ const rawTermProgram = typeof env.TERM_PROGRAM === 'string' ? env.TERM_PROGRAM.trim() : '';
150
+ if (rawTermProgram) {
151
+ return rawTermProgram;
152
+ }
153
+
154
+ if (env.TMUX) {
155
+ return 'tmux';
156
+ }
157
+
158
+ const term = typeof env.TERM === 'string' ? env.TERM.toLowerCase() : '';
159
+ if (term.includes('kitty')) return 'kitty';
160
+ if (term.includes('ghostty')) return 'ghostty';
161
+ if (term.includes('tmux')) return 'tmux';
162
+
163
+ return null;
164
+ }
165
+
166
+ function formatSessionTerminal(session) {
167
+ const terminal = session.terminal || session.termProgram || null;
168
+ const term = session.term || null;
169
+ if (terminal && term) {
170
+ return `${terminal} (${term})`;
171
+ }
172
+ return terminal || term || 'unknown';
173
+ }
174
+
175
+ function formatSessionHealth(session) {
176
+ const status = session.healthStatus || 'UNKNOWN';
177
+ const reason = session.healthReason || null;
178
+ if (reason && reason !== status) {
179
+ return `${status} (${reason})`;
180
+ }
181
+ return status;
182
+ }
183
+
184
+ function formatApiError(data, fallback = 'Request failed.') {
185
+ if (!data) {
186
+ return fallback;
187
+ }
188
+
189
+ const code = data.code ? `[${data.code}] ` : '';
190
+ const message = data.error || fallback;
191
+ return `${code}${message}`;
192
+ }
193
+
194
+ function buildInjectRequestBody(prompt, options = {}) {
195
+ const body = {
196
+ prompt,
197
+ no_enter: options.noEnter === true
198
+ };
199
+
200
+ if (options.fromId) body.from = options.fromId;
201
+ if (options.replyTo) body.reply_to = options.replyTo;
202
+ if (options.replyExpected) body.reply_expected = true;
203
+
204
+ return body;
205
+ }
206
+
207
+ function buildSessionStateReportBody(options = {}) {
208
+ const body = {
209
+ phase: options.phase,
210
+ source: options.source || 'self_report'
211
+ };
212
+
213
+ if (options.currentTask !== undefined) body.current_task = options.currentTask;
214
+ if (options.blocker !== undefined) body.blocker = options.blocker;
215
+ if (options.needsInput !== undefined) body.needs_input = options.needsInput;
216
+ if (options.threadId !== undefined) body.thread_id = options.threadId;
217
+ if (options.seq !== undefined) body.seq = options.seq;
218
+
219
+ return body;
220
+ }
221
+
222
+ function splitSessionsByTransport(sessions) {
223
+ const local = [];
224
+ const remoteByPeer = new Map();
225
+
226
+ for (const session of sessions) {
227
+ if (!isRemoteSession(session)) {
228
+ local.push(session);
229
+ continue;
230
+ }
231
+
232
+ const peerName = session.peerName || session.host;
233
+ if (!remoteByPeer.has(peerName)) {
234
+ remoteByPeer.set(peerName, []);
235
+ }
236
+ remoteByPeer.get(peerName).push(session);
237
+ }
238
+
239
+ return { local, remoteByPeer };
240
+ }
241
+
242
+ function parseRefOption(argv) {
243
+ const refIndex = argv.indexOf('--ref');
244
+ if (refIndex === -1) {
245
+ return { useRef: false, refFilePath: null };
246
+ }
247
+
248
+ argv.splice(refIndex, 1);
249
+ const candidate = argv[refIndex];
250
+ if (!candidate || candidate.startsWith('--')) {
251
+ return { useRef: true, refFilePath: null };
252
+ }
253
+
254
+ try {
255
+ if (fs.statSync(candidate).isFile()) {
256
+ argv.splice(refIndex, 1);
257
+ return { useRef: true, refFilePath: candidate };
258
+ }
259
+ } catch {
260
+ // Fall through to inline ref mode when the candidate is not a readable file.
261
+ }
262
+
263
+ return { useRef: true, refFilePath: null };
264
+ }
265
+
266
+ function createSharedReferenceDescriptor(prompt, refFilePath) {
267
+ if (refFilePath) {
268
+ const fileContent = fs.readFileSync(refFilePath, 'utf8');
269
+ return createSharedContextDescriptor(fileContent);
270
+ }
271
+
272
+ return createSharedContextDescriptor(prompt);
273
+ }
274
+
275
+ function buildSharedReferenceInjectPrompt(referencePath, message = '') {
276
+ const basePrompt = buildSharedContextPrompt(referencePath);
277
+ const normalizedMessage = String(message ?? '').trim();
278
+ return normalizedMessage ? `${basePrompt} ${normalizedMessage}` : basePrompt;
279
+ }
280
+
281
+ function ensureLocalSharedReference(descriptor, message = '') {
282
+ const reference = ensureSharedContextFile(descriptor);
283
+ return {
284
+ descriptor: reference,
285
+ referencePath: reference.promptPath,
286
+ prompt: buildSharedReferenceInjectPrompt(reference.promptPath, message)
287
+ };
288
+ }
289
+
290
+ function ensureRemoteSharedReference(peerName, descriptor, message = '') {
291
+ const result = crossMachine.remoteEnsureSharedContext(peerName, descriptor);
292
+ if (!result.success) {
293
+ throw new Error(result.error || `Failed to prepare shared context on ${peerName}`);
294
+ }
295
+
296
+ const referencePath = result.promptPath || descriptor.promptPath;
297
+ return {
298
+ descriptor,
299
+ referencePath,
300
+ prompt: buildSharedReferenceInjectPrompt(referencePath, message)
301
+ };
302
+ }
303
+
304
+ function printSessionInfo(session, options = {}) {
305
+ const host = options.host || session.host || '127.0.0.1';
306
+ console.log('\x1b[1mSession Info:\x1b[0m');
307
+ console.log(` - ID: \x1b[36m${session.id}\x1b[0m`);
308
+ console.log(` Host: ${formatHostLabel(host)}`);
309
+ console.log(` Command: ${session.command}`);
310
+ console.log(` Type: ${session.type || 'unknown'}`);
311
+ console.log(` Status: ${formatSessionHealth(session)}`);
312
+ console.log(` Terminal: ${session.terminal || session.termProgram || 'unknown'}`);
313
+ console.log(` TERM: ${session.term || 'n/a'}`);
314
+ console.log(` CWD: ${session.cwd}`);
315
+ console.log(` Clients: ${session.active_clients ?? 0}`);
316
+ if (session.createdAt) {
317
+ console.log(` Started: ${new Date(session.createdAt).toLocaleString()}`);
318
+ }
319
+ if (session.lastActivityAt) {
320
+ console.log(` Last Activity: ${new Date(session.lastActivityAt).toLocaleString()}`);
321
+ }
322
+ if (typeof session.idleSeconds === 'number') {
323
+ console.log(` Idle: ${session.idleSeconds}s`);
324
+ }
325
+ if (session.semantic && session.semantic.phase) {
326
+ console.log(` Phase: ${session.semantic.phase}`);
327
+ }
328
+ if (session.semantic && session.semantic.current_task) {
329
+ console.log(` Current Task: ${session.semantic.current_task}`);
330
+ }
331
+ if (session.semantic && session.semantic.blocker) {
332
+ console.log(` Blocker: ${session.semantic.blocker}`);
333
+ }
334
+ console.log('');
335
+ }
336
+
147
337
  function startDetachedDaemon() {
148
338
  const cp = spawn(process.argv[0], [process.argv[1], 'daemon'], {
149
339
  detached: true,
@@ -423,7 +613,7 @@ async function manageInteractive() {
423
613
  console.log('\x1b[1mAvailable Sessions:\x1b[0m');
424
614
  sessions.forEach(s => {
425
615
  const hostLabel = formatHostLabel(s.host);
426
- console.log(` - \x1b[36m${s.id}\x1b[0m (\x1b[33m${hostLabel}\x1b[0m) [${s.command}] - Clients: ${s.active_clients}`);
616
+ console.log(` - \x1b[36m${s.id}\x1b[0m (\x1b[33m${hostLabel}\x1b[0m) [${s.command}] - Status: ${s.healthStatus || 'UNKNOWN'} - Clients: ${s.active_clients}`);
427
617
  });
428
618
  }
429
619
  console.log('\n');
@@ -530,7 +720,7 @@ async function manageInteractive() {
530
720
  method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: promptText })
531
721
  });
532
722
  const data = await res.json();
533
- if (!res.ok) console.error(`\n❌ Error: ${data.error}\n`);
723
+ if (!res.ok) console.error(`\n❌ ${formatApiError(data)}\n`);
534
724
  else console.log(`\n✅ Injected successfully into '\x1b[36m${target.id}\x1b[0m'.\n`);
535
725
  } catch (e) { console.error('\n❌ Failed to connect.\n'); }
536
726
  continue;
@@ -589,12 +779,18 @@ async function main() {
589
779
  if (cmd === 'list') {
590
780
  try {
591
781
  const sessions = await discoverSessions({ silent: true });
782
+ if (args.includes('--json')) {
783
+ console.log(JSON.stringify(sessions, null, 2));
784
+ return;
785
+ }
592
786
  if (sessions.length === 0) { console.log('No active sessions found.'); return; }
593
787
  console.log('\x1b[1mActive Sessions:\x1b[0m');
594
788
  sessions.forEach(s => {
595
789
  console.log(` - ID: \x1b[36m${s.id}\x1b[0m`);
596
790
  console.log(` Host: ${formatHostLabel(s.host)}`);
597
791
  console.log(` Command: ${s.command}`);
792
+ console.log(` Status: ${formatSessionHealth(s)}`);
793
+ console.log(` Terminal: ${formatSessionTerminal(s)}`);
598
794
  console.log(` CWD: ${s.cwd}`);
599
795
  console.log(` Clients: ${s.active_clients}`);
600
796
  console.log(` Started: ${new Date(s.createdAt).toLocaleString()}`);
@@ -688,6 +884,8 @@ async function main() {
688
884
  const detectedBackend = process.env.CMUX_WORKSPACE_ID ? 'cmux' : (findKittySocketCli() ? 'kitty' : 'pty');
689
885
 
690
886
  // Register session with daemon
887
+ const terminalProgram = detectTerminalProgram(process.env);
888
+ const terminalType = process.env.TERM || null;
691
889
  try {
692
890
  const res = await fetchWithAuth(`${DAEMON_URL}/api/sessions/register`, {
693
891
  method: 'POST',
@@ -698,7 +896,9 @@ async function main() {
698
896
  cwd: process.cwd(),
699
897
  backend: detectedBackend,
700
898
  cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
701
- cmux_surface_id: process.env.CMUX_SURFACE_ID || null
899
+ cmux_surface_id: process.env.CMUX_SURFACE_ID || null,
900
+ term_program: terminalProgram,
901
+ term: terminalType
702
902
  })
703
903
  });
704
904
  const data = await res.json();
@@ -843,7 +1043,9 @@ async function main() {
843
1043
  cwd: process.cwd(),
844
1044
  backend: detectedBackend,
845
1045
  cmux_workspace_id: process.env.CMUX_WORKSPACE_ID || null,
846
- cmux_surface_id: process.env.CMUX_SURFACE_ID || null
1046
+ cmux_surface_id: process.env.CMUX_SURFACE_ID || null,
1047
+ term_program: terminalProgram,
1048
+ term: terminalType
847
1049
  })
848
1050
  });
849
1051
  } catch (e) {
@@ -868,24 +1070,35 @@ async function main() {
868
1070
  try {
869
1071
  const msg = JSON.parse(message);
870
1072
  if (msg.type === 'inject') {
871
- const isCr = msg.data === '\r';
872
- if (isCr && injectQueue.length > 0) {
873
- // CR with pending queued text queue CR too and flush immediately.
874
- injectQueue.push(msg.data);
875
- if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
876
- flushInjectQueue();
877
- } else if (isCr) {
878
- // CR always written immediately — never idle-gated
879
- child.write(msg.data);
880
- } else if (isIdle()) {
881
- // Text when idle write immediately
882
- child.write(msg.data);
883
- promptReady = false;
884
- lastInjectTextTime = Date.now();
885
- } else {
886
- // Text when not idle — queue for safe delivery
887
- injectQueue.push(msg.data);
888
- scheduleIdleFlush();
1073
+ const chunks = [];
1074
+ const rawData = typeof msg.data === 'string' ? msg.data : String(msg.data ?? '');
1075
+ // Keep text+CR combineddo NOT split them.
1076
+ chunks.push(rawData);
1077
+
1078
+ for (const chunk of chunks) {
1079
+ if (!chunk) {
1080
+ continue;
1081
+ }
1082
+
1083
+ const isCr = chunk === '\r';
1084
+ if (isCr && injectQueue.length > 0) {
1085
+ // CR with pending queued text — queue CR too and flush immediately.
1086
+ injectQueue.push(chunk);
1087
+ if (queueFlushTimer) { clearTimeout(queueFlushTimer); queueFlushTimer = null; }
1088
+ flushInjectQueue();
1089
+ } else if (isCr) {
1090
+ // CR always written immediately — never idle-gated.
1091
+ child.write(chunk);
1092
+ } else if (isIdle()) {
1093
+ // Text when idle — write immediately.
1094
+ child.write(chunk);
1095
+ promptReady = false;
1096
+ lastInjectTextTime = Date.now();
1097
+ } else {
1098
+ // Text when not idle — queue for safe delivery.
1099
+ injectQueue.push(chunk);
1100
+ scheduleIdleFlush();
1101
+ }
889
1102
  }
890
1103
  } else if (msg.type === 'resize') {
891
1104
  child.resize(msg.cols, msg.rows);
@@ -1201,10 +1414,17 @@ async function main() {
1201
1414
  }
1202
1415
 
1203
1416
  if (cmd === 'inject') {
1204
- // Check for --no-enter flag
1205
- const noEnterIndex = args.indexOf('--no-enter');
1206
- const noEnter = noEnterIndex !== -1;
1207
- if (noEnter) args.splice(noEnterIndex, 1);
1417
+ const { useRef, refFilePath } = parseRefOption(args);
1418
+
1419
+ if (args.includes('--no-enter')) {
1420
+ console.error('❌ telepty inject always submits after text. Use `telepty enter <session_id>` to send Enter only.');
1421
+ process.exit(1);
1422
+ }
1423
+
1424
+ // Extract --submit flag (terminal-level submit instead of deferred PTY CR)
1425
+ const submitIndex = args.indexOf('--submit');
1426
+ const useSubmit = submitIndex !== -1;
1427
+ if (useSubmit) args.splice(submitIndex, 1);
1208
1428
 
1209
1429
  // Extract --from flag
1210
1430
  let fromId;
@@ -1229,8 +1449,10 @@ async function main() {
1229
1449
  const replyExpected = replyExpectedIndex !== -1;
1230
1450
  if (replyExpected) args.splice(replyExpectedIndex, 1);
1231
1451
 
1232
- const sessionId = args[1]; const prompt = args.slice(2).join(' ');
1233
- if (!sessionId || !prompt) { console.error('❌ Usage: telepty inject [--no-enter] [--from <id>] [--reply-to <id>] <session_id> "<prompt text>"'); process.exit(1); }
1452
+ const sessionId = args[1];
1453
+ const hasPromptArgument = args.length >= 3;
1454
+ const prompt = args.slice(2).join(' ');
1455
+ if (!sessionId || (!refFilePath && !hasPromptArgument)) { console.error('❌ Usage: telepty inject [--ref [file]] [--from <id>] [--reply-to <id>] <session_id> "<prompt text>"'); process.exit(1); }
1234
1456
  try {
1235
1457
  const target = await resolveSessionTarget(sessionId);
1236
1458
  if (!target) {
@@ -1238,6 +1460,10 @@ async function main() {
1238
1460
  process.exit(1);
1239
1461
  }
1240
1462
 
1463
+ let injectPrompt = prompt;
1464
+ let referencePath = null;
1465
+ const refDescriptor = useRef ? createSharedReferenceDescriptor(prompt, refFilePath) : null;
1466
+
1241
1467
  // Remote session: use SSH direct execution
1242
1468
  if (isRemoteSession(target)) {
1243
1469
  const { checkEntitlement } = require('./entitlement');
@@ -1246,33 +1472,139 @@ async function main() {
1246
1472
  console.error(`⚠️ ${ent.reason}\n Upgrade: ${ent.upgrade_url}`);
1247
1473
  process.exit(1);
1248
1474
  }
1249
- const result = crossMachine.remoteInject(target.peerName, target.id, prompt, {
1475
+
1476
+ if (useRef) {
1477
+ const reference = ensureRemoteSharedReference(target.peerName, refDescriptor, refFilePath ? prompt : '');
1478
+ injectPrompt = reference.prompt;
1479
+ referencePath = reference.referencePath;
1480
+ }
1481
+
1482
+ const result = crossMachine.remoteInject(target.peerName, target.id, injectPrompt, {
1250
1483
  from: fromId,
1251
- no_enter: noEnter
1484
+ reply_to: replyTo,
1485
+ reply_expected: replyExpected
1252
1486
  });
1253
1487
  if (result.success) {
1254
- console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m' @ ${target.peerName}.`);
1488
+ const refSuffix = referencePath ? ` (ref: ${referencePath})` : '';
1489
+ console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m' @ ${target.peerName}.${refSuffix}`);
1255
1490
  } else {
1256
- console.error(`❌ Error: ${result.error}`);
1491
+ console.error(`❌ ${result.error}`);
1257
1492
  }
1258
1493
  return;
1259
1494
  }
1260
1495
 
1261
- const body = { prompt, no_enter: noEnter };
1262
- if (fromId) body.from = fromId;
1263
- if (replyTo) body.reply_to = replyTo;
1264
- if (replyExpected) body.reply_expected = true;
1496
+ if (useRef) {
1497
+ const reference = ensureLocalSharedReference(refDescriptor, refFilePath ? prompt : '');
1498
+ injectPrompt = reference.prompt;
1499
+ referencePath = reference.referencePath;
1500
+ }
1501
+
1502
+ const body = buildInjectRequestBody(injectPrompt, {
1503
+ fromId,
1504
+ replyTo,
1505
+ replyExpected,
1506
+ noEnter: useSubmit
1507
+ });
1265
1508
 
1266
1509
  const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
1267
1510
  method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
1268
1511
  });
1269
1512
  const data = await res.json();
1270
- if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
1271
- console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m'.`);
1513
+ if (!res.ok) { console.error(`❌ ${formatApiError(data)}`); return; }
1514
+ const refSuffix = referencePath ? ` (ref: ${referencePath})` : '';
1515
+ console.log(`✅ Context injected successfully into '\x1b[36m${target.id}\x1b[0m'.${refSuffix}`);
1516
+
1517
+ // Terminal-level submit: POST /submit after text injection
1518
+ if (useSubmit) {
1519
+ await new Promise(resolve => setTimeout(resolve, 500));
1520
+ try {
1521
+ const submitRes = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/submit`, {
1522
+ method: 'POST',
1523
+ headers: { 'Content-Type': 'application/json' },
1524
+ body: JSON.stringify({ pre_delay_ms: 200, retries: 2, retry_delay_ms: 500 })
1525
+ });
1526
+ const submitData = await submitRes.json();
1527
+ if (submitRes.ok) {
1528
+ console.log(`✅ Submitted via ${submitData.strategy}${submitData.attempts > 1 ? ` (${submitData.attempts} attempts)` : ''}.`);
1529
+ } else {
1530
+ console.error(`⚠️ Submit failed: ${formatApiError(submitData)}`);
1531
+ }
1532
+ } catch (submitErr) {
1533
+ console.error(`⚠️ Submit failed: ${submitErr.message}`);
1534
+ }
1535
+ }
1272
1536
  } catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
1273
1537
  return;
1274
1538
  }
1275
1539
 
1540
+ if (cmd === 'enter') {
1541
+ const sessionId = args[1];
1542
+ if (!sessionId) { console.error('❌ Usage: telepty enter <session_id>'); process.exit(1); }
1543
+
1544
+ try {
1545
+ const target = await resolveSessionTarget(sessionId);
1546
+ if (!target) {
1547
+ console.error(`❌ Session '${sessionId}' was not found on any discovered host.`);
1548
+ process.exit(1);
1549
+ }
1550
+
1551
+ if (isRemoteSession(target)) {
1552
+ const { checkEntitlement } = require('./entitlement');
1553
+ const ent = checkEntitlement({ feature: 'telepty.remote_sessions' });
1554
+ if (!ent.allowed) {
1555
+ console.error(`⚠️ ${ent.reason}\n Upgrade: ${ent.upgrade_url}`);
1556
+ process.exit(1);
1557
+ }
1558
+
1559
+ const result = crossMachine.remoteInject(target.peerName, target.id, '', {});
1560
+ if (result.success) {
1561
+ console.log(`✅ Enter sent successfully into '\x1b[36m${target.id}\x1b[0m' @ ${target.peerName}.`);
1562
+ } else {
1563
+ console.error(`❌ ${result.error}`);
1564
+ }
1565
+ return;
1566
+ }
1567
+
1568
+ const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
1569
+ method: 'POST',
1570
+ headers: { 'Content-Type': 'application/json' },
1571
+ body: JSON.stringify(buildInjectRequestBody('', {}))
1572
+ });
1573
+ const data = await res.json();
1574
+ if (!res.ok) { console.error(`❌ ${formatApiError(data)}`); return; }
1575
+ console.log(`✅ Enter sent successfully into '\x1b[36m${target.id}\x1b[0m'.`);
1576
+ } catch (e) {
1577
+ console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`);
1578
+ }
1579
+ return;
1580
+ }
1581
+
1582
+ if (cmd === 'send-key') {
1583
+ const sessionId = args[1];
1584
+ const key = (args[2] || '').toLowerCase();
1585
+ if (!sessionId || !key) { console.error('❌ Usage: telepty send-key <session_id> <key>\n Supported keys: enter'); process.exit(1); }
1586
+ if (key !== 'enter' && key !== 'return') {
1587
+ console.error(`❌ Unsupported key: '${key}'. Supported keys: enter`);
1588
+ process.exit(1);
1589
+ }
1590
+
1591
+ try {
1592
+ const target = await resolveSessionTarget(sessionId);
1593
+ if (!target) {
1594
+ console.error(`❌ Session '${sessionId}' was not found on any discovered host.`);
1595
+ process.exit(1);
1596
+ }
1597
+
1598
+ const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/submit`, { method: 'POST' });
1599
+ const data = await res.json();
1600
+ if (!res.ok) { console.error(`❌ ${formatApiError(data)}`); return; }
1601
+ console.log(`✅ Key '${key}' sent to '\x1b[36m${target.id}\x1b[0m'. (strategy: ${data.strategy})`);
1602
+ } catch (e) {
1603
+ console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`);
1604
+ }
1605
+ return;
1606
+ }
1607
+
1276
1608
  if (cmd === 'reply') {
1277
1609
  const mySessionId = process.env.TELEPTY_SESSION_ID;
1278
1610
  if (!mySessionId) { console.error('❌ TELEPTY_SESSION_ID env var is required for reply command'); process.exit(1); }
@@ -1291,12 +1623,94 @@ async function main() {
1291
1623
  method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
1292
1624
  });
1293
1625
  const data = await res.json();
1294
- if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
1626
+ if (!res.ok) { console.error(`❌ ${formatApiError(data)}`); return; }
1295
1627
  console.log(`✅ Reply sent to '\x1b[36m${replyTo}\x1b[0m'.`);
1296
1628
  } catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
1297
1629
  return;
1298
1630
  }
1299
1631
 
1632
+ if (cmd === 'status-report') {
1633
+ const reportArgs = args.slice(1);
1634
+ let sessionId = process.env.TELEPTY_SESSION_ID || undefined;
1635
+
1636
+ const idIndex = reportArgs.indexOf('--id');
1637
+ if (idIndex !== -1) {
1638
+ if (!reportArgs[idIndex + 1]) {
1639
+ console.error('❌ Usage: telepty status-report [--id <session_id>] --phase <phase> [--task <text>] [--blocker <text>] [--needs-input] [--thread-id <id>] [--seq <n>]');
1640
+ process.exit(1);
1641
+ }
1642
+ sessionId = reportArgs[idIndex + 1];
1643
+ reportArgs.splice(idIndex, 2);
1644
+ }
1645
+
1646
+ function takeFlagValue(flag) {
1647
+ const index = reportArgs.indexOf(flag);
1648
+ if (index === -1) return undefined;
1649
+ if (!reportArgs[index + 1]) {
1650
+ console.error(`❌ ${flag} requires a value.`);
1651
+ process.exit(1);
1652
+ }
1653
+ const value = reportArgs[index + 1];
1654
+ reportArgs.splice(index, 2);
1655
+ return value;
1656
+ }
1657
+
1658
+ const phase = takeFlagValue('--phase');
1659
+ const currentTask = takeFlagValue('--task') ?? takeFlagValue('--current-task');
1660
+ const blocker = takeFlagValue('--blocker');
1661
+ const threadId = takeFlagValue('--thread-id');
1662
+ const source = takeFlagValue('--source');
1663
+ const seqRaw = takeFlagValue('--seq');
1664
+ const needsInputIndex = reportArgs.indexOf('--needs-input');
1665
+ const needsInput = needsInputIndex !== -1;
1666
+ if (needsInput) reportArgs.splice(needsInputIndex, 1);
1667
+
1668
+ if (!sessionId || !phase || reportArgs.length > 0) {
1669
+ console.error('❌ Usage: telepty status-report [--id <session_id>] --phase <phase> [--task <text>] [--blocker <text>] [--needs-input] [--thread-id <id>] [--seq <n>]');
1670
+ process.exit(1);
1671
+ }
1672
+
1673
+ if (seqRaw !== undefined && (!Number.isInteger(Number(seqRaw)) || Number(seqRaw) < 0)) {
1674
+ console.error('❌ --seq must be a non-negative integer.');
1675
+ process.exit(1);
1676
+ }
1677
+
1678
+ try {
1679
+ const target = await resolveSessionTarget(sessionId);
1680
+ if (!target) {
1681
+ console.error(`❌ Session '${sessionId}' was not found on any discovered host.`);
1682
+ process.exit(1);
1683
+ }
1684
+ if (isRemoteSession(target)) {
1685
+ console.error('❌ telepty status-report currently supports local daemon sessions only.');
1686
+ process.exit(1);
1687
+ }
1688
+
1689
+ const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/state`, {
1690
+ method: 'POST',
1691
+ headers: { 'Content-Type': 'application/json' },
1692
+ body: JSON.stringify(buildSessionStateReportBody({
1693
+ phase,
1694
+ currentTask,
1695
+ blocker,
1696
+ needsInput,
1697
+ threadId,
1698
+ source,
1699
+ seq: seqRaw === undefined ? undefined : Number(seqRaw)
1700
+ }))
1701
+ });
1702
+ const data = await res.json();
1703
+ if (!res.ok) {
1704
+ console.error(`❌ ${formatApiError(data)}`);
1705
+ return;
1706
+ }
1707
+ console.log(`✅ Session state reported for '\x1b[36m${target.id}\x1b[0m' (${phase}).`);
1708
+ } catch (e) {
1709
+ console.error(`❌ ${e.message || 'Failed to report session state.'}`);
1710
+ }
1711
+ return;
1712
+ }
1713
+
1300
1714
  if (cmd === 'multicast') {
1301
1715
  const sessionIdsRaw = args[1]; const prompt = args.slice(2).join(' ');
1302
1716
  if (!sessionIdsRaw || !prompt) { console.error('❌ Usage: telepty multicast <id1,id2,...> "<prompt text>"'); process.exit(1); }
@@ -1324,7 +1738,7 @@ async function main() {
1324
1738
  });
1325
1739
  const data = await res.json();
1326
1740
  if (!res.ok) {
1327
- throw new Error(data.error || `Multicast failed on ${host}`);
1741
+ throw new Error(formatApiError(data, `Multicast failed on ${host}`));
1328
1742
  }
1329
1743
  aggregate.successful.push(...data.results.successful.map((id) => `${id}@${host}`));
1330
1744
  aggregate.failed.push(...data.results.failed.map((item) => ({ ...item, host })));
@@ -1332,37 +1746,70 @@ async function main() {
1332
1746
 
1333
1747
  console.log(`✅ Context multicasted successfully to ${aggregate.successful.length} session(s).`);
1334
1748
  if (aggregate.failed.length > 0) {
1335
- console.warn(`⚠️ Failed to inject into ${aggregate.failed.length} session(s):`, aggregate.failed.map((item) => `${item.id}@${item.host}`).join(', '));
1749
+ console.warn(`⚠️ Failed to inject into ${aggregate.failed.length} session(s):`, aggregate.failed.map((item) => `${item.id}@${item.host} [${item.code || 'UNKNOWN'}] ${item.error || ''}`.trim()).join(', '));
1336
1750
  }
1337
1751
  } catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
1338
1752
  return;
1339
1753
  }
1340
1754
 
1341
1755
  if (cmd === 'broadcast') {
1756
+ const { useRef, refFilePath } = parseRefOption(args);
1757
+
1342
1758
  const prompt = args.slice(1).join(' ');
1343
- if (!prompt) { console.error('❌ Usage: telepty broadcast "<prompt text>"'); process.exit(1); }
1759
+ if (!prompt && !refFilePath) { console.error('❌ Usage: telepty broadcast [--ref [file]] "<prompt text>"'); process.exit(1); }
1344
1760
  try {
1345
1761
  const discovered = await discoverSessions({ silent: true });
1346
- const grouped = groupSessionsByHost(discovered);
1347
1762
  const aggregate = { successful: [], failed: [] };
1763
+ const { local, remoteByPeer } = splitSessionsByTransport(discovered);
1764
+ let descriptor = useRef ? createSharedReferenceDescriptor(prompt, refFilePath) : null;
1765
+ let referencePath = null;
1766
+
1767
+ if (local.length > 0) {
1768
+ let localPrompt = prompt;
1769
+ if (useRef) {
1770
+ const reference = ensureLocalSharedReference(descriptor, refFilePath ? prompt : '');
1771
+ descriptor = reference.descriptor;
1772
+ referencePath = reference.referencePath;
1773
+ localPrompt = reference.prompt;
1774
+ }
1348
1775
 
1349
- for (const host of grouped.keys()) {
1350
- const res = await fetchWithAuth(`http://${host}:${PORT}/api/sessions/broadcast/inject`, {
1351
- method: 'POST',
1352
- headers: { 'Content-Type': 'application/json' },
1353
- body: JSON.stringify({ prompt })
1354
- });
1355
- const data = await res.json();
1356
- if (!res.ok) {
1357
- throw new Error(data.error || `Broadcast failed on ${host}`);
1776
+ for (const host of groupSessionsByHost(local).keys()) {
1777
+ const res = await fetchWithAuth(`http://${host}:${PORT}/api/sessions/broadcast/inject`, {
1778
+ method: 'POST',
1779
+ headers: { 'Content-Type': 'application/json' },
1780
+ body: JSON.stringify({ prompt: localPrompt })
1781
+ });
1782
+ const data = await res.json();
1783
+ if (!res.ok) {
1784
+ throw new Error(formatApiError(data, `Broadcast failed on ${host}`));
1785
+ }
1786
+ aggregate.successful.push(...data.results.successful.map((id) => `${id}@${host}`));
1787
+ aggregate.failed.push(...data.results.failed.map((item) => ({ ...item, host })));
1788
+ }
1789
+ }
1790
+
1791
+ for (const [peerName, sessions] of remoteByPeer.entries()) {
1792
+ let remotePrompt = prompt;
1793
+ if (useRef) {
1794
+ const reference = ensureRemoteSharedReference(peerName, descriptor, refFilePath ? prompt : '');
1795
+ referencePath ||= reference.referencePath;
1796
+ remotePrompt = reference.prompt;
1797
+ }
1798
+
1799
+ for (const session of sessions) {
1800
+ const result = crossMachine.remoteInject(peerName, session.id, remotePrompt);
1801
+ if (result.success) {
1802
+ aggregate.successful.push(`${session.id}@${session.host}`);
1803
+ } else {
1804
+ aggregate.failed.push({ id: session.id, host: session.host, error: result.error });
1805
+ }
1358
1806
  }
1359
- aggregate.successful.push(...data.results.successful.map((id) => `${id}@${host}`));
1360
- aggregate.failed.push(...data.results.failed.map((item) => ({ ...item, host })));
1361
1807
  }
1362
1808
 
1363
- console.log(`✅ Context broadcasted successfully to ${aggregate.successful.length} active session(s).`);
1809
+ const refSuffix = referencePath ? ` (ref: ${referencePath})` : '';
1810
+ console.log(`✅ Context broadcasted successfully to ${aggregate.successful.length} active session(s).${refSuffix}`);
1364
1811
  if (aggregate.failed.length > 0) {
1365
- console.warn(`⚠️ Failed to inject into ${aggregate.failed.length} session(s):`, aggregate.failed.map((item) => `${item.id}@${item.host}`).join(', '));
1812
+ console.warn(`⚠️ Failed to inject into ${aggregate.failed.length} session(s):`, aggregate.failed.map((item) => `${item.id}@${item.host} [${item.code || 'UNKNOWN'}] ${item.error || ''}`.trim()).join(', '));
1366
1813
  }
1367
1814
  } catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
1368
1815
  return;
@@ -1389,6 +1836,56 @@ async function main() {
1389
1836
  return;
1390
1837
  }
1391
1838
 
1839
+ if (cmd === 'session' && args[1] === 'info') {
1840
+ const sessionRef = args[2];
1841
+ if (!sessionRef) {
1842
+ console.error('❌ Usage: telepty session info <id[@host]>');
1843
+ process.exit(1);
1844
+ }
1845
+
1846
+ try {
1847
+ const sessions = await discoverSessions({ silent: true });
1848
+ const target = await resolveSessionTarget(sessionRef, { sessions });
1849
+ if (!target) {
1850
+ console.error(`❌ Session '${sessionRef}' was not found on any discovered host.`);
1851
+ process.exit(1);
1852
+ }
1853
+
1854
+ if (args.includes('--json')) {
1855
+ if (isRemoteSession(target)) {
1856
+ console.log(JSON.stringify(target, null, 2));
1857
+ return;
1858
+ }
1859
+
1860
+ const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}`);
1861
+ const data = await res.json();
1862
+ if (!res.ok) {
1863
+ console.error(`❌ Error: ${data.error}`);
1864
+ process.exit(1);
1865
+ }
1866
+ console.log(JSON.stringify({ host: target.host, ...data }, null, 2));
1867
+ return;
1868
+ }
1869
+
1870
+ if (isRemoteSession(target)) {
1871
+ printSessionInfo(target, { host: target.host });
1872
+ return;
1873
+ }
1874
+
1875
+ const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}`);
1876
+ const data = await res.json();
1877
+ if (!res.ok) {
1878
+ console.error(`❌ Error: ${data.error}`);
1879
+ process.exit(1);
1880
+ }
1881
+
1882
+ printSessionInfo(data, { host: target.host });
1883
+ } catch (e) {
1884
+ console.error(`❌ ${e.message || 'Failed to fetch session info.'}`);
1885
+ }
1886
+ return;
1887
+ }
1888
+
1392
1889
  if (cmd === 'session' && args[1] === 'start') {
1393
1890
  // Generate kitty session file and launch
1394
1891
  const configArg = args.find(a => a.startsWith('--config='));
@@ -2216,14 +2713,17 @@ Usage:
2216
2713
  telepty daemon Start the background daemon
2217
2714
  telepty spawn --id <id> <command> [args...] Spawn a new background CLI
2218
2715
  telepty allow [--id <id>] [--auto-restart] <command> [args...] Allow inject on a CLI (auto-restart on crash)
2219
- telepty list List all active sessions across discovered hosts
2716
+ telepty list [--json] List all active sessions across discovered hosts
2220
2717
  telepty attach [id[@host]] Attach to a session (Interactive picker if no ID)
2221
- telepty inject [--no-enter] [--from <id>] [--reply-to <id>] <id[@host]> "<prompt>" Inject text into a single session
2718
+ telepty inject [--ref [file]] [--from <id>] [--reply-to <id>] <id[@host]> "<prompt>" Inject text into a single session
2719
+ telepty enter <id[@host]> Send only Enter/Return to a single session
2222
2720
  telepty read-screen <id[@host]> [--lines N] [--raw] Read session screen buffer
2223
2721
  telepty reply "<text>" Reply to the session that last injected into $TELEPTY_SESSION_ID
2722
+ telepty status-report [--id <id>] --phase <phase> [--task <text>] [--blocker <text>] [--needs-input] [--thread-id <id>] [--seq N]
2224
2723
  telepty multicast <id1[@host],id2[@host]> "<prompt>" Inject text into multiple specific sessions
2225
- telepty broadcast "<prompt>" Inject text into ALL active sessions
2724
+ telepty broadcast [--ref [file]] "<prompt>" Inject text into ALL active sessions
2226
2725
  telepty rename <old_id[@host]> <new_id> Rename a session (updates terminal title too)
2726
+ telepty session info <id[@host]> [--json] Show detailed session metadata
2227
2727
  telepty connect <user@host> [--name N] [--port P] Connect to a remote machine via SSH tunnel
2228
2728
  telepty disconnect <name> | --all Disconnect from a remote machine
2229
2729
  telepty peers [--remove <name>] List connected and known peers