evolclaw 2.8.2 → 3.0.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 (106) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +105 -30
  3. package/dist/agents/codex-runner.js +15 -7
  4. package/dist/agents/gemini-runner.js +14 -5
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1064 -279
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/baseagent-loader.js +48 -0
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +1090 -838
  40. package/dist/core/evolagent-registry.js +191 -360
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +326 -145
  48. package/dist/core/message/message-queue.js +5 -5
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +437 -273
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -576
  92. package/dist/core/agent-loader.js +0 -39
  93. package/dist/core/agent-registry.js +0 -450
  94. package/dist/core/evolagent-schema.js +0 -72
  95. package/dist/core/message/stream-flusher.js +0 -238
  96. package/dist/core/message/thought-emitter.js +0 -162
  97. package/dist/core/reload-hooks.js +0 -87
  98. package/dist/prompts/templates.js +0 -122
  99. package/dist/templates/skills.md +0 -66
  100. package/dist/utils/channel-fingerprint.js +0 -59
  101. package/dist/utils/error-dict.js +0 -63
  102. package/dist/utils/format.js +0 -32
  103. package/dist/utils/init.js +0 -645
  104. package/dist/utils/migrate-project.js +0 -122
  105. package/dist/utils/reload-hooks.js +0 -87
  106. package/dist/utils/stats-collector.js +0 -99
