@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 +76 -0
- package/cli.js +10 -0
- package/daemon.js +17 -0
- package/entitlement.js +70 -0
- package/package.json +1 -1
- package/tui.js +50 -2
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
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
|
-
|
|
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);
|