@dmsdc-ai/aigentry-telepty 0.3.3 → 0.4.0

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 (47) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +67 -1
  3. package/cli.js +161 -54
  4. package/cross-machine.js +132 -0
  5. package/daemon.js +355 -5
  6. package/host-spec.js +60 -0
  7. package/mcp-server/index.mjs +24 -3
  8. package/package.json +30 -5
  9. package/session-state.js +23 -0
  10. package/skill-installer.js +42 -6
  11. package/skills/telepty/SKILL.md +1 -1
  12. package/skills/telepty-allow/SKILL.md +1 -1
  13. package/skills/telepty-attach/SKILL.md +1 -1
  14. package/skills/telepty-broadcast/SKILL.md +1 -1
  15. package/skills/telepty-daemon/SKILL.md +1 -1
  16. package/skills/telepty-inject/SKILL.md +76 -4
  17. package/skills/telepty-list/SKILL.md +1 -1
  18. package/skills/telepty-listen/SKILL.md +1 -1
  19. package/skills/telepty-rename/SKILL.md +1 -1
  20. package/skills/telepty-session/SKILL.md +1 -1
  21. package/src/init/print-snippet.js +114 -0
  22. package/src/init/snippets/agents.md +15 -0
  23. package/src/init/snippets/claude.md +15 -0
  24. package/src/init/snippets/gemini.md +15 -0
  25. package/src/prompt-symbol-registry.js +43 -1
  26. package/.claude/commands/telepty-allow.md +0 -58
  27. package/.claude/commands/telepty-attach.md +0 -22
  28. package/.claude/commands/telepty-inject.md +0 -72
  29. package/.claude/commands/telepty-list.md +0 -22
  30. package/.claude/commands/telepty-manual-test.md +0 -73
  31. package/.claude/commands/telepty-start.md +0 -25
  32. package/.claude/commands/telepty-test.md +0 -25
  33. package/.claude/commands/telepty.md +0 -82
  34. package/AGENTS.md +0 -74
  35. package/BOUNDARY.md +0 -31
  36. package/BUS_EVENT_SCHEMA.md +0 -206
  37. package/CLAUDE.md +0 -100
  38. package/GEMINI.md +0 -10
  39. package/URGENT_ISSUES.resolved.md +0 -1
  40. package/docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md +0 -447
  41. package/docs/superpowers/specs/2026-04-26-prompt-symbol-render-gate.md +0 -571
  42. package/docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md +0 -608
  43. package/docs/superpowers/specs/2026-05-02-submit-force-and-retry.md +0 -139
  44. package/protocol/mailbox.md +0 -244
  45. package/specs/codex-inject-spec.md +0 -201
  46. package/specs/enforce-report-spec.md +0 -237
  47. package/templates/AGENTS.md +0 -71
package/daemon.js CHANGED
@@ -15,6 +15,7 @@ const { UnixSocketNotifier } = require('./src/mailbox/notifier');
15
15
  const { SessionStateManager, STATE_DISPLAY, stripAnsi: stripAnsiState } = require('./session-state');
16
16
  const { classifyReportPrompt, buildAutoSummary } = require('./src/report-enforcement');
17
17
  const submitGate = require('./src/submit-gate');
18
+ const readyRegistry = require('./src/prompt-symbol-registry');
18
19
 
19
20
  const config = getConfig();
20
21
  const EXPECTED_TOKEN = config.authToken;
