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