@usebitl/cli 0.1.3 → 0.1.5

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.
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // Antigravity(agy) 승인 대기 감시. bitl-hook.mjs 가 PreToolUse 마다 따로 띄운다 (agy 는 기다리지 않는다).
2
+ // Antigravity (agy) approval-wait watcher. bitl-hook.mjs spawns it separately on each PreToolUse (agy doesn't wait).
3
3
  //
4
- // node bitl-agy-watch.mjs <PreToolUse 페이로드를 담은 임시 파일>
4
+ // node bitl-agy-watch.mjs <temp file holding the PreToolUse payload>
5
5
  //
6
- // agy hook 에는 승인 대기 이벤트가 없다 → 대화 DB 의 그 단계(stepIdx) 상태를 잠깐 지켜본다:
7
- // 승인 대기가 되면 bitl:PermissionWaiting, 승인·거부·취소되면 bitl:PermissionResolved 를 보낸다.
8
- // 그 전에 끝나면(대부분의 도구) 아무것도 보내지 않고 끝난다.
9
- // node:sqlite 가 없는 Node(22.5 미만)·DB 가 없는 환경(IDE 등)에서는 조용히 끝난다. 항상 exit 0.
6
+ // agy hooks have no approval-wait event → briefly watch that step's (stepIdx) status in the conversation DB:
7
+ // when it starts waiting for approval, send bitl:PermissionWaiting; when approved/denied/cancelled, bitl:PermissionResolved.
8
+ // If it finishes before that (most tools), send nothing and exit.
9
+ // On Node without node:sqlite (< 22.5) or without the DB (IDE etc.), exit quietly. Always exit 0.
10
10
 
11
11
  import { readFileSync, unlinkSync } from 'node:fs';
12
12
  import { homedir } from 'node:os';
@@ -14,8 +14,8 @@ import { join } from 'node:path';
14
14
  import { sendRecord } from './send.mjs';
15
15
 
16
16
  const POLL_MS = 400;
17
- const MAX_MS = 10 * 60_000; // 승인을 이보다 오래 미루면 더 지켜보지 않는다
18
- const NO_STEP_MS = 15_000; // 단계가 DB 에 나타나기를 기다리는 시간
17
+ const MAX_MS = 10 * 60_000; // stop watching if approval is put off longer than this
18
+ const NO_STEP_MS = 15_000; // how long to wait for the step to appear in the DB
19
19
 
20
20
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21
21
 
@@ -29,7 +29,7 @@ async function main() {
29
29
  try {
30
30
  unlinkSync(file);
31
31
  } catch {
32
- // 이미 없음
32
+ // already gone
33
33
  }
34
34
  }
35
35
  const { conversationId, stepIdx } = payload ?? {};