@@ -0,0 +1,529 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { resolvePaths, getPackageRoot } from './paths.js';
4
+ import { decodeDirSegment, readAllJsonlLines } from './core/session/session-fs-store.js';
5
+ // ==================== ANSI ====================
6
+ const isTTY = !!process.stdout.isTTY;
7
+ const RST = isTTY ? '\x1b[0m' : '';
8
+ const DIM = isTTY ? '\x1b[2m' : '';
9
+ const BOLD = isTTY ? '\x1b[1m' : '';
10
+ const CYAN = isTTY ? '\x1b[36m' : '';
11
+ const GREEN = isTTY ? '\x1b[32m' : '';
12
+ const BLUE = isTTY ? '\x1b[34m' : '';
13
+ const ORANGE = isTTY ? '\x1b[38;5;208m' : '';
14
+ // ==================== Helpers ====================
15
+ function visualWidth(s) {
16
+ const stripped = s.replace(/\x1b\[[0-9;]*m/g, '');
17
+ let w = 0;
18
+ for (const ch of stripped) {
19
+ const code = ch.charCodeAt(0);
20
+ w += (code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3000 && code <= 0x30ff) ||
21
+ (code >= 0xff00 && code <= 0xffef) ? 2 : 1;
22
+ }
23
+ return w;
24
+ }
25
+ function padRight(s, width) {
26
+ const pad = Math.max(0, width - visualWidth(s));
27
+ return s + ' '.repeat(pad);
28
+ }
29
+ function truncate(s, maxWidth) {
30
+ let w = 0;
31
+ let i = 0;
32
+ for (const ch of s) {
33
+ const code = ch.charCodeAt(0);
34
+ const cw = (code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3000 && code <= 0x30ff) ||
35
+ (code >= 0xff00 && code <= 0xffef) ? 2 : 1;
36
+ if (w + cw > maxWidth - 1)
37
+ return s.slice(0, i) + '…';
38
+ w += cw;
39
+ i += ch.length;
40
+ }
41
+ return s;
42
+ }
43
+ function formatTimeAgo(ms) {
44
+ const sec = Math.floor(ms / 1000);
45
+ if (sec < 60)
46
+ return `${sec}s`;
47
+ const min = Math.floor(sec / 60);
48
+ if (min < 60)
49
+ return `${min}m`;
50
+ const hour = Math.floor(min / 60);
51
+ if (hour < 24)
52
+ return `${hour}h`;
53
+ return `${Math.floor(hour / 24)}d`;
54
+ }
55
+ function formatTime(ts) {
56
+ const d = new Date(ts);
57
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
58
+ }
59
+ function shortAid(aid) {
60
+ return aid.split('.')[0];
61
+ }
62
+ // ==================== Data Layer ====================
63
+ function getSessionsAunDir() {
64
+ const p = resolvePaths();
65
+ return path.join(p.sessionsDir, 'aun');
66
+ }
67
+ function listLocalAids(aunDir) {
68
+ try {
69
+ return fs.readdirSync(aunDir, { withFileTypes: true })
70
+ .filter(e => e.isDirectory())
71
+ .map(e => decodeDirSegment(e.name));
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ }
77
+ function listPeers(aunDir, localAid) {
78
+ const aidDir = path.join(aunDir, encodeSegment(localAid));
79
+ try {
80
+ return fs.readdirSync(aidDir, { withFileTypes: true })
81
+ .filter(e => e.isDirectory() && !e.name.startsWith('_'))
82
+ .map(e => decodeDirSegment(e.name));
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ }
88
+ function readMessages(aunDir, localAid, peerId) {
89
+ const msgPath = path.join(aunDir, encodeSegment(localAid), encodeSegment(peerId), 'messages.jsonl');
90
+ return readAllJsonlLines(msgPath);
91
+ }
92
+ function readPeerName(aunDir, localAid, peerId) {
93
+ const activePath = path.join(aunDir, encodeSegment(localAid), encodeSegment(peerId), 'active.json');
94
+ try {
95
+ const data = JSON.parse(fs.readFileSync(activePath, 'utf-8'));
96
+ return data?.metadata?.peerName || null;
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ function encodeSegment(s) {
103
+ return s.replace(/[/%\\:*?"<>|]/g, ch => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
104
+ }
105
+ function loadAidInfo(aunDir, aid) {
106
+ const peers = listPeers(aunDir, aid);
107
+ let totalIn = 0, totalOut = 0;
108
+ for (const peer of peers) {
109
+ const msgs = readMessages(aunDir, aid, peer);
110
+ for (const m of msgs) {
111
+ if (m.dir === 'in')
112
+ totalIn++;
113
+ else
114
+ totalOut++;
115
+ }
116
+ }
117
+ return { aid, totalIn, totalOut, peerCount: peers.length };
118
+ }
119
+ function loadPeerInfos(aunDir, localAid) {
120
+ const peers = listPeers(aunDir, localAid);
121
+ const infos = [];
122
+ for (const peerId of peers) {
123
+ const msgs = readMessages(aunDir, localAid, peerId);
124
+ let inbound = 0, outbound = 0, lastAt = 0;
125
+ for (const m of msgs) {
126
+ if (m.dir === 'in')
127
+ inbound++;
128
+ else
129
+ outbound++;
130
+ if (m.ts > lastAt)
131
+ lastAt = m.ts;
132
+ }
133
+ const peerName = readPeerName(aunDir, localAid, peerId);
134
+ infos.push({ peerId, peerName, inbound, outbound, lastAt });
135
+ }
136
+ infos.sort((a, b) => b.lastAt - a.lastAt);
137
+ return infos;
138
+ }
139
+ function loadAllMessages(aunDir, localAid) {
140
+ const peers = listPeers(aunDir, localAid);
141
+ const all = [];
142
+ for (const peer of peers) {
143
+ all.push(...readMessages(aunDir, localAid, peer));
144
+ }
145
+ all.sort((a, b) => a.ts - b.ts);
146
+ if (all.length > 1000)
147
+ return all.slice(-1000);
148
+ return all;
149
+ }
150
+ // ==================== Rendering ====================
151
+ function renderScrollbar(totalLines, visibleLines, offset, height) {
152
+ if (totalLines <= visibleLines)
153
+ return Array(height).fill(' ');
154
+ const thumbSize = Math.max(1, Math.floor(height * visibleLines / totalLines));
155
+ const maxOffset = totalLines - visibleLines;
156
+ const thumbPos = Math.floor((maxOffset - offset) / maxOffset * (height - thumbSize));
157
+ const bar = [];
158
+ for (let i = 0; i < height; i++) {
159
+ bar.push(i >= thumbPos && i < thumbPos + thumbSize ? `${DIM}█${RST}` : `${DIM}░${RST}`);
160
+ }
161
+ return bar;
162
+ }
163
+ function renderScopePanel(state, width, height) {
164
+ const lines = [];
165
+ const title = `${DIM}─ Scope ─${RST}`;
166
+ lines.push(padRight(title, width));
167
+ const isActive = state.activePanel === 'scope';
168
+ for (let i = 0; i < state.localAids.length && lines.length < height; i++) {
169
+ const a = state.localAids[i];
170
+ const sel = isActive && i === state.scopeIndex;
171
+ const chosen = state.selectedLocalAid === a.aid;
172
+ const marker = sel ? `${CYAN}${BOLD}▸ ` : (chosen ? `${CYAN} ` : ' ');
173
+ const name = truncate(shortAid(a.aid), width - 4);
174
+ lines.push(padRight(`${marker}${name}${RST}`, width));
175
+ const stats = ` ${DIM}↓${a.totalIn} ↑${a.totalOut} peers:${a.peerCount}${RST}`;
176
+ lines.push(padRight(stats, width));
177
+ if (lines.length < height)
178
+ lines.push(padRight('', width));
179
+ }
180
+ while (lines.length < height)
181
+ lines.push(padRight('', width));
182
+ return lines.slice(0, height);
183
+ }
184
+ function renderStatsPanel(state, width, height) {
185
+ const lines = [];
186
+ const title = `${DIM}─ Stats ─${RST}`;
187
+ lines.push(padRight(title, width));
188
+ if (!state.selectedLocalAid) {
189
+ lines.push(padRight(`${DIM} select an AID${RST}`, width));
190
+ while (lines.length < height)
191
+ lines.push(padRight('', width));
192
+ return lines.slice(0, height);
193
+ }
194
+ const isActive = state.activePanel === 'stats';
195
+ const now = Date.now();
196
+ // "All" item at index 0
197
+ const allSel = isActive && state.statsIndex === 0;
198
+ const allMarker = allSel ? `${CYAN}${BOLD}▸ ` : ' ';
199
+ lines.push(padRight(`${allMarker}All (${state.peers.length} peers)${RST}`, width));
200
+ if (lines.length < height)
201
+ lines.push(padRight('', width));
202
+ for (let i = 0; i < state.peers.length && lines.length < height; i++) {
203
+ const p = state.peers[i];
204
+ const sel = isActive && state.statsIndex === i + 1;
205
+ const marker = sel ? `${CYAN}${BOLD}▸ ` : ' ';
206
+ const displayName = p.peerName || shortAid(p.peerId);
207
+ const name = truncate(displayName, width - 4);
208
+ lines.push(padRight(`${marker}${name}${RST}`, width));
209
+ const ago = p.lastAt ? formatTimeAgo(now - p.lastAt) : '-';
210
+ const detail = ` ${DIM}↓${p.inbound} ↑${p.outbound} ${ago}${RST}`;
211
+ lines.push(padRight(detail, width));
212
+ if (lines.length < height)
213
+ lines.push(padRight('', width));
214
+ }
215
+ while (lines.length < height)
216
+ lines.push(padRight('', width));
217
+ return lines.slice(0, height);
218
+ }
219
+ // ==================== Messages Panel ====================
220
+ function renderMessagesPanel(state, width, height) {
221
+ const lines = [];
222
+ const title = `${DIM}─ Messages ─${RST}`;
223
+ lines.push(padRight(title, width));
224
+ const contentHeight = height - 1;
225
+ const msgs = state.messages;
226
+ const totalMsgs = msgs.length;
227
+ const visibleCount = contentHeight;
228
+ const startIdx = Math.max(0, totalMsgs - visibleCount - state.messageScrollOffset);
229
+ const endIdx = Math.min(totalMsgs, startIdx + visibleCount);
230
+ const scrollbar = renderScrollbar(totalMsgs, visibleCount, state.messageScrollOffset, contentHeight);
231
+ const msgWidth = width - 3;
232
+ for (let i = startIdx; i < endIdx; i++) {
233
+ const m = msgs[i];
234
+ const time = formatTime(m.ts);
235
+ const dir = m.dir === 'in' ? `${GREEN}↓${RST}` : `${BLUE}↑${RST}`;
236
+ const peer = m.dir === 'in' ? shortAid(m.from) : shortAid(m.to);
237
+ const header = `${DIM}${time}${RST} ${dir} ${ORANGE}${peer}${RST}`;
238
+ const headerLine = padRight(header, msgWidth);
239
+ const sbIdx = lines.length - 1;
240
+ lines.push(`${headerLine} ${scrollbar[sbIdx] || ' '}`);
241
+ if (lines.length - 1 < contentHeight) {
242
+ const content = truncate(m.content.replace(/\n/g, ' '), msgWidth - 2);
243
+ const contentLine = padRight(` ${content}`, msgWidth);
244
+ const sbIdx2 = lines.length - 1;
245
+ lines.push(`${contentLine} ${scrollbar[sbIdx2] || ' '}`);
246
+ }
247
+ }
248
+ while (lines.length < height) {
249
+ const sbIdx = lines.length - 1;
250
+ lines.push(padRight('', msgWidth) + ` ${scrollbar[sbIdx] || ' '}`);
251
+ }
252
+ return lines.slice(0, height);
253
+ }
254
+ // ==================== Main Render ====================
255
+ function renderFrame(state) {
256
+ const cols = process.stdout.columns || 120;
257
+ const rows = (process.stdout.rows || 40) - 3;
258
+ const leftW = Math.max(24, Math.floor(cols * 0.28));
259
+ const midW = Math.max(28, Math.floor(cols * 0.30));
260
+ const rightW = Math.max(30, cols - leftW - midW - 4);
261
+ const bodyHeight = rows - 2;
262
+ const leftLines = renderScopePanel(state, leftW, bodyHeight);
263
+ const midLines = renderStatsPanel(state, midW, bodyHeight);
264
+ const msgLines = renderMessagesPanel(state, rightW, bodyHeight);
265
+ const sep = `${DIM}│${RST}`;
266
+ let buf = '\x1b[H';
267
+ const topBorder = `${DIM}┌${'─'.repeat(leftW)}┬${'─'.repeat(midW)}┬${'─'.repeat(rightW + 1)}┐${RST}`;
268
+ buf += `\x1b[2K${topBorder}\n`;
269
+ for (let i = 0; i < bodyHeight; i++) {
270
+ const l = leftLines[i] || padRight('', leftW);
271
+ const m = midLines[i] || padRight('', midW);
272
+ const r = msgLines[i] || padRight('', rightW);
273
+ buf += `\x1b[2K${sep}${l}${sep}${m}${sep}${r}${sep}\n`;
274
+ }
275
+ const bottomBorder = `${DIM}├${'─'.repeat(leftW)}┴${'─'.repeat(midW)}┴${'─'.repeat(rightW + 1)}┤${RST}`;
276
+ buf += `\x1b[2K${bottomBorder}\n`;
277
+ const pkgRoot = getPackageRoot();
278
+ const helpLine = `${DIM}│ Tab: panel ↑↓: nav Enter: select Backspace: back ESC: exit ${pkgRoot}${RST}`;
279
+ buf += `\x1b[2K${helpLine}\n`;
280
+ const closeBorder = `${DIM}└${'─'.repeat(cols - 2)}┘${RST}`;
281
+ buf += `\x1b[2K${closeBorder}\n`;
282
+ return buf;
283
+ }
284
+ // ==================== Main ====================
285
+ export async function cmdWatchMsg() {
286
+ const aunDir = getSessionsAunDir();
287
+ if (!fs.existsSync(aunDir)) {
288
+ console.log('No session data found.');
289
+ return;
290
+ }
291
+ let watcher = null;
292
+ const state = {
293
+ activePanel: 'scope',
294
+ localAids: [],
295
+ scopeIndex: 0,
296
+ selectedLocalAid: null,
297
+ peers: [],
298
+ statsIndex: 0,
299
+ selectedPeer: null,
300
+ messages: [],
301
+ messageScrollOffset: 0,
302
+ dirty: true,
303
+ };
304
+ function loadScope() {
305
+ const aids = listLocalAids(aunDir);
306
+ state.localAids = aids.map(aid => loadAidInfo(aunDir, aid));
307
+ state.localAids.sort((a, b) => (b.totalIn + b.totalOut) - (a.totalIn + a.totalOut));
308
+ }
309
+ function selectAid(aid) {
310
+ state.selectedLocalAid = aid;
311
+ state.peers = loadPeerInfos(aunDir, aid);
312
+ state.statsIndex = 0;
313
+ state.selectedPeer = null;
314
+ state.messages = loadAllMessages(aunDir, aid);
315
+ state.messageScrollOffset = 0;
316
+ startWatching(aid);
317
+ }
318
+ function selectPeer(peerId) {
319
+ state.selectedPeer = peerId;
320
+ if (!state.selectedLocalAid)
321
+ return;
322
+ if (peerId) {
323
+ state.messages = readMessages(aunDir, state.selectedLocalAid, peerId);
324
+ if (state.messages.length > 1000)
325
+ state.messages = state.messages.slice(-1000);
326
+ }
327
+ else {
328
+ state.messages = loadAllMessages(aunDir, state.selectedLocalAid);
329
+ }
330
+ state.messageScrollOffset = 0;
331
+ }
332
+ function startWatching(aid) {
333
+ if (watcher) {
334
+ watcher.close();
335
+ watcher = null;
336
+ }
337
+ const aidDir = path.join(aunDir, encodeSegment(aid));
338
+ try {
339
+ watcher = fs.watch(aidDir, { recursive: true }, (_, filename) => {
340
+ if (filename && filename.endsWith('messages.jsonl')) {
341
+ refreshData();
342
+ render();
343
+ }
344
+ });
345
+ }
346
+ catch { /* directory may not exist */ }
347
+ }
348
+ function refreshData() {
349
+ if (!state.selectedLocalAid)
350
+ return;
351
+ state.peers = loadPeerInfos(aunDir, state.selectedLocalAid);
352
+ if (state.selectedPeer) {
353
+ state.messages = readMessages(aunDir, state.selectedLocalAid, state.selectedPeer);
354
+ if (state.messages.length > 1000)
355
+ state.messages = state.messages.slice(-1000);
356
+ }
357
+ else {
358
+ state.messages = loadAllMessages(aunDir, state.selectedLocalAid);
359
+ }
360
+ // Also refresh scope stats for the selected AID
361
+ const idx = state.localAids.findIndex(a => a.aid === state.selectedLocalAid);
362
+ if (idx >= 0) {
363
+ state.localAids[idx] = loadAidInfo(aunDir, state.selectedLocalAid);
364
+ }
365
+ }
366
+ function render() {
367
+ process.stdout.write(renderFrame(state));
368
+ }
369
+ function cleanup() {
370
+ if (watcher) {
371
+ watcher.close();
372
+ watcher = null;
373
+ }
374
+ if (process.stdin.isTTY)
375
+ try {
376
+ process.stdin.setRawMode(false);
377
+ }
378
+ catch { }
379
+ process.stdin.pause();
380
+ process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
381
+ }
382
+ function handleKey(data) {
383
+ // ESC
384
+ if (data[0] === 0x1b && data.length === 1) {
385
+ cleanup();
386
+ process.exit(0);
387
+ }
388
+ // Ctrl+C
389
+ if (data[0] === 0x03) {
390
+ cleanup();
391
+ process.exit(0);
392
+ }
393
+ // Arrow keys
394
+ if (data[0] === 0x1b && data[1] === 0x5b) {
395
+ const code = data[2];
396
+ if (code === 0x41)
397
+ handleUp();
398
+ else if (code === 0x42)
399
+ handleDown();
400
+ else if (code === 0x43)
401
+ handleRight();
402
+ else if (code === 0x44)
403
+ handleLeft();
404
+ else if (code === 0x35)
405
+ handlePageUp(); // Page Up: \x1b[5~
406
+ else if (code === 0x36)
407
+ handlePageDown(); // Page Down: \x1b[6~
408
+ render();
409
+ return;
410
+ }
411
+ // Tab
412
+ if (data[0] === 0x09) {
413
+ handleRight();
414
+ render();
415
+ return;
416
+ }
417
+ // Shift+Tab (some terminals: \x1b[Z)
418
+ if (data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x5a) {
419
+ handleLeft();
420
+ render();
421
+ return;
422
+ }
423
+ // Enter
424
+ if (data[0] === 0x0d) {
425
+ handleEnter();
426
+ render();
427
+ return;
428
+ }
429
+ // Backspace
430
+ if (data[0] === 0x7f || data[0] === 0x08) {
431
+ handleBackspace();
432
+ render();
433
+ return;
434
+ }
435
+ }
436
+ function handleUp() {
437
+ if (state.activePanel === 'scope') {
438
+ state.scopeIndex = Math.max(0, state.scopeIndex - 1);
439
+ }
440
+ else if (state.activePanel === 'stats') {
441
+ state.statsIndex = Math.max(0, state.statsIndex - 1);
442
+ }
443
+ else if (state.activePanel === 'messages') {
444
+ state.messageScrollOffset = Math.min(Math.max(0, state.messages.length - 5), state.messageScrollOffset + 3);
445
+ }
446
+ }
447
+ function handleDown() {
448
+ if (state.activePanel === 'scope') {
449
+ state.scopeIndex = Math.min(state.localAids.length - 1, state.scopeIndex + 1);
450
+ }
451
+ else if (state.activePanel === 'stats') {
452
+ state.statsIndex = Math.min(state.peers.length, state.statsIndex + 1);
453
+ }
454
+ else if (state.activePanel === 'messages') {
455
+ state.messageScrollOffset = Math.max(0, state.messageScrollOffset - 3);
456
+ }
457
+ }
458
+ function handleLeft() {
459
+ if (state.activePanel === 'messages')
460
+ state.activePanel = 'stats';
461
+ else if (state.activePanel === 'stats')
462
+ state.activePanel = 'scope';
463
+ }
464
+ function handleRight() {
465
+ if (state.activePanel === 'scope')
466
+ state.activePanel = 'stats';
467
+ else if (state.activePanel === 'stats')
468
+ state.activePanel = 'messages';
469
+ }
470
+ function handlePageUp() {
471
+ if (state.activePanel === 'messages') {
472
+ const pageSize = (process.stdout.rows || 40) - 6;
473
+ state.messageScrollOffset = Math.min(Math.max(0, state.messages.length - 5), state.messageScrollOffset + pageSize);
474
+ }
475
+ }
476
+ function handlePageDown() {
477
+ if (state.activePanel === 'messages') {
478
+ const pageSize = (process.stdout.rows || 40) - 6;
479
+ state.messageScrollOffset = Math.max(0, state.messageScrollOffset - pageSize);
480
+ }
481
+ }
482
+ function handleEnter() {
483
+ if (state.activePanel === 'scope' && state.localAids.length > 0) {
484
+ const aid = state.localAids[state.scopeIndex];
485
+ selectAid(aid.aid);
486
+ state.activePanel = 'stats';
487
+ }
488
+ else if (state.activePanel === 'stats') {
489
+ if (state.statsIndex === 0) {
490
+ selectPeer(null);
491
+ }
492
+ else {
493
+ const peer = state.peers[state.statsIndex - 1];
494
+ if (peer)
495
+ selectPeer(peer.peerId);
496
+ }
497
+ state.activePanel = 'messages';
498
+ }
499
+ }
500
+ function handleBackspace() {
501
+ if (state.activePanel === 'messages') {
502
+ state.activePanel = 'stats';
503
+ state.messageScrollOffset = 0;
504
+ }
505
+ else if (state.activePanel === 'stats') {
506
+ state.activePanel = 'scope';
507
+ state.selectedLocalAid = null;
508
+ state.peers = [];
509
+ state.messages = [];
510
+ if (watcher) {
511
+ watcher.close();
512
+ watcher = null;
513
+ }
514
+ }
515
+ }
516
+ // ── Init ──
517
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
518
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
519
+ loadScope();
520
+ process.stdout.write('\x1b[?25l\x1b[2J\x1b[H');
521
+ render();
522
+ if (process.stdin.isTTY) {
523
+ process.stdin.setRawMode(true);
524
+ process.stdin.resume();
525
+ process.stdin.on('data', handleKey);
526
+ }
527
+ // Keep process alive
528
+ await new Promise(() => { });
529
+ }