@usebitl/cli 0.1.14 → 0.1.15

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.
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code interrupt watcher. bitl-hook.mjs spawns it separately on each UserPromptSubmit (Claude Code doesn't wait).
3
+ //
4
+ // node bitl-cc-watch.mjs <session id> <transcript path> <cwd>
5
+ //
6
+ // Claude Code sends no hook when the user interrupts a turn, so the card would stay "running". Follow the transcript
7
+ // until the turn ends; if it was interrupted, send bitl:Interrupted (ends the turn on the board). Only the event goes
8
+ // out — no transcript content. Exits quietly on a normal end, after 2 hours, or on any error. Always exit 0.
9
+
10
+ import { sendRecord } from './send.mjs';
11
+
12
+ async function main() {
13
+ const [sessionId, transcriptPath, cwd] = process.argv.slice(2);
14
+ if (!sessionId || !transcriptPath) return;
15
+ const { watchClaudeTurn } = await import('./dist/core.mjs');
16
+ const mark = await watchClaudeTurn(transcriptPath);
17
+ if (mark?.mark !== 'interrupted') return;
18
+ await sendRecord({
19
+ source: 'claude-code',
20
+ receivedAt: mark.at ?? new Date().toISOString(),
21
+ payload: { hook_event_name: 'bitl:Interrupted', session_id: sessionId, transcript_path: transcriptPath, ...(cwd ? { cwd } : {}) },
22
+ });
23
+ }
24
+
25
+ main()
26
+ .catch(() => {})
27
+ .finally(() => process.exit(0));
@@ -56,6 +56,22 @@ function watchAgyPermission(payload) {
56
56
  }
57
57
  }
58
58
 