@@ -39,7 +39,7 @@ async function main() {
39
39
  try {
40
40
  ({ DatabaseSync } = await import('node:sqlite'));
41
41
  } catch {
42
- return; // 오래된 Node
42
+ return; // old Node
43
43
  }
44
44
  const { agyWatchStep } = await import('./dist/core.mjs');
45
45
  const dbPath = join(homedir(), '.gemini', 'antigravity-cli', 'conversations', `${conversationId}.db`);
@@ -48,7 +48,7 @@ async function main() {
48
48
  try {
49
49
  db = new DatabaseSync(dbPath, { readOnly: true });
50
50
  } catch {
51
- return; // DB 없음 (IDE 등)
51
+ return; // no DB (IDE etc.)
52
52
  }
53
53
  const query = db.prepare('SELECT status FROM steps WHERE idx = ?');
54
54
  const send = (event) => sendRecord({ source: 'antigravity', payload: { ...payload, hook_event_name: event } });
@@ -61,7 +61,7 @@ async function main() {
61
61
  try {
62
62
  status = query.get(stepIdx)?.status;
63
63
  } catch {
64
- // agy 가 쓰는 중 (잠김) — 다음에 다시
64
+ // agy is writing (locked) — try again next time
65
65
  }
66
66
  if (status === undefined && !waited && Date.now() - started > NO_STEP_MS) return;
67
67
  const action = agyWatchStep(status, waited);
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // 원격 승인 hook (Claude Code PermissionRequest, 동기). bitl install 이 bitl-hook.mjs 와 함께 설치한다.
2
+ // Remote approval hook (Claude Code PermissionRequest, synchronous). bitl install installs it alongside bitl-hook.mjs.
3
3
  //
4
- // node bitl-approve.mjs [--source claude-code] (페이로드는 stdin)
4
+ // node bitl-approve.mjs [--source claude-code] (payload on stdin)
5
5
  //
6
- // 원격 승인이 켜졌을 때만 (보드의 "원격 승인" 스위치 또는 bitl remote on) 폰의 허용·거부를 기다린다 (기본 10분, $BITL_APPROVAL_WAIT_SEC).
7
- // 그 밖에는 아무것도 출력하지 않고 바로 끝나 → Claude Code 가 평소처럼 터미널에 승인 창을 띄운다.
8
- // 폰의 결정은 계정 키 서명을 확인한 뒤에만 따른다 (서버가 승인을 만들어 낼 수 없다). 실패는 모두 "터미널에서 승인"으로.
6
+ // Only when remote approval is on (the board's "remote approval" switch or bitl remote on) does it wait for allow/deny from the phone (default 10 min, $BITL_APPROVAL_WAIT_SEC).
7
+ // Otherwise it prints nothing and exits right away → Claude Code shows its approval prompt in the terminal as usual.
8
+ // The phone's decision is followed only after verifying the account key signature (the server can't forge an approval). Every failure falls back to "approve in the terminal".
9
9
  import { appendFileSync, mkdirSync, statSync } from 'node:fs';
10
10
  import { homedir } from 'node:os';
11
11
  import { join } from 'node:path';
@@ -57,5 +57,5 @@ async function main() {
57
57
  }
58
58
 
59
59
  main()
60
- .catch((err) => log(`원격 승인: ${err?.stack ?? err}`))
60
+ .catch((err) => log(`remote approval:${err?.stack ?? err}`))
61
61
  .finally(() => process.exit(0));
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- // 에이전트 hook/notify 페이로드를 bitl-server 로 전달한다.
2
+ // Forwards agent hook/notify payloads to bitl-server.
3
3
  //
4
- // Claude Code / Codex hooks : node bitl-hook.mjs --source claude-code|codex (페이로드는 stdin)
5
- // Codex notify : node bitl-hook.mjs --source codex-notify (페이로드는 마지막 argv)
6
- // Antigravity hooks : node bitl-hook.mjs --source antigravity --event PreToolUse (이벤트 이름이 페이로드에 없다)
4
+ // Claude Code / Codex hooks : node bitl-hook.mjs --source claude-code|codex (payload on stdin)
5
+ // Codex notify : node bitl-hook.mjs --source codex-notify (payload is the last argv)
6
+ // Antigravity hooks : node bitl-hook.mjs --source antigravity --event PreToolUse (the payload has no event name)
7
7
  //
8
- // 서버: $BITL_URL (기본 http://127.0.0.1:$BITL_PORT, 7777)
9
- // 서버가 꺼져 있으면 $BITL_HOME/queue.jsonl (기본 ~/.bitl) 에 (암호화한 채로) 쌓아두고, 서버가 시작할 때 처리한다.
8
+ // Server: $BITL_URL (default http://127.0.0.1:$BITL_PORT, 7777)
9
+ // If the server is down, records pile up (encrypted) in $BITL_HOME/queue.jsonl (default ~/.bitl) and are processed when the server starts.
10
10
  //
11
- // 원칙: 에이전트를 절대 방해하지 않는다 — stdout 무출력, 항상 exit 0, 짧은 타임아웃.
12
- // 해석(정규화)·최소화·암호화는 이 PC 에서 한다 (send.mjs → dist/core.mjs). 서버는 내용을 읽지 못한다.
13
- // 규칙이 바뀌면 번들만 다시 빌드하면 된다 (pnpm build). 설치 경로가 그대로라 재설치는 필요 없다.
11
+ // Principle: never get in the agent's way — no stdout, always exit 0, short timeouts.
12
+ // Parsing (normalization), minimization and encryption happen on this PC (send.mjs → dist/core.mjs). The server can't read the content.
13
+ // When the rules change, just rebuild the bundle (pnpm build). The install path stays the same, so no reinstall is needed.
14
14
 
15
15
  import { spawn } from 'node:child_process';
16
16
  import { writeFileSync } from 'node:fs';
@@ -43,7 +43,7 @@ function readStdin() {
43
43
  });
44
44
  }
45
45
 
46
- /** agy 는 승인 대기를 hook 으로 알리지 않는다 → 그 단계를 지켜볼 프로세스를 따로 띄운다 (bitl-agy-watch.mjs) */
46
+ /** agy doesn't report approval waits via hooks → spawn a separate process to watch that step (bitl-agy-watch.mjs) */
47
47
  function watchAgyPermission(payload) {
48
48
  try {
49
49
  if (typeof payload.stepIdx !== 'number') return;
@@ -52,12 +52,12 @@ function watchAgyPermission(payload) {
52
52
  const script = join(import.meta.dirname, 'bitl-agy-watch.mjs');
53
53
  spawn(process.execPath, [script, file], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
54
54
  } catch {
55
- // 감시를 못 띄워도 hook 은 계속
55
+ // the hook carries on even if the watcher can't start
56
56
  }
57
57
  }
58
58
 
59
59
  async function main() {
60
- const receivedAt = new Date().toISOString(); // hook 이 실행된 시각 (서버 도착 시각이 아님)
60
+ const receivedAt = new Date().toISOString(); // when the hook ran (not when it reached the server)
61
61
  const source = argValue('--source') ?? 'claude-code';
62
62
 
63
63
  const lastArg = process.argv[process.argv.length - 1];
@@ -66,9 +66,9 @@ async function main() {
66
66
  try {
67
67
  payload = JSON.parse(raw);
68
68
  } catch {
69
- return; // 해석할 수 없는 입력은 버린다
69
+ return; // drop input we can't parse
70
70
  }
71
- // Antigravity 는 페이로드에 이벤트 이름이 없어 --event 로 받는다
71
+ // Antigravity payloads have no event name, so it comes via --event
72
72
  const event = argValue('--event');
73
73
  if (event && payload && typeof payload === 'object' && payload.hook_event_name === undefined) payload.hook_event_name = event;
74
74
  if (source === 'antigravity' && event === 'PreToolUse') watchAgyPermission(payload);
@@ -19,7 +19,7 @@ function fromBase64Url(s) {
19
19
  var generateMasterKey = () => globalThis.crypto.getRandomValues(new Uint8Array(MASTER_KEY_BYTES));
20
20
  var info = (name) => ({ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: utf8.encode(`bitl/v1/${name}`) });
21
21
  async function deriveKeys(master) {
22
- if (master.length !== MASTER_KEY_BYTES) throw new Error(`\uD0A4 \uAE38\uC774\uAC00 ${MASTER_KEY_BYTES}\uBC14\uC774\uD2B8\uAC00 \uC544\uB2D9\uB2C8\uB2E4`);
22
+ if (master.length !== MASTER_KEY_BYTES) throw new Error(`key length is not ${MASTER_KEY_BYTES} bytes`);
23
23
  const base = await subtle().importKey("raw", new Uint8Array(master), "HKDF", false, ["deriveKey", "deriveBits"]);
24
24
  const [enc, tag2, kidBits] = await Promise.all([
25
25
  subtle().deriveKey(info("enc"), base, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]),
@@ -71,7 +71,7 @@ function summarizeToolInput(input) {
71
71
  const i = input;
72
72
  const first = (list, key) => Array.isArray(list) ? str(list[0]?.[key]) : void 0;
73
73
  const pick = commandText(i.command) ?? str(i.file_path) ?? str(i.path) ?? str(i.pattern) ?? str(i.url) ?? str(i.query) ?? str(i.description) ?? str(i.prompt) ?? first(i.questions, "question") ?? str(i.plan) ?? // ExitPlanMode: { plan }
74
- // Codex webrun: { search_query: [{ q }] } 또는 { weather: [{ location }] }
74
+ // Codex webrun: { search_query: [{ q }] } or { weather: [{ location }] }
75
75
  first(i.search_query, "q") ?? first(i.weather, "location");
76
76
  return pick ? oneLine(pick) : void 0;
77
77
  }
@@ -243,7 +243,7 @@ function normalizeAntigravity(raw, receivedAt) {
243
243
  type = error ? "error" : "turn_end";
244
244
  text = error ?? str(raw.last_assistant_message);
245
245
  break;
246
- // 아래 둘은 agy 가 아니라 bitl-agy-watch.mjs 가 대화 DB 의 단계 상태를 보고 만든다
246
+ // The two below come not from agy but from bitl-agy-watch.mjs, based on step state in the conversation DB
247
247
  case "bitl:PermissionWaiting":
248
248
  type = "waiting";
249
249
  waitingReason = "permission";
@@ -292,7 +292,7 @@ function normalizeReport(source, raw, receivedAt) {
292
292
  nativeType: `report:${event}`,
293
293
  at: receivedAt,
294
294
  cwd: str(raw.cwd),
295
- // start 는 제목이 곧 지시 내용, 나머지는 상세 설명
295
+ // For start the title is the instruction itself; for the rest it's the detail
296
296
  text: event === "start" ? title ?? detail : detail,
297
297
  request: str(raw.request),
298
298
  waitingReason: event === "waiting" ? "input" : void 0,
@@ -466,13 +466,17 @@ function readCloudConfig(home = bitlHome()) {
466
466
  return null;
467
467
  }
468
468
  }
469
+ function clientVersionHeader() {
470
+ const v = "0.1.5";
471
+ return v ? { "x-bitl-client": v } : {};
472
+ }
469
473
  function resolveTarget(home = bitlHome()) {
470
474
  const cloud = readCloudConfig(home);
471
475
  if (cloud) {
472
476
  return {
473
477
  kind: "cloud",
474
478
  url: cloud.url.replace(/\/+$/, ""),
475
- headers: { authorization: `Bearer ${cloud.apiKey}` },
479
+ headers: { authorization: `Bearer ${cloud.apiKey}`, ...clientVersionHeader() },
476
480
  keys: () => keysFromBase64Url(readFileSync2(cloudKeyPath(home), "utf8"))
477
481
  };
478
482
  }
@@ -576,7 +580,7 @@ function describeToolRequest(toolName, input, extra = {}) {
576
580
  if (typeof i.command === "string") detail = i.command;
577
581
  else if (typeof i.file_path === "string") {
578
582
  const body = str2(i.content) ?? (typeof i.new_string === "string" ? `- ${oneLine(String(i.old_string ?? ""), 300)}
579
- + ${i.new_string}` : void 0) ?? (Array.isArray(i.edits) ? `${i.edits.length}\uACF3 \uC218\uC815` : void 0);
583
+ + ${i.new_string}` : void 0) ?? (Array.isArray(i.edits) ? `${i.edits.length} edits` : void 0);
580
584
  detail = body ? `${i.file_path}
581
585
 
582
586
  ${body}` : i.file_path;
@@ -621,7 +625,7 @@ async function verifyDecision(keys, id, decision, sig) {
621
625
  var DEFAULT_APPROVAL_WAIT_MS = 10 * 6e4;
622
626
  var SKIP_TOOLS = /* @__PURE__ */ new Set(["AskUserQuestion"]);
623
627
  var POLL_MS = 25e3;
624
- var DENY_MESSAGE = "\uC0AC\uC6A9\uC790\uAC00 \uD734\uB300\uD3F0(BITL \uC6D0\uACA9 \uC2B9\uC778)\uC5D0\uC11C \uAC70\uBD80\uD588\uC2B5\uB2C8\uB2E4.";
628
+ var DENY_MESSAGE = "The user denied this from their phone (BITL remote approval).";
625
629
  var sleep = (ms, signal) => new Promise((resolve) => {
626
630
  const t = setTimeout(resolve, ms);
627
631
  signal?.addEventListener("abort", () => (clearTimeout(t), resolve()), { once: true });
@@ -646,7 +650,7 @@ async function requestRemoteApproval(payload, opts = {}) {
646
650
  try {
647
651
  if ((await api("/api/approvals/mode", { timeoutMs: 2e3 })).away !== true) return null;
648
652
  } catch (err) {
649
- log(`\uC6D0\uACA9 \uC2B9\uC778: \uC11C\uBC84\uC5D0 \uBB3C\uC744 \uC218 \uC5C6\uC5B4 \uD130\uBBF8\uB110\uC5D0\uC11C \uC2B9\uC778\uD569\uB2C8\uB2E4 (${err.message})`);
653
+ log(`remote approval: can't reach the server, approving in the terminal (${err.message})`);
650
654
  return null;
651
655
  }
652
656
  const keys = await target.keys();
@@ -663,7 +667,7 @@ async function requestRemoteApproval(payload, opts = {}) {
663
667
  timeoutMs: 5e3
664
668
  });
665
669
  } catch (err) {
666
- log(`\uC6D0\uACA9 \uC2B9\uC778: \uC694\uCCAD\uC744 \uC62C\uB9AC\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 (${err.message})`);
670
+ log(`remote approval: failed to upload the request (${err.message})`);
667
671
  return null;
668
672
  }
669
673
  const deadline = Date.now() + waitMs;
@@ -681,7 +685,7 @@ async function requestRemoteApproval(payload, opts = {}) {
681
685
  if (status === "pending") continue;
682
686
  if (status === "allow" || status === "deny") {
683
687
  if (!await verifyDecision(keys, id, status, typeof a.sig === "string" ? a.sig : "")) {
684
- log(`\uC6D0\uACA9 \uC2B9\uC778: \uC11C\uBA85\uC774 \uB9DE\uC9C0 \uC54A\uB294 \uACB0\uC815\uC744 \uBC1B\uC558\uC2B5\uB2C8\uB2E4 (${id}) \u2014 \uB530\uB974\uC9C0 \uC54A\uACE0 \uD130\uBBF8\uB110\uC5D0\uC11C \uC2B9\uC778\uD569\uB2C8\uB2E4`);
688
+ log(`remote approval: received a decision with an invalid signature (${id}) \u2014 ignoring it and approving in the terminal`);
685
689
  return null;
686
690
  }
687
691
  return status === "allow" ? { behavior: "allow" } : { behavior: "deny", message: DENY_MESSAGE };
package/adapters/send.mjs CHANGED
@@ -1,34 +1,34 @@
1
- // bitl-hook.mjs · bitl-mcp.mjs 공용: 원본을 정규화·최소화·암호화해 보내고, 실패하면 queue.jsonl 에 쌓는다.
2
- // 보낼 곳: bitl login 으로 클라우드 계정에 연결했으면 그 계정(기기 API 키), 아니면 이 PC 의 로컬 서버.
3
- // 서버에는 신호(상태 판단용)와 암호문만 간다.
1
+ // Shared by bitl-hook.mjs and bitl-mcp.mjs: normalizes, minimizes and encrypts the raw payload and sends it; on failure, appends to queue.jsonl.
2
+ // Destination: the cloud account (device API key) if linked via bitl login, otherwise the local server on this PC.
3
+ // Only signals (for status detection) and ciphertext reach the server.
4
4
  import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
 
8
8
  const HOME = process.env.BITL_HOME?.trim() || join(homedir(), '.bitl');
9
9
  const POST_TIMEOUT_MS = 1500;
10
- const MAX_QUEUE_BYTES = 20 * 1024 * 1024; // 서버를 오래 못 만나도 디스크를 채우지 않도록
10
+ const MAX_QUEUE_BYTES = 20 * 1024 * 1024; // don't fill the disk even if the server stays unreachable for long
11
11
  const MAX_ERROR_LOG_BYTES = 1024 * 1024;
12
- const FLUSH_LIMIT = 500; // 한 번에 다시 보낼 최대 줄 수 (hook 을 오래 붙잡지 않게)
12
+ const FLUSH_LIMIT = 500; // max lines to resend at once (so the hook isn't held up for long)
13
13
  const QUEUE = join(HOME, 'queue.jsonl');
14
14
 
15
- // 정규화·암호화 로직은 shared(TypeScript)를 묶은 번들 (pnpm install / pnpm build 가 만든다)
15
+ // normalization/encryption logic is a bundle of shared (TypeScript) (built by pnpm install / pnpm build)
16
16
  let core;
17
17
  const loadCore = () => (core ??= import('./dist/core.mjs'));
18
18
 
19
- /** 에이전트 화면에는 아무것도 쓰지 않는다 — 문제는 여기에만 남긴다 */
19
+ /** Never write to the agent's screen — problems are logged only here */
20
20
  function logError(err) {
21
21
  try {
22
22
  const file = join(HOME, 'adapter-error.log');
23
23
  try {
24
24
  if (statSync(file).size > MAX_ERROR_LOG_BYTES) return;
25
25
  } catch {
26
- // 파일 없음
26
+ // no file
27
27
  }
28
28
  mkdirSync(HOME, { recursive: true });
29
29
  appendFileSync(file, `${new Date().toISOString()} ${err?.stack ?? err}\n`, 'utf8');
30
30
  } catch {
31
- // 기록도 못 하면 조용히 넘어간다
31
+ // if even logging fails, move on quietly
32
32
  }
33
33
  }
34
34
 
@@ -40,10 +40,10 @@ async function post(target, body) {
40
40
  body,
41
41
  signal: AbortSignal.timeout(POST_TIMEOUT_MS),
42
42
  });
43
- if (res.status === 401) logError(`${target.url}: 401 — 이 PC 의 연결이 끊겼습니다. bitl login 으로 다시 연결하세요`);
43
+ if (res.status === 401) logError(`${target.url}: 401 — this PC is no longer linked. Run bitl login to reconnect`);
44
44
  return res.ok;
45
45
  } catch {
46
- return false; // 서버 꺼짐 / 네트워크 / 타임아웃
46
+ return false; // server down / network / timeout
47
47
  }
48
48
  }
49
49
 
@@ -51,14 +51,14 @@ function enqueue(lines) {
51
51
  try {
52
52
  if (statSync(QUEUE).size > MAX_QUEUE_BYTES) return false;
53
53
  } catch {
54
- // 큐 파일 없음
54
+ // no queue file
55
55
  }
56
56
  mkdirSync(HOME, { recursive: true });
57
57
  appendFileSync(QUEUE, lines.map((l) => l + '\n').join(''), 'utf8');
58
58
  return true;
59
59
  }
60
60
 
61
- /** 이 키로 봉인된 레코드인지 (연결을 바꾸기 전에 쌓인 것은 새 계정으로 보내지 않는다) */
61
+ /** Whether the record is sealed with this key (anything queued before the link changed is not sent to the new account) */
62
62
  function sealedWith(line, kid) {
63
63
  try {
64
64
  return Object.values(JSON.parse(line).content ?? {}).every((v) => typeof v === 'string' && v.startsWith(`v1.${kid}.`));
@@ -68,8 +68,8 @@ function sealedWith(line, kid) {
68
68
  }
69
69
 
70
70
  /**
71
- * 클라우드로 보냈으면 쌓아 둔 이벤트도 보낸다 (로컬 모드는 로컬 서버가 시작할 때 큐를 읽는다).
72
- * 다른 hook 과 겹치지 않게 파일을 먼저 옮기고, 못 보낸 줄은 다시 쌓는다.
71
+ * After a successful send to the cloud, also send queued events (in local mode the local server reads the queue on start).
72
+ * Move the file first so other hooks don't overlap, and re-queue lines that couldn't be sent.
73
73
  */
74
74
  async function flushQueue(target, kid) {
75
75
  if (!existsSync(QUEUE)) return;
@@ -77,14 +77,14 @@ async function flushQueue(target, kid) {
77
77
  try {
78
78
  renameSync(QUEUE, processing);
79
79
  } catch {
80
- return; // 다른 hook 이 먼저 가져감
80
+ return; // another hook took it first
81
81
  }
82
82
  const lines = readFileSync(processing, 'utf8').split('\n').filter(Boolean);
83
83
  unlinkSync(processing);
84
84
  const later = [];
85
85
  let failed = false;
86
86
  for (const [i, line] of lines.entries()) {
87
- if (!sealedWith(line, kid)) continue; // 다른 키로 봉인됐거나 옛 형식 — 버린다
87
+ if (!sealedWith(line, kid)) continue; // sealed with another key or old format — drop it
88
88
  if (failed || i >= FLUSH_LIMIT || !(await post(target, line))) {
89
89
  failed = true;
90
90
  later.push(line);
@@ -99,10 +99,10 @@ export async function sendRecord({ source, receivedAt = new Date().toISOString()
99
99
  try {
100
100
  prepared = await (await loadCore()).prepareForTarget({ source, receivedAt, payload }, HOME);
101
101
  } catch (err) {
102
- logError(err); // 번들이 없거나(빌드 전) 키를 못 읽음
102
+ logError(err); // no bundle (not built yet) or key unreadable
103
103
  return 'dropped';
104
104
  }
105
- if (!prepared.record) return 'dropped'; // 해석할 수 없는 이벤트
105
+ if (!prepared.record) return 'dropped'; // unparseable event
106
106
  const body = JSON.stringify(prepared.record);
107
107
  if (await post(prepared, body)) {
108
108
  if (prepared.kind === 'cloud') await flushQueue(prepared, prepared.kid).catch(logError);
package/bin/bitl.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // bitl 명령 (npm 패키지 @usebitl/cli) 진입점.
3
- // hook 이 실행할 어댑터를 ~/.bitl/adapters 에 복사해 두고(버전이 바뀌었을 때만) 그 경로로 CLI 를 실행한다.
4
- // npm 전역 폴더는 Node 버전(nvm 등)에 따라 바뀌어서, hook 이 그 경로를 직접 가리키면 조용히 끊길 수 있다.
5
- // node bin/bitl.mjs __sync — 복사만 (npm postinstall). 실패해도 설치는 막지 않는다
2
+ // Entry point for the bitl command (npm package @usebitl/cli).
3
+ // Copies the adapters that hooks run into ~/.bitl/adapters (only when the version changed) and runs the CLI with that path.
4
+ // The global npm folder changes with the Node version (nvm etc.), so hooks pointing at it directly can silently break.
5
+ // node bin/bitl.mjs __sync — copy only (npm postinstall). A failure does not block the install
6
6
  import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
7
7
  import { homedir } from 'node:os';
8
8
  import { join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
9
10
 
10
11
  const syncOnly = process.argv[2] === '__sync';
11
12
  const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
@@ -21,10 +22,12 @@ try {
21
22
  }
22
23
  } catch (err) {
23
24
  if (syncOnly) process.exit(0);
24
- console.error(`bitl: ${dir} 에 어댑터를 복사하지 못했습니다 — ${err?.message ?? err}`);
25
+ console.error(`bitl: could not copy adapters to ${dir} — ${err?.message ?? err}`);
25
26
  process.exit(1);
26
27
  }
27
28
  if (syncOnly) process.exit(0);
28
29
 
29
30
  process.env.BITL_ADAPTER_DIR = dir;
31
+ // Web UI that bitl start serves (built into the package)
32
+ process.env.BITL_WEB_DIST ??= fileURLToPath(new URL('../web/', import.meta.url));
30
33
  await import('../dist/cli.mjs');