@three333/termbuddy 0.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.
package/dist/cli.js ADDED
@@ -0,0 +1,957 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/views/MainMenu.tsx
13
+ import { Box, Text, useInput } from "ink";
14
+ import { jsx, jsxs } from "react/jsx-runtime";
15
+ function MainMenu(props) {
16
+ useInput((input, key) => {
17
+ if (key.escape || input === "q") props.onExit();
18
+ if (input === "1") props.onHost();
19
+ if (input === "2") props.onJoin();
20
+ });
21
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", padding: 1, children: [
22
+ /* @__PURE__ */ jsx(Text, { children: String.raw`
23
+ ████████╗███████╗██████╗ ███╗ ███╗██████╗ ██╗ ██╗██████╗ ██████╗ ██╗ ██╗
24
+ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██╔══██╗██║ ██║██╔══██╗██╔══██╗╚██╗ ██╔╝
25
+ ██║ █████╗ ██████╔╝██╔████╔██║██████╔╝██║ ██║██║ ██║██║ ██║ ╚████╔╝
26
+ ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══██╗██║ ██║██║ ██║██║ ██║ ╚██╔╝
27
+ ██║ ███████╗██║ ██║██║ ╚═╝ ██║██████╔╝╚██████╔╝██████╔╝██████╔╝ ██║
28
+ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝
29
+ ` }),
30
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
31
+ /* @__PURE__ */ jsx(Text, { children: "Terminal Body Doubling \u2014 \u6781\u7B80 / \u6781\u5BA2 / \u79C1\u5BC6" }),
32
+ /* @__PURE__ */ jsx(Text, { children: " " }),
33
+ /* @__PURE__ */ jsxs(Text, { children: [
34
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "[1]" }),
35
+ " \u5EFA\u623F (Host)"
36
+ ] }),
37
+ /* @__PURE__ */ jsxs(Text, { children: [
38
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "[2]" }),
39
+ " \u52A0\u5165 (Join)"
40
+ ] }),
41
+ /* @__PURE__ */ jsxs(Text, { children: [
42
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "[q]" }),
43
+ " \u9000\u51FA"
44
+ ] })
45
+ ] })
46
+ ] });
47
+ }
48
+ var init_MainMenu = __esm({
49
+ "src/views/MainMenu.tsx"() {
50
+ "use strict";
51
+ }
52
+ });
53
+
54
+ // src/hooks/useActivityMonitor.ts
55
+ import { useEffect, useRef, useState } from "react";
56
+ import { useInput as useInput2 } from "ink";
57
+ function useActivityMonitor(options) {
58
+ const idleAfterMs = options?.idleAfterMs ?? 1500;
59
+ const [state, setState] = useState("IDLE");
60
+ const lastActivityRef = useRef(Date.now());
61
+ useInput2(() => {
62
+ lastActivityRef.current = Date.now();
63
+ setState("TYPING");
64
+ });
65
+ useEffect(() => {
66
+ const id = setInterval(() => {
67
+ const delta = Date.now() - lastActivityRef.current;
68
+ if (delta >= idleAfterMs) setState("IDLE");
69
+ }, 200);
70
+ return () => clearInterval(id);
71
+ }, [idleAfterMs]);
72
+ return { state };
73
+ }
74
+ var init_useActivityMonitor = __esm({
75
+ "src/hooks/useActivityMonitor.ts"() {
76
+ "use strict";
77
+ }
78
+ });
79
+
80
+ // src/hooks/useAiAgent.ts
81
+ import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
82
+ import { createAgent, initChatModel, tool } from "langchain";
83
+ function contentToText(content) {
84
+ if (typeof content === "string") return content;
85
+ if (!content) return "";
86
+ if (Array.isArray(content)) {
87
+ return content.map((part) => {
88
+ if (typeof part === "string") return part;
89
+ if (typeof part === "object" && part && "text" in part) return String(part.text ?? "");
90
+ return "";
91
+ }).join("");
92
+ }
93
+ if (typeof content === "object" && "text" in content) return String(content.text ?? "");
94
+ return String(content);
95
+ }
96
+ function lastAiText(messages) {
97
+ for (let i = messages.length - 1; i >= 0; i--) {
98
+ const m = messages[i];
99
+ const type = typeof m?.getType === "function" ? m.getType() : typeof m?._getType === "function" ? m._getType() : m?.type;
100
+ if (type === "ai") {
101
+ const t = contentToText(m?.content);
102
+ return t || "";
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+ function createSystemPrompt(context) {
108
+ return [
109
+ "\u4F60\u662F TermBuddy \u91CC\u7684\u201C\u58F3\u4E2D\u5E7D\u7075 (Ghost in the Shell)\u201D\u3002",
110
+ "\u9ED8\u8BA4\u9690\u5F62\uFF1B\u88AB / \u5524\u9192\u65F6\u51FA\u73B0\u3002\u98CE\u683C\uFF1A\u6781\u7B80\u3001\u5E72\u7EC3\u3001\u5C11\u5E9F\u8BDD\u3002",
111
+ "\u4F60\u53EF\u4EE5\u4F7F\u7528\u5DE5\u5177\u6765\u64CD\u63A7\u5E94\u7528\u529F\u80FD\uFF08\u4F8B\u5982\u5012\u8BA1\u65F6\uFF09\u3002",
112
+ "\u5982\u679C\u7528\u6237\u63D0\u5230\u201C\u5012\u8BA1\u65F6/\u4E13\u6CE8/\u8BA1\u65F6/countdown\u201D\uFF0C\u4F18\u5148\u8C03\u7528 start_countdown\u3002",
113
+ `\u5F53\u524D\u4E0A\u4E0B\u6587\uFF1A\u6211\u53EB ${context.localName}\uFF1B\u540C\u684C\u53EB ${context.peerName}\u3002`
114
+ ].join("\n");
115
+ }
116
+ function useAiAgent(options) {
117
+ const [lines, setLines] = useState2([]);
118
+ const [busy, setBusy] = useState2(false);
119
+ const agentRef = useRef2(null);
120
+ const agentInitRef = useRef2(null);
121
+ const stateRef = useRef2({ messages: [] });
122
+ const abortRef = useRef2(null);
123
+ const append = useCallback((line) => {
124
+ setLines((prev) => [...prev, line]);
125
+ }, []);
126
+ const updateLine = useCallback((at, text) => {
127
+ setLines((prev) => {
128
+ const idx = prev.findIndex((l) => l.at === at);
129
+ if (idx === -1) return prev;
130
+ const next = [...prev];
131
+ next[idx] = { ...next[idx], text };
132
+ return next;
133
+ });
134
+ }, []);
135
+ const ensureAgent = useCallback(async () => {
136
+ if (agentRef.current) return agentRef.current;
137
+ agentInitRef.current ??= (async () => {
138
+ const startCountdown = tool(
139
+ async (input) => {
140
+ const minutes = Number(input.minutes);
141
+ if (!Number.isFinite(minutes) || minutes <= 0) return "\u5012\u8BA1\u65F6\u5206\u949F\u6570\u65E0\u6548\u3002";
142
+ options.onStartCountdown?.(minutes);
143
+ return `\u5DF2\u5F00\u59CB\u5012\u8BA1\u65F6 ${minutes} \u5206\u949F\u3002`;
144
+ },
145
+ {
146
+ name: "start_countdown",
147
+ description: "\u5F00\u59CB\u4E00\u4E2A\u4E13\u6CE8\u5012\u8BA1\u65F6\uFF08\u5206\u949F\uFF09\u3002",
148
+ schema: {
149
+ type: "object",
150
+ properties: {
151
+ minutes: { type: "integer", minimum: 1, maximum: 180, description: "\u5012\u8BA1\u65F6\u5206\u949F\u6570" }
152
+ },
153
+ required: ["minutes"],
154
+ additionalProperties: false
155
+ }
156
+ }
157
+ );
158
+ const sessionInfo = tool(
159
+ async () => {
160
+ return JSON.stringify(
161
+ {
162
+ localName: options.localName,
163
+ peerName: options.peerName
164
+ },
165
+ null,
166
+ 2
167
+ );
168
+ },
169
+ {
170
+ name: "session_info",
171
+ description: "\u83B7\u53D6\u5F53\u524D\u4F1A\u8BDD\u4E0A\u4E0B\u6587\uFF08\u672C\u5730\u6635\u79F0\u3001\u540C\u684C\u6635\u79F0\uFF09\u3002",
172
+ schema: { type: "object", properties: {}, additionalProperties: false }
173
+ }
174
+ );
175
+ const modelId = process.env.TERMBUDDY_MODEL ?? "openai:gpt-4o-mini";
176
+ const llm = await initChatModel(modelId, {
177
+ temperature: 0.2,
178
+ maxTokens: 800,
179
+ timeout: 3e4
180
+ });
181
+ return createAgent({
182
+ llm,
183
+ tools: [startCountdown, sessionInfo],
184
+ prompt: createSystemPrompt({ localName: options.localName, peerName: options.peerName }),
185
+ name: "ghost"
186
+ });
187
+ })();
188
+ agentRef.current = await agentInitRef.current;
189
+ return agentRef.current;
190
+ }, [options.localName, options.onStartCountdown, options.peerName]);
191
+ const ask = useCallback(
192
+ async (text) => {
193
+ append({ kind: "user", text: `> ${text}`, at: Date.now() });
194
+ const aiAt = Date.now() + 1;
195
+ append({ kind: "ai", text: "\u2026", at: aiAt });
196
+ abortRef.current?.abort();
197
+ abortRef.current = new AbortController();
198
+ setBusy(true);
199
+ try {
200
+ const agent = await ensureAgent();
201
+ const stream = await agent.stream(
202
+ {
203
+ messages: [...stateRef.current.messages, { role: "user", content: text }]
204
+ },
205
+ {
206
+ streamMode: "values",
207
+ signal: abortRef.current.signal
208
+ }
209
+ );
210
+ for await (const chunk of stream) {
211
+ const messages = chunk?.messages ?? [];
212
+ if (messages.length > 0) stateRef.current.messages = messages;
213
+ const t = lastAiText(messages);
214
+ if (t !== null) updateLine(aiAt, t);
215
+ }
216
+ } catch (e) {
217
+ const msg = e instanceof Error ? e.message : String(e);
218
+ updateLine(aiAt, `\uFF08AI \u51FA\u9519\uFF09${msg}`);
219
+ } finally {
220
+ setBusy(false);
221
+ }
222
+ },
223
+ [append, ensureAgent, updateLine]
224
+ );
225
+ useEffect2(() => {
226
+ return () => abortRef.current?.abort();
227
+ }, []);
228
+ return { lines, ask, busy };
229
+ }
230
+ var init_useAiAgent = __esm({
231
+ "src/hooks/useAiAgent.ts"() {
232
+ "use strict";
233
+ }
234
+ });
235
+
236
+ // src/constants.ts
237
+ var UDP_PORT, TCP_DEFAULT_PORT, DISCOVERY_VERSION;
238
+ var init_constants = __esm({
239
+ "src/constants.ts"() {
240
+ "use strict";
241
+ UDP_PORT = 45888;
242
+ TCP_DEFAULT_PORT = 45999;
243
+ DISCOVERY_VERSION = 1;
244
+ }
245
+ });
246
+
247
+ // src/net/broadcast.ts
248
+ import os from "os";
249
+ function ipv4ToInt(ip) {
250
+ return ip.split(".").map((n) => Number.parseInt(n, 10)).reduce((acc, n) => (acc << 8 | n & 255) >>> 0, 0);
251
+ }
252
+ function intToIpv4(n) {
253
+ return [24, 16, 8, 0].map((shift) => String(n >>> shift & 255)).join(".");
254
+ }
255
+ function getBroadcastTargets() {
256
+ const out = /* @__PURE__ */ new Set(["255.255.255.255"]);
257
+ const ifaces = os.networkInterfaces();
258
+ for (const entries of Object.values(ifaces)) {
259
+ if (!entries) continue;
260
+ for (const e of entries) {
261
+ if (e.family !== "IPv4") continue;
262
+ if (e.internal) continue;
263
+ if (!e.address || !e.netmask) continue;
264
+ const ip = ipv4ToInt(e.address);
265
+ const mask = ipv4ToInt(e.netmask);
266
+ const broadcast = (ip | ~mask >>> 0) >>> 0;
267
+ out.add(intToIpv4(broadcast));
268
+ }
269
+ }
270
+ return [...out];
271
+ }
272
+ var init_broadcast = __esm({
273
+ "src/net/broadcast.ts"() {
274
+ "use strict";
275
+ }
276
+ });
277
+
278
+ // src/net/index.ts
279
+ var init_net = __esm({
280
+ "src/net/index.ts"() {
281
+ "use strict";
282
+ init_broadcast();
283
+ }
284
+ });
285
+
286
+ // src/hooks/useBroadcaster.ts
287
+ import { useEffect as useEffect3 } from "react";
288
+ import dgram from "dgram";
289
+ function useBroadcaster(options) {
290
+ const depKey = options.enabled ? `${options.hostName}|${options.roomName}|${options.tcpPort ?? ""}|${options.intervalMs ?? 1e3}` : "disabled";
291
+ useEffect3(() => {
292
+ if (!options.enabled) return;
293
+ if (!options.tcpPort) return;
294
+ const socket = dgram.createSocket("udp4");
295
+ socket.on("error", () => {
296
+ });
297
+ socket.bind(() => {
298
+ socket.setBroadcast(true);
299
+ });
300
+ const targets = getBroadcastTargets();
301
+ const send = () => {
302
+ const packet = {
303
+ type: "termbuddy_discovery",
304
+ version: DISCOVERY_VERSION,
305
+ hostName: options.hostName,
306
+ roomName: options.roomName,
307
+ tcpPort: options.tcpPort,
308
+ sentAt: Date.now()
309
+ };
310
+ const msg = Buffer.from(JSON.stringify(packet));
311
+ for (const address of targets) {
312
+ socket.send(msg, UDP_PORT, address);
313
+ }
314
+ };
315
+ send();
316
+ const id = setInterval(send, options.intervalMs ?? 1e3);
317
+ return () => {
318
+ clearInterval(id);
319
+ socket.close();
320
+ };
321
+ }, [depKey]);
322
+ }
323
+ var init_useBroadcaster = __esm({
324
+ "src/hooks/useBroadcaster.ts"() {
325
+ "use strict";
326
+ init_constants();
327
+ init_net();
328
+ }
329
+ });
330
+
331
+ // src/hooks/useCountdown.ts
332
+ import { useCallback as useCallback2, useEffect as useEffect4, useRef as useRef3, useState as useState3 } from "react";
333
+ function formatMMSS(totalSeconds) {
334
+ const m = Math.floor(totalSeconds / 60);
335
+ const s = totalSeconds % 60;
336
+ return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
337
+ }
338
+ function useCountdown() {
339
+ const [remainingSeconds, setRemainingSeconds] = useState3(null);
340
+ const timerRef = useRef3(null);
341
+ const start = useCallback2((minutes) => {
342
+ const seconds = Math.max(1, Math.floor(minutes * 60));
343
+ setRemainingSeconds(seconds);
344
+ if (timerRef.current) clearInterval(timerRef.current);
345
+ timerRef.current = setInterval(() => {
346
+ setRemainingSeconds((prev) => {
347
+ if (prev === null) return null;
348
+ if (prev <= 1) return null;
349
+ return prev - 1;
350
+ });
351
+ }, 1e3);
352
+ }, []);
353
+ useEffect4(() => {
354
+ if (remainingSeconds !== null) return;
355
+ if (timerRef.current) clearInterval(timerRef.current);
356
+ timerRef.current = null;
357
+ }, [remainingSeconds]);
358
+ useEffect4(() => {
359
+ return () => {
360
+ if (timerRef.current) clearInterval(timerRef.current);
361
+ };
362
+ }, []);
363
+ return {
364
+ start,
365
+ label: remainingSeconds === null ? null : formatMMSS(remainingSeconds)
366
+ };
367
+ }
368
+ var init_useCountdown = __esm({
369
+ "src/hooks/useCountdown.ts"() {
370
+ "use strict";
371
+ }
372
+ });
373
+
374
+ // src/hooks/useScanner.ts
375
+ import { useEffect as useEffect5, useState as useState4 } from "react";
376
+ import dgram2 from "dgram";
377
+ function safeParse(msg) {
378
+ try {
379
+ const parsed = JSON.parse(msg.toString("utf8"));
380
+ if (parsed?.type !== "termbuddy_discovery") return null;
381
+ if (parsed?.version !== DISCOVERY_VERSION) return null;
382
+ if (!parsed.hostName || !parsed.roomName || !parsed.tcpPort) return null;
383
+ return parsed;
384
+ } catch {
385
+ return null;
386
+ }
387
+ }
388
+ function useScanner(options) {
389
+ const staleAfterMs = options?.staleAfterMs ?? 3500;
390
+ const [rooms, setRooms] = useState4([]);
391
+ useEffect5(() => {
392
+ const socket = dgram2.createSocket("udp4");
393
+ socket.on("error", () => {
394
+ });
395
+ socket.on("message", (msg, rinfo) => {
396
+ const packet = safeParse(msg);
397
+ if (!packet) return;
398
+ const now = Date.now();
399
+ setRooms((prev) => {
400
+ const key = `${rinfo.address}:${packet.tcpPort}`;
401
+ const next = prev.filter((r) => `${r.ip}:${r.tcpPort}` !== key);
402
+ next.push({
403
+ ip: rinfo.address,
404
+ hostName: packet.hostName,
405
+ roomName: packet.roomName,
406
+ tcpPort: packet.tcpPort,
407
+ lastSeenAt: now
408
+ });
409
+ return next;
410
+ });
411
+ });
412
+ socket.bind(UDP_PORT, () => {
413
+ });
414
+ const prune = setInterval(() => {
415
+ const now = Date.now();
416
+ setRooms((prev) => prev.filter((r) => now - r.lastSeenAt <= staleAfterMs));
417
+ }, 500);
418
+ return () => {
419
+ clearInterval(prune);
420
+ socket.close();
421
+ };
422
+ }, [staleAfterMs]);
423
+ return rooms;
424
+ }
425
+ var init_useScanner = __esm({
426
+ "src/hooks/useScanner.ts"() {
427
+ "use strict";
428
+ init_constants();
429
+ }
430
+ });
431
+
432
+ // src/hooks/useTcpSync.ts
433
+ import { useCallback as useCallback3, useEffect as useEffect6, useRef as useRef4, useState as useState5 } from "react";
434
+ import net from "net";
435
+ function writePacket(socket, packet) {
436
+ socket.write(`${JSON.stringify(packet)}
437
+ `, "utf8");
438
+ }
439
+ function useTcpSync(options) {
440
+ const [status, setStatus] = useState5(options.role === "host" ? "waiting" : "connecting");
441
+ const [listenPort, setListenPort] = useState5(void 0);
442
+ const [peerName, setPeerName] = useState5(void 0);
443
+ const [remoteState, setRemoteState] = useState5(void 0);
444
+ const socketRef = useRef4(null);
445
+ const lastSeenRef = useRef4(Date.now());
446
+ const heartbeatRef = useRef4(null);
447
+ const cleanupSocket = useCallback3(() => {
448
+ if (heartbeatRef.current) clearInterval(heartbeatRef.current);
449
+ heartbeatRef.current = null;
450
+ const s = socketRef.current;
451
+ socketRef.current = null;
452
+ if (s && !s.destroyed) s.destroy();
453
+ }, []);
454
+ const attachSocket = useCallback3(
455
+ (s) => {
456
+ cleanupSocket();
457
+ socketRef.current = s;
458
+ lastSeenRef.current = Date.now();
459
+ setStatus("connected");
460
+ setRemoteState("IDLE");
461
+ let buf = "";
462
+ s.setNoDelay(true);
463
+ s.setEncoding("utf8");
464
+ const onData = (chunk) => {
465
+ buf += chunk;
466
+ while (true) {
467
+ const idx = buf.indexOf("\n");
468
+ if (idx === -1) break;
469
+ const line = buf.slice(0, idx).trim();
470
+ buf = buf.slice(idx + 1);
471
+ if (!line) continue;
472
+ try {
473
+ const packet = JSON.parse(line);
474
+ lastSeenRef.current = Date.now();
475
+ if (packet.type === "hello") {
476
+ if (options.role === "host") setPeerName(packet.clientName);
477
+ else setPeerName(packet.hostName);
478
+ }
479
+ if (packet.type === "status") setRemoteState(packet.state);
480
+ if (packet.type === "ping") writePacket(s, { type: "pong", sentAt: Date.now() });
481
+ if (packet.type === "pong") {
482
+ }
483
+ } catch {
484
+ }
485
+ }
486
+ };
487
+ s.on("data", onData);
488
+ s.on("close", () => {
489
+ setStatus(options.role === "host" ? "waiting" : "disconnected");
490
+ setRemoteState("OFFLINE");
491
+ cleanupSocket();
492
+ });
493
+ s.on("error", () => {
494
+ setStatus(options.role === "host" ? "waiting" : "disconnected");
495
+ setRemoteState("OFFLINE");
496
+ });
497
+ writePacket(s, {
498
+ type: "hello",
499
+ hostName: options.role === "host" ? options.localName : options.hostName ?? "Host",
500
+ clientName: options.role === "client" ? options.localName : "Client",
501
+ sentAt: Date.now()
502
+ });
503
+ heartbeatRef.current = setInterval(() => {
504
+ const sock = socketRef.current;
505
+ if (!sock || sock.destroyed) return;
506
+ writePacket(sock, { type: "ping", sentAt: Date.now() });
507
+ const age = Date.now() - lastSeenRef.current;
508
+ if (age > 6e3) {
509
+ setStatus("disconnected");
510
+ setRemoteState("OFFLINE");
511
+ cleanupSocket();
512
+ }
513
+ }, 2e3);
514
+ },
515
+ [cleanupSocket, options]
516
+ );
517
+ useEffect6(() => {
518
+ if (options.role === "host") {
519
+ const server = net.createServer((socket2) => {
520
+ attachSocket(socket2);
521
+ });
522
+ server.on("error", () => {
523
+ });
524
+ server.listen(options.port ?? TCP_DEFAULT_PORT, () => {
525
+ const address = server.address();
526
+ if (address && typeof address === "object") setListenPort(address.port);
527
+ });
528
+ return () => {
529
+ cleanupSocket();
530
+ server.close();
531
+ };
532
+ }
533
+ setStatus("connecting");
534
+ const socket = net.createConnection({ host: options.hostIp, port: options.tcpPort }, () => {
535
+ attachSocket(socket);
536
+ });
537
+ socket.on("error", () => {
538
+ setStatus("disconnected");
539
+ setRemoteState("OFFLINE");
540
+ });
541
+ return () => {
542
+ socket.destroy();
543
+ cleanupSocket();
544
+ };
545
+ }, [attachSocket, cleanupSocket, options]);
546
+ const sendStatus = useCallback3((state) => {
547
+ const socket = socketRef.current;
548
+ if (!socket || socket.destroyed) return;
549
+ writePacket(socket, { type: "status", state, sentAt: Date.now() });
550
+ }, []);
551
+ return { status, listenPort, peerName, remoteState, sendStatus };
552
+ }
553
+ var init_useTcpSync = __esm({
554
+ "src/hooks/useTcpSync.ts"() {
555
+ "use strict";
556
+ init_constants();
557
+ }
558
+ });
559
+
560
+ // src/hooks/index.ts
561
+ var init_hooks = __esm({
562
+ "src/hooks/index.ts"() {
563
+ "use strict";
564
+ init_useActivityMonitor();
565
+ init_useAiAgent();
566
+ init_useBroadcaster();
567
+ init_useCountdown();
568
+ init_useScanner();
569
+ init_useTcpSync();
570
+ }
571
+ });
572
+
573
+ // src/views/RoomScanner.tsx
574
+ import { useMemo } from "react";
575
+ import { Box as Box2, Text as Text2, useInput as useInput3 } from "ink";
576
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
577
+ function RoomScanner(props) {
578
+ const rooms = useScanner();
579
+ const sortedRooms = useMemo(() => {
580
+ return [...rooms].sort((a, b) => b.lastSeenAt - a.lastSeenAt);
581
+ }, [rooms]);
582
+ useInput3((input, key) => {
583
+ if (key.escape || input === "b") props.onBack();
584
+ if (input === "q") props.onExit();
585
+ const index = Number.parseInt(input, 10);
586
+ if (Number.isNaN(index)) return;
587
+ const room = sortedRooms[index - 1];
588
+ if (!room) return;
589
+ props.onSelectRoom(room);
590
+ });
591
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", padding: 1, children: [
592
+ /* @__PURE__ */ jsxs2(Text2, { children: [
593
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "\u6B63\u5728\u626B\u63CF\u5C40\u57DF\u7F51..." }),
594
+ " (\u6309 ",
595
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "b" }),
596
+ " \u8FD4\u56DE,",
597
+ " ",
598
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "q" }),
599
+ " \u9000\u51FA)"
600
+ ] }),
601
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: sortedRooms.length === 0 ? /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "\u6682\u65E0\u623F\u95F4\u5E7F\u64AD\u3002" }) : sortedRooms.map((room, i) => /* @__PURE__ */ jsxs2(Text2, { children: [
602
+ /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
603
+ "[",
604
+ i + 1,
605
+ "]"
606
+ ] }),
607
+ " ",
608
+ room.roomName,
609
+ " \u2014 ",
610
+ room.hostName,
611
+ " @ ",
612
+ room.ip,
613
+ ":",
614
+ room.tcpPort
615
+ ] }, `${room.ip}:${room.tcpPort}`)) })
616
+ ] });
617
+ }
618
+ var init_RoomScanner = __esm({
619
+ "src/views/RoomScanner.tsx"() {
620
+ "use strict";
621
+ init_hooks();
622
+ }
623
+ });
624
+
625
+ // src/components/AiConsole.tsx
626
+ import { useMemo as useMemo2, useState as useState6 } from "react";
627
+ import { Box as Box3, Text as Text3, useInput as useInput4 } from "ink";
628
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
629
+ function AiConsole(props) {
630
+ const [input, setInput] = useState6("");
631
+ const agent = useAiAgent({
632
+ localName: props.localName,
633
+ peerName: props.peerName,
634
+ onStartCountdown: props.onStartCountdown
635
+ });
636
+ const helpLine = useMemo2(
637
+ () => "\u793A\u4F8B\uFF1A\u5012\u8BA1\u65F620\u5206\u949F / countdown 20 / \u95EE\u4E2A\u6280\u672F\u95EE\u9898",
638
+ []
639
+ );
640
+ useInput4(
641
+ (ch, key) => {
642
+ if (key.escape) {
643
+ props.onClose();
644
+ return;
645
+ }
646
+ if (key.return) {
647
+ const line = input.trim();
648
+ setInput("");
649
+ if (!line) return;
650
+ void agent.ask(line);
651
+ return;
652
+ }
653
+ if (key.backspace || key.delete) {
654
+ setInput((s) => s.slice(0, -1));
655
+ return;
656
+ }
657
+ if (key.ctrl || key.meta) return;
658
+ if (ch) setInput((s) => s + ch);
659
+ },
660
+ { isActive: true }
661
+ );
662
+ const lines = agent.lines.slice(-12);
663
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", paddingX: 1, paddingY: 0, children: [
664
+ /* @__PURE__ */ jsxs3(Box3, { justifyContent: "space-between", children: [
665
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "AI Console" }),
666
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: agent.busy ? "Thinking\u2026" : "Esc \u5173\u95ED" })
667
+ ] }),
668
+ /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: "gray", children: helpLine }) }),
669
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
670
+ lines.length === 0 ? /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "\uFF08\u5E7D\u7075\u8FD8\u5728\u58F3\u91CC\u2026\uFF09" }) : null,
671
+ lines.map((l, i) => /* @__PURE__ */ jsx3(Text3, { color: l.kind === "user" ? "yellow" : "white", children: l.text }, `${l.kind}:${l.at}:${i}`))
672
+ ] }),
673
+ /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
674
+ /* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
675
+ ">",
676
+ " "
677
+ ] }),
678
+ /* @__PURE__ */ jsx3(Text3, { children: input })
679
+ ] })
680
+ ] });
681
+ }
682
+ var init_AiConsole = __esm({
683
+ "src/components/AiConsole.tsx"() {
684
+ "use strict";
685
+ init_hooks();
686
+ }
687
+ });
688
+
689
+ // src/components/AvatarDisplay.tsx
690
+ import { Box as Box4, Text as Text4 } from "ink";
691
+ import { jsx as jsx4 } from "react/jsx-runtime";
692
+ var init_AvatarDisplay = __esm({
693
+ "src/components/AvatarDisplay.tsx"() {
694
+ "use strict";
695
+ }
696
+ });
697
+
698
+ // src/components/BuddyAvatar.tsx
699
+ import { Box as Box5, Text as Text5 } from "ink";
700
+ import { jsx as jsx5 } from "react/jsx-runtime";
701
+ function BuddyAvatar(props) {
702
+ const frame = FRAMES[props.state];
703
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginTop: 1, children: frame.lines.map((line, i) => /* @__PURE__ */ jsx5(Text5, { color: frame.color, children: line }, `${props.state}:${i}`)) });
704
+ }
705
+ var FRAMES;
706
+ var init_BuddyAvatar = __esm({
707
+ "src/components/BuddyAvatar.tsx"() {
708
+ "use strict";
709
+ FRAMES = {
710
+ TYPING: {
711
+ color: "green",
712
+ lines: [" /\\_/\\ ", "( >_<) ", " /|_|\\\\ ", " / \\\\ "]
713
+ },
714
+ IDLE: {
715
+ color: "yellow",
716
+ lines: [" /\\_/\\ ", "( -.-) ", " /|_|\\\\ ", " / \\\\ "]
717
+ },
718
+ OFFLINE: {
719
+ color: "gray",
720
+ lines: [" /\\_/\\ ", "( x_x) ", " /|_|\\\\ ", " / \\\\ "]
721
+ }
722
+ };
723
+ }
724
+ });
725
+
726
+ // src/components/StatusHeader.tsx
727
+ import { Box as Box6, Text as Text6 } from "ink";
728
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
729
+ function statusText(status) {
730
+ switch (status) {
731
+ case "waiting":
732
+ return { label: "Waiting", color: "yellow" };
733
+ case "connecting":
734
+ return { label: "Connecting", color: "yellow" };
735
+ case "connected":
736
+ return { label: "Connected via TCP", color: "green" };
737
+ case "disconnected":
738
+ return { label: "Disconnected", color: "red" };
739
+ }
740
+ }
741
+ function StatusHeader(props) {
742
+ const st = statusText(props.status);
743
+ return /* @__PURE__ */ jsxs4(Box6, { justifyContent: "space-between", children: [
744
+ /* @__PURE__ */ jsxs4(Box6, { children: [
745
+ /* @__PURE__ */ jsx6(Text6, { color: st.color, children: st.label }),
746
+ props.role === "host" ? /* @__PURE__ */ jsx6(Text6, { color: "gray", children: props.tcpPort ? ` \u2014 TCP :${props.tcpPort}` : "" }) : /* @__PURE__ */ jsx6(Text6, { color: "gray", children: props.hostIp && props.tcpPort ? ` \u2014 ${props.hostIp}:${props.tcpPort}` : "" })
747
+ ] }),
748
+ /* @__PURE__ */ jsx6(Box6, { children: props.countdownLabel ? /* @__PURE__ */ jsxs4(Text6, { color: "cyan", children: [
749
+ "Focus ",
750
+ props.countdownLabel
751
+ ] }) : /* @__PURE__ */ jsx6(Text6, { children: " " }) })
752
+ ] });
753
+ }
754
+ var init_StatusHeader = __esm({
755
+ "src/components/StatusHeader.tsx"() {
756
+ "use strict";
757
+ }
758
+ });
759
+
760
+ // src/components/index.ts
761
+ var init_components = __esm({
762
+ "src/components/index.ts"() {
763
+ "use strict";
764
+ init_AiConsole();
765
+ init_AvatarDisplay();
766
+ init_BuddyAvatar();
767
+ init_StatusHeader();
768
+ }
769
+ });
770
+
771
+ // src/views/Session.tsx
772
+ import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo3, useState as useState7 } from "react";
773
+ import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
774
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
775
+ function Session(props) {
776
+ const roomName = useMemo3(() => `${props.localName}'s Room`, [props.localName]);
777
+ const [showAi, setShowAi] = useState7(false);
778
+ const countdown = useCountdown();
779
+ const tcpOptions = useMemo3(() => {
780
+ return props.role === "host" ? { role: "host", localName: props.localName } : {
781
+ role: "client",
782
+ localName: props.localName,
783
+ hostIp: props.hostIp,
784
+ tcpPort: props.tcpPort,
785
+ hostName: props.hostName
786
+ };
787
+ }, [
788
+ props.role,
789
+ props.localName,
790
+ props.role === "client" ? props.hostIp : "",
791
+ props.role === "client" ? props.tcpPort : 0,
792
+ props.role === "client" ? props.hostName : ""
793
+ ]);
794
+ const tcp = useTcpSync(tcpOptions);
795
+ const broadcasterOptions = useMemo3(() => {
796
+ return props.role === "host" ? {
797
+ enabled: true,
798
+ hostName: props.localName,
799
+ roomName,
800
+ tcpPort: tcp.listenPort
801
+ } : { enabled: false };
802
+ }, [props.role, props.localName, roomName, tcp.listenPort]);
803
+ useBroadcaster(broadcasterOptions);
804
+ const localActivity = useActivityMonitor();
805
+ const remoteActivity = tcp.remoteState ?? "OFFLINE";
806
+ const onToggleAi = useCallback4(() => setShowAi((v) => !v), []);
807
+ const onCloseAi = useCallback4(() => setShowAi(false), []);
808
+ useInput5(
809
+ (input, key) => {
810
+ if (input === "q") props.onExit();
811
+ if (input === "/" && !key.ctrl && !key.meta) onToggleAi();
812
+ },
813
+ { isActive: !showAi }
814
+ );
815
+ const buddyName = props.role === "host" ? tcp.peerName ?? "Waiting..." : `${props.hostName ?? "Host"} (${props.roomName ?? "Room"})`;
816
+ const localState = localActivity.state;
817
+ const localLabel = props.role === "host" ? `${props.localName} (Host)` : `${props.localName} (Client)`;
818
+ useEffect7(() => {
819
+ if (tcp.status !== "connected") return;
820
+ tcp.sendStatus(localState);
821
+ }, [localState, tcp.status, tcp.sendStatus]);
822
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", padding: 1, children: [
823
+ /* @__PURE__ */ jsx7(
824
+ StatusHeader,
825
+ {
826
+ role: props.role,
827
+ status: tcp.status,
828
+ hostIp: props.role === "client" ? props.hostIp : void 0,
829
+ tcpPort: props.role === "client" ? props.tcpPort : tcp.listenPort,
830
+ countdownLabel: countdown.label
831
+ }
832
+ ),
833
+ /* @__PURE__ */ jsxs5(Box7, { flexDirection: "row", gap: 4, marginTop: 1, children: [
834
+ /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", width: "50%", children: [
835
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: localLabel }),
836
+ /* @__PURE__ */ jsx7(BuddyAvatar, { state: localState })
837
+ ] }),
838
+ /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", width: "50%", children: [
839
+ /* @__PURE__ */ jsx7(Text7, { color: "magenta", children: buddyName }),
840
+ /* @__PURE__ */ jsx7(BuddyAvatar, { state: remoteActivity })
841
+ ] })
842
+ ] }),
843
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { color: "gray", children: [
844
+ "\u6309 ",
845
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "/" }),
846
+ " \u53EC\u5524 AI Console\uFF0C\u6309 ",
847
+ /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "q" }),
848
+ " \u8FD4\u56DE\u83DC\u5355\u3002"
849
+ ] }) }),
850
+ showAi ? /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(
851
+ AiConsole,
852
+ {
853
+ onClose: onCloseAi,
854
+ onStartCountdown: countdown.start,
855
+ localName: props.localName,
856
+ peerName: tcp.peerName ?? (props.role === "client" ? props.hostName : void 0) ?? "Buddy"
857
+ }
858
+ ) }) : null
859
+ ] });
860
+ }
861
+ var init_Session = __esm({
862
+ "src/views/Session.tsx"() {
863
+ "use strict";
864
+ init_components();
865
+ init_hooks();
866
+ }
867
+ });
868
+
869
+ // src/views/index.ts
870
+ var init_views = __esm({
871
+ "src/views/index.ts"() {
872
+ "use strict";
873
+ init_MainMenu();
874
+ init_RoomScanner();
875
+ init_Session();
876
+ }
877
+ });
878
+
879
+ // src/app/App.tsx
880
+ import { useCallback as useCallback5, useMemo as useMemo4, useState as useState8 } from "react";
881
+ import os2 from "os";
882
+ import { useApp } from "ink";
883
+ import { jsx as jsx8 } from "react/jsx-runtime";
884
+ function App() {
885
+ const { exit } = useApp();
886
+ const [view, setView] = useState8({ name: "MENU" });
887
+ const localName = useMemo4(() => os2.hostname(), []);
888
+ const goMenu = useCallback5(() => setView({ name: "MENU" }), []);
889
+ if (view.name === "MENU") {
890
+ return /* @__PURE__ */ jsx8(
891
+ MainMenu,
892
+ {
893
+ onHost: () => setView({ name: "SESSION", role: "host" }),
894
+ onJoin: () => setView({ name: "SCANNING" }),
895
+ onExit: () => exit()
896
+ }
897
+ );
898
+ }
899
+ if (view.name === "SCANNING") {
900
+ return /* @__PURE__ */ jsx8(
901
+ RoomScanner,
902
+ {
903
+ onBack: goMenu,
904
+ onExit: () => exit(),
905
+ onSelectRoom: (room) => setView({
906
+ name: "SESSION",
907
+ role: "client",
908
+ hostIp: room.ip,
909
+ tcpPort: room.tcpPort,
910
+ roomName: room.roomName,
911
+ hostName: room.hostName
912
+ })
913
+ }
914
+ );
915
+ }
916
+ if (view.name === "SESSION" && view.role === "host") {
917
+ return /* @__PURE__ */ jsx8(Session, { localName, role: "host", onExit: goMenu });
918
+ }
919
+ return /* @__PURE__ */ jsx8(
920
+ Session,
921
+ {
922
+ localName,
923
+ role: "client",
924
+ onExit: goMenu,
925
+ hostIp: view.hostIp,
926
+ tcpPort: view.tcpPort,
927
+ roomName: view.roomName,
928
+ hostName: view.hostName
929
+ }
930
+ );
931
+ }
932
+ var init_App = __esm({
933
+ "src/app/App.tsx"() {
934
+ "use strict";
935
+ init_views();
936
+ }
937
+ });
938
+
939
+ // src/app/index.ts
940
+ var app_exports = {};
941
+ __export(app_exports, {
942
+ App: () => App
943
+ });
944
+ var init_app = __esm({
945
+ "src/app/index.ts"() {
946
+ "use strict";
947
+ init_App();
948
+ }
949
+ });
950
+
951
+ // src/cli.tsx
952
+ process.env.NODE_ENV ??= "production";
953
+ var React5 = await import("react");
954
+ var { render } = await import("ink");
955
+ var { App: App2 } = await Promise.resolve().then(() => (init_app(), app_exports));
956
+ render(React5.createElement(App2));
957
+ //# sourceMappingURL=cli.js.map