@usebitl/cli 0.1.13 → 0.1.14

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.
@@ -326,7 +326,8 @@ function activityOf(e) {
326
326
  const base = e.tool.summary ? `${e.tool.name}: ${e.tool.summary}` : e.tool.name;
327
327
  return e.agentId && e.agentType ? `[${e.agentType}] ${base}` : base;
328
328
  }
329
- function splitEvent(e, extra = {}) {
329
+ function splitEvent(e, enrichment = {}) {
330
+ const { resumedFrom, ...extra } = enrichment;
330
331
  const newCwd = e.nativeType === "CwdChanged" && typeof e.raw.new_cwd === "string" ? e.raw.new_cwd : void 0;
331
332
  const signal = {
332
333
  source: e.source,
@@ -338,7 +339,8 @@ function splitEvent(e, extra = {}) {
338
339
  agentId: e.agentId,
339
340
  agentNamed: e.agentId || e.agentType !== void 0 ? Boolean(e.agentType) : void 0,
340
341
  waitingReason: e.waitingReason,
341
- toolFailed: e.tool?.failed || void 0
342
+ toolFailed: e.tool?.failed || void 0,
343
+ resumedFrom
342
344
  };
343
345
  const content = {
344
346
  title: e.type === "prompt" && e.text ? oneLine(e.text, TITLE_LIMIT) : void 0,
@@ -467,7 +469,7 @@ function readCloudConfig(home = bitlHome()) {
467
469
  }
468
470
  }
469
471
  function clientVersionHeader() {
470
- const v = "0.1.13";
472
+ const v = "0.1.14";
471
473
  return v ? { "x-bitl-client": v } : {};
472
474
  }
473
475
  function resolveTarget(home = bitlHome()) {
@@ -490,11 +492,73 @@ function resolveTarget(home = bitlHome()) {
490
492
 
491
493
  // ../../shared/src/node/enrich.ts
492
494
  import { execFile } from "node:child_process";
493
- import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
495
+ import { readFile as readFile3, stat as stat3 } from "node:fs/promises";
494
496
  import { homedir as homedir2 } from "node:os";
495
- import { basename, join as join3 } from "node:path";
496
- var BRANCH_TTL_MS = 6e4;
497
+ import { basename as basename2, join as join4 } from "node:path";
498
+
499
+ // ../../shared/src/node/resume.ts
500
+ import { readdir, readFile as readFile2, stat as stat2, writeFile } from "node:fs/promises";
501
+ import { basename, dirname as dirname3, join as join3 } from "node:path";
497
502
  var MAX_TRANSCRIPT_BYTES2 = 20 * 1024 * 1024;
503
+ var MAX_CANDIDATES = 200;
504
+ var CACHE_FILE = "resume-links.json";
505
+ var CACHE_LIMIT = 200;
506
+ var UUID = /"uuid":"([0-9a-f-]{36})"/g;
507
+ var messageIds = (text) => [...text.matchAll(UUID)].map((m) => m[1]);
508
+ async function findResumedFrom(transcriptPath) {
509
+ const own = basename(transcriptPath, ".jsonl");
510
+ if ((await stat2(transcriptPath)).size > MAX_TRANSCRIPT_BYTES2) return null;
511
+ const ids = messageIds(await readFile2(transcriptPath, "utf8"));
512
+ if (ids.length === 0) return void 0;
513
+ const dir = dirname3(transcriptPath);
514
+ const candidates = [];
515
+ const names = (await readdir(dir)).filter((n) => n.endsWith(".jsonl") && basename(n, ".jsonl") !== own);
516
+ for (const name of names.slice(0, MAX_CANDIDATES)) {
517
+ try {
518
+ const file = join3(dir, name);
519
+ const info2 = await stat2(file);
520
+ if (info2.size > MAX_TRANSCRIPT_BYTES2) continue;
521
+ const list = messageIds(await readFile2(file, "utf8"));
522
+ if (list.length) candidates.push({ id: basename(name, ".jsonl"), ids: new Set(list), last: list.at(-1), mtime: info2.mtimeMs });
523
+ } catch {
524
+ }
525
+ }
526
+ const ourLast = ids.at(-1);
527
+ const earlier = candidates.filter((c) => !(c.ids.has(ourLast) && c.last !== ourLast));
528
+ for (let i = ids.length - 1; i >= 0; i--) {
529
+ const id = ids[i];
530
+ const holders = earlier.filter((c) => c.ids.has(id));
531
+ if (!holders.length) continue;
532
+ const ended = holders.find((c) => c.last === id);
533
+ return (ended ?? holders.sort((a, b) => b.mtime - a.mtime)[0]).id;
534
+ }
535
+ return null;
536
+ }
537
+ async function readCache(home) {
538
+ try {
539
+ return JSON.parse(await readFile2(join3(home, CACHE_FILE), "utf8"));
540
+ } catch {
541
+ return {};
542
+ }
543
+ }
544
+ async function resumedFromFor(e, home) {
545
+ if (e.source !== "claude-code" || !e.transcriptPath) return void 0;
546
+ const cache = await readCache(home);
547
+ const hit = cache[e.sessionId];
548
+ if (hit && !hit.pending) return hit.from;
549
+ const resumed = e.nativeType === "SessionStart" && e.raw.source === "resume";
550
+ if (!resumed && !(hit?.pending && e.type === "prompt")) return void 0;
551
+ const found = await findResumedFrom(e.transcriptPath).catch(() => null);
552
+ cache[e.sessionId] = found === void 0 ? { pending: true, at: e.at } : { ...found ? { from: found } : {}, at: e.at };
553
+ const kept = Object.entries(cache).sort((a, b) => b[1].at.localeCompare(a[1].at)).slice(0, CACHE_LIMIT);
554
+ await writeFile(join3(home, CACHE_FILE), JSON.stringify(Object.fromEntries(kept))).catch(() => {
555
+ });
556
+ return found ?? void 0;
557
+ }
558
+
559
+ // ../../shared/src/node/enrich.ts
560
+ var BRANCH_TTL_MS = 6e4;
561
+ var MAX_TRANSCRIPT_BYTES3 = 20 * 1024 * 1024;
498
562
  var branchCache = /* @__PURE__ */ new Map();
499
563
  function gitBranch(cwd) {
500
564
  const hit = branchCache.get(cwd);
@@ -509,8 +573,8 @@ function gitBranch(cwd) {
509
573
  }
510
574
  async function titleFromTranscript(path) {
511
575
  try {
512
- if ((await stat2(path)).size > MAX_TRANSCRIPT_BYTES2) return void 0;
513
- const text = await readFile2(path, "utf8");
576
+ if ((await stat3(path)).size > MAX_TRANSCRIPT_BYTES3) return void 0;
577
+ const text = await readFile3(path, "utf8");
514
578
  let aiTitle;
515
579
  let customTitle;
516
580
  for (const line of text.split("\n")) {
@@ -527,12 +591,12 @@ async function titleFromTranscript(path) {
527
591
  return void 0;
528
592
  }
529
593
  }
530
- async function codexThreadName(threadId, codexHome = process.env.CODEX_HOME?.trim() || join3(homedir2(), ".codex")) {
594
+ async function codexThreadName(threadId, codexHome = process.env.CODEX_HOME?.trim() || join4(homedir2(), ".codex")) {
531
595
  try {
532
- const file = join3(codexHome, "session_index.jsonl");
533
- if ((await stat2(file)).size > MAX_TRANSCRIPT_BYTES2) return void 0;
596
+ const file = join4(codexHome, "session_index.jsonl");
597
+ if ((await stat3(file)).size > MAX_TRANSCRIPT_BYTES3) return void 0;
534
598
  let name;
535
- for (const line of (await readFile2(file, "utf8")).split("\n")) {
599
+ for (const line of (await readFile3(file, "utf8")).split("\n")) {
536
600
  if (!line.includes(threadId)) continue;
537
601
  try {
538
602
  const rec = JSON.parse(line);
@@ -557,9 +621,9 @@ async function importSqliteQuietly() {
557
621
  process.emitWarning = emitWarning;
558
622
  }
559
623
  }
560
- async function antigravityTitle(conversationId, dbPath = join3(homedir2(), ".gemini", "antigravity-cli", "conversation_summaries.db")) {
624
+ async function antigravityTitle(conversationId, dbPath = join4(homedir2(), ".gemini", "antigravity-cli", "conversation_summaries.db")) {
561
625
  try {
562
- await stat2(dbPath);
626
+ await stat3(dbPath);
563
627
  const { DatabaseSync } = await importSqliteQuietly();
564
628
  const db = new DatabaseSync(dbPath, { readOnly: true });
565
629
  try {
@@ -586,9 +650,9 @@ function gitRoot(cwd) {
586
650
  var rootCache = /* @__PURE__ */ new Map();
587
651
  async function projectName(cwd) {
588
652
  const root = await gitRoot(cwd);
589
- return basename((root ?? cwd).replace(/[\\/]+$/, ""));
653
+ return basename2((root ?? cwd).replace(/[\\/]+$/, ""));
590
654
  }
591
- async function enrichEvent(e) {
655
+ async function enrichEvent(e, home = bitlHome()) {
592
656
  const extra = {};
593
657
  const newCwd = e.nativeType === "CwdChanged" && typeof e.raw.new_cwd === "string" ? e.raw.new_cwd : void 0;
594
658
  const cwd = newCwd ?? e.cwd;
@@ -610,6 +674,11 @@ async function enrichEvent(e) {
610
674
  })
611
675
  );
612
676
  }
677
+ tasks.push(
678
+ resumedFromFor(e, home).then((from) => {
679
+ if (from) extra.resumedFrom = from;
680
+ })
681
+ );
613
682
  await Promise.all(tasks.map((t) => t.catch(() => {
614
683
  })));
615
684
  return extra;
@@ -751,7 +820,7 @@ async function prepareRaw(rec, opts = {}) {
751
820
  const filled = rec.source === "antigravity" && opts.enrich !== false ? { ...rec, payload: await fillAntigravityText(rec.payload) } : rec;
752
821
  const e = normalizeRecord(filled);
753
822
  if (!e) return null;
754
- const extra = opts.enrich === false ? {} : await enrichEvent(e);
823
+ const extra = opts.enrich === false ? {} : await enrichEvent(e, opts.home ?? bitlHome());
755
824
  const keys = opts.keys ?? await loadKeys(opts.home ?? bitlHome());
756
825
  return prepareRecord(e, sealerFor(keys), extra);
757
826
  }
@@ -19997,7 +19997,8 @@ var SignalSchema = external_exports.object({
19997
19997
  agentNamed: external_exports.boolean().optional(),
19998
19998
  waitingReason: WaitingReason.optional(),
19999
19999
  toolFailed: external_exports.boolean().optional(),
20000
- requestTag: external_exports.string().max(100).optional()
20000
+ requestTag: external_exports.string().max(100).optional(),
20001
+ resumedFrom: id.optional()
20001
20002
  });
20002
20003
  var WireRecordSchema = external_exports.object({
20003
20004
  v: external_exports.literal(WIRE_VERSION),
@@ -20061,7 +20062,7 @@ function removeCloudLogin(home = bitlHome()) {
20061
20062
  rmSync(cloudKeyPath(home), { force: true });
20062
20063
  }
20063
20064
  function clientVersionHeader() {
20064
- const v = "0.1.13";
20065
+ const v = "0.1.14";
20065
20066
  return v ? { "x-bitl-client": v } : {};
20066
20067
  }
20067
20068
  function resolveTarget(home = bitlHome()) {
@@ -20118,7 +20119,7 @@ async function latestVersion(home, now) {
20118
20119
  return latest;
20119
20120
  }
20120
20121
  var updateCommand = (install = process.env.BITL_INSTALL) => install === "npx" ? "npx @usebitl/cli@latest start" : "npm i -g @usebitl/cli";
20121
- async function checkCliUpdate(current = "0.1.13", opts = {}) {
20122
+ async function checkCliUpdate(current = "0.1.14", opts = {}) {
20122
20123
  if (!current || process.env.BITL_NO_UPDATE_CHECK === "1") return void 0;
20123
20124
  const latest = await latestVersion(opts.home ?? bitlHome(), opts.now ?? Date.now());
20124
20125
  if (!latest || compareVersions(latest, current) <= 0) return void 0;
@@ -20128,9 +20129,12 @@ async function checkCliUpdate(current = "0.1.13", opts = {}) {
20128
20129
  // ../../shared/src/node/antigravity.ts
20129
20130
  var MAX_TRANSCRIPT_BYTES = 20 * 1024 * 1024;
20130
20131
 
20131
- // ../../shared/src/node/enrich.ts
20132
+ // ../../shared/src/node/resume.ts
20132
20133
  var MAX_TRANSCRIPT_BYTES2 = 20 * 1024 * 1024;
20133
20134
 
20135
+ // ../../shared/src/node/enrich.ts
20136
+ var MAX_TRANSCRIPT_BYTES3 = 20 * 1024 * 1024;
20137
+
20134
20138
  // ../../shared/src/node/approve.ts
20135
20139
  var DEFAULT_APPROVAL_WAIT_MS = 10 * 6e4;
20136
20140
 
@@ -11,7 +11,7 @@ import {
11
11
  isEmptySession,
12
12
  parseWireRecord,
13
13
  sessionKey
14
- } from "./chunk-VVNOFPMO.mjs";
14
+ } from "./chunk-RIX6JB6S.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({
@@ -9232,8 +9232,21 @@ var SqlStore = class _SqlStore {
9232
9232
  return session2;
9233
9233
  }, _SqlStore.lockKey(accountId, key));
9234
9234
  this.emit({ type: "session", accountId, session, prevStatus });
9235
+ if (e.resumedFrom) return await this.linkResumed(accountId, session, sessionKey(e.source, e.resumedFrom)) ?? session;
9235
9236
  return session;
9236
9237
  }
9238
+ /**
9239
+ * `--resume` that started a new session: the earlier card leaves the board (replacedBy) and its history shows in the
9240
+ * new card's detail (resumedFrom, carrying the earlier card's own predecessors). Content can't move between cards —
9241
+ * it is sealed with each session's key — so the cards stay apart and are read together
9242
+ */
9243
+ async linkResumed(accountId, session, oldKey) {
9244
+ if (oldKey === session.key || session.resumedFrom?.includes(oldKey)) return void 0;
9245
+ const old = await this.update(accountId, oldKey, (cur) => cur.replacedBy === session.key ? null : { ...cur, replacedBy: session.key });
9246
+ if (!old) return void 0;
9247
+ const chain = [oldKey, ...old.resumedFrom ?? []].filter((k) => k !== session.key);
9248
+ return this.update(accountId, session.key, (cur) => ({ ...cur, resumedFrom: [.../* @__PURE__ */ new Set([...chain, ...cur.resumedFrom ?? []])] }));
9249
+ }
9237
9250
  getSession(accountId, key) {
9238
9251
  return _SqlStore.read(this.db.query, accountId, key);
9239
9252
  }
@@ -9293,37 +9306,42 @@ var SqlStore = class _SqlStore {
9293
9306
  acknowledge(accountId, key, now3 = /* @__PURE__ */ new Date()) {
9294
9307
  return this.update(accountId, key, (cur) => acknowledge(cur, now3));
9295
9308
  }
9296
- async deleteSession(accountId, key) {
9309
+ /** Deletes one session with its turns and events (onlyEnded: only if it is still ended, checked under lock) */
9310
+ async remove(accountId, key, onlyEnded = false) {
9297
9311
  const deleted = await this.db.transaction(async (q) => {
9298
- const { changes } = await q("DELETE FROM sessions WHERE account_id = ? AND key = ?", [accountId, key]);
9312
+ const { changes } = await q(`DELETE FROM sessions WHERE account_id = ? AND key = ?${onlyEnded ? ` AND status = 'ended'` : ""}`, [accountId, key]);
9313
+ if (changes === 0) return false;
9299
9314
  await q("DELETE FROM turns WHERE account_id = ? AND session_key = ?", [accountId, key]);
9300
9315
  await q("DELETE FROM events WHERE account_id = ? AND session_key = ?", [accountId, key]);
9301
- return changes > 0;
9316
+ return true;
9302
9317
  }, _SqlStore.lockKey(accountId, key));
9303
9318
  if (deleted) this.emit({ type: "removed", accountId, key });
9304
9319
  return deleted;
9305
9320
  }
9321
+ /** The earlier sessions a card continues go with it — they are off the board and only shown through it */
9322
+ async removePredecessors(accountId, s) {
9323
+ for (const key of s.resumedFrom ?? []) await this.remove(accountId, key);
9324
+ }
9325
+ async deleteSession(accountId, key) {
9326
+ const cur = await this.getSession(accountId, key);
9327
+ const deleted = await this.remove(accountId, key);
9328
+ if (deleted && cur) await this.removePredecessors(accountId, cur);
9329
+ return deleted;
9330
+ }
9306
9331
  /**
9307
9332
  * Deletes ended cards with their history (the board's "Ended → Clear all"). With keys, only the ended ones among them (goal-filtered view).
9308
9333
  * To spare cards revived by a new event after the list was read, the status is rechecked under lock while deleting. Returns the number deleted
9309
9334
  */
9310
9335
  async deleteEnded(accountId, keys) {
9311
- const { rows } = await this.db.query(`SELECT key FROM sessions WHERE account_id = ? AND status = 'ended' AND archived = 0`, [accountId]);
9336
+ const { rows } = await this.db.query(`SELECT data FROM sessions WHERE account_id = ? AND status = 'ended' AND archived = 0`, [accountId]);
9312
9337
  const only = keys ? new Set(keys) : null;
9313
9338
  let n = 0;
9314
9339
  for (const r of rows) {
9315
- const key = r.key;
9316
- if (only && !only.has(key)) continue;
9317
- const deleted = await this.db.transaction(async (q) => {
9318
- const { changes } = await q(`DELETE FROM sessions WHERE account_id = ? AND key = ? AND status = 'ended'`, [accountId, key]);
9319
- if (changes === 0) return false;
9320
- await q("DELETE FROM turns WHERE account_id = ? AND session_key = ?", [accountId, key]);
9321
- await q("DELETE FROM events WHERE account_id = ? AND session_key = ?", [accountId, key]);
9322
- return true;
9323
- }, _SqlStore.lockKey(accountId, key));
9324
- if (deleted) {
9340
+ const s = JSON.parse(r.data);
9341
+ if (s.replacedBy || only && !only.has(s.key)) continue;
9342
+ if (await this.remove(accountId, s.key, true)) {
9325
9343
  n++;
9326
- this.emit({ type: "removed", accountId, key });
9344
+ await this.removePredecessors(accountId, s);
9327
9345
  }
9328
9346
  }
9329
9347
  return n;
@@ -9334,7 +9352,7 @@ var SqlStore = class _SqlStore {
9334
9352
  let cleared = 0;
9335
9353
  for (const r of rows) {
9336
9354
  await this.update(accountId, r.key, (cur) => {
9337
- if (cur.cleared || cur.archived) return null;
9355
+ if (cur.cleared || cur.archived || cur.replacedBy) return null;
9338
9356
  cleared++;
9339
9357
  return { ...cur, cleared: true };
9340
9358
  });
@@ -12129,10 +12147,10 @@ async function ingestRecord(store, accountId, body) {
12129
12147
  const rec = parseWireRecord(body);
12130
12148
  return rec ? store.ingest(accountId, rec) : null;
12131
12149
  }
12132
- function createApp({ store, webDist, account, push, approvals, goals, access = allowAll, extension }) {
12150
+ function createApp({ store, webDist, account, push, approvals, goals, access = allowAll, extension, build }) {
12133
12151
  const app = new Hono2();
12134
12152
  const resolve2 = account ?? (() => LOCAL_ACCOUNT);
12135
- app.get("/api/health", (c) => c.json({ ok: true }));
12153
+ app.get("/api/health", (c) => c.json(build ? { ok: true, build } : { ok: true }));
12136
12154
  app.get("/api/me", async (c) => c.json(extension?.me ? await extension.me(c) : { mode: "local" }));
12137
12155
  extension?.mountPublic?.(app);
12138
12156
  app.get("/api/push/key", (c) => c.json({ publicKey: push?.publicKey ?? null }));
@@ -12178,6 +12196,11 @@ function createApp({ store, webDist, account, push, approvals, goals, access = a
12178
12196
  const session = await store.getSession(accountId, key);
12179
12197
  if (!session) return c.json({ error: "not found" }, 404);
12180
12198
  const [turns, timeline] = await Promise.all([store.listTurns(accountId, key), store.listTimeline(accountId, key)]);
12199
+ for (const prev of session.resumedFrom ?? []) {
12200
+ const [t, items] = await Promise.all([store.listTurns(accountId, prev), store.listTimeline(accountId, prev)]);
12201
+ turns.push(...t);
12202
+ timeline.push(...items.map((item) => ({ ...item, sessionKey: prev })));
12203
+ }
12181
12204
  return c.json({ session, turns, timeline });
12182
12205
  });
12183
12206
  const found = (c, s) => s ? c.json(s) : c.json({ error: "not found" }, 404);
@@ -12380,13 +12403,13 @@ function startPushNotifier(opts) {
12380
12403
  };
12381
12404
  const notify = async (accountId, s) => {
12382
12405
  if (!(await push.list(accountId)).length) return;
12383
- const attention = (await store.listSessions(accountId)).filter((x) => ALERT.has(x.status) && !isEmptySession(x) && !x.cleared).length;
12406
+ const attention = (await store.listSessions(accountId)).filter((x) => ALERT.has(x.status) && !isEmptySession(x) && !x.cleared && !x.replacedBy).length;
12384
12407
  await sendAll(accountId, buildPayload(s, attention), topicOf(s.key));
12385
12408
  };
12386
12409
  const off = store.subscribe((change) => {
12387
12410
  if (change.type !== "session") return;
12388
12411
  const s = change.session;
12389
- if (!ALERT.has(s.status) || change.prevStatus === s.status || isEmptySession(s) || s.cleared) return;
12412
+ if (!ALERT.has(s.status) || change.prevStatus === s.status || isEmptySession(s) || s.cleared || s.replacedBy) return;
12390
12413
  const id = `${change.accountId}
12391
12414
  ${s.key}`;
12392
12415
  const t = Date.now();
package/dist/cli.mjs CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  unwrapMasterKey,
22
22
  verifyPublicKey,
23
23
  wrapMasterKey
24
- } from "./chunks/chunk-VVNOFPMO.mjs";
24
+ } from "./chunks/chunk-RIX6JB6S.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.13") {
456
+ async function updateNotice(current = "0.1.14") {
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-MWG3MA6Y.mjs");
612
+ const { startServer } = await import("./chunks/src-WI4MJOSQ.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.13");
634
+ console.log("0.1.14");
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.13",
3
+ "version": "0.1.14",
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": {