@dmsdc-ai/aigentry-telepty 0.1.61 → 0.1.63

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/CLAUDE.md ADDED
@@ -0,0 +1,76 @@
1
+ # telepty — PTY Multiplexer & Session Orchestrator
2
+
3
+ aigentry 에코시스템의 **통신 인프라**. 멀티 AI 세션을 생성·연결·제어하는 PTY 멀티플렉서.
4
+
5
+ ## 에코시스템 내 역할
6
+
7
+ 9개 aigentry 세션(orchestrator, brain, amplify, dustcraw 등) 간 통신을 담당:
8
+ - 세션 생성/관리 (`allow`, `spawn`)
9
+ - 메시지 전달 (`inject`, `broadcast`, `multicast`, `reply`)
10
+ - 실시간 모니터링 (`tui`, `monitor`, `listen`)
11
+ - 세션 간 토론 조율 (`deliberate`)
12
+
13
+ ## 아키텍처
14
+
15
+ ```
16
+ CLI (cli.js) ──→ HTTP/WS ──→ Daemon (daemon.js:3848)
17
+ ├── Session WS (/api/sessions/:id)
18
+ ├── Event Bus WS (/api/bus)
19
+ └── REST API (/api/sessions/*)
20
+ ```
21
+
22
+ ### 핵심 모듈
23
+
24
+ | 파일 | 줄수 | 역할 |
25
+ |------|------|------|
26
+ | `cli.js` | ~1950 | CLI 명령 + allow-bridge (PTY 래핑) |
27
+ | `daemon.js` | ~1550 | HTTP/WS 서버, 세션 상태, inject 전달 |
28
+ | `tui.js` | ~500 | blessed 기반 TUI 대시보드 |
29
+ | `session-routing.js` | 81 | 세션 ID 해석, alias 매칭, 호스트 그룹핑 |
30
+ | `daemon-control.js` | 223 | 싱글톤 daemon PID 관리 |
31
+ | `auth.js` | 33 | UUID 토큰 기반 인증 |
32
+ | `interactive-terminal.js` | 71 | raw mode stdin/stdout 관리 |
33
+ | `skill-installer.js` | 269 | Claude/Codex/Gemini 스킬 설치 |
34
+
35
+ ### Inject 전달 경로 (wrapped session)
36
+
37
+ 1. **Primary**: `kitty @ send-text` (터미널 직접 전달, allow-bridge 우회)
38
+ 2. **Fallback**: WS → allow-bridge → `child.write()` (PTY)
39
+ 3. **Submit**: `osascript` Return 키 → kitty fallback → WS `\r`
40
+
41
+ busy 세션: CR은 큐에 대기 중인 텍스트와 함께 큐잉 후 올바른 순서로 flush.
42
+
43
+ ## 명령어
44
+
45
+ ```bash
46
+ # 테스트
47
+ npm test # 43 tests (node:test)
48
+
49
+ # 실행
50
+ telepty daemon # daemon 시작 (포트 3848)
51
+ telepty allow --id <name> claude # 세션 래핑
52
+ telepty tui # TUI 대시보드
53
+ telepty list # 세션 목록
54
+ telepty inject <id> "msg" # 메시지 주입
55
+ telepty broadcast "msg" # 전체 브로드캐스트
56
+ telepty session start --launch # kitty 탭으로 다중 세션 시작
57
+
58
+ # 릴리스
59
+ npm version patch --no-git-tag-version && npm publish --access public
60
+ ```
61
+
62
+ ## 주요 규칙
63
+
64
+ - inject 후 submit은 항상 `osascript`로 통일 (`--no-enter` + osascript keystroke)
65
+ - inject 시 발신자 session ID (`--from`)를 항상 포함
66
+ - PTY `\r` 직접 의존 금지
67
+
68
+ ## 최근 주요 변경 (v0.1.58–0.1.62)
69
+
70
+ | 버전 | 변경 |
71
+ |------|------|
72
+ | 0.1.62 | TUI 태스크 추적 — bus 이벤트에서 [태그] 자동 파싱, 세션별 상태 표시 |
73
+ | 0.1.61 | reconnect 시 resize/\x0c 제거 (멀티터미널 깜빡임 수정) |
74
+ | 0.1.60 | TUI P1 — s=start, k=kill, p=purge stale |
75
+ | 0.1.59 | wrapped inject 503 반환 + 테스트 43/43 정합성 |
76
+ | 0.1.58 | inject busy-session CR/text 순서 수정 + kittyWindowId 캐시 무효화 |
package/cli.js CHANGED
@@ -1039,6 +1039,16 @@ async function main() {
1039
1039
  process.exit(1);
1040
1040
  }
