bom-pi 0.0.7 → 0.0.8
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/package.json +1 -1
- package/packages/cli/src/bom-pi.mjs +106 -15
package/package.json
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
// submit <text...> [--model p/m] | reset [handoff...] | transcript | watch | shutdown
|
|
5
5
|
// Mọi lệnh conversation nhận --cid N (mặc định: conversation active của daemon).
|
|
6
6
|
import { connect } from 'node:net';
|
|
7
|
-
import { join, resolve } from 'node:path';
|
|
7
|
+
import { dirname, join, resolve } from 'node:path';
|
|
8
8
|
import { userInfo, homedir, hostname } from 'node:os';
|
|
9
9
|
import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
|
|
10
|
-
import {
|
|
10
|
+
import { openSync } from 'node:fs';
|
|
11
|
+
import { execFile, spawn } from 'node:child_process';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
11
13
|
import { promisify } from 'node:util';
|
|
12
14
|
import process from 'node:process';
|
|
13
15
|
|
|
@@ -27,6 +29,13 @@ const cid = arg('cid', null); // conversation id cho submit/transcript/reset/wat
|
|
|
27
29
|
const cidParams = () => (cid ? { conversationId: Number(cid) } : {});
|
|
28
30
|
const cmd = process.argv[2] ?? 'status';
|
|
29
31
|
|
|
32
|
+
// HTTP loopback của daemon TRÊN MÁY NÀY (Windows / không unix socket)
|
|
33
|
+
const LOCAL_HTTP = `http://127.0.0.1:${arg('port', process.env.BOM_PI_PORT || 8790)}`;
|
|
34
|
+
const dataDir = process.env.BOM_PI_DATA_DIR || join(homedir(), '.local/state/bom-pi');
|
|
35
|
+
const httpTokenPath = join(dataDir, 'http.token');
|
|
36
|
+
// Tự khởi động daemon nền khi CLI cần mà chưa có — trừ lệnh tắt/hỏi trạng thái
|
|
37
|
+
const AUTO_START = cmd !== 'shutdown' && !(cmd === 'daemon' && ['stop', 'status', 'log'].includes(process.argv[3]));
|
|
38
|
+
|
|
30
39
|
// Remote (thiết bị khác — Windows/phone): pair MỘT lần bằng 'bom-pi pair <code> --url …'
|
|
31
40
|
// ⇒ CLI tự lưu {url, token} vào ~/.bom-pi.json và dùng mãi (env BOM_PI_URL/BOM_PI_TOKEN
|
|
32
41
|
// vẫn đè lên nếu đặt; --url/--token đè lên tất cả). CLI nói chuyện web /rpc thay unix socket.
|
|
@@ -48,14 +57,15 @@ if (cmd === '-v' || cmd === '--version' || cmd === 'version') {
|
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
// Không thấy daemon: hướng dẫn theo nơi đang chạy — máy này (chưa start?) hay
|
|
51
|
-
// thiết bị khác (pair remote một lần).
|
|
60
|
+
// thiết bị khác (pair remote một lần). CLI đã THỬ tự khởi động daemon nền trước khi
|
|
61
|
+
// in cái này — thấy nó nghĩa là auto-start cũng chết (xem log).
|
|
52
62
|
function unreachable(e) {
|
|
53
|
-
const head = `cannot reach bom-core at ${socketPath} (và
|
|
63
|
+
const head = `cannot reach bom-core at ${socketPath} (và ${LOCAL_HTTP}): ${e.message}`;
|
|
54
64
|
if (process.platform === 'win32') {
|
|
55
65
|
return `${head}
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
CLI đã thử tự khởi động daemon nền mà không lên — xem log: ${join(dataDir, 'bom-core.log')}
|
|
68
|
+
thử chạy tay để thấy lỗi: bom-core
|
|
59
69
|
hoặc điều khiển MÁY KHÁC: pair một lần bằng
|
|
60
70
|
bom-pi pair <CODE> --url http://<ip-máy-khác>:8791`;
|
|
61
71
|
}
|
|
@@ -63,7 +73,8 @@ function unreachable(e) {
|
|
|
63
73
|
|
|
64
74
|
daemon chưa chạy? thử: systemctl --user start bom-core (xem trạng thái: bom-pi status)
|
|
65
75
|
chưa cài daemon?: bom-pi setup
|
|
66
|
-
chạy tay không systemd?: bom-
|
|
76
|
+
chạy tay không systemd?: bom-pi daemon start
|
|
77
|
+
log daemon do CLI khởi động: bom-pi daemon log
|
|
67
78
|
ở THIẾT BỊ KHÁC?: pair một lần rồi mọi lệnh chạy remote:
|
|
68
79
|
bom-pi pair <CODE> --url http://<ip-máy-chạy-daemon>:8791`;
|
|
69
80
|
}
|
|
@@ -108,9 +119,6 @@ function rpcSocket(method, params) {
|
|
|
108
119
|
|
|
109
120
|
// HTTP loopback (Windows / nơi không có unix socket): cùng /rpc, bearer http.token
|
|
110
121
|
// của daemon trên MÁY NÀY — token tự sinh lúc daemon khởi động, mode 0600.
|
|
111
|
-
const LOCAL_HTTP = `http://127.0.0.1:${arg('port', process.env.BOM_PI_PORT || 8790)}`;
|
|
112
|
-
const dataDir = process.env.BOM_PI_DATA_DIR || join(homedir(), '.local/state/bom-pi');
|
|
113
|
-
const httpTokenPath = join(dataDir, 'http.token');
|
|
114
122
|
let httpToken = null;
|
|
115
123
|
async function localHttpToken() {
|
|
116
124
|
if (httpToken !== null) return httpToken;
|
|
@@ -137,13 +145,64 @@ async function rpcHttp(method, params) {
|
|
|
137
145
|
return j.result;
|
|
138
146
|
}
|
|
139
147
|
|
|
148
|
+
// ---- Auto-start daemon nền (mô hình "mỗi máy một daemon") ----
|
|
149
|
+
// Bài học log Windows 2026-11-03: daemon foreground chiếm terminal ⇒ Ctrl+C giết daemon
|
|
150
|
+
// giữa chừng ⇒ mọi lệnh sau lỗi. CLI giờ tự đưa daemon lên NỀN (detached, log ra file)
|
|
151
|
+
// khi cần và chưa có — không ai phải giữ terminal.
|
|
152
|
+
const daemonMjsPath = () => resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'packages', 'core', 'src', 'daemon.mjs');
|
|
153
|
+
const daemonLogPath = () => join(dataDir, 'bom-core.log');
|
|
154
|
+
let daemonAliveCache = false;
|
|
155
|
+
|
|
156
|
+
async function daemonAlive() {
|
|
157
|
+
if (daemonAliveCache) return true;
|
|
158
|
+
const sockOk = await new Promise((done) => {
|
|
159
|
+
const c = connect(socketPath);
|
|
160
|
+
c.once('connect', () => { c.destroy(); done(true); });
|
|
161
|
+
c.once('error', () => done(false));
|
|
162
|
+
});
|
|
163
|
+
if (sockOk) { daemonAliveCache = true; return true; }
|
|
164
|
+
try {
|
|
165
|
+
const r = await fetch(`${LOCAL_HTTP}/healthz`, { signal: AbortSignal.timeout(800) });
|
|
166
|
+
daemonAliveCache = r.ok;
|
|
167
|
+
return r.ok;
|
|
168
|
+
} catch { return false; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function ensureDaemon() {
|
|
172
|
+
if (await daemonAlive()) return true;
|
|
173
|
+
const daemon = daemonMjsPath();
|
|
174
|
+
try { await stat(daemon); } catch { return false; } // không tìm thấy file daemon — cứ báo unreachable
|
|
175
|
+
await mkdir(dataDir, { recursive: true });
|
|
176
|
+
const logFd = openSync(daemonLogPath(), 'a');
|
|
177
|
+
const args = [daemon];
|
|
178
|
+
const portArg = process.argv.indexOf('--port');
|
|
179
|
+
if (portArg >= 0 && process.argv[portArg + 1]) args.push('--port', process.argv[portArg + 1]);
|
|
180
|
+
const child = spawn(process.execPath, args, {
|
|
181
|
+
detached: true, // sống sót sau khi CLI exit — Ctrl+C terminal không đụng được
|
|
182
|
+
stdio: ['ignore', logFd, logFd],
|
|
183
|
+
windowsHide: true,
|
|
184
|
+
env: { ...process.env },
|
|
185
|
+
});
|
|
186
|
+
child.unref();
|
|
187
|
+
for (let i = 0; i < 80; i++) {
|
|
188
|
+
await new Promise((r) => setTimeout(r, 250)); // mở storage mất vài giây lần đầu
|
|
189
|
+
if (await daemonAlive()) return true;
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
140
194
|
// Local: ưu tiên unix socket; socket không có (Windows / chạy không socket) → HTTP loopback
|
|
141
195
|
async function rpcLocal(method, params) {
|
|
142
196
|
try {
|
|
143
197
|
return await rpcSocket(method, params);
|
|
144
198
|
} catch (e) {
|
|
145
199
|
if (e?.code === 'ENOENT' || e?.code === 'ECONNREFUSED') {
|
|
146
|
-
try { return await rpcHttp(method, params); } catch {
|
|
200
|
+
try { return await rpcHttp(method, params); } catch {
|
|
201
|
+
// không socket, không HTTP → tự đưa daemon nền lên rồi thử lại lần cuối
|
|
202
|
+
if (AUTO_START && (await ensureDaemon())) {
|
|
203
|
+
return await rpcHttp(method, params);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
147
206
|
}
|
|
148
207
|
throw e;
|
|
149
208
|
}
|
|
@@ -227,8 +286,12 @@ function watch() {
|
|
|
227
286
|
|
|
228
287
|
// Windows / không unix socket: stream /events của HTTP loopback (NDJSON, cùng grammar socket)
|
|
229
288
|
async function watchHttp(onMsg, gotReply) {
|
|
230
|
-
let token;
|
|
231
|
-
try { token = await localHttpToken(); }
|
|
289
|
+
let token = null;
|
|
290
|
+
try { token = await localHttpToken(); }
|
|
291
|
+
catch {
|
|
292
|
+
if (AUTO_START && (await ensureDaemon())) { try { token = await localHttpToken(); } catch { /* vẫn không có — báo dưới */ } }
|
|
293
|
+
}
|
|
294
|
+
if (!token) { console.error(unreachable(new Error('no socket, no http.token'))); process.exit(1); }
|
|
232
295
|
const q = cid ? `?conversationId=${encodeURIComponent(cid)}` : '';
|
|
233
296
|
let res;
|
|
234
297
|
try { res = await fetch(`${LOCAL_HTTP}/events${q}`, { headers: { Authorization: `Bearer ${token}` } }); }
|
|
@@ -449,6 +512,17 @@ try {
|
|
|
449
512
|
console.error(`máy này đã pair remote tới ${REMOTE_URL} (config ${CONFIG_PATH}).\n 'bom-pi pair' (in code/QR) là lệnh chạy TRÊN MÁY CHỦ — chạy đó, hoặc gỡ remote: bom-pi unpair.`);
|
|
450
513
|
process.exit(64);
|
|
451
514
|
} else if (!sub2) {
|
|
515
|
+
// Link pair cần WEB phục vụ trang /pair — máy này chưa chạy web thì mint code vô ích
|
|
516
|
+
let webUp = false;
|
|
517
|
+
try { const r = await fetch('http://127.0.0.1:8791/', { redirect: 'manual', signal: AbortSignal.timeout(1200) }); webUp = r.status > 0; } catch { /* web không nghe */ }
|
|
518
|
+
if (!webUp) {
|
|
519
|
+
console.error(`máy này chưa chạy web (:8791) — link pair sẽ không mở được ở thiết bị khác.
|
|
520
|
+
|
|
521
|
+
chạy web máy này: bom-web --port 8791 (rồi chạy lại 'bom-pi pair')
|
|
522
|
+
hoặc GHÉP máy này vào máy khác (máy đã chạy daemon + web):
|
|
523
|
+
bom-pi pair <CODE> --url http://<ip-máy-đó>:8791`);
|
|
524
|
+
process.exit(64);
|
|
525
|
+
}
|
|
452
526
|
const r = await rpc('pair.code');
|
|
453
527
|
const nets = await import('node:os');
|
|
454
528
|
const lan = Object.values(nets.networkInterfaces() ?? {}).flat()
|
|
@@ -513,7 +587,7 @@ try {
|
|
|
513
587
|
// Sinh systemd user units đúng đường dẫn cài (npm global HOẶC working tree) + enable.
|
|
514
588
|
// root = 3 cấp trên file CLI này: packages/cli/src → gốc package (cả hai kiểu cài đều đúng)
|
|
515
589
|
const { fileURLToPath } = await import('node:url');
|
|
516
|
-
const root = resolve(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
590
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); // resolve(file,'..') pop filename — đi từ dirname
|
|
517
591
|
const daemon = resolve(root, 'packages/core/src/daemon.mjs');
|
|
518
592
|
const web = resolve(root, 'apps/web/server.mjs');
|
|
519
593
|
for (const f of [daemon, web]) {
|
|
@@ -560,6 +634,23 @@ WantedBy=default.target
|
|
|
560
634
|
console.log('✓ enabled: bom-core + bom-web (systemd user)');
|
|
561
635
|
console.log('\nTiếp theo: bom-pi pair → quét QR trên phone → bom-pi pair approve <mã ghép>');
|
|
562
636
|
console.log('Log: journalctl --user -u bom-core -f');
|
|
637
|
+
} else if (cmd === 'daemon') {
|
|
638
|
+
// bom-pi daemon [start|stop|status|log] — daemon nền do CLI quản (systemd máy chủ tự lo phần mình)
|
|
639
|
+
const sub3 = process.argv[3] ?? 'start';
|
|
640
|
+
if (sub3 === 'start') {
|
|
641
|
+
if (await ensureDaemon()) console.log(`daemon sống — ${LOCAL_HTTP} · log: ${daemonLogPath()}`);
|
|
642
|
+
else { console.error(`không đưa được daemon lên sau 20s — xem log: ${daemonLogPath()}`); process.exit(1); }
|
|
643
|
+
} else if (sub3 === 'stop') {
|
|
644
|
+
try { pretty(await rpc('shutdown')); console.log('đã gửi shutdown — daemon đóng storage rồi exit'); }
|
|
645
|
+
catch (e) { console.error('daemon không chạy (hoặc không tắt được):', e.message); process.exit(1); }
|
|
646
|
+
} else if (sub3 === 'status') {
|
|
647
|
+
console.log(await daemonAlive() ? 'daemon ĐANG chạy' : 'daemon KHÔNG chạy');
|
|
648
|
+
} else if (sub3 === 'log') {
|
|
649
|
+
try { console.log(await readFile(daemonLogPath(), 'utf8')); }
|
|
650
|
+
catch { console.error(`chưa có log CLI (${daemonLogPath()}) — systemd: journalctl --user -u bom-core`); }
|
|
651
|
+
} else {
|
|
652
|
+
console.error('usage: bom-pi daemon [start|stop|status|log]'); process.exit(64);
|
|
653
|
+
}
|
|
563
654
|
} else if (cmd === 'device') {
|
|
564
655
|
// bom-pi device rm <id>
|
|
565
656
|
const sub2 = process.argv[3];
|
|
@@ -643,7 +734,7 @@ WantedBy=default.target
|
|
|
643
734
|
} else if (cmd === 'shutdown') {
|
|
644
735
|
pretty(await rpc('shutdown'));
|
|
645
736
|
} else {
|
|
646
|
-
console.error('usage: bom-pi <hello|status|models|agents|sessions|new|use|submit|reset|transcript|approvals|approve|deny|schedules|schedule|unschedule|factory|limits|abort|watch|unpair|version> [--socket PATH] [--cid N] [--url http://host:8791] [--token bom_…]\n thiết bị khác: bom-pi pair <CODE> --url http://host:8791 — pair một lần, CLI tự nhớ (xem bom-pi version)');
|
|
737
|
+
console.error('usage: bom-pi <hello|status|models|agents|sessions|new|use|submit|reset|transcript|approvals|approve|deny|schedules|schedule|unschedule|factory|limits|abort|watch|unpair|daemon|version> [--socket PATH] [--cid N] [--url http://host:8791] [--token bom_…]\n thiết bị khác: bom-pi pair <CODE> --url http://host:8791 — pair một lần, CLI tự nhớ (xem bom-pi version)');
|
|
647
738
|
process.exit(64);
|
|
648
739
|
}
|
|
649
740
|
if (cmd !== 'watch') process.exit(0);
|