@@ -26,6 +27,8 @@ const SESSION_STALE_SECONDS = Math.max(1, Number(process.env.TELEPTY_SESSION_STA
26
27
  const SESSION_CLEANUP_SECONDS = Math.max(SESSION_STALE_SECONDS, Number(process.env.TELEPTY_SESSION_CLEANUP_SECONDS || 300));
27
28
  const DELIVERY_TIMEOUT_MS = Math.max(100, Number(process.env.TELEPTY_DELIVERY_TIMEOUT_MS || 5000));
28
29
  const HEALTH_POLL_MS = Math.max(100, Number(process.env.TELEPTY_HEALTH_POLL_MS || 10000));
30
+ const BOOTSTRAP_READY_TIMEOUT_MS = Math.max(500, Number(process.env.TELEPTY_BOOTSTRAP_READY_TIMEOUT_MS || 30000));
31
+ const WRAPPED_SUBMIT_DELAY_MS = 500;
29
32
 
30
33
  // Session state machine manager — auto-detects session state from PTY output
31
34
  const sessionStateManager = new SessionStateManager({
@@ -354,6 +357,292 @@ function getSessionHealthReason(session, healthStatus) {
354
357
  return session.ptyProcess && !session.ptyProcess.killed ? 'PTY_RUNNING' : 'PTY_EXITED';
355
358
  }
356
359
 
360
+ function sleep(ms) {
361
+ return new Promise((resolve) => setTimeout(resolve, ms));
362
+ }
363
+
364
+ function isBootstrapGatedSession(session) {
365
+ return !!(session && session.type === 'wrapped' && readyRegistry.isKnownAiCli(session.command));
366
+ }
367
+
368
+ function initializeBootstrapState(session) {
369
+ if (!session) return session;
370
+ if (!Array.isArray(session.bootstrapQueue)) {
371
+ session.bootstrapQueue = [];
372
+ }
373
+ session.bootstrapDraining = session.bootstrapDraining === true;
374
+ session.bootstrapDrainPromise = session.bootstrapDrainPromise || null;
375
+ session.bootstrapPromptPoll = session.bootstrapPromptPoll || null;
376
+
377
+ if (isBootstrapGatedSession(session)) {
378
+ session.bootstrapReady = session.bootstrapReady === true;
379
+ session.bootstrapReadyAt = session.bootstrapReadyAt || null;
380
+ session.bootstrapReadyReason = session.bootstrapReadyReason || null;
381
+ session.ready = session.bootstrapReady === true;
382
+ } else {
383
+ session.bootstrapReady = true;
384
+ session.bootstrapReadyAt = session.bootstrapReadyAt || new Date().toISOString();
385
+ session.bootstrapReadyReason = session.bootstrapReadyReason || 'generic_command_compat';
386
+ session.ready = true;
387
+ }
388
+ return session;
389
+ }
390
+
391
+ function isBootstrapReady(session) {
392
+ return !isBootstrapGatedSession(session) || session.bootstrapReady === true;
393
+ }
394
+
395
+ function buildBootstrapBlock(session) {
396
+ return {
397
+ gated: isBootstrapGatedSession(session),
398
+ ready: isBootstrapReady(session),
399
+ ready_at: session.bootstrapReadyAt || null,
400
+ reason: session.bootstrapReadyReason || null,
401
+ queued: Array.isArray(session.bootstrapQueue) ? session.bootstrapQueue.length : 0,
402
+ draining: session.bootstrapDraining === true
403
+ };
404
+ }
405
+
406
+ function shouldQueueBootstrapOperation(session) {
407
+ return isBootstrapGatedSession(session) && !isBootstrapReady(session);
408
+ }
409
+
410
+ function hasBootstrapBacklog(session) {
411
+ return !!(session && Array.isArray(session.bootstrapQueue) && session.bootstrapQueue.length > 0);
412
+ }
413
+
414
+ function emitBootstrapEvent(eventType, sessionId, session, extra = {}) {
415
+ broadcastSessionEvent(eventType, sessionId, session, {
416
+ extra: {
417
+ bootstrap: buildBootstrapBlock(session),
418
+ ...extra
419
+ }
420
+ });
421
+ }
422
+
423
+ function enqueueBootstrapOperation(sessionId, session, operation) {
424
+ initializeBootstrapState(session);
425
+ const op = {
426
+ op_id: crypto.randomUUID(),
427
+ queued_at: new Date().toISOString(),
428
+ ...operation
429
+ };
430
+
431
+ if (op.type === 'submit') {
432
+ op.promise = new Promise((resolve) => {
433
+ op.resolve = resolve;
434
+ });
435
+ }
436
+
437
+ session.bootstrapQueue.push(op);
438
+ emitBootstrapEvent('bootstrap_queue_queued', sessionId, session, {
439
+ op_id: op.op_id,
440
+ operation: op.type,
441
+ depth: session.bootstrapQueue.length
442
+ });
443
+ scheduleBootstrapPromptPoll(sessionId, session);
444
+ return op;
445
+ }
446
+
447
+ function resolveBootstrapSubmit(op, result) {
448
+ if (op && typeof op.resolve === 'function') {
449
+ op.resolve(result);
450
+ op.resolve = null;
451
+ }
452
+ }
453
+
454
+ function bootstrapQueuedResponse(op, extra = {}) {
455
+ return {
456
+ success: true,
457
+ strategy: 'bootstrap_queue',
458
+ queued: true,
459
+ bootstrap_queued: true,
460
+ bootstrap_op_id: op.op_id,
461
+ ...extra
462
+ };
463
+ }
464
+
465
+ async function executeBootstrapInject(sessionId, session, op) {
466
+ const prompt = typeof op.prompt === 'string' ? op.prompt : '';
467
+ const textResult = await writeDataToSession(sessionId, session, prompt);
468
+ if (!textResult.success) return textResult;
469
+
470
+ if (!op.noEnter) {
471
+ await sleep(WRAPPED_SUBMIT_DELAY_MS);
472
+ const submitResult = await writeDataToSession(sessionId, session, '\r');
473
+ if (!submitResult.success) return submitResult;
474
+ }
475
+
476
+ session.lastActivityAt = new Date().toISOString();
477
+ return {
478
+ success: true,
479
+ strategy: 'bootstrap_direct',
480
+ submit: op.noEnter ? 'skipped' : 'sent'
481
+ };
482
+ }
483
+
484
+ async function executeBootstrapSubmit(sessionId, session, op) {
485
+ const strategy = terminalLevelSubmit(sessionId, session);
486
+ if (!strategy) {
487
+ return {
488
+ status: 503,
489
+ body: {
490
+ error: 'Submit failed via all strategies (kitty/cmux/pty)',
491
+ strategy: 'none',
492
+ attempts: 0,
493
+ gated: false,
494
+ bootstrap_queued: true
495
+ }
496
+ };
497
+ }
498
+ return {
499
+ status: 200,
500
+ body: {
501
+ success: true,
502
+ strategy,
503
+ attempts: 1,
504
+ gated: false,
505
+ verify: null,
506
+ bootstrap_queued: true
507
+ }
508
+ };
509
+ }
510
+
511
+ async function drainBootstrapQueue(sessionId, session) {
512
+ if (!session || session.bootstrapDraining) {
513
+ return session ? session.bootstrapDrainPromise : null;
514
+ }
515
+ if (!isBootstrapReady(session)) {
516
+ return null;
517
+ }
518
+
519
+ session.bootstrapDraining = true;
520
+ session.bootstrapDrainPromise = (async () => {
521
+ while (hasBootstrapBacklog(session)) {
522
+ const op = session.bootstrapQueue.shift();
523
+ try {
524
+ if (op.cancelled) {
525
+ continue;
526
+ }
527
+ if (op.type === 'inject') {
528
+ const result = await executeBootstrapInject(sessionId, session, op);
529
+ if (!result.success) {
530
+ emitBootstrapEvent('bootstrap_queue_failed', sessionId, session, {
531
+ op_id: op.op_id,
532
+ operation: op.type,
533
+ code: result.code || 'DELIVERY_FAILED',
534
+ error: result.error || 'bootstrap delivery failed'
535
+ });
536
+ }
537
+ } else if (op.type === 'submit') {
538
+ const result = await executeBootstrapSubmit(sessionId, session, op);
539
+ resolveBootstrapSubmit(op, result);
540
+ if (result.status >= 400) {
541
+ emitBootstrapEvent('bootstrap_queue_failed', sessionId, session, {
542
+ op_id: op.op_id,
543
+ operation: op.type,
544
+ code: result.body.code || 'SUBMIT_FAILED',
545
+ error: result.body.error || 'bootstrap submit failed'
546
+ });
547
+ }
548
+ }
549
+ } catch (error) {
550
+ if (op.type === 'submit') {
551
+ resolveBootstrapSubmit(op, {
552
+ status: 500,
553
+ body: {
554
+ error: error.message || 'bootstrap submit failed',
555
+ strategy: 'none',
556
+ attempts: 0,
557
+ gated: false,
558
+ bootstrap_queued: true
559
+ }
560
+ });
561
+ }
562
+ emitBootstrapEvent('bootstrap_queue_failed', sessionId, session, {
563
+ op_id: op.op_id,
564
+ operation: op.type,
565
+ code: 'BOOTSTRAP_DRAIN_FAILED',
566
+ error: error.message || 'bootstrap drain failed'
567
+ });
568
+ }
569
+ }
570
+
571
+ emitBootstrapEvent('bootstrap_queue_drained', sessionId, session);
572
+ })().finally(() => {
573
+ session.bootstrapDraining = false;
574
+ session.bootstrapDrainPromise = null;
575
+ });
576
+
577
+ return session.bootstrapDrainPromise;
578
+ }
579
+
580
+ function markBootstrapReady(sessionId, session, reason) {
581
+ if (!session) return false;
582
+ initializeBootstrapState(session);
583
+ if (!isBootstrapGatedSession(session)) {
584
+ return false;
585
+ }
586
+ if (session.bootstrapReady === true) {
587
+ return false;
588
+ }
589
+
590
+ session.bootstrapReady = true;
591
+ session.bootstrapReadyAt = new Date().toISOString();
592
+ session.bootstrapReadyReason = reason || 'ready';
593
+ session.ready = true;
594
+ emitBootstrapEvent('bootstrap_ready', sessionId, session, { reason: session.bootstrapReadyReason });
595
+ drainBootstrapQueue(sessionId, session);
596
+ return true;
597
+ }
598
+
599
+ function scheduleBootstrapPromptPoll(sessionId, session) {
600
+ if (!session || !isBootstrapGatedSession(session) || isBootstrapReady(session)) return;
601
+ if (session.bootstrapPromptPoll || session.backend !== 'cmux' || !session.cmuxWorkspaceId) return;
602
+ if (!isOpenWebSocket(session.ownerWs)) return;
603
+
604
+ session.bootstrapPromptPoll = submitGate.awaitPromptSymbol(session, {
605
+ timeoutMs: BOOTSTRAP_READY_TIMEOUT_MS
606
+ }).then((result) => {
607
+ session.bootstrapPromptPoll = null;
608
+ if (result && result.ready && isOpenWebSocket(session.ownerWs)) {
609
+ markBootstrapReady(sessionId, session, 'cmux_prompt_symbol');
610
+ } else if (result && result.reason) {
611
+ emitBootstrapEvent('bootstrap_ready_timeout', sessionId, session, {
612
+ reason: result.reason,
613
+ waited_ms: result.waited_ms || 0
614
+ });
615
+ }
616
+ }).catch((error) => {
617
+ session.bootstrapPromptPoll = null;
618
+ emitBootstrapEvent('bootstrap_ready_timeout', sessionId, session, {
619
+ reason: 'prompt_symbol_error',
620
+ error: error.message || String(error)
621
+ });
622
+ });
623
+ }
624
+
625
+ async function waitForBootstrapSubmit(op, session, timeoutMs) {
626
+ const timeout = sleep(timeoutMs).then(() => {
627
+ op.cancelled = true;
628
+ return {
629
+ status: 504,
630
+ body: {
631
+ error: 'Submit bootstrap-timeout — target CLI did not become ready',
632
+ reason: 'bootstrap_not_ready',
633
+ last_state: sessionStateManager.getState(session.id)?.state || null,
634
+ strategy: 'none',
635
+ attempts: 0,
636
+ gated: true,
637
+ bootstrap_queued: true,
638
+ bootstrap_op_id: op.op_id,
639
+ bootstrap: buildBootstrapBlock(session)
640
+ }
641
+ };
642
+ });
643
+ return Promise.race([op.promise, timeout]);
644
+ }
645
+
357
646
  function buildSessionTransportBlock(session, options = {}) {
358
647
  if (!session) {
359
648
  return null;
@@ -380,7 +669,8 @@ function buildSessionTransportBlock(session, options = {}) {
380
669
  last_disconnected_at: session.lastDisconnectedAt || null,
381
670
  last_inject_from: session.lastInjectFrom || null,
382
671
  last_reply_to: session.lastInjectReplyTo || null,
383
- last_thread_id: session.lastThreadId || null
672
+ last_thread_id: session.lastThreadId || null,
673
+ bootstrap: buildBootstrapBlock(session)
384
674
  };
385
675
  }
386
676
 
@@ -645,6 +935,28 @@ function terminalLevelSubmit(id, session) {
645
935
 
646
936
  async function deliverInjectionToSession(id, session, prompt, options = {}) {
647
937
  const now = Date.now();
938
+ if (!options.bypassBootstrapQueue && shouldQueueBootstrapOperation(session)) {
939
+ const healthStatus = getSessionHealthStatus(session, { nowMs: now });
940
+ if (healthStatus === 'STALE') {
941
+ return { success: false, httpStatus: 410, code: 'STALE', error: 'Session is stale and awaiting cleanup.' };
942
+ }
943
+ const op = enqueueBootstrapOperation(id, session, {
944
+ type: 'inject',
945
+ prompt,
946
+ noEnter: !!options.noEnter,
947
+ options: {
948
+ source: options.source || 'inject',
949
+ from: options.from || 'daemon'
950
+ }
951
+ });
952
+ session.lastActivityAt = new Date(now).toISOString();
953
+ return bootstrapQueuedResponse(op, {
954
+ msg_id: op.op_id,
955
+ pending: session.bootstrapQueue.length,
956
+ submit: options.noEnter ? 'skipped' : 'queued'
957
+ });
958
+ }
959
+
648
960
  const injectFailure = getInjectFailure(session, { nowMs: now });
649
961
  if (injectFailure) {
650
962
  return { success: false, ...injectFailure };
@@ -834,6 +1146,7 @@ for (const [id, meta] of Object.entries(_persisted)) {
834
1146
  lastStateReportAt: meta.lastStateReportAt || null,
835
1147
  stateReport: meta.stateReport || null,
836
1148
  clients: new Set(), isClosing: false, outputRing: [], ready: true, };
1149
+ initializeBootstrapState(sessions[id]);
837
1150
  console.log(`[PERSIST] Restored session ${id} (awaiting reconnect)`);
838
1151
  }
839
1152
  }
@@ -1020,6 +1333,7 @@ app.post('/api/sessions/register', (req, res) => {
1020
1333
  existing.ready = true;
1021
1334
  markSessionConnected(existing);
1022
1335
  }
1336
+ initializeBootstrapState(existing);
1023
1337
  console.log(`[REGISTER] Re-registered session ${session_id} (type: ${existing.type}, updated metadata)`);
1024
1338
  return res.status(200).json({ session_id, type: existing.type, command: existing.command, cwd: existing.cwd, reregistered: true });
1025
1339
  }
@@ -1049,8 +1363,9 @@ app.post('/api/sessions/register', (req, res) => {
1049
1363
  clients: new Set(),
1050
1364
  isClosing: false,
1051
1365
  outputRing: [],
1052
- ready: true, // all sessions are injectable once registered (#150)
1053
- };
1366
+ ready: true, // unknown commands remain injectable once registered (#150)
1367
+ };
1368
+ initializeBootstrapState(sessionRecord);
1054
1369
  // Check for existing session with same base alias and emit replaced event
1055
1370
  const baseAlias = session_id.replace(/-\d+$/, '');
1056
1371
  const replaced = Object.keys(sessions).find(id => {
@@ -1521,6 +1836,18 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
1521
1836
 
1522
1837
  console.log(`[SUBMIT] Session ${id} (${session.command})${retries > 0 ? `, retries: ${retries}, pre_delay: ${preDelayMs}ms` : ''}${gateOff ? ' [gate=off]' : ''}`);
1523
1838
 
1839
+ if (isBootstrapGatedSession(session) && (!isBootstrapReady(session) || hasBootstrapBacklog(session) || session.bootstrapDraining)) {
1840
+ const op = enqueueBootstrapOperation(id, session, {
1841
+ type: 'submit',
1842
+ body: { ...(req.body || {}) }
1843
+ });
1844
+ if (isBootstrapReady(session)) {
1845
+ drainBootstrapQueue(id, session);
1846
+ }
1847
+ const queuedSubmit = await waitForBootstrapSubmit(op, session, gateTimeoutMs);
1848
+ return res.status(queuedSubmit.status).json(queuedSubmit.body);
1849
+ }
1850
+
1524
1851
  function emitSubmitBus(payload) {
1525
1852
  const busMsg = JSON.stringify({
1526
1853
  type: 'submit',
@@ -1805,6 +2132,12 @@ app.post('/api/sessions/:id/inject', async (req, res) => {
1805
2132
  delete pendingReports[senderAlias];
1806
2133
  const elapsedSecs = Number(((Date.now() - new Date(senderPending.injectedAt).getTime()) / 1000).toFixed(1));
1807
2134
  const senderSession = sessions[senderAlias];
2135
+ sessionStateManager.markIdle(senderAlias, 1.0, {
2136
+ trigger: 'report_inject',
2137
+ report_inject_id: inject_id,
2138
+ report_status: classification,
2139
+ source: senderPending.source
2140
+ });
1808
2141
  const eventType =
1809
2142
  classification === 'report_blocked' ? 'TASK_BLOCKED_WITH_REASON' :
1810
2143
  classification === 'report_dismissed' ? 'TASK_DISMISSED' :
@@ -1881,7 +2214,17 @@ app.post('/api/sessions/:id/inject', async (req, res) => {
1881
2214
  });
1882
2215
  }
1883
2216
 
1884
- res.json({ success: true, inject_id, strategy: delivery.strategy, submit: delivery.submit });
2217
+ res.json({
2218
+ success: true,
2219
+ inject_id,
2220
+ strategy: delivery.strategy,
2221
+ submit: delivery.submit,
2222
+ ...(delivery.bootstrap_queued ? {
2223
+ bootstrap_queued: true,
2224
+ bootstrap_op_id: delivery.bootstrap_op_id || delivery.msg_id,
2225
+ pending: delivery.pending
2226
+ } : {})
2227
+ });
1885
2228
  } catch (err) {
1886
2229
  emitInjectFailureEvent(id, 'DELIVERY_FAILED', err.message, { inject_id }, session);
1887
2230
  res.status(500).json(buildErrorBody('DELIVERY_FAILED', err.message));
@@ -2607,6 +2950,7 @@ wss.on('connection', (ws, req) => {
2607
2950
  outputRing: [],
2608
2951
  ready: true,
2609
2952
  };
2953
+ initializeBootstrapState(autoSession);
2610
2954
  sessions[sessionId] = autoSession;
2611
2955
  console.log(`[WS] Auto-registered wrapped session ${sessionId} on reconnect`);
2612
2956
  // Set tab title via kitty (no \x0c redraw — it causes flickering on multi-session reconnect)
@@ -2639,7 +2983,9 @@ wss.on('connection', (ws, req) => {
2639
2983
  }
2640
2984
  activeSession.ownerWs = ws;
2641
2985
  markSessionConnected(activeSession);
2986
+ initializeBootstrapState(activeSession);
2642
2987
  console.log(`[WS] Wrap owner ${isOwnerConnect && activeSession.clients.size > 1 ? 're-' : ''}connected for session ${sessionId} (Total: ${activeSession.clients.size})`);
2988
+ scheduleBootstrapPromptPoll(sessionId, activeSession);
2643
2989
  if (hadDisconnectedOwner) {
2644
2990
  emitSessionLifecycleEvent('session_reconnect', sessionId, activeSession);
2645
2991
  }
@@ -2665,7 +3011,11 @@ wss.on('connection', (ws, req) => {
2665
3011
  }
2666
3012
  });
2667
3013
  } else if (type === 'ready') {
2668
- activeSession.ready = true;
3014
+ if (isBootstrapGatedSession(activeSession)) {
3015
+ markBootstrapReady(sessionId, activeSession, 'bridge_ready');
3016
+ } else {
3017
+ activeSession.ready = true;
3018
+ }
2669
3019
  activeSession.lastActivityAt = new Date().toISOString();
2670
3020
  console.log(`[READY] Session ${sessionId} CLI is ready for inject`);
2671
3021
  // Broadcast readiness to bus (cmux/kitty paths now enabled for this session)
package/host-spec.js ADDED
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_PORT = 3848;
4
+
5
+ function parseHostSpec(value, defaultPort = DEFAULT_PORT) {
6
+ if (value === undefined || value === null || value === '') {
7
+ return { host: '127.0.0.1', port: defaultPort };
8
+ }
9
+
10
+ let raw = String(value).trim();
11
+ if (!raw) {
12
+ return { host: '127.0.0.1', port: defaultPort };
13
+ }
14
+
15
+ raw = raw.replace(/^https?:\/\//i, '');
16
+ raw = raw.replace(/\/.*$/, '');
17
+
18
+ const ipv6Bracketed = raw.match(/^\[([^\]]+)\](?::(\d+))?$/);
19
+ if (ipv6Bracketed) {
20
+ const port = ipv6Bracketed[2] ? Number(ipv6Bracketed[2]) : defaultPort;
21
+ return { host: ipv6Bracketed[1], port };
22
+ }
23
+
24
+ const colonCount = (raw.match(/:/g) || []).length;
25
+ if (colonCount > 1) {
26
+ return { host: raw, port: defaultPort };
27
+ }
28
+
29
+ const hostPort = raw.match(/^(.+):(\d+)$/);
30
+ if (hostPort) {
31
+ return { host: hostPort[1], port: Number(hostPort[2]) };
32
+ }
33
+
34
+ return { host: raw, port: defaultPort };
35
+ }
36
+
37
+ function formatHostForUrl(host) {
38
+ if (host && host.includes(':') && !host.startsWith('[')) {
39
+ return `[${host}]`;
40
+ }
41
+ return host;
42
+ }
43
+
44
+ function buildDaemonUrl(value, defaultPort = DEFAULT_PORT) {
45
+ const { host, port } = parseHostSpec(value, defaultPort);
46
+ return `http://${formatHostForUrl(host)}:${port}`;
47
+ }
48
+
49
+ function buildDaemonWsUrl(value, defaultPort = DEFAULT_PORT) {
50
+ const { host, port } = parseHostSpec(value, defaultPort);
51
+ return `ws://${formatHostForUrl(host)}:${port}`;
52
+ }
53
+
54
+ module.exports = {
55
+ DEFAULT_PORT,
56
+ parseHostSpec,
57
+ formatHostForUrl,
58
+ buildDaemonUrl,
59
+ buildDaemonWsUrl
60
+ };
@@ -34,9 +34,30 @@ function getAuthToken() {
34
34
  }
35
35
 
36
36
  function getDaemonUrl() {
37
- const port = process.env.TELEPTY_PORT || "3848";
38
- const host = process.env.TELEPTY_HOST || "127.0.0.1";
39
- return `http://${host}:${port}`;
37
+ // TELEPTY_HOST accepts: `host`, `host:port`, or `http://host:port`. Embedded
38
+ // port from TELEPTY_HOST is used unless TELEPTY_PORT is set explicitly.
39
+ const explicitPort = process.env.TELEPTY_PORT ? Number(process.env.TELEPTY_PORT) : null;
40
+ const raw = process.env.TELEPTY_HOST || "127.0.0.1";
41
+ let stripped = String(raw).trim().replace(/^https?:\/\//i, "").replace(/\/.*$/, "");
42
+ let host = stripped;
43
+ let embeddedPort = null;
44
+ const ipv6Bracketed = stripped.match(/^\[([^\]]+)\](?::(\d+))?$/);
45
+ if (ipv6Bracketed) {
46
+ host = ipv6Bracketed[1];
47
+ if (ipv6Bracketed[2]) embeddedPort = Number(ipv6Bracketed[2]);
48
+ } else {
49
+ const colonCount = (stripped.match(/:/g) || []).length;
50
+ if (colonCount === 1) {
51
+ const m = stripped.match(/^(.+):(\d+)$/);
52
+ if (m) {
53
+ host = m[1];
54
+ embeddedPort = Number(m[2]);
55
+ }
56
+ }
57
+ }
58
+ const port = explicitPort != null ? explicitPort : (embeddedPort != null ? embeddedPort : 3848);
59
+ const hostForUrl = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
60
+ return `http://${hostForUrl}:${port}`;
40
61
  }
41
62
 
42
63
  async function daemonFetch(endpoint, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
@@ -8,10 +8,35 @@
8
8
  "telepty-install": "install.js",
9
9
  "telepty-mcp": "mcp-server/index.mjs"
10
10
  },
11
+ "files": [
12
+ "cli.js",
13
+ "daemon.js",
14
+ "install.js",
15
+ "auth.js",
16
+ "cross-machine.js",
17
+ "daemon-control.js",
18
+ "entitlement.js",
19
+ "host-spec.js",
20
+ "interactive-terminal.js",
21
+ "runtime-info.js",
22
+ "session-routing.js",
23
+ "session-state.js",
24
+ "shared-context.js",
25
+ "skill-installer.js",
26
+ "terminal-backend.js",
27
+ "tui.js",
28
+ "install.sh",
29
+ "install.ps1",
30
+ "mcp-server/",
31
+ "src/",
32
+ "skills/",
33
+ "CHANGELOG.md"
34
+ ],
11
35
  "scripts": {
12
- "test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js",
13
- "test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js",
14
- "test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js"
36
+ "test": "node --test test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js && git diff --exit-code tests/snippet-protocol/v1/",
37
+ "test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js",
38
+ "test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/daemon-singleton.test.js test/cli.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/submit-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/host-spec.test.js test/cross-host-inject.test.js test/init.test.js && git diff --exit-code tests/snippet-protocol/v1/",
39
+ "regen-fixtures": "node scripts/regen-snippet-fixtures.js"
15
40
  },
16
41
  "keywords": [
17
42
  "pty",
@@ -48,7 +73,7 @@
48
73
  "node-pty": "^1.2.0-beta.11",
49
74
  "prompts": "^2.4.2",
50
75
  "update-notifier": "^5.1.0",
51
- "uuid": "^13.0.0",
76
+ "uuid": "^9.0.0",
52
77
  "ws": "^8.19.0",
53
78
  "zod": "^3.24.0"
54
79
  }
package/session-state.js CHANGED
@@ -203,6 +203,9 @@ class SessionStateMachine {
203
203
 
204
204
  // Strip ANSI and split into lines for pattern analysis
205
205
  const cleaned = stripAnsi(data);
206
+ if (cleaned.trim().length === 0) {
207
+ return;
208
+ }
206
209
  const lines = cleaned.split(/\r?\n/).filter(l => l.trim().length > 0);
207
210
 
208
211
  for (const line of lines) {
@@ -269,6 +272,16 @@ class SessionStateMachine {
269
272
  this._transition(STATES.RESTARTING, 1.0, { trigger: 'lifecycle' });
270
273
  }
271
274
 
275
+ markIdle(confidence = 1.0, detail = {}) {
276
+ if (this._state === STATES.DEAD || this._state === STATES.RESTARTING) {
277
+ return;
278
+ }
279
+ this._transition(STATES.IDLE, confidence, {
280
+ trigger: 'manual_idle',
281
+ ...detail,
282
+ });
283
+ }
284
+
272
285
  // --- Internal ---
273
286
 
274
287
  _transition(newState, confidence, detail) {
@@ -538,6 +551,16 @@ class SessionStateManager {
538
551
  if (sm) sm.markRestarting();
539
552
  }
540
553
 
554
+ /**
555
+ * Mark a session as idle from a semantic daemon event.
556
+ */
557
+ markIdle(sessionId, confidence = 1.0, detail = {}) {
558
+ const sm = this._machines.get(sessionId);
559
+ if (!sm) return false;
560
+ sm.markIdle(confidence, detail);
561
+ return true;
562
+ }
563
+
541
564
  /**
542
565
  * Unregister and cleanup a session's state machine.
543
566
  */