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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bom-pi",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "description": "bom × pi — hệ sinh thái durable cho pi trên nền pi-durable: core daemon + web UI pocket + CLI",
6
6
  "engines": {
@@ -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 { execFile } from 'node:child_process';
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à http://127.0.0.1:8790): ${e.message}`;
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
- daemon chưa chạy trên máy này. Mở terminal khác và chạy: bom-core
58
- (giữ cửa sổ mở — daemon HTTP loopback 127.0.0.1:8790; sau này sẽ có service tự khởi động)
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-core
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 { /* rơi xuống lỗi socket gốc cho đúng thông điệp */ }
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(); } catch { console.error(unreachable(new Error('no socket, no http.token'))); process.exit(1); }
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);