@dmsdc-ai/aigentry-telepty 0.1.62 → 0.1.64
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 +96 -0
- package/cli.js +11 -1
- package/daemon.js +17 -0
- package/entitlement.js +70 -0
- package/package.json +1 -1
- package/tui.js +1 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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 캐시 무효화 |
|
|
77
|
+
|
|
78
|
+
## 자율 재귀적 오케스트레이션
|
|
79
|
+
|
|
80
|
+
이 세션은 작업 복잡도가 높거나, 독립 도메인이 식별되거나, 컨텍스트 분리가 필요하다고 판단되면 — 사용자 지시 없이 자율적으로:
|
|
81
|
+
|
|
82
|
+
1. 자기 프로젝트 하위에 폴더를 생성한다
|
|
83
|
+
2. 해당 폴더에 CLAUDE.md를 작성한다 (역할, 범위, 상위 세션과의 통신 방법)
|
|
84
|
+
3. aigentry scaffold 또는 telepty allow로 하위 Claude 세션을 생성한다
|
|
85
|
+
4. telepty inject로 하위 세션에 태스크를 주입한다
|
|
86
|
+
5. 하위 세션의 결과를 수신하고 통합한다
|
|
87
|
+
|
|
88
|
+
판단 기준:
|
|
89
|
+
- 현재 작업이 2개 이상 독립 도메인을 포함할 때
|
|
90
|
+
- 컨텍스트 윈도우의 30% 이상을 단일 하위 작업이 차지할 때
|
|
91
|
+
- 반복적 전문 작업이 식별될 때 (SEO, 특정 채널, 특정 데이터 소스 등)
|
|
92
|
+
|
|
93
|
+
원칙:
|
|
94
|
+
- YAGNI: 진짜 필요할 때만 생성. 과잉 분리 금지.
|
|
95
|
+
- 하위 세션 완료 시 오케스트레이터(상위)에게 반드시 보고.
|
|
96
|
+
- 하위 세션의 결과물은 상위 프로젝트에 통합(커밋).
|
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;
|
|
@@ -1261,7 +1271,7 @@ async function main() {
|
|
|
1261
1271
|
const sessionIdForProject = `${name}-${cli.split(' ')[0]}`;
|
|
1262
1272
|
const shellCmd = `unset TELEPTY_SESSION_ID; ${nodeFullPath} ${teleptyFullPath} allow --id ${sessionIdForProject} ${cliFullPath}${cliFullArgs ? ' ' + cliFullArgs : ''}`;
|
|
1263
1273
|
const launchArgs = ['@', '--to', `unix:${kittySocket}`,
|
|
1264
|
-
'launch', '--type=
|
|
1274
|
+
'launch', '--type=os-window', '--title', name, '--cwd', cwd,
|
|
1265
1275
|
'--env', 'TELEPTY_SESSION_ID=',
|
|
1266
1276
|
'--env', `PATH=${process.env.PATH}`,
|
|
1267
1277
|
'/bin/zsh', '-c', shellCmd];
|
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
|
@@ -139,7 +139,7 @@ class TuiDashboard {
|
|
|
139
139
|
|
|
140
140
|
try {
|
|
141
141
|
execFileSync('kitty', ['@', '--to', `unix:${sock}`,
|
|
142
|
-
'launch', '--type=
|
|
142
|
+
'launch', '--type=os-window', '--title', project.name, '--cwd', project.cwd,
|
|
143
143
|
'--env', 'TELEPTY_SESSION_ID=',
|
|
144
144
|
'--env', `PATH=${process.env.PATH}`,
|
|
145
145
|
'/bin/zsh', '-c', shellCmd
|