1041
1041
 
1042
+ // Entitlement: remote session check
1043
+ if (target.host && target.host !== '127.0.0.1' && target.host !== 'localhost') {
1044
+ const { checkEntitlement } = require('./entitlement');
1045
+ const ent = checkEntitlement({ feature: 'telepty.remote_sessions' });
1046
+ if (!ent.allowed) {
1047
+ console.error(`⚠️ ${ent.reason}\n Upgrade: ${ent.upgrade_url}`);
1048
+ process.exit(1);
1049
+ }
1050
+ }
1051
+
1042
1052
  const body = { prompt, no_enter: noEnter };
1043
1053
  if (fromId) body.from = fromId;
1044
1054
  if (replyTo) body.reply_to = replyTo;
package/daemon.js CHANGED
@@ -7,6 +7,7 @@ const { WebSocketServer } = require('ws');
7
7
  const { getConfig } = require('./auth');
8
8
  const pkg = require('./package.json');
9
9
  const { claimDaemonState, clearDaemonState } = require('./daemon-control');
10
+ const { checkEntitlement } = require('./entitlement');
10
11
 
11
12
  const config = getConfig();
12
13
  const EXPECTED_TOKEN = config.authToken;
@@ -209,6 +210,13 @@ app.post('/api/sessions/spawn', (req, res) => {
209
210
  const { session_id, command, args = [], cwd = process.cwd(), cols = 80, rows = 30, type = 'AGENT' } = req.body;
210
211
  if (!session_id) return res.status(400).json({ error: 'session_id is strictly required.' });
211
212
  if (sessions[session_id]) return res.status(409).json({ error: `Session ID '${session_id}' is already active.` });
213
+ // Entitlement: check session limit
214
+ const sessionCount = Object.keys(sessions).length;
215
+ const ent = checkEntitlement({ feature: 'telepty.multi_session', currentUsage: sessionCount });
216
+ if (!ent.allowed) {
217
+ console.log(`[ENTITLEMENT] Session limit reached (${sessionCount}/${ent.limit?.max || '?'}), tier: ${ent.tier}`);
218
+ return res.status(402).json({ error: ent.reason, upgrade_url: ent.upgrade_url, tier: ent.tier });
219
+ }
212
220
  if (!command) return res.status(400).json({ error: 'command is required' });
213
221
 
214
222
  const isWin = os.platform() === 'win32';
@@ -300,6 +308,15 @@ app.post('/api/sessions/spawn', (req, res) => {
300
308
  app.post('/api/sessions/register', (req, res) => {
301
309
  const { session_id, command, cwd = process.cwd() } = req.body;
302
310
  if (!session_id) return res.status(400).json({ error: 'session_id is required' });
311
+ // Entitlement: check session limit for new registrations
312
+ if (!sessions[session_id]) {
313
+ const sessionCount = Object.keys(sessions).length;
314
+ const ent = checkEntitlement({ feature: 'telepty.multi_session', currentUsage: sessionCount });
315
+ if (!ent.allowed) {
316
+ console.log(`[ENTITLEMENT] Session limit reached (${sessionCount}/${ent.limit?.max || '?'}), tier: ${ent.tier}`);
317
+ return res.status(402).json({ error: ent.reason, upgrade_url: ent.upgrade_url, tier: ent.tier });
318
+ }
319
+ }
303
320
  // Idempotent: allow re-registration (update command/cwd, keep clients)
304
321
  if (sessions[session_id]) {
305
322
  const existing = sessions[session_id];
package/entitlement.js ADDED
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const LICENSE_PATH = path.join(os.homedir(), '.aigentry', 'license.json');
8
+ const UPGRADE_URL = 'https://aigentry.dev/upgrade';
9
+
10
+ const FEATURES = {
11
+ 'telepty.core': { tiers: ['free', 'pro', 'team'] },
12
+ 'telepty.multi_session': { tiers: ['pro', 'team'], freeLimit: 3 },
13
+ 'telepty.remote_sessions': { tiers: ['pro', 'team'] },
14
+ 'telepty.team_broadcast': { tiers: ['team'] }
15
+ };
16
+
17
+ function readLicense() {
18
+ try {
19
+ if (fs.existsSync(LICENSE_PATH)) {
20
+ const data = JSON.parse(fs.readFileSync(LICENSE_PATH, 'utf8'));
21
+ // Grace period: expired Pro/Team gets 30 days before downgrade
22
+ if (data.expires_at) {
23
+ const expiry = new Date(data.expires_at).getTime();
24
+ const grace = 30 * 24 * 60 * 60 * 1000;
25
+ if (Date.now() > expiry + grace) {
26
+ return { tier: 'free', expired: true };
27
+ }
28
+ }
29
+ return { tier: data.tier || 'free', expired: false };
30
+ }
31
+ } catch {}
32
+ return { tier: 'free', expired: false };
33
+ }
34
+
35
+ function checkEntitlement({ feature, currentUsage }) {
36
+ const def = FEATURES[feature];
37
+ if (!def) return { allowed: true, tier: 'free', reason: 'Unknown feature — allowing' };
38
+
39
+ const license = readLicense();
40
+ const tier = license.tier;
41
+
42
+ // Feature available on this tier
43
+ if (def.tiers.includes(tier)) {
44
+ return { allowed: true, tier, upgrade_url: UPGRADE_URL };
45
+ }
46
+
47
+ // Free tier with limit
48
+ if (tier === 'free' && def.freeLimit != null) {
49
+ const current = currentUsage || 0;
50
+ if (current < def.freeLimit) {
51
+ return { allowed: true, tier, limit: { current, max: def.freeLimit } };
52
+ }
53
+ return {
54
+ allowed: false, tier,
55
+ reason: `Free limit reached: ${current}/${def.freeLimit}. Upgrade to Pro for unlimited.`,
56
+ upgrade_url: UPGRADE_URL,
57
+ limit: { current, max: def.freeLimit }
58
+ };
59
+ }
60
+
61
+ // Not available on this tier
62
+ const requiredTier = def.tiers[0];
63
+ return {
64
+ allowed: false, tier,
65
+ reason: `Requires ${requiredTier.charAt(0).toUpperCase() + requiredTier.slice(1)} tier`,
66
+ upgrade_url: UPGRADE_URL
67
+ };
68
+ }
69
+
70
+ module.exports = { checkEntitlement, readLicense, LICENSE_PATH, FEATURES };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.61",
3
+ "version": "0.1.63",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
package/tui.js CHANGED
@@ -23,6 +23,7 @@ class TuiDashboard {
23
23
  this.pollTimer = null;
24
24
  this.busWs = null;
25
25
  this.busLog = [];
26
+ this.sessionTasks = {}; // { sessionId: { summary, state, updatedAt } }
26
27
  this.setupScreen();
27
28
  this.startPolling();
28
29
  this.connectBus();
@@ -248,6 +249,35 @@ class TuiDashboard {
248
249
  this.screen.render();
249
250
  }
250
251
 
252
+ // ── Task extraction from bus events ─────────────────────────
253
+
254
+ parseTaskInfo(content) {
255
+ if (!content || typeof content !== 'string') return null;
256
+ const firstLine = content.split('\n')[0].trim();
257
+ // Extract [tag] patterns: [P0 착수], [완료 보고], [telepty 관점], etc.
258
+ const tagMatch = firstLine.match(/\[([^\]]{2,30})\]/);
259
+ const tag = tagMatch ? tagMatch[1] : null;
260
+ // Detect state from keywords
261
+ let state = 'working';
262
+ if (/완료|complete|done|finish/i.test(firstLine)) state = 'done';
263
+ else if (/토론|deliberat|discuss|synthesis|합의/i.test(firstLine)) state = 'discussing';
264
+ else if (/동의|반대|vote|찬성/i.test(firstLine)) state = 'voting';
265
+ else if (/대기|standby|waiting|idle/i.test(firstLine)) state = 'idle';
266
+ // Build summary (tag or truncated first line)
267
+ const summary = tag || firstLine.replace(/\[.*?\]/g, '').trim().slice(0, 30);
268
+ return { summary, state };
269
+ }
270
+
271
+ updateSessionTask(sessionId, content) {
272
+ const info = this.parseTaskInfo(content);
273
+ if (!info || !info.summary) return;
274
+ this.sessionTasks[sessionId] = {
275
+ summary: info.summary,
276
+ state: info.state,
277
+ updatedAt: Date.now()
278
+ };
279
+ }
280
+
251
281
  // ── Event Bus ────────────────────────────────────────────────
252
282
 
253
283
  connectBus() {
@@ -263,16 +293,20 @@ class TuiDashboard {
263
293
  let line = `${ts} `;
264
294
  if (msg.type === 'inject_written') {
265
295
  line += `{cyan-fg}inject{/} -> ${msg.target || '?'}`;
296
+ // Track task from inject content
297
+ if (msg.target) this.updateSessionTask(msg.target, msg.content || msg.prompt);
266
298
  } else if (msg.type === 'injection') {
267
299
  line += `{cyan-fg}broadcast{/} -> ${msg.target_agent || 'all'}`;
268
300
  } else if (msg.type === 'message_routed') {
269
301
  line += `{yellow-fg}${msg.from || '?'}{/} -> {green-fg}${msg.to || '?'}{/}`;
302
+ if (msg.from) this.updateSessionTask(msg.from, msg.content || msg.prompt);
270
303
  } else {
271
304
  line += `{white-fg}${msg.type || 'event'}{/}`;
272
305
  }
273
306
  this.busLog.push(line);
274
307
  if (this.busLog.length > 100) this.busLog.shift();
275
308
  this.renderBusLog();
309
+ this.renderSessionList(); // refresh task info
276
310
  } catch { /* ignore malformed */ }
277
311
  });
278
312
  this.busWs.on('close', () => {
@@ -428,9 +462,20 @@ class TuiDashboard {
428
462
  getStatusInfo(session) {
429
463
  const idle = session.idleSeconds;
430
464
  const clients = session.active_clients || 0;
431
-
465
+ const task = this.sessionTasks[session.id];
466
+ // Task-aware state (bus events override idle heuristic)
432
467
  if (clients === 0) return { icon: '{red-fg}✕{/}', label: '{red-fg}dead{/}' };
433
468
  if (idle !== null && idle > STALE_THRESHOLD) return { icon: '{yellow-fg}○{/}', label: '{yellow-fg}stale{/}' };
469
+ if (task && (Date.now() - task.updatedAt) < 300000) { // 5min freshness
470
+ const stateMap = {
471
+ done: { icon: '{green-fg}✓{/}', label: '{green-fg}done{/}' },
472
+ discussing: { icon: '{magenta-fg}◉{/}', label: '{magenta-fg}discuss{/}' },
473
+ voting: { icon: '{magenta-fg}◎{/}', label: '{magenta-fg}vote{/}' },
474
+ working: { icon: '{cyan-fg}●{/}', label: '{cyan-fg}working{/}' },
475
+ idle: { icon: '{green-fg}●{/}', label: '{white-fg}idle{/}' }
476
+ };
477
+ return stateMap[task.state] || stateMap.working;
478
+ }
434
479
  if (idle !== null && idle < 10) return { icon: '{green-fg}●{/}', label: '{green-fg}busy{/}' };
435
480
  return { icon: '{green-fg}●{/}', label: '{white-fg}idle{/}' };
436
481
  }
@@ -439,7 +484,10 @@ class TuiDashboard {
439
484
  const items = this.sessions.map((s) => {
440
485
  const { icon, label } = this.getStatusInfo(s);
441
486
  const shortId = s.id.replace(/^aigentry-/, '').replace(/-claude$/, '');
442
- return ` ${icon} ${shortId.padEnd(24)} ${label} {gray-fg}C:${s.active_clients}{/}`;
487
+ const task = this.sessionTasks[s.id];
488
+ const taskStr = (task && (Date.now() - task.updatedAt) < 300000)
489
+ ? ` {gray-fg}${task.summary.slice(0, 20)}{/}` : '';
490
+ return ` ${icon} ${shortId.padEnd(20)} ${label.padEnd(18)}${taskStr}`;
443
491
  });
444
492
 
445
493
  this.sessionList.setItems(items);