@dmsdc-ai/aigentry-telepty 0.1.16 → 0.1.18

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.
Files changed (4) hide show
  1. package/README.md +11 -0
  2. package/cli.js +506 -4
  3. package/daemon.js +267 -2
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -49,6 +49,17 @@ The installer now stops older local telepty daemons before starting the new one,
49
49
  telepty inject my-session "echo 'Hello from nowhere!'"
50
50
  ```
51
51
 
52
+ 4. **Universal CLI submit (split_cr):**
53
+
54
+ All AI CLIs (Claude, Codex, Gemini) submit reliably via the `split_cr` strategy — text is injected first, then `\r` is sent separately after a 300ms delay. This works universally across all CLIs without any per-CLI workarounds.
55
+
56
+ ```bash
57
+ # The inject API handles split_cr automatically
58
+ curl -X POST http://127.0.0.1:3848/api/sessions/my-session/inject \
59
+ -H "Content-Type: application/json" \
60
+ -d '{"prompt": "your command here"}'
61
+ ```
62
+
52
63
  CLI commands such as `list`, `attach`, `inject`, `rename`, `multicast`, and `broadcast` now auto-discover sessions across your Tailnet by default. If the same session ID exists on multiple hosts, disambiguate with `session_id@host`.
53
64
 
54
65
  ## Testing
package/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const path = require('path');
4
4
  const os = require('os');
5
+ const fs = require('fs');
5
6
  const WebSocket = require('ws');
6
7
  const { execSync, spawn } = require('child_process');
7
8
  const readline = require('readline');
@@ -556,6 +557,11 @@ async function main() {
556
557
  return manageInteractive();
557
558
  }
558
559
 
560
+ if (cmd === '--version' || cmd === '-v' || cmd === 'version') {
561
+ console.log(pkg.version);
562
+ return;
563
+ }
564
+
559
565
  if (cmd === 'update') {
560
566
  console.log('\x1b[36m🔄 Updating telepty to the latest version...\x1b[0m');
561
567
  try {
@@ -870,9 +876,27 @@ async function main() {
870
876
  const noEnterIndex = args.indexOf('--no-enter');
871
877
  const noEnter = noEnterIndex !== -1;
872
878
  if (noEnter) args.splice(noEnterIndex, 1);
873
-
879
+
880
+ // Extract --from flag
881
+ let fromId;
882
+ const fromIndex = args.indexOf('--from');
883
+ if (fromIndex !== -1 && args[fromIndex + 1]) {
884
+ fromId = args[fromIndex + 1];
885
+ args.splice(fromIndex, 2);
886
+ } else {
887
+ fromId = process.env.TELEPTY_SESSION_ID || undefined;
888
+ }
889
+
890
+ // Extract --reply-to flag
891
+ let replyTo;
892
+ const replyToIndex = args.indexOf('--reply-to');
893
+ if (replyToIndex !== -1 && args[replyToIndex + 1]) {
894
+ replyTo = args[replyToIndex + 1];
895
+ args.splice(replyToIndex, 2);
896
+ }
897
+
874
898
  const sessionId = args[1]; const prompt = args.slice(2).join(' ');
875
- if (!sessionId || !prompt) { console.error('❌ Usage: telepty inject [--no-enter] <session_id> "<prompt text>"'); process.exit(1); }
899
+ if (!sessionId || !prompt) { console.error('❌ Usage: telepty inject [--no-enter] [--from <id>] [--reply-to <id>] <session_id> "<prompt text>"'); process.exit(1); }
876
900
  try {
877
901
  const target = await resolveSessionTarget(sessionId);
878
902
  if (!target) {
@@ -880,8 +904,12 @@ async function main() {
880
904
  process.exit(1);
881
905
  }
882
906
 
907
+ const body = { prompt, no_enter: noEnter };
908
+ if (fromId) body.from = fromId;
909
+ if (replyTo) body.reply_to = replyTo;
910
+
883
911
  const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
884
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, no_enter: noEnter })
912
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
885
913
  });
886
914
  const data = await res.json();
887
915
  if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
@@ -891,6 +919,31 @@ async function main() {
891
919
  return;
892
920
  }
893
921
 
922
+ if (cmd === 'reply') {
923
+ const mySessionId = process.env.TELEPTY_SESSION_ID;
924
+ if (!mySessionId) { console.error('❌ TELEPTY_SESSION_ID env var is required for reply command'); process.exit(1); }
925
+ const replyText = args.slice(1).join(' ');
926
+ if (!replyText) { console.error('❌ Usage: telepty reply "<text>"'); process.exit(1); }
927
+ try {
928
+ const metaRes = await fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(mySessionId)}`);
929
+ if (!metaRes.ok) { console.error(`❌ Could not fetch session metadata for '${mySessionId}'`); process.exit(1); }
930
+ const meta = await metaRes.json();
931
+ const replyTo = meta.lastInjectReplyTo;
932
+ if (!replyTo) { console.error(`❌ No pending reply-to found for session '${mySessionId}'`); process.exit(1); }
933
+ const target = await resolveSessionTarget(replyTo);
934
+ if (!target) { console.error(`❌ Session '${replyTo}' was not found on any discovered host.`); process.exit(1); }
935
+ const fullPrompt = `[from: ${mySessionId}] [reply-to: ${mySessionId}] ${replyText}`;
936
+ const body = { prompt: fullPrompt, from: mySessionId, reply_to: mySessionId };
937
+ const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
938
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
939
+ });
940
+ const data = await res.json();
941
+ if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
942
+ console.log(`✅ Reply sent to '\x1b[36m${replyTo}\x1b[0m'.`);
943
+ } catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
944
+ return;
945
+ }
946
+
894
947
  if (cmd === 'multicast') {
895
948
  const sessionIdsRaw = args[1]; const prompt = args.slice(2).join(' ');
896
949
  if (!sessionIdsRaw || !prompt) { console.error('❌ Usage: telepty multicast <id1,id2,...> "<prompt text>"'); process.exit(1); }
@@ -983,6 +1036,441 @@ async function main() {
983
1036
  return;
984
1037
  }
985
1038
 
1039
+ if (cmd === 'deliberate') {
1040
+ await ensureDaemonRunning();
1041
+ const subCmd = args[1];
1042
+
1043
+ if (subCmd === 'status') {
1044
+ // telepty deliberate status [thread_id]
1045
+ const threadId = args[2];
1046
+ try {
1047
+ if (threadId) {
1048
+ const resp = await fetchWithAuth(`${DAEMON_URL}/api/threads/${threadId}`);
1049
+ const thread = await resp.json();
1050
+ if (!resp.ok) { console.error('Error:', thread.error); process.exit(1); }
1051
+ console.log(`\n Thread: ${thread.id}`);
1052
+ console.log(` Topic: ${thread.topic}`);
1053
+ console.log(` Status: ${thread.status}`);
1054
+ console.log(` Orchestrator: ${thread.orchestrator_session_id || '(none)'}`);
1055
+ console.log(` Participants: ${thread.participant_session_ids.join(', ') || '(none)'}`);
1056
+ console.log(` Messages: ${thread.message_count}`);
1057
+ console.log(` Created: ${thread.created_at}`);
1058
+ if (thread.closed_at) console.log(` Closed: ${thread.closed_at}`);
1059
+ console.log();
1060
+ } else {
1061
+ const resp = await fetchWithAuth(`${DAEMON_URL}/api/threads`);
1062
+ const list = await resp.json();
1063
+ if (list.length === 0) {
1064
+ console.log('No deliberation threads found.');
1065
+ } else {
1066
+ console.log(`\n Deliberation Threads (${list.length}):\n`);
1067
+ for (const t of list) {
1068
+ const icon = t.status === 'active' ? '🟢' : '⏹️';
1069
+ console.log(` ${icon} ${t.id.slice(0, 8)} ${t.status.padEnd(8)} msgs:${t.message_count} participants:${t.participant_count} "${t.topic}"`);
1070
+ }
1071
+ console.log();
1072
+ }
1073
+ }
1074
+ } catch (err) {
1075
+ console.error('Failed:', err.message);
1076
+ process.exit(1);
1077
+ }
1078
+ return;
1079
+ }
1080
+
1081
+ if (subCmd === 'end') {
1082
+ // telepty deliberate end <thread_id>
1083
+ const threadId = args[2];
1084
+ if (!threadId) { console.error('Usage: telepty deliberate end <thread_id>'); process.exit(1); }
1085
+ try {
1086
+ const resp = await fetchWithAuth(`${DAEMON_URL}/api/threads/${threadId}`, {
1087
+ method: 'PATCH',
1088
+ headers: { 'Content-Type': 'application/json' },
1089
+ body: JSON.stringify({ status: 'closed' })
1090
+ });
1091
+ const result = await resp.json();
1092
+ if (!resp.ok) { console.error('Error:', result.error); process.exit(1); }
1093
+ console.log(`Deliberation thread ${threadId} closed.`);
1094
+ } catch (err) {
1095
+ console.error('Failed:', err.message);
1096
+ process.exit(1);
1097
+ }
1098
+ return;
1099
+ }
1100
+
1101
+ // telepty deliberate --topic "..." [--sessions id1,id2,...] [--context path]
1102
+ // Extract flags
1103
+ const topicIdx = args.indexOf('--topic');
1104
+ const sessionsIdx = args.indexOf('--sessions');
1105
+ const contextIdx = args.indexOf('--context');
1106
+
1107
+ const topic = topicIdx !== -1 && args[topicIdx + 1] ? args[topicIdx + 1] : null;
1108
+ const sessionsArg = sessionsIdx !== -1 && args[sessionsIdx + 1] ? args[sessionsIdx + 1] : null;
1109
+ const contextPath = contextIdx !== -1 && args[contextIdx + 1] ? args[contextIdx + 1] : null;
1110
+
1111
+ if (!topic) {
1112
+ console.error('Usage: telepty deliberate --topic "topic description" [--sessions id1,id2,...] [--context file]');
1113
+ console.error(' telepty deliberate status [thread_id]');
1114
+ console.error(' telepty deliberate end <thread_id>');
1115
+ process.exit(1);
1116
+ }
1117
+
1118
+ const orchestratorId = process.env.TELEPTY_SESSION_ID || null;
1119
+
1120
+ // Read context file if provided
1121
+ let contextContent = null;
1122
+ if (contextPath) {
1123
+ try {
1124
+ contextContent = fs.readFileSync(contextPath, 'utf-8');
1125
+ } catch (err) {
1126
+ console.error(`Failed to read context file: ${err.message}`);
1127
+ process.exit(1);
1128
+ }
1129
+ }
1130
+
1131
+ // Discover target sessions
1132
+ let targetSessions;
1133
+ try {
1134
+ const discovered = await discoverSessions({ silent: true });
1135
+ if (sessionsArg) {
1136
+ const requestedIds = sessionsArg.split(',').map(s => s.trim());
1137
+ targetSessions = discovered.filter(s => requestedIds.includes(s.id));
1138
+ const foundIds = targetSessions.map(s => s.id);
1139
+ const missing = requestedIds.filter(id => !foundIds.includes(id));
1140
+ if (missing.length > 0) {
1141
+ console.error(`Warning: Sessions not found: ${missing.join(', ')}`);
1142
+ }
1143
+ } else {
1144
+ // All sessions except orchestrator
1145
+ targetSessions = discovered.filter(s => s.id !== orchestratorId);
1146
+ }
1147
+ } catch (err) {
1148
+ console.error('Failed to discover sessions:', err.message);
1149
+ process.exit(1);
1150
+ }
1151
+
1152
+ if (targetSessions.length === 0) {
1153
+ console.error('No target sessions found.');
1154
+ process.exit(1);
1155
+ }
1156
+
1157
+ const participantIds = targetSessions.map(s => s.id);
1158
+
1159
+ // Create thread on daemon
1160
+ let threadId;
1161
+ try {
1162
+ const resp = await fetchWithAuth(`${DAEMON_URL}/api/threads`, {
1163
+ method: 'POST',
1164
+ headers: { 'Content-Type': 'application/json' },
1165
+ body: JSON.stringify({
1166
+ topic,
1167
+ orchestrator_session_id: orchestratorId,
1168
+ participant_session_ids: participantIds,
1169
+ context: contextContent
1170
+ })
1171
+ });
1172
+ const result = await resp.json();
1173
+ if (!resp.ok) { console.error('Error:', result.error); process.exit(1); }
1174
+ threadId = result.thread_id;
1175
+ } catch (err) {
1176
+ console.error('Failed to create thread:', err.message);
1177
+ process.exit(1);
1178
+ }
1179
+
1180
+ // Build session directory
1181
+ const sessionDirectory = targetSessions.map(s => {
1182
+ const proj = s.cwd ? s.cwd.split('/').pop() : '(unknown)';
1183
+ return ` - ${s.id} (${s.command || 'unknown'}, project: ${proj})`;
1184
+ }).join('\n');
1185
+
1186
+ // Build protocol template
1187
+ const protocolTemplate = `[from: ${orchestratorId || 'orchestrator'}] [reply-to: ${orchestratorId || 'orchestrator'}]
1188
+
1189
+ ## Bidirectional Multi-Session Deliberation
1190
+
1191
+ **Thread ID:** ${threadId}
1192
+ **Topic:** ${topic}
1193
+ **Orchestrator:** ${orchestratorId || '(not set)'}
1194
+
1195
+ ### Session Directory
1196
+ ${sessionDirectory}
1197
+
1198
+ ${contextContent ? `### Context\n${contextContent}\n` : ''}
1199
+ ### Protocol Rules (MANDATORY)
1200
+
1201
+ 1. **Always include sender identity**: Every message you send to another session MUST include \`[from: YOUR_SESSION_ID] [reply-to: YOUR_SESSION_ID]\` at the beginning.
1202
+
1203
+ 2. **Use telepty for cross-session communication**: To send a message to another session:
1204
+ \`\`\`
1205
+ telepty inject --from YOUR_SESSION_ID --reply-to YOUR_SESSION_ID <target_session_id> "your message"
1206
+ \`\`\`
1207
+ Or use: \`telepty reply "your message"\` to reply to the last sender.
1208
+
1209
+ 3. **Do NOT self-resolve cross-cutting concerns**: If a question involves another project's domain, ASK that session directly via telepty inject. Do not guess or assume.
1210
+
1211
+ 4. **Sub-deliberation allowed**: You may initiate side conversations with specific sessions for detailed technical discussions.
1212
+
1213
+ 5. **Thread tracking**: Include \`thread_id: ${threadId}\` in bus events for this deliberation.
1214
+
1215
+ 6. **Completion**: When you believe the discussion on your part is complete, send a summary to the orchestrator (${orchestratorId || 'orchestrator'}).
1216
+
1217
+ ### Your Task
1218
+ Discuss the following topic from your project's perspective. Engage with other sessions to align on interfaces and implementation details.
1219
+
1220
+ **Topic:** ${topic}
1221
+ `;
1222
+
1223
+ // Inject protocol to all target sessions
1224
+ console.log(`\nStarting deliberation thread ${threadId.slice(0, 8)}...`);
1225
+ console.log(`Topic: ${topic}`);
1226
+ console.log(`Participants: ${participantIds.length}\n`);
1227
+
1228
+ let successCount = 0;
1229
+ let failCount = 0;
1230
+
1231
+ for (const session of targetSessions) {
1232
+ try {
1233
+ const host = session._host || '127.0.0.1';
1234
+ const body = {
1235
+ prompt: protocolTemplate,
1236
+ no_enter: true,
1237
+ from: orchestratorId,
1238
+ reply_to: orchestratorId,
1239
+ thread_id: threadId
1240
+ };
1241
+ const resp = await fetchWithAuth(`http://${host}:${PORT}/api/sessions/${encodeURIComponent(session.id)}/inject`, {
1242
+ method: 'POST',
1243
+ headers: { 'Content-Type': 'application/json' },
1244
+ body: JSON.stringify(body)
1245
+ });
1246
+ if (resp.ok) {
1247
+ // Submit after text injection (300ms delay handled by daemon)
1248
+ setTimeout(async () => {
1249
+ try {
1250
+ await fetchWithAuth(`http://${host}:${PORT}/api/sessions/${encodeURIComponent(session.id)}/submit`, { method: 'POST' });
1251
+ } catch {}
1252
+ }, 500);
1253
+ console.log(` ✅ Injected to ${session.id}`);
1254
+ successCount++;
1255
+ } else {
1256
+ const err = await resp.json();
1257
+ console.log(` ❌ Failed ${session.id}: ${err.error}`);
1258
+ failCount++;
1259
+ }
1260
+ } catch (err) {
1261
+ console.log(` ❌ Failed ${session.id}: ${err.message}`);
1262
+ failCount++;
1263
+ }
1264
+ }
1265
+
1266
+ console.log(`\nDeliberation started: ${successCount} injected, ${failCount} failed`);
1267
+ console.log(`Thread ID: ${threadId}`);
1268
+ console.log(`Monitor: telepty deliberate status ${threadId}`);
1269
+ console.log(`End: telepty deliberate end ${threadId}`);
1270
+
1271
+ // Wait for submit timeouts to complete
1272
+ await new Promise(resolve => setTimeout(resolve, 1500));
1273
+ return;
1274
+ }
1275
+
1276
+ if (cmd === 'handoff') {
1277
+ const handoffCmd = args[1];
1278
+
1279
+ if (!handoffCmd || handoffCmd === 'list') {
1280
+ // telepty handoff list [--status=pending]
1281
+ const statusFilter = args.find(a => a.startsWith('--status='));
1282
+ const qs = statusFilter ? `?status=${statusFilter.split('=')[1]}` : '';
1283
+ try {
1284
+ const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff${qs}`);
1285
+ const list = await resp.json();
1286
+ if (list.length === 0) {
1287
+ console.log('No handoffs found.');
1288
+ } else {
1289
+ console.log(`\n Handoffs (${list.length}):\n`);
1290
+ for (const h of list) {
1291
+ const statusIcon = { pending: '⏳', claimed: '🔄', executing: '⚙️', completed: '✅', failed: '❌' }[h.status] || '?';
1292
+ console.log(` ${statusIcon} ${h.id.slice(0, 8)} ${h.status.padEnd(10)} tasks:${h.task_count} ${h.deliberation_id || '(no delib)'} ${h.created_at}`);
1293
+ }
1294
+ console.log();
1295
+ }
1296
+ } catch (err) {
1297
+ console.error('Failed to list handoffs:', err.message);
1298
+ process.exit(1);
1299
+ }
1300
+
1301
+ } else if (handoffCmd === 'drop') {
1302
+ // telepty handoff drop [--delib=ID] [--source=SESSION] [--auto-execute] < synthesis.json
1303
+ // Or: telepty handoff drop --summary="..." --tasks='[{"task":"do X","files":["a.js"]}]'
1304
+ const delibFlag = args.find(a => a.startsWith('--delib='));
1305
+ const sourceFlag = args.find(a => a.startsWith('--source='));
1306
+ const autoExec = args.includes('--auto-execute');
1307
+ const summaryFlag = args.find(a => a.startsWith('--summary='));
1308
+ const tasksFlag = args.find(a => a.startsWith('--tasks='));
1309
+
1310
+ let synthesis;
1311
+ if (summaryFlag && tasksFlag) {
1312
+ synthesis = {
1313
+ summary: summaryFlag.split('=').slice(1).join('='),
1314
+ tasks: JSON.parse(tasksFlag.split('=').slice(1).join('='))
1315
+ };
1316
+ } else if (!process.stdin.isTTY) {
1317
+ // Read from stdin
1318
+ const chunks = [];
1319
+ for await (const chunk of process.stdin) {
1320
+ chunks.push(chunk);
1321
+ }
1322
+ synthesis = JSON.parse(Buffer.concat(chunks).toString());
1323
+ } else {
1324
+ console.error('Usage: telepty handoff drop --summary="..." --tasks=\'[...]\'');
1325
+ console.error(' Or pipe JSON: echo \'{"summary":"...","tasks":[...]}\' | telepty handoff drop');
1326
+ process.exit(1);
1327
+ }
1328
+
1329
+ try {
1330
+ const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff`, {
1331
+ method: 'POST',
1332
+ headers: { 'Content-Type': 'application/json' },
1333
+ body: JSON.stringify({
1334
+ deliberation_id: delibFlag ? delibFlag.split('=').slice(1).join('=') : null,
1335
+ source_session_id: sourceFlag ? sourceFlag.split('=').slice(1).join('=') : (process.env.TELEPTY_SESSION_ID || null),
1336
+ synthesis,
1337
+ auto_execute: autoExec
1338
+ })
1339
+ });
1340
+ const result = await resp.json();
1341
+ if (resp.ok) {
1342
+ console.log(`Handoff created: ${result.handoff_id}`);
1343
+ } else {
1344
+ console.error('Failed:', result.error);
1345
+ process.exit(1);
1346
+ }
1347
+ } catch (err) {
1348
+ console.error('Failed to create handoff:', err.message);
1349
+ process.exit(1);
1350
+ }
1351
+
1352
+ } else if (handoffCmd === 'claim') {
1353
+ // telepty handoff claim <handoff_id> [--agent=SESSION_ID]
1354
+ const handoffId = args[2];
1355
+ if (!handoffId) {
1356
+ console.error('Usage: telepty handoff claim <handoff_id> [--agent=SESSION_ID]');
1357
+ process.exit(1);
1358
+ }
1359
+ const agentFlag = args.find(a => a.startsWith('--agent='));
1360
+ const agentId = agentFlag ? agentFlag.split('=').slice(1).join('=') : process.env.TELEPTY_SESSION_ID;
1361
+ if (!agentId) {
1362
+ console.error('Error: --agent=SESSION_ID or TELEPTY_SESSION_ID env required');
1363
+ process.exit(1);
1364
+ }
1365
+
1366
+ try {
1367
+ const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff/${handoffId}/claim`, {
1368
+ method: 'POST',
1369
+ headers: { 'Content-Type': 'application/json' },
1370
+ body: JSON.stringify({ agent_session_id: agentId })
1371
+ });
1372
+ const result = await resp.json();
1373
+ if (resp.ok) {
1374
+ console.log(`Claimed handoff ${handoffId}`);
1375
+ } else {
1376
+ console.error('Failed:', result.error);
1377
+ process.exit(1);
1378
+ }
1379
+ } catch (err) {
1380
+ console.error('Failed to claim handoff:', err.message);
1381
+ process.exit(1);
1382
+ }
1383
+
1384
+ } else if (handoffCmd === 'status') {
1385
+ // telepty handoff status <handoff_id> [executing|completed|failed] [--message="..."]
1386
+ const handoffId = args[2];
1387
+ if (!handoffId) {
1388
+ console.error('Usage: telepty handoff status <handoff_id> [new_status] [--message="..."]');
1389
+ process.exit(1);
1390
+ }
1391
+
1392
+ const newStatus = args[3] && !args[3].startsWith('--') ? args[3] : null;
1393
+ const msgFlag = args.find(a => a.startsWith('--message='));
1394
+
1395
+ if (!newStatus) {
1396
+ // GET status
1397
+ try {
1398
+ const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff/${handoffId}`);
1399
+ const handoff = await resp.json();
1400
+ if (!resp.ok) {
1401
+ console.error('Error:', handoff.error);
1402
+ process.exit(1);
1403
+ }
1404
+ console.log(`\n Handoff: ${handoff.id}`);
1405
+ console.log(` Status: ${handoff.status}`);
1406
+ console.log(` Deliberation: ${handoff.deliberation_id || '(none)'}`);
1407
+ console.log(` Claimed by: ${handoff.claimed_by || '(unclaimed)'}`);
1408
+ console.log(` Tasks: ${Array.isArray(handoff.synthesis.tasks) ? handoff.synthesis.tasks.length : 0}`);
1409
+ if (handoff.synthesis.summary) console.log(` Summary: ${handoff.synthesis.summary}`);
1410
+ if (handoff.progress.length > 0) {
1411
+ console.log(` Progress:`);
1412
+ for (const p of handoff.progress) {
1413
+ console.log(` - ${p.timestamp}: ${p.message}`);
1414
+ }
1415
+ }
1416
+ console.log();
1417
+ } catch (err) {
1418
+ console.error('Failed:', err.message);
1419
+ process.exit(1);
1420
+ }
1421
+ } else {
1422
+ // PATCH status
1423
+ try {
1424
+ const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff/${handoffId}`, {
1425
+ method: 'PATCH',
1426
+ headers: { 'Content-Type': 'application/json' },
1427
+ body: JSON.stringify({
1428
+ status: newStatus,
1429
+ message: msgFlag ? msgFlag.split('=').slice(1).join('=') : null
1430
+ })
1431
+ });
1432
+ const result = await resp.json();
1433
+ if (resp.ok) {
1434
+ console.log(`Handoff ${handoffId} -> ${newStatus}`);
1435
+ } else {
1436
+ console.error('Failed:', result.error);
1437
+ process.exit(1);
1438
+ }
1439
+ } catch (err) {
1440
+ console.error('Failed:', err.message);
1441
+ process.exit(1);
1442
+ }
1443
+ }
1444
+
1445
+ } else if (handoffCmd === 'get') {
1446
+ // telepty handoff get <handoff_id> — dump full synthesis JSON
1447
+ const handoffId = args[2];
1448
+ if (!handoffId) {
1449
+ console.error('Usage: telepty handoff get <handoff_id>');
1450
+ process.exit(1);
1451
+ }
1452
+ try {
1453
+ const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff/${handoffId}`);
1454
+ const handoff = await resp.json();
1455
+ if (!resp.ok) {
1456
+ console.error('Error:', handoff.error);
1457
+ process.exit(1);
1458
+ }
1459
+ // Output raw JSON for piping to other tools
1460
+ console.log(JSON.stringify(handoff.synthesis, null, 2));
1461
+ } catch (err) {
1462
+ console.error('Failed:', err.message);
1463
+ process.exit(1);
1464
+ }
1465
+
1466
+ } else {
1467
+ console.error(`Unknown handoff command: ${handoffCmd}`);
1468
+ console.error('Available: list, drop, claim, status, get');
1469
+ process.exit(1);
1470
+ }
1471
+ return;
1472
+ }
1473
+
986
1474
  if (cmd === 'listen' || cmd === 'monitor') {
987
1475
  await ensureDaemonRunning();
988
1476
 
@@ -1063,13 +1551,27 @@ Usage:
1063
1551
  telepty allow [--id <id>] <command> [args...] Allow inject on a CLI
1064
1552
  telepty list List all active sessions across discovered hosts
1065
1553
  telepty attach [id[@host]] Attach to a session (Interactive picker if no ID)
1066
- telepty inject [--no-enter] <id[@host]> "<prompt>" Inject text into a single session
1554
+ telepty inject [--no-enter] [--from <id>] [--reply-to <id>] <id[@host]> "<prompt>" Inject text into a single session
1555
+ telepty reply "<text>" Reply to the session that last injected into $TELEPTY_SESSION_ID
1067
1556
  telepty multicast <id1[@host],id2[@host]> "<prompt>" Inject text into multiple specific sessions
1068
1557
  telepty broadcast "<prompt>" Inject text into ALL active sessions
1069
1558
  telepty rename <old_id[@host]> <new_id> Rename a session (updates terminal title too)
1070
1559
  telepty listen Listen to the event bus and print JSON to stdout
1071
1560
  telepty monitor Human-readable real-time billboard of bus events
1072
1561
  telepty update Update telepty to the latest version
1562
+
1563
+ Handoff Commands:
1564
+ handoff list [--status=S] List handoffs (filter: pending/claimed/executing/completed)
1565
+ handoff drop [options] Create handoff from synthesis (pipe JSON or use --summary/--tasks)
1566
+ handoff claim <id> [--agent=S] Claim a pending handoff
1567
+ handoff status <id> [status] Get or update handoff status
1568
+ handoff get <id> Get full synthesis JSON (for piping)
1569
+
1570
+ Deliberation Commands:
1571
+ deliberate --topic "..." [--sessions s1,s2] [--context file]
1572
+ Start multi-session deliberation
1573
+ deliberate status [thread_id] List threads or show thread details
1574
+ deliberate end <thread_id> Close a deliberation thread
1073
1575
  `);
1074
1576
  }
1075
1577
 
package/daemon.js CHANGED
@@ -46,6 +46,8 @@ if (!daemonClaim.claimed) {
46
46
  }
47
47
 
48
48
  const sessions = {};
49
+ const handoffs = {};
50
+ const threads = {};
49
51
  const STRIPPED_SESSION_ENV_KEYS = [
50
52
  'CLAUDECODE',
51
53
  'CODEX_CI',
@@ -208,6 +210,22 @@ app.get('/api/sessions', (req, res) => {
208
210
  res.json(list);
209
211
  });
210
212
 
213
+ app.get('/api/sessions/:id', (req, res) => {
214
+ const { id } = req.params;
215
+ const session = sessions[id];
216
+ if (!session) return res.status(404).json({ error: 'Session not found' });
217
+ res.json({
218
+ id,
219
+ type: session.type || 'spawned',
220
+ command: session.command,
221
+ cwd: session.cwd,
222
+ createdAt: session.createdAt,
223
+ active_clients: session.clients ? session.clients.size : 0,
224
+ lastInjectFrom: session.lastInjectFrom || null,
225
+ lastInjectReplyTo: session.lastInjectReplyTo || null
226
+ });
227
+ });
228
+
211
229
  app.get('/api/meta', (req, res) => {
212
230
  res.json({
213
231
  name: pkg.name,
@@ -215,7 +233,7 @@ app.get('/api/meta', (req, res) => {
215
233
  pid: process.pid,
216
234
  host: HOST,
217
235
  port: Number(PORT),
218
- capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon']
236
+ capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon', 'handoff-inbox', 'deliberation-threads']
219
237
  });
220
238
  });
221
239
 
@@ -458,10 +476,12 @@ app.post('/api/sessions/submit-all', (req, res) => {
458
476
 
459
477
  app.post('/api/sessions/:id/inject', (req, res) => {
460
478
  const { id } = req.params;
461
- const { prompt, no_enter, auto_submit } = req.body;
479
+ const { prompt, no_enter, auto_submit, from, reply_to } = req.body;
462
480
  const session = sessions[id];
463
481
  if (!session) return res.status(404).json({ error: 'Session not found' });
464
482
  if (!prompt) return res.status(400).json({ error: 'prompt is required' });
483
+ if (from) session.lastInjectFrom = from;
484
+ if (reply_to) session.lastInjectReplyTo = reply_to;
465
485
  const inject_id = crypto.randomUUID();
466
486
  try {
467
487
  // Always inject text WITHOUT \r first, then send \r separately after delay
@@ -501,12 +521,31 @@ app.post('/api/sessions/:id/inject', (req, res) => {
501
521
  sender: 'daemon',
502
522
  target_agent: id,
503
523
  content: prompt,
524
+ from: from || null,
525
+ reply_to: reply_to || null,
504
526
  timestamp: new Date().toISOString()
505
527
  });
506
528
  busClients.forEach(client => {
507
529
  if (client.readyState === 1) client.send(busMsg);
508
530
  });
509
531
 
532
+ if (from && reply_to) {
533
+ const routedMsg = JSON.stringify({
534
+ type: 'message_routed',
535
+ message_id: inject_id,
536
+ from,
537
+ to: id,
538
+ reply_to,
539
+ inject_id,
540
+ deliberation_session_id: req.body.deliberation_session_id || null,
541
+ thread_id: req.body.thread_id || null,
542
+ timestamp: new Date().toISOString()
543
+ });
544
+ busClients.forEach(client => {
545
+ if (client.readyState === 1) client.send(routedMsg);
546
+ });
547
+ }
548
+
510
549
  res.json({ success: true, inject_id, submit: submitResult });
511
550
  } catch (err) {
512
551
  const busFailMsg = JSON.stringify({
@@ -593,6 +632,232 @@ app.post('/api/bus/publish', (req, res) => {
593
632
  res.json({ success: true, delivered: deliveredCount });
594
633
  });
595
634
 
635
+ app.post('/api/handoff', (req, res) => {
636
+ const { source_session_id, deliberation_id, synthesis, auto_execute } = req.body;
637
+ if (!synthesis) return res.status(400).json({ error: 'synthesis is required' });
638
+
639
+ const handoff_id = crypto.randomUUID();
640
+ const handoff = {
641
+ id: handoff_id,
642
+ source_session_id: source_session_id || null,
643
+ deliberation_id: deliberation_id || null,
644
+ synthesis,
645
+ status: 'pending',
646
+ auto_execute: !!auto_execute,
647
+ claimed_by: null,
648
+ created_at: new Date().toISOString(),
649
+ updated_at: new Date().toISOString(),
650
+ progress: [],
651
+ result: null
652
+ };
653
+ handoffs[handoff_id] = handoff;
654
+
655
+ const busMsg = JSON.stringify({
656
+ type: 'handoff.created',
657
+ handoff_id,
658
+ source_session_id: handoff.source_session_id,
659
+ deliberation_id: handoff.deliberation_id,
660
+ auto_execute: handoff.auto_execute,
661
+ task_count: Array.isArray(synthesis.tasks) ? synthesis.tasks.length : 0,
662
+ timestamp: handoff.created_at
663
+ });
664
+ busClients.forEach(client => {
665
+ if (client.readyState === 1) client.send(busMsg);
666
+ });
667
+
668
+ console.log(`[HANDOFF] Created ${handoff_id} (${Array.isArray(synthesis.tasks) ? synthesis.tasks.length : 0} tasks)`);
669
+ res.status(201).json({ handoff_id, status: 'pending' });
670
+ });
671
+
672
+ app.get('/api/handoff', (req, res) => {
673
+ const status = req.query.status;
674
+ const list = Object.values(handoffs)
675
+ .filter(h => !status || h.status === status)
676
+ .map(h => ({
677
+ id: h.id,
678
+ status: h.status,
679
+ deliberation_id: h.deliberation_id,
680
+ source_session_id: h.source_session_id,
681
+ auto_execute: h.auto_execute,
682
+ claimed_by: h.claimed_by,
683
+ task_count: Array.isArray(h.synthesis.tasks) ? h.synthesis.tasks.length : 0,
684
+ created_at: h.created_at,
685
+ updated_at: h.updated_at
686
+ }));
687
+ res.json(list);
688
+ });
689
+
690
+ app.get('/api/handoff/:id', (req, res) => {
691
+ const handoff = handoffs[req.params.id];
692
+ if (!handoff) return res.status(404).json({ error: 'Handoff not found' });
693
+ res.json(handoff);
694
+ });
695
+
696
+ app.post('/api/handoff/:id/claim', (req, res) => {
697
+ const handoff = handoffs[req.params.id];
698
+ if (!handoff) return res.status(404).json({ error: 'Handoff not found' });
699
+ if (handoff.status !== 'pending') {
700
+ return res.status(409).json({ error: `Handoff already ${handoff.status}`, claimed_by: handoff.claimed_by });
701
+ }
702
+
703
+ const { agent_session_id } = req.body;
704
+ if (!agent_session_id) return res.status(400).json({ error: 'agent_session_id is required' });
705
+
706
+ handoff.status = 'claimed';
707
+ handoff.claimed_by = agent_session_id;
708
+ handoff.updated_at = new Date().toISOString();
709
+
710
+ const busMsg = JSON.stringify({
711
+ type: 'handoff.claimed',
712
+ handoff_id: handoff.id,
713
+ agent_session_id,
714
+ timestamp: handoff.updated_at
715
+ });
716
+ busClients.forEach(client => {
717
+ if (client.readyState === 1) client.send(busMsg);
718
+ });
719
+
720
+ console.log(`[HANDOFF] ${handoff.id} claimed by ${agent_session_id}`);
721
+ res.json({ success: true, handoff_id: handoff.id, status: 'claimed' });
722
+ });
723
+
724
+ app.patch('/api/handoff/:id', (req, res) => {
725
+ const handoff = handoffs[req.params.id];
726
+ if (!handoff) return res.status(404).json({ error: 'Handoff not found' });
727
+
728
+ const { status, message, result } = req.body;
729
+ const validTransitions = {
730
+ pending: ['claimed'],
731
+ claimed: ['executing', 'failed'],
732
+ executing: ['completed', 'failed'],
733
+ };
734
+
735
+ if (status) {
736
+ const allowed = validTransitions[handoff.status] || [];
737
+ if (!allowed.includes(status)) {
738
+ return res.status(400).json({ error: `Invalid transition: ${handoff.status} -> ${status}` });
739
+ }
740
+ handoff.status = status;
741
+ }
742
+
743
+ if (message) {
744
+ handoff.progress.push({ message, timestamp: new Date().toISOString() });
745
+ }
746
+
747
+ if (result) {
748
+ handoff.result = result;
749
+ }
750
+
751
+ handoff.updated_at = new Date().toISOString();
752
+
753
+ const busMsg = JSON.stringify({
754
+ type: `handoff.${handoff.status}`,
755
+ handoff_id: handoff.id,
756
+ claimed_by: handoff.claimed_by,
757
+ message: message || null,
758
+ timestamp: handoff.updated_at
759
+ });
760
+ busClients.forEach(client => {
761
+ if (client.readyState === 1) client.send(busMsg);
762
+ });
763
+
764
+ console.log(`[HANDOFF] ${handoff.id} -> ${handoff.status}${message ? ': ' + message : ''}`);
765
+ res.json({ success: true, handoff_id: handoff.id, status: handoff.status });
766
+ });
767
+
768
+ // --- Deliberation Thread Tracking ---
769
+
770
+ app.post('/api/threads', (req, res) => {
771
+ const { topic, orchestrator_session_id, participant_session_ids, context } = req.body;
772
+ if (!topic) return res.status(400).json({ error: 'topic is required' });
773
+
774
+ const thread_id = crypto.randomUUID();
775
+ const thread = {
776
+ id: thread_id,
777
+ topic,
778
+ orchestrator_session_id: orchestrator_session_id || null,
779
+ participant_session_ids: participant_session_ids || [],
780
+ context: context || null,
781
+ status: 'active',
782
+ message_count: 0,
783
+ created_at: new Date().toISOString(),
784
+ updated_at: new Date().toISOString(),
785
+ closed_at: null
786
+ };
787
+ threads[thread_id] = thread;
788
+
789
+ const busMsg = JSON.stringify({
790
+ type: 'thread.opened',
791
+ thread_id,
792
+ topic,
793
+ orchestrator_session_id: thread.orchestrator_session_id,
794
+ participant_session_ids: thread.participant_session_ids,
795
+ timestamp: thread.created_at
796
+ });
797
+ busClients.forEach(client => {
798
+ if (client.readyState === 1) client.send(busMsg);
799
+ });
800
+
801
+ console.log(`[THREAD] Opened ${thread_id}: "${topic}" (${thread.participant_session_ids.length} participants)`);
802
+ res.status(201).json({ thread_id, status: 'active' });
803
+ });
804
+
805
+ app.get('/api/threads', (req, res) => {
806
+ const status = req.query.status;
807
+ const list = Object.values(threads)
808
+ .filter(t => !status || t.status === status)
809
+ .map(t => ({
810
+ id: t.id,
811
+ topic: t.topic,
812
+ status: t.status,
813
+ orchestrator_session_id: t.orchestrator_session_id,
814
+ participant_count: t.participant_session_ids.length,
815
+ message_count: t.message_count,
816
+ created_at: t.created_at,
817
+ updated_at: t.updated_at
818
+ }));
819
+ res.json(list);
820
+ });
821
+
822
+ app.get('/api/threads/:id', (req, res) => {
823
+ const thread = threads[req.params.id];
824
+ if (!thread) return res.status(404).json({ error: 'Thread not found' });
825
+ res.json(thread);
826
+ });
827
+
828
+ app.patch('/api/threads/:id', (req, res) => {
829
+ const thread = threads[req.params.id];
830
+ if (!thread) return res.status(404).json({ error: 'Thread not found' });
831
+
832
+ const { status, message_count } = req.body;
833
+
834
+ if (status === 'closed' && thread.status === 'active') {
835
+ thread.status = 'closed';
836
+ thread.closed_at = new Date().toISOString();
837
+ thread.updated_at = thread.closed_at;
838
+
839
+ const busMsg = JSON.stringify({
840
+ type: 'thread.closed',
841
+ thread_id: thread.id,
842
+ topic: thread.topic,
843
+ message_count: thread.message_count,
844
+ timestamp: thread.closed_at
845
+ });
846
+ busClients.forEach(client => {
847
+ if (client.readyState === 1) client.send(busMsg);
848
+ });
849
+
850
+ console.log(`[THREAD] Closed ${thread.id}: "${thread.topic}" (${thread.message_count} messages)`);
851
+ }
852
+
853
+ if (typeof message_count === 'number') {
854
+ thread.message_count = message_count;
855
+ thread.updated_at = new Date().toISOString();
856
+ }
857
+
858
+ res.json({ success: true, thread_id: thread.id, status: thread.status });
859
+ });
860
+
596
861
  const server = app.listen(PORT, HOST, () => {
597
862
  console.log(`🚀 aigentry-telepty daemon listening on http://${HOST}:${PORT}`);
598
863
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",