59
+ /**
60
+ * Claude Code sends no hook when the user interrupts a turn → on each prompt, spawn a watcher that reads the transcript
61
+ * until the turn ends and reports an interrupt (bitl-cc-watch.mjs). Only ids and the path are passed, never the prompt
62
+ */
63
+ function watchClaudeTurn(payload) {
64
+ try {
65
+ const { session_id: sessionId, transcript_path: transcriptPath, cwd } = payload;
66
+ if (typeof sessionId !== 'string' || typeof transcriptPath !== 'string') return;
67
+ const script = join(import.meta.dirname, 'bitl-cc-watch.mjs');
68
+ const args = [script, sessionId, transcriptPath, typeof cwd === 'string' ? cwd : ''];
69
+ spawn(process.execPath, args, { detached: true, stdio: 'ignore', windowsHide: true }).unref();
70
+ } catch {
71
+ // the hook carries on even if the watcher can't start
72
+ }
73
+ }
74
+
59
75
  /**
60
76
  * Claude Code agent view (`claude agents`): the daemon keeps pre-warmed sessions parked as spares (CLAUDE_BG_SOURCE=spare).
61
77
  * A parked spare fires SessionStart but gets no prompt until a task is dispatched to it, and is retired after an hour idle
@@ -84,6 +100,7 @@ async function main() {
84
100
  const event = argValue('--event');
85
101
  if (event && payload && typeof payload === 'object' && payload.hook_event_name === undefined) payload.hook_event_name = event;
86
102
  if (source === 'antigravity' && event === 'PreToolUse') watchAgyPermission(payload);
103
+ if (source === 'claude-code' && payload?.hook_event_name === 'UserPromptSubmit') watchClaudeTurn(payload);
87
104
  if (isParkedSpareStart(source, payload)) return;
88
105
  await sendRecord({ source, receivedAt, payload });
89
106
  }
@@ -153,6 +153,8 @@ function normalizeHook(source, raw, receivedAt) {
153
153
  }
154
154
  case "Stop":
155
155
  case "Interrupt":
156
+ // Codex: the in-progress turn was interrupted
157
+ case "bitl:Interrupted":
156
158
  type = "turn_end";
157
159
  text = str(raw.last_assistant_message);
158
160
  break;
@@ -469,7 +471,7 @@ function readCloudConfig(home = bitlHome()) {
469
471
  }
470
472
  }
471
473
  function clientVersionHeader() {
472
- const v = "0.1.14";
474
+ const v = "0.1.15";
473
475
  return v ? { "x-bitl-client": v } : {};
474
476
  }
475
477
  function resolveTarget(home = bitlHome()) {
@@ -815,6 +817,65 @@ async function requestRemoteApproval(payload, opts = {}) {
815
817
  return null;
816
818
  }
817
819
 
820
+ // ../../shared/src/node/claude-watch.ts
821
+ import { open as open2, stat as stat4 } from "node:fs/promises";
822
+ var INTERRUPTED = "[Request interrupted by user";
823
+ function claudeTurnMark(line) {
824
+ if (!line.includes(INTERRUPTED) && !line.includes('"turn_duration"')) return null;
825
+ let rec;
826
+ try {
827
+ rec = JSON.parse(line);
828
+ } catch {
829
+ return null;
830
+ }
831
+ if (rec.isSidechain) return null;
832
+ const at = typeof rec.timestamp === "string" ? rec.timestamp : void 0;
833
+ if (rec.type === "system" && rec.subtype === "turn_duration") return { mark: "ended", at };
834
+ if (rec.type !== "user" || rec.isMeta) return null;
835
+ const c = rec.message?.content;
836
+ const text = typeof c === "string" ? c : Array.isArray(c) && c[0]?.type === "text" ? String(c[0].text ?? "") : "";
837
+ return text.startsWith(INTERRUPTED) ? { mark: "interrupted", at } : null;
838
+ }
839
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
840
+ async function watchClaudeTurn(path, opts = {}) {
841
+ const pollMs = opts.pollMs ?? 1500;
842
+ const until = Date.now() + (opts.maxMs ?? 2 * 60 * 6e4);
843
+ let offset;
844
+ try {
845
+ offset = (await stat4(path)).size;
846
+ } catch {
847
+ return null;
848
+ }
849
+ let rest = "";
850
+ while (Date.now() < until) {
851
+ await sleep2(pollMs);
852
+ let size;
853
+ try {
854
+ size = (await stat4(path)).size;
855
+ } catch {
856
+ return null;
857
+ }
858
+ if (size < offset) return null;
859
+ if (size === offset) continue;
860
+ const file = await open2(path, "r");
861
+ try {
862
+ const buf = Buffer.alloc(size - offset);
863
+ await file.read(buf, 0, buf.length, offset);
864
+ offset = size;
865
+ const lines = (rest + buf.toString("utf8")).split("\n");
866
+ rest = lines.pop() ?? "";
867
+ for (const line of lines) {
868
+ const mark = claudeTurnMark(line);
869
+ if (mark?.mark === "interrupted") return mark;
870
+ if (mark?.mark === "ended") return null;
871
+ }
872
+ } finally {
873
+ await file.close();
874
+ }
875
+ }
876
+ return null;
877
+ }
878
+
818
879
  // ../../shared/src/node/index.ts
819
880
  async function prepareRaw(rec, opts = {}) {
820
881
  const filled = rec.source === "antigravity" && opts.enrich !== false ? { ...rec, payload: await fillAntigravityText(rec.payload) } : rec;
@@ -834,5 +895,6 @@ export {
834
895
  agyWatchStep,
835
896
  prepareForTarget,
836
897
  prepareRaw,
837
- requestRemoteApproval
898
+ requestRemoteApproval,
899
+ watchClaudeTurn
838
900
  };
@@ -20062,7 +20062,7 @@ function removeCloudLogin(home = bitlHome()) {
20062
20062
  rmSync(cloudKeyPath(home), { force: true });
20063
20063
  }
20064
20064
  function clientVersionHeader() {
20065
- const v = "0.1.14";
20065
+ const v = "0.1.15";
20066
20066
  return v ? { "x-bitl-client": v } : {};
20067
20067
  }
20068
20068
  function resolveTarget(home = bitlHome()) {
@@ -20119,7 +20119,7 @@ async function latestVersion(home, now) {
20119
20119
  return latest;
20120
20120
  }
20121
20121
  var updateCommand = (install = process.env.BITL_INSTALL) => install === "npx" ? "npx @usebitl/cli@latest start" : "npm i -g @usebitl/cli";
20122
- async function checkCliUpdate(current = "0.1.14", opts = {}) {
20122
+ async function checkCliUpdate(current = "0.1.15", opts = {}) {
20123
20123
  if (!current || process.env.BITL_NO_UPDATE_CHECK === "1") return void 0;
20124
20124
  const latest = await latestVersion(opts.home ?? bitlHome(), opts.now ?? Date.now());
20125
20125
  if (!latest || compareVersions(latest, current) <= 0) return void 0;
@@ -11,7 +11,7 @@ import {
11
11
  isEmptySession,
12
12
  parseWireRecord,
13
13
  sessionKey
14
- } from "./chunk-RIX6JB6S.mjs";
14
+ } from "./chunk-65TI66EM.mjs";
15
15
 
16
16
  // ../../../node_modules/.pnpm/bn.js@4.12.5/node_modules/bn.js/lib/bn.js
17
17
  var require_bn = __commonJS({
package/dist/cli.mjs CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  unwrapMasterKey,
22
22
  verifyPublicKey,
23
23
  wrapMasterKey
24
- } from "./chunks/chunk-RIX6JB6S.mjs";
24
+ } from "./chunks/chunk-65TI66EM.mjs";
25
25
 
26
26
  // ../src/index.ts
27
27
  import { homedir as homedir2 } from "node:os";
@@ -453,7 +453,7 @@ async function logout() {
453
453
  }
454
454
 
455
455
  // ../src/update.ts
456
- async function updateNotice(current = "0.1.14") {
456
+ async function updateNotice(current = "0.1.15") {
457
457
  const u = await checkCliUpdate(current);
458
458
  return u && `${t("update.available", { ...u })}
459
459
  ${t("update.changelog")}`;
@@ -609,7 +609,7 @@ async function main(argv) {
609
609
  if (String(typeof warning === "string" ? warning : warning.message).startsWith("SQLite is an experimental feature")) return;
610
610
  emitWarning(warning, ...rest2);
611
611
  });
612
- const { startServer } = await import("./chunks/src-WI4MJOSQ.mjs");
612
+ const { startServer } = await import("./chunks/src-TJPPU3TC.mjs");
613
613
  await startServer();
614
614
  console.log(t("start.pair", { url: localPairUrl() }));
615
615
  return;
@@ -631,7 +631,7 @@ async function main(argv) {
631
631
  case "--version":
632
632
  case "-v":
633
633
  case "version":
634
- console.log("0.1.14");
634
+ console.log("0.1.15");
635
635
  return;
636
636
  case "remote":
637
637
  case "away":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebitl/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "BITL — see which of your AI coding agents (Claude Code, Codex, Antigravity) are waiting for you. Local board or usebitl.com",
5
5
  "type": "module",
6
6
  "bin": {