bertrand 0.22.0 → 0.22.2

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/dist/bertrand.js CHANGED
@@ -599,8 +599,6 @@ var init_router = __esm(() => {
599
599
  aliases = new Map;
600
600
  HOOK_COMMANDS = new Set([
601
601
  "update",
602
- "snapshot",
603
- "recap-thinking",
604
602
  "assistant-message",
605
603
  "notify",
606
604
  "badge"
@@ -610,7 +608,6 @@ var init_router = __esm(() => {
610
608
  // src/db/schema.ts
611
609
  var exports_schema = {};
612
610
  __export(exports_schema, {
613
- worktreeAssociations: () => worktreeAssociations,
614
611
  sessions: () => sessions,
615
612
  sessionStats: () => sessionStats,
616
613
  sessionLabels: () => sessionLabels,
@@ -621,7 +618,7 @@ __export(exports_schema, {
621
618
  });
622
619
  import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core";
623
620
  import { sql } from "drizzle-orm";
624
- var categories, labels, sessions, sessionLabels, conversations, events, worktreeAssociations, sessionStats;
621
+ var categories, labels, sessions, sessionLabels, conversations, events, sessionStats;
625
622
  var init_schema = __esm(() => {
626
623
  categories = sqliteTable("categories", {
627
624
  id: text("id").primaryKey(),
@@ -677,7 +674,6 @@ var init_schema = __esm(() => {
677
674
  startedAt: text("started_at").notNull().default(sql`(datetime('now'))`),
678
675
  endedAt: text("ended_at"),
679
676
  discarded: integer("discarded", { mode: "boolean" }).notNull().default(false),
680
- lastQuestion: text("last_question"),
681
677
  eventCount: integer("event_count").notNull().default(0)
682
678
  }, (t) => [index("conv_session").on(t.sessionId)]);
683
679
  events = sqliteTable("events", {
@@ -694,24 +690,11 @@ var init_schema = __esm(() => {
694
690
  index("ev_event_created").on(t.event, t.createdAt),
695
691
  index("ev_conversation").on(t.conversationId)
696
692
  ]);
697
- worktreeAssociations = sqliteTable("worktree_associations", {
698
- id: text("id").primaryKey(),
699
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
700
- branch: text("branch").notNull(),
701
- worktreePath: text("worktree_path"),
702
- active: integer("active", { mode: "boolean" }).notNull().default(true),
703
- enteredAt: text("entered_at").notNull().default(sql`(datetime('now'))`),
704
- exitedAt: text("exited_at")
705
- }, (t) => [
706
- index("wt_session").on(t.sessionId),
707
- index("wt_active").on(t.active)
708
- ]);
709
693
  sessionStats = sqliteTable("session_stats", {
710
694
  sessionId: text("session_id").primaryKey().references(() => sessions.id, { onDelete: "cascade" }),
711
695
  eventCount: integer("event_count").notNull().default(0),
712
696
  conversationCount: integer("conversation_count").notNull().default(0),
713
697
  interactionCount: integer("interaction_count").notNull().default(0),
714
- prCount: integer("pr_count").notNull().default(0),
715
698
  claudeWorkS: integer("claude_work_s").notNull().default(0),
716
699
  userWaitS: integer("user_wait_s").notNull().default(0),
717
700
  activePct: integer("active_pct").notNull().default(0),
@@ -859,9 +842,6 @@ function getConversationsBySession(sessionId) {
859
842
  function endConversation(id) {
860
843
  return getDb().update(conversations).set({ endedAt: sql3`(datetime('now'))` }).where(eq2(conversations.id, id)).returning().get();
861
844
  }
862
- function updateLastQuestion(id, question) {
863
- return getDb().update(conversations).set({ lastQuestion: question }).where(eq2(conversations.id, id)).returning().get();
864
- }
865
845
  var init_conversations = __esm(() => {
866
846
  init_client();
867
847
  init_schema();
@@ -966,50 +946,6 @@ var init_events = __esm(() => {
966
946
  });
967
947
 
968
948
  // src/db/events/emit.ts
969
- function emitSessionStarted(args) {
970
- return insertEvent({
971
- sessionId: args.sessionId,
972
- conversationId: args.conversationId,
973
- event: "session.started",
974
- meta: {
975
- category_path: args.categoryPath,
976
- session_name: args.sessionName,
977
- session_slug: args.sessionSlug,
978
- labels: args.labels,
979
- summary: args.summary ?? null
980
- }
981
- });
982
- }
983
- function emitSessionResumed(args) {
984
- return insertEvent({
985
- sessionId: args.sessionId,
986
- conversationId: args.conversationId,
987
- event: "session.resumed",
988
- meta: { claude_id: args.conversationId }
989
- });
990
- }
991
- function emitSessionPaused(args) {
992
- return insertEvent({
993
- sessionId: args.sessionId,
994
- conversationId: args.conversationId,
995
- event: "session.paused",
996
- meta: { claude_id: args.conversationId }
997
- });
998
- }
999
- function emitSessionPausedByRecovery(args) {
1000
- return insertEvent({
1001
- sessionId: args.sessionId,
1002
- event: "session.paused",
1003
- summary: "Recovered from stale state (process not found)",
1004
- meta: { stale_pid: args.stalePid }
1005
- });
1006
- }
1007
- function emitSessionEnded(args) {
1008
- return insertEvent({
1009
- sessionId: args.sessionId,
1010
- event: "session.end"
1011
- });
1012
- }
1013
949
  function emitClaudeStarted(args) {
1014
950
  return insertEvent({
1015
951
  sessionId: args.sessionId,
@@ -1017,9 +953,6 @@ function emitClaudeStarted(args) {
1017
953
  event: "claude.started",
1018
954
  meta: {
1019
955
  claude_id: args.conversationId,
1020
- model: args.model,
1021
- claude_version: args.claudeVersion,
1022
- git: args.git,
1023
956
  cwd: args.cwd
1024
957
  }
1025
958
  });
@@ -1073,31 +1006,6 @@ function emitSessionRecap(args) {
1073
1006
  meta: { recap: args.recap, claude_id: args.conversationId }
1074
1007
  });
1075
1008
  }
1076
- function emitPermissionRequested(args) {
1077
- return insertEvent({
1078
- sessionId: args.sessionId,
1079
- conversationId: args.conversationId,
1080
- event: "permission.request",
1081
- meta: {
1082
- tool: args.tool,
1083
- detail: args.detail,
1084
- claude_id: args.conversationId
1085
- }
1086
- });
1087
- }
1088
- function emitPermissionResolved(args) {
1089
- return insertEvent({
1090
- sessionId: args.sessionId,
1091
- conversationId: args.conversationId,
1092
- event: "permission.resolve",
1093
- meta: {
1094
- tool: args.tool,
1095
- detail: args.detail,
1096
- outcome: args.outcome,
1097
- claude_id: args.conversationId
1098
- }
1099
- });
1100
- }
1101
1009
  function emitToolUsed(args) {
1102
1010
  const summary = formatToolSummary(args.tool, args.detail);
1103
1011
  return insertEvent({
@@ -1171,23 +1079,6 @@ function emitAssistantRecap(args) {
1171
1079
  meta: { recap: args.recap, claude_id: args.conversationId }
1172
1080
  });
1173
1081
  }
1174
- function emitContextSnapshot(args) {
1175
- return insertEvent({
1176
- sessionId: args.sessionId,
1177
- conversationId: args.conversationId,
1178
- event: "context.snapshot",
1179
- summary: `${args.remainingPct}% remaining`,
1180
- meta: {
1181
- model: args.model,
1182
- input_tokens: String(args.inputTokens),
1183
- cache_creation_tokens: String(args.cacheCreationTokens),
1184
- cache_read_tokens: String(args.cacheReadTokens),
1185
- context_window_tokens: String(args.totalContextTokens),
1186
- remaining_pct: String(args.remainingPct),
1187
- claude_id: args.conversationId
1188
- }
1189
- });
1190
- }
1191
1082
  var init_emit = __esm(() => {
1192
1083
  init_events();
1193
1084
  });
@@ -1237,26 +1128,6 @@ function dispatchHookEvent(event, ctx) {
1237
1128
  recap: String(meta.recap ?? "")
1238
1129
  });
1239
1130
  return true;
1240
- case "session.paused":
1241
- emitSessionPaused({ sessionId, conversationId });
1242
- return true;
1243
- case "permission.request":
1244
- emitPermissionRequested({
1245
- sessionId,
1246
- conversationId,
1247
- tool: String(meta.tool ?? ""),
1248
- detail: String(meta.detail ?? "")
1249
- });
1250
- return true;
1251
- case "permission.resolve":
1252
- emitPermissionResolved({
1253
- sessionId,
1254
- conversationId,
1255
- tool: String(meta.tool ?? ""),
1256
- detail: String(meta.detail ?? ""),
1257
- outcome: meta.outcome === "denied" ? "denied" : "approved"
1258
- });
1259
- return true;
1260
1131
  case "tool.applied":
1261
1132
  emitToolApplied({
1262
1133
  sessionId,
@@ -1283,15 +1154,11 @@ var init_update = __esm(() => {
1283
1154
  init_router();
1284
1155
  init_sessions();
1285
1156
  init_conversations();
1286
- init_trigger();
1287
1157
  init_emit();
1288
1158
  EVENT_STATUS_MAP = {
1289
1159
  "session.waiting": "waiting",
1290
1160
  "session.answered": "active",
1291
- "session.active": "active",
1292
- "session.paused": "paused",
1293
- "session.started": "active",
1294
- "session.end": "paused"
1161
+ "session.paused": "paused"
1295
1162
  };
1296
1163
  register("update", async (args) => {
1297
1164
  let sessionId = "";
@@ -1340,35 +1207,28 @@ var init_update = __esm(() => {
1340
1207
  }
1341
1208
  const rawConvoId = meta?.claude_id || process.env.BERTRAND_CLAUDE_ID || undefined;
1342
1209
  const conversationId = rawConvoId && getConversation(rawConvoId) ? rawConvoId : undefined;
1343
- const dispatched = dispatchHookEvent(event, {
1210
+ dispatchHookEvent(event, {
1344
1211
  sessionId,
1345
1212
  conversationId,
1346
1213
  meta,
1347
1214
  summary: summaryArg
1348
1215
  });
1349
- if (!dispatched) {
1350
- return;
1351
- }
1352
1216
  if (newStatus && !ignoreStatusFlip) {
1353
1217
  updateSessionStatus(sessionId, newStatus);
1354
1218
  }
1355
- if (event === "session.waiting" && conversationId && meta?.question) {
1356
- updateLastQuestion(conversationId, meta.question);
1357
- }
1358
- if (event === "session.end") {
1359
- triggerBackgroundPush();
1360
- }
1361
1219
  });
1362
1220
  });
1363
1221
 
1364
1222
  // src/lib/transcript.ts
1365
1223
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
1366
- function getContextWindowSize(model) {
1367
- for (const [prefix, size] of Object.entries(CONTEXT_WINDOW_SIZES)) {
1368
- if (model.startsWith(prefix))
1369
- return size;
1370
- }
1371
- return 200000;
1224
+ import { homedir as homedir2 } from "os";
1225
+ import { join as join6 } from "path";
1226
+ function claudeTranscriptPath(sessionId, cwd) {
1227
+ const dir = (cwd ?? process.cwd()).replace(/\//g, "-");
1228
+ return join6(homedir2(), ".claude", "projects", dir, `${sessionId}.jsonl`);
1229
+ }
1230
+ function claudeSessionExists(sessionId, cwd) {
1231
+ return existsSync5(claudeTranscriptPath(sessionId, cwd));
1372
1232
  }
1373
1233
  function getLatestAssistantTurn(filePath) {
1374
1234
  if (!existsSync5(filePath))
@@ -1430,108 +1290,7 @@ function getLatestAssistantTurn(filePath) {
1430
1290
  thinkingBytes
1431
1291
  };
1432
1292
  }
1433
- function getContextSnapshot(filePath) {
1434
- if (!existsSync5(filePath))
1435
- return null;
1436
- const text2 = readFileSync4(filePath, "utf-8");
1437
- const lines = text2.split(`
1438
- `);
1439
- for (let i = lines.length - 1;i >= 0; i--) {
1440
- const line = lines[i];
1441
- if (!line)
1442
- continue;
1443
- let entry;
1444
- try {
1445
- entry = JSON.parse(line);
1446
- } catch {
1447
- continue;
1448
- }
1449
- if (entry.type !== "assistant")
1450
- continue;
1451
- const message = entry.message;
1452
- const usage = message?.usage;
1453
- if (!usage)
1454
- continue;
1455
- const model = message?.model ?? "";
1456
- const inputTokens = usage.input_tokens ?? 0;
1457
- const outputTokens = usage.output_tokens ?? 0;
1458
- const cacheCreationTokens = usage.cache_creation_input_tokens ?? 0;
1459
- const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
1460
- const totalContextTokens = inputTokens + cacheCreationTokens + cacheReadTokens;
1461
- const windowSize = getContextWindowSize(model);
1462
- const remainingPct = Math.max(0, Math.min(100, Math.round(100 - totalContextTokens * 100 / windowSize)));
1463
- return {
1464
- model,
1465
- inputTokens,
1466
- outputTokens,
1467
- cacheCreationTokens,
1468
- cacheReadTokens,
1469
- totalContextTokens,
1470
- remainingPct
1471
- };
1472
- }
1473
- return null;
1474
- }
1475
- var CONTEXT_WINDOW_SIZES;
1476
- var init_transcript = __esm(() => {
1477
- CONTEXT_WINDOW_SIZES = {
1478
- "claude-opus-4": 1e6,
1479
- "claude-sonnet-4": 200000,
1480
- "claude-haiku-4": 200000
1481
- };
1482
- });
1483
-
1484
- // src/cli/commands/snapshot.ts
1485
- var exports_snapshot = {};
1486
- var init_snapshot = __esm(() => {
1487
- init_router();
1488
- init_sessions();
1489
- init_conversations();
1490
- init_emit();
1491
- init_transcript();
1492
- register("snapshot", async (args) => {
1493
- let sessionId = "";
1494
- let transcriptPath = "";
1495
- let conversationId = "";
1496
- for (let i = 0;i < args.length; i++) {
1497
- const arg = args[i];
1498
- const next = args[i + 1];
1499
- if (arg === "--session-id" && next) {
1500
- sessionId = next;
1501
- i++;
1502
- } else if (arg === "--transcript-path" && next) {
1503
- transcriptPath = next;
1504
- i++;
1505
- } else if (arg === "--conversation-id" && next) {
1506
- conversationId = next;
1507
- i++;
1508
- }
1509
- }
1510
- if (!sessionId || !transcriptPath) {
1511
- console.error("Usage: bertrand snapshot --session-id <id> --transcript-path <path> [--conversation-id <id>]");
1512
- process.exit(1);
1513
- }
1514
- const session = getSession(sessionId);
1515
- if (!session) {
1516
- console.error(`Session not found: ${sessionId}`);
1517
- process.exit(1);
1518
- }
1519
- const snapshot = getContextSnapshot(transcriptPath);
1520
- if (!snapshot)
1521
- return;
1522
- const convoId = conversationId && getConversation(conversationId) ? conversationId : undefined;
1523
- emitContextSnapshot({
1524
- sessionId,
1525
- conversationId: convoId,
1526
- model: snapshot.model,
1527
- inputTokens: snapshot.inputTokens,
1528
- cacheCreationTokens: snapshot.cacheCreationTokens,
1529
- cacheReadTokens: snapshot.cacheReadTokens,
1530
- totalContextTokens: snapshot.totalContextTokens,
1531
- remainingPct: snapshot.remainingPct
1532
- });
1533
- });
1534
- });
1293
+ var init_transcript = () => {};
1535
1294
 
1536
1295
  // src/cli/commands/assistant-message.ts
1537
1296
  var exports_assistant_message = {};
@@ -1615,56 +1374,6 @@ var init_assistant_message = __esm(() => {
1615
1374
  });
1616
1375
  });
1617
1376
 
1618
- // src/cli/commands/recap-thinking.ts
1619
- var exports_recap_thinking = {};
1620
- var RECAP_RE2;
1621
- var init_recap_thinking = __esm(() => {
1622
- init_router();
1623
- init_sessions();
1624
- init_conversations();
1625
- init_emit();
1626
- init_transcript();
1627
- RECAP_RE2 = /<recap>([\s\S]*?)<\/recap>/i;
1628
- register("recap-thinking", async (args) => {
1629
- let sessionId = "";
1630
- let transcriptPath = "";
1631
- let conversationId = "";
1632
- for (let i = 0;i < args.length; i++) {
1633
- const arg = args[i];
1634
- const next = args[i + 1];
1635
- if (arg === "--session-id" && next) {
1636
- sessionId = next;
1637
- i++;
1638
- } else if (arg === "--transcript-path" && next) {
1639
- transcriptPath = next;
1640
- i++;
1641
- } else if (arg === "--conversation-id" && next) {
1642
- conversationId = next;
1643
- i++;
1644
- }
1645
- }
1646
- if (!sessionId || !transcriptPath) {
1647
- console.error("Usage: bertrand recap-thinking --session-id <id> --transcript-path <path> [--conversation-id <id>]");
1648
- process.exit(1);
1649
- }
1650
- if (!getSession(sessionId))
1651
- return;
1652
- const turn = getLatestAssistantTurn(transcriptPath);
1653
- if (!turn?.text)
1654
- return;
1655
- const match = turn.text.match(RECAP_RE2);
1656
- const recap = match?.[1]?.trim();
1657
- if (!recap)
1658
- return;
1659
- const convoId = conversationId && getConversation(conversationId) ? conversationId : undefined;
1660
- emitAssistantRecap({
1661
- sessionId,
1662
- conversationId: convoId,
1663
- recap
1664
- });
1665
- });
1666
- });
1667
-
1668
1377
  // src/terminal/wave.ts
1669
1378
  import { execSync } from "child_process";
1670
1379
 
@@ -1712,7 +1421,7 @@ class NoopAdapter {
1712
1421
 
1713
1422
  // src/terminal/index.ts
1714
1423
  import { readFileSync as readFileSync5 } from "fs";
1715
- import { join as join6 } from "path";
1424
+ import { join as join7 } from "path";
1716
1425
  function getTerminalAdapter() {
1717
1426
  if (cachedAdapter)
1718
1427
  return cachedAdapter;
@@ -1731,7 +1440,7 @@ function getTerminalAdapter() {
1731
1440
  }
1732
1441
  function readConfigTerminal() {
1733
1442
  try {
1734
- const config = JSON.parse(readFileSync5(join6(paths.root, "config.json"), "utf-8"));
1443
+ const config = JSON.parse(readFileSync5(join7(paths.root, "config.json"), "utf-8"));
1735
1444
  return config.terminal ?? null;
1736
1445
  } catch {
1737
1446
  return null;
@@ -1926,7 +1635,6 @@ function computeTimings(events2) {
1926
1635
  function computeSessionStats(sessionId) {
1927
1636
  const events2 = getEventsBySession(sessionId);
1928
1637
  const summary = computeTimings(events2);
1929
- const prEvents = getEventsByType(sessionId, "gh.pr.created");
1930
1638
  const conversationIds = new Set(events2.filter((e) => e.conversationId).map((e) => e.conversationId));
1931
1639
  const interactionCount = events2.filter((e) => e.event === "session.waiting" || e.event === "session.answered").length;
1932
1640
  const diff = computeDiffStats(sessionId);
@@ -1934,7 +1642,6 @@ function computeSessionStats(sessionId) {
1934
1642
  eventCount: events2.length,
1935
1643
  conversationCount: conversationIds.size,
1936
1644
  interactionCount,
1937
- prCount: prEvents.length,
1938
1645
  claudeWorkS: Math.round(summary.totalClaudeWorkMs / 1000),
1939
1646
  userWaitS: Math.round(summary.totalUserWaitMs / 1000),
1940
1647
  activePct: summary.activePct,
@@ -1972,14 +1679,6 @@ function aggregateToolUsage(sessionId) {
1972
1679
  counts[p.tool] = (counts[p.tool] ?? 0) + (p.count ?? 1);
1973
1680
  }
1974
1681
  }
1975
- const resolves = getEventsByType(sessionId, "permission.resolve");
1976
- for (const ev of resolves) {
1977
- const meta = ev.meta;
1978
- const tool = meta?.tool;
1979
- if (!tool)
1980
- continue;
1981
- counts[tool] = (counts[tool] ?? 0) + 1;
1982
- }
1983
1682
  const used = getEventsByType(sessionId, "tool.used");
1984
1683
  for (const ev of used) {
1985
1684
  const meta = ev.meta;
@@ -1990,40 +1689,6 @@ function aggregateToolUsage(sessionId) {
1990
1689
  }
1991
1690
  return counts;
1992
1691
  }
1993
- function contextTokenStats(sessionId) {
1994
- const snapshots = getEventsByType(sessionId, "context.snapshot");
1995
- const samples = [];
1996
- for (const ev of snapshots) {
1997
- const meta = ev.meta;
1998
- const total = parseInt(meta?.context_window_tokens ?? "0", 10);
1999
- if (Number.isFinite(total) && total > 0)
2000
- samples.push(total);
2001
- }
2002
- if (samples.length === 0)
2003
- return { avg: 0, max: 0, latest: 0 };
2004
- let sum = 0;
2005
- let max = 0;
2006
- for (const n of samples) {
2007
- sum += n;
2008
- if (n > max)
2009
- max = n;
2010
- }
2011
- const avg = Math.round(sum / samples.length);
2012
- const latest = samples[samples.length - 1] ?? 0;
2013
- return { avg, max, latest };
2014
- }
2015
- function permissionDenialCount(sessionId) {
2016
- const all = getEventsBySession(sessionId);
2017
- let requests = 0;
2018
- let resolves = 0;
2019
- for (const ev of all) {
2020
- if (ev.event === "permission.request")
2021
- requests++;
2022
- else if (ev.event === "permission.resolve")
2023
- resolves++;
2024
- }
2025
- return Math.max(0, requests - resolves);
2026
- }
2027
1692
  function discardRate(sessionId) {
2028
1693
  const row = getDb().select({
2029
1694
  total: sql5`count(*)`,
@@ -2037,8 +1702,6 @@ function discardRate(sessionId) {
2037
1702
  function computeEngagementStats(sessionId) {
2038
1703
  return {
2039
1704
  toolUsage: aggregateToolUsage(sessionId),
2040
- contextTokens: contextTokenStats(sessionId),
2041
- permissionDenials: permissionDenialCount(sessionId),
2042
1705
  discardRate: discardRate(sessionId)
2043
1706
  };
2044
1707
  }
@@ -2091,7 +1754,7 @@ var init_session_archive = __esm(() => {
2091
1754
  // src/server/index.ts
2092
1755
  import { execFile } from "child_process";
2093
1756
  import { existsSync as existsSync6 } from "fs";
2094
- import { join as join7 } from "path";
1757
+ import { join as join8 } from "path";
2095
1758
  function liveStats(sessionId) {
2096
1759
  return {
2097
1760
  sessionId,
@@ -2164,11 +1827,11 @@ function match(pathname, url) {
2164
1827
  }
2165
1828
  function findDashboardDir() {
2166
1829
  const candidates = [
2167
- join7(import.meta.dir, "dashboard"),
2168
- join7(import.meta.dir, "..", "dashboard")
1830
+ join8(import.meta.dir, "dashboard"),
1831
+ join8(import.meta.dir, "..", "dashboard")
2169
1832
  ];
2170
1833
  for (const dir of candidates) {
2171
- if (existsSync6(join7(dir, "index.html")))
1834
+ if (existsSync6(join8(dir, "index.html")))
2172
1835
  return dir;
2173
1836
  }
2174
1837
  return null;
@@ -2177,13 +1840,13 @@ async function serveDashboard(pathname) {
2177
1840
  if (!DASHBOARD_DIR)
2178
1841
  return null;
2179
1842
  const requested = pathname === "/" ? "/index.html" : pathname;
2180
- const filePath = join7(DASHBOARD_DIR, requested);
1843
+ const filePath = join8(DASHBOARD_DIR, requested);
2181
1844
  if (!filePath.startsWith(DASHBOARD_DIR))
2182
1845
  return null;
2183
1846
  const file = Bun.file(filePath);
2184
1847
  if (await file.exists())
2185
1848
  return new Response(file);
2186
- return new Response(Bun.file(join7(DASHBOARD_DIR, "index.html")));
1849
+ return new Response(Bun.file(join8(DASHBOARD_DIR, "index.html")));
2187
1850
  }
2188
1851
  function startServer(port = PORT) {
2189
1852
  const server = Bun.serve({
@@ -2355,7 +2018,7 @@ function cleanupSnapshot() {
2355
2018
  }
2356
2019
  }
2357
2020
  var SIDECAR_SUFFIXES;
2358
- var init_snapshot2 = __esm(() => {
2021
+ var init_snapshot = __esm(() => {
2359
2022
  init_resolve();
2360
2023
  SIDECAR_SUFFIXES = ["", "-wal", "-shm"];
2361
2024
  });
@@ -2554,7 +2217,7 @@ var init_engine = __esm(() => {
2554
2217
  init_resolve();
2555
2218
  init_lsof();
2556
2219
  init_config();
2557
- init_snapshot2();
2220
+ init_snapshot();
2558
2221
  init_crypto();
2559
2222
  });
2560
2223
 
@@ -3272,77 +2935,10 @@ var init_process = __esm(() => {
3272
2935
  init_resolve();
3273
2936
  });
3274
2937
 
3275
- // src/engine/spawn-context.ts
3276
- import { execFile as execFile2 } from "child_process";
3277
- import { promisify } from "util";
3278
- function captureModel() {
3279
- return process.env.BERTRAND_MODEL || process.env.CLAUDE_MODEL || undefined;
3280
- }
3281
- async function captureClaudeVersion() {
3282
- if (cachedClaudeVersion !== null)
3283
- return cachedClaudeVersion;
3284
- try {
3285
- const { stdout } = await execFileAsync("claude", ["--version"], {
3286
- timeout: 5000
3287
- });
3288
- const match2 = stdout.trim().match(/(\d+\.\d+\.\d+(?:\.\d+)?)/);
3289
- cachedClaudeVersion = match2 ? match2[1] : stdout.trim() || undefined;
3290
- return cachedClaudeVersion;
3291
- } catch {
3292
- cachedClaudeVersion = undefined;
3293
- return;
3294
- }
3295
- }
3296
- async function captureGit() {
3297
- try {
3298
- const [statusRes, shaRes] = await Promise.all([
3299
- execFileAsync("git", ["status", "--porcelain=v2", "--branch"], {
3300
- timeout: 5000
3301
- }),
3302
- execFileAsync("git", ["rev-parse", "HEAD"], { timeout: 5000 })
3303
- ]);
3304
- let branch;
3305
- let dirty = false;
3306
- for (const line of statusRes.stdout.split(`
3307
- `)) {
3308
- if (line.startsWith("# branch.head ")) {
3309
- branch = line.slice("# branch.head ".length).trim();
3310
- } else if (line && !line.startsWith("#")) {
3311
- dirty = true;
3312
- }
3313
- }
3314
- if (!branch)
3315
- return;
3316
- return {
3317
- branch,
3318
- sha: shaRes.stdout.trim(),
3319
- dirty
3320
- };
3321
- } catch {
3322
- return;
3323
- }
3324
- }
3325
- async function captureSpawnContext() {
3326
- const [claudeVersion, git] = await Promise.all([
3327
- captureClaudeVersion(),
3328
- captureGit()
3329
- ]);
3330
- return {
3331
- model: captureModel(),
3332
- claudeVersion,
3333
- git,
3334
- cwd: process.cwd()
3335
- };
3336
- }
3337
- var execFileAsync, cachedClaudeVersion = null;
3338
- var init_spawn_context = __esm(() => {
3339
- execFileAsync = promisify(execFile2);
3340
- });
3341
-
3342
2938
  // src/lib/server-lifecycle.ts
3343
2939
  import { spawn as spawn3 } from "child_process";
3344
2940
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2 } from "fs";
3345
- import { join as join8 } from "path";
2941
+ import { join as join9 } from "path";
3346
2942
  function readPidFile() {
3347
2943
  try {
3348
2944
  const pid = Number(readFileSync7(deps.pidFile, "utf-8").trim());
@@ -3410,11 +3006,11 @@ var init_server_lifecycle = __esm(() => {
3410
3006
  init_paths();
3411
3007
  init_sessions();
3412
3008
  defaultDeps = {
3413
- pidFile: join8(paths.root, "server.pid"),
3009
+ pidFile: join9(paths.root, "server.pid"),
3414
3010
  port: Number(process.env.BERTRAND_PORT ?? 5200),
3415
3011
  resolveBin() {
3416
3012
  try {
3417
- const config = JSON.parse(readFileSync7(join8(paths.root, "config.json"), "utf-8"));
3013
+ const config = JSON.parse(readFileSync7(join9(paths.root, "config.json"), "utf-8"));
3418
3014
  return typeof config?.bin === "string" ? config.bin : null;
3419
3015
  } catch {
3420
3016
  return null;
@@ -3487,24 +3083,11 @@ async function launch(opts) {
3487
3083
  liveSession = { sessionId: session.id, claudeId };
3488
3084
  installExitHandlers();
3489
3085
  await ensureServerStarted();
3490
- const spawnContext = await captureSpawnContext();
3491
3086
  const sessionName = `${opts.categoryPath}/${opts.slug}`;
3492
- emitSessionStarted({
3493
- sessionId: session.id,
3494
- conversationId: claudeId,
3495
- categoryPath: opts.categoryPath,
3496
- sessionName: opts.name ?? opts.slug,
3497
- sessionSlug: opts.slug,
3498
- labels: opts.labelNames ?? [],
3499
- summary: session.summary ?? null
3500
- });
3501
3087
  emitClaudeStarted({
3502
3088
  sessionId: session.id,
3503
3089
  conversationId: claudeId,
3504
- model: spawnContext.model,
3505
- claudeVersion: spawnContext.claudeVersion,
3506
- git: spawnContext.git,
3507
- cwd: spawnContext.cwd
3090
+ cwd: process.cwd()
3508
3091
  });
3509
3092
  const siblingContext = buildSiblingContext(categoryId, opts.categoryPath, session.id);
3510
3093
  const contract = buildContract(sessionName, siblingContext);
@@ -3524,14 +3107,18 @@ async function resume(opts) {
3524
3107
  throw new Error(`Session not found: ${opts.sessionId}`);
3525
3108
  const category = getCategory(session.categoryId);
3526
3109
  const sessionName = category ? `${category.path}/${session.slug}` : session.name;
3110
+ const isFreshClaudeSession = !claudeSessionExists(opts.conversationId);
3527
3111
  updateSession(session.id, { status: "active", pid: process.pid });
3528
3112
  liveSession = { sessionId: session.id, claudeId: opts.conversationId };
3529
3113
  installExitHandlers();
3530
3114
  await ensureServerStarted();
3531
- emitSessionResumed({
3532
- sessionId: session.id,
3533
- conversationId: opts.conversationId
3534
- });
3115
+ if (isFreshClaudeSession) {
3116
+ emitClaudeStarted({
3117
+ sessionId: session.id,
3118
+ conversationId: opts.conversationId,
3119
+ cwd: process.cwd()
3120
+ });
3121
+ }
3535
3122
  const categoryPath = category?.path ?? "";
3536
3123
  const siblingContext = buildSiblingContext(session.categoryId, categoryPath, session.id);
3537
3124
  const contract = buildContract(sessionName, siblingContext);
@@ -3541,7 +3128,7 @@ async function resume(opts) {
3541
3128
  sessionName,
3542
3129
  sessionSlug: session.slug,
3543
3130
  contract,
3544
- resume: true
3131
+ resume: !isFreshClaudeSession
3545
3132
  });
3546
3133
  finalizeSession(session.id, opts.conversationId, exitCode);
3547
3134
  return session.id;
@@ -3566,7 +3153,6 @@ function finalizeSession(sessionId, conversationId, exitCode) {
3566
3153
  });
3567
3154
  if (liveSession?.sessionId === sessionId)
3568
3155
  liveSession = null;
3569
- emitSessionEnded({ sessionId });
3570
3156
  computeAndPersist(sessionId);
3571
3157
  stopServerIfIdle();
3572
3158
  triggerBackgroundPush();
@@ -3581,20 +3167,20 @@ var init_session = __esm(() => {
3581
3167
  init_template2();
3582
3168
  init_context();
3583
3169
  init_process();
3584
- init_spawn_context();
3585
3170
  init_timing();
3586
3171
  init_server_lifecycle();
3587
3172
  init_trigger();
3173
+ init_transcript();
3588
3174
  });
3589
3175
 
3590
3176
  // src/tui/app.tsx
3591
3177
  import { spawn as spawn4 } from "child_process";
3592
3178
  import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
3593
3179
  import { tmpdir } from "os";
3594
- import { join as join9 } from "path";
3180
+ import { join as join10 } from "path";
3595
3181
  import { randomUUID as randomUUID2 } from "crypto";
3596
3182
  async function runScreen(screen, ...args) {
3597
- const tmpFile = join9(tmpdir(), `bertrand-tui-${randomUUID2()}.json`);
3183
+ const tmpFile = join10(tmpdir(), `bertrand-tui-${randomUUID2()}.json`);
3598
3184
  if (process.env.BERTRAND_DEBUG_TUI) {
3599
3185
  try {
3600
3186
  const { appendFileSync } = await import("fs");
@@ -3751,8 +3337,8 @@ var init_app = __esm(() => {
3751
3337
  init_create();
3752
3338
  init_resolve();
3753
3339
  SCREEN_ENTRY = (() => {
3754
- const built = join9(import.meta.dir, "run-screen.js");
3755
- return existsSync8(built) ? built : join9(import.meta.dir, "run-screen.tsx");
3340
+ const built = join10(import.meta.dir, "run-screen.js");
3341
+ return existsSync8(built) ? built : join10(import.meta.dir, "run-screen.tsx");
3756
3342
  })();
3757
3343
  });
3758
3344
 
@@ -3798,10 +3384,6 @@ function recoverStaleSessions() {
3798
3384
  status: "paused",
3799
3385
  pid: null
3800
3386
  });
3801
- emitSessionPausedByRecovery({
3802
- sessionId: session.id,
3803
- stalePid: session.pid
3804
- });
3805
3387
  recovered++;
3806
3388
  }
3807
3389
  }
@@ -3809,7 +3391,6 @@ function recoverStaleSessions() {
3809
3391
  }
3810
3392
  var init_recovery = __esm(() => {
3811
3393
  init_sessions();
3812
- init_emit();
3813
3394
  });
3814
3395
 
3815
3396
  // src/cli/commands/launch.ts
@@ -3878,14 +3459,11 @@ rm -f "${runtimeDir}/working-$sid"
3878
3459
 
3879
3460
  bq update --session-id "$sid" --event session.waiting --meta "$(jq -n --arg q "$question" --arg cid "$cid" '{question:$q, claude_id:$cid}')"
3880
3461
 
3881
- # Context snapshot \u2014 extract transcript path and capture token usage.
3882
- # assistant-message captures the latest turn's text + recap tag in one pass;
3883
- # replaces the older recap-thinking call which only grabbed the recap. Dedup
3884
- # inside the command makes it idempotent vs the matching Stop-time capture,
3885
- # so the same turn never lands twice.
3462
+ # Capture the latest assistant turn's text + recap tag. Dedup inside the
3463
+ # command makes it idempotent vs the matching Stop-time capture so the same
3464
+ # turn never lands twice.
3886
3465
  tpath="$(printf '%s' "$input" | grep -o '"transcript_path":"[^"]*"' | cut -d'"' -f4)"
3887
3466
  if [ -n "$tpath" ]; then
3888
- bq snapshot --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
3889
3467
  bq assistant-message --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
3890
3468
  fi
3891
3469
 
@@ -3949,35 +3527,9 @@ fi
3949
3527
  wait
3950
3528
  `;
3951
3529
  }
3952
- function activeScript(bin, runtimeDir) {
3953
- return `#!/usr/bin/env bash
3954
- # Hook: PreToolUse (catch-all) \u2192 flip waiting to active
3955
- ${quietHelper(bin)}
3956
- sid="\${BERTRAND_SESSION:-}"
3957
- [ -z "$sid" ] && exit 0
3958
-
3959
- # Debounce: skip if we already sent session.active within the last 5 seconds.
3960
- # This avoids spawning bertrand (~31ms) on every tool call during rapid sequences.
3961
- marker="${runtimeDir}/working-$sid"
3962
- if [ -f "$marker" ]; then
3963
- age=$(( $(date +%s) - $(stat -f%m "$marker" 2>/dev/null || echo 0) ))
3964
- [ "$age" -lt 5 ] && exit 0
3965
- fi
3966
-
3967
- input="$(cat)"
3968
- ${EXTRACT_TOOL}
3969
-
3970
- # AskUserQuestion has its own PreToolUse hook
3971
- [ "$tool" = "AskUserQuestion" ] && exit 0
3972
-
3973
- touch "$marker"
3974
- cid="\${BERTRAND_CLAUDE_ID:-}"
3975
- bq update --session-id "$sid" --event session.active --meta "$(jq -n --arg cid "$cid" '{claude_id:$cid}')"
3976
- `;
3977
- }
3978
3530
  function permissionWaitScript(bin, runtimeDir) {
3979
3531
  return `#!/usr/bin/env bash
3980
- # Hook: PermissionRequest \u2192 mark pending, emit permission.request
3532
+ # Hook: PermissionRequest \u2192 mark pending, badge + notify
3981
3533
  ${quietHelper(bin)}
3982
3534
  sid="\${BERTRAND_SESSION:-}"
3983
3535
  [ -z "$sid" ] && exit 0
@@ -3986,19 +3538,11 @@ input="$(cat)"
3986
3538
  ${EXTRACT_TOOL}
3987
3539
  [ "$tool" = "AskUserQuestion" ] && exit 0
3988
3540
 
3989
- # Write marker so PostToolUse knows this was a real permission prompt (not auto-approved)
3541
+ # Marker tells the PostToolUse hook to emit tool.used with outcome:approved
3542
+ # instead of outcome:auto. Without it, every prompted-then-approved tool call
3543
+ # would look identical to an auto-approved one.
3990
3544
  touch "${runtimeDir}/perm-pending-$sid"
3991
3545
 
3992
- # Extract detail from tool_input via grep (avoid jq for simple fields)
3993
- detail=""
3994
- case "$tool" in
3995
- Bash) detail="$(printf '%s' "$input" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-1000)" ;;
3996
- Edit|Write|Read) detail="$(printf '%s' "$input" | grep -o '"file_path":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-1000)" ;;
3997
- esac
3998
-
3999
- cid="\${BERTRAND_CLAUDE_ID:-}"
4000
- bq update --session-id "$sid" --event permission.request --meta "$(jq -n --arg t "$tool" --arg d "$detail" --arg cid "$cid" '{tool:$t, detail:$d, claude_id:$cid}')"
4001
-
4002
3546
  # Badge + notify in background
4003
3547
  bq badge bell-exclamation --color '#ff6b35' --priority 25 --beep &
4004
3548
  bq notify bertrand "Needs permission: $tool" &
@@ -4009,17 +3553,15 @@ function permissionDoneScript(bin, runtimeDir) {
4009
3553
  return `#!/usr/bin/env bash
4010
3554
  # Hook: PostToolUse (catch-all)
4011
3555
  #
4012
- # Captures every tool call Claude makes. Three event flows:
3556
+ # Captures every tool call Claude makes. Two event flows:
4013
3557
  # 1. Edit/Write/MultiEdit \u2192 tool.applied with diff payload. Keeps the
4014
3558
  # existing dashboard diff-renderer happy and is the only place we get
4015
3559
  # old_string/new_string on auto-approved edits.
4016
- # 2. Tools that went through a permission prompt \u2192 permission.resolve.
4017
- # The PermissionRequest hook set a marker; we clear it and log the
4018
- # approval. (Denials never reach PostToolUse, so absence-of-resolve
4019
- # means the user said no.)
4020
- # 3. Everything else (auto-approved Bash/Read/Grep/Glob/etc.) \u2192 tool.used
4021
- # with outcome:"auto". Previously this case dropped the call entirely;
4022
- # now Claude's read-only / shell activity shows up in the timeline.
3560
+ # 2. Everything else \u2192 tool.used. The PermissionRequest hook may have set
3561
+ # a marker; if so the call was prompted-then-approved (outcome:approved),
3562
+ # otherwise it was auto-approved (outcome:auto). Denials never reach
3563
+ # PostToolUse, so absence of a tool.used after a permission.request means
3564
+ # the user said no.
4023
3565
  ${quietHelper(bin)}
4024
3566
  sid="\${BERTRAND_SESSION:-}"
4025
3567
  [ -z "$sid" ] && exit 0
@@ -4078,14 +3620,9 @@ case "$tool" in
4078
3620
  WebSearch) detail="$(printf '%s' "$input" | grep -o '"query":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-200)" ;;
4079
3621
  esac
4080
3622
 
4081
- if [ "$had_marker" = "1" ]; then
4082
- # Prompted-then-approved path. Keep permission.resolve for back-compat
4083
- # rendering; downstream consumers can migrate to tool.used at their pace.
4084
- bq update --session-id "$sid" --event permission.resolve --meta "$(jq -n --arg t "$tool" --arg d "$detail" --arg cid "$cid" '{tool:$t, detail:$d, outcome:"approved", claude_id:$cid}')"
4085
- else
4086
- # Auto-approved path. Without tool.used, these calls were invisible.
4087
- bq update --session-id "$sid" --event tool.used --meta "$(jq -n --arg t "$tool" --arg d "$detail" --arg cid "$cid" '{tool:$t, detail:$d, outcome:"auto", claude_id:$cid}')"
4088
- fi
3623
+ outcome="auto"
3624
+ [ "$had_marker" = "1" ] && outcome="approved"
3625
+ bq update --session-id "$sid" --event tool.used --meta "$(jq -n --arg t "$tool" --arg d "$detail" --arg o "$outcome" --arg cid "$cid" '{tool:$t, detail:$d, outcome:$o, claude_id:$cid}')"
4089
3626
  wait
4090
3627
  `;
4091
3628
  }
@@ -4107,22 +3644,22 @@ bq update --session-id "$sid" --event user.prompt --meta "$meta"
4107
3644
  }
4108
3645
  function doneScript(bin, _runtimeDir) {
4109
3646
  return `#!/usr/bin/env bash
4110
- # Hook: Stop \u2192 mark session as paused
3647
+ # Hook: Stop \u2192 flip session status to paused; no event row written.
3648
+ # The status flip is driven by EVENT_STATUS_MAP[session.paused]; dispatchHookEvent
3649
+ # no longer has a session.paused case, so update.ts flips status without inserting.
4111
3650
  ${quietHelper(bin)}
4112
3651
  sid="\${BERTRAND_SESSION:-}"
4113
3652
  [ -z "$sid" ] && exit 0
4114
3653
 
4115
3654
  input="$(cat)"
4116
3655
  cid="\${BERTRAND_CLAUDE_ID:-}"
4117
- bq update --session-id "$sid" --event session.paused --meta "$(jq -n --arg cid "$cid" '{claude_id:$cid}')"
3656
+ bq update --session-id "$sid" --event session.paused
4118
3657
 
4119
- # Final transcript reads. assistant-message dedups against the most-recent
4120
- # AskUQ-time capture, so a Done-for-now exit (no work between AskUQ and Stop)
4121
- # lands zero new events here; intermediate Stops with fresh assistant output
4122
- # do land new ones.
3658
+ # Final assistant-message read. Dedups against the most-recent AskUQ-time
3659
+ # capture, so a Done-for-now exit (no work between AskUQ and Stop) lands zero
3660
+ # new events here; intermediate Stops with fresh assistant output do.
4123
3661
  tpath="$(printf '%s' "$input" | grep -o '"transcript_path":"[^"]*"' | cut -d'"' -f4)"
4124
3662
  if [ -n "$tpath" ]; then
4125
- bq snapshot --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
4126
3663
  bq assistant-message --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
4127
3664
  fi
4128
3665
 
@@ -4134,7 +3671,6 @@ var init_scripts = __esm(() => {
4134
3671
  HOOK_SCRIPTS = {
4135
3672
  "on-waiting.sh": waitingScript,
4136
3673
  "on-answered.sh": answeredScript,
4137
- "on-active.sh": activeScript,
4138
3674
  "on-permission-wait.sh": permissionWaitScript,
4139
3675
  "on-permission-done.sh": permissionDoneScript,
4140
3676
  "on-user-prompt.sh": userPromptScript,
@@ -4144,12 +3680,12 @@ var init_scripts = __esm(() => {
4144
3680
 
4145
3681
  // src/hooks/install.ts
4146
3682
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5, chmodSync as chmodSync2 } from "fs";
4147
- import { join as join10 } from "path";
3683
+ import { join as join11 } from "path";
4148
3684
  function installHookScripts(bin) {
4149
3685
  mkdirSync7(paths.hooks, { recursive: true });
4150
3686
  mkdirSync7(paths.runtime, { recursive: true });
4151
3687
  for (const [filename, scriptFn] of Object.entries(HOOK_SCRIPTS)) {
4152
- const filePath = join10(paths.hooks, filename);
3688
+ const filePath = join11(paths.hooks, filename);
4153
3689
  writeFileSync5(filePath, scriptFn(bin, paths.runtime));
4154
3690
  chmodSync2(filePath, 493);
4155
3691
  }
@@ -4162,8 +3698,8 @@ var init_install = __esm(() => {
4162
3698
 
4163
3699
  // src/hooks/settings.ts
4164
3700
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync8 } from "fs";
4165
- import { join as join11, dirname as dirname4 } from "path";
4166
- import { homedir as homedir2 } from "os";
3701
+ import { join as join12, dirname as dirname4 } from "path";
3702
+ import { homedir as homedir3 } from "os";
4167
3703
  function isBertrandGroup(group) {
4168
3704
  return group.hooks?.some((h) => h.command?.includes(".bertrand/hooks/")) ?? false;
4169
3705
  }
@@ -4187,16 +3723,12 @@ function installHookSettings() {
4187
3723
  var SETTINGS_PATH, BERTRAND_HOOKS;
4188
3724
  var init_settings = __esm(() => {
4189
3725
  init_paths();
4190
- SETTINGS_PATH = join11(homedir2(), ".claude", "settings.json");
3726
+ SETTINGS_PATH = join12(homedir3(), ".claude", "settings.json");
4191
3727
  BERTRAND_HOOKS = {
4192
3728
  PreToolUse: [
4193
3729
  {
4194
3730
  matcher: "AskUserQuestion",
4195
3731
  hooks: [{ type: "command", command: `${paths.hooks}/on-waiting.sh` }]
4196
- },
4197
- {
4198
- matcher: "",
4199
- hooks: [{ type: "command", command: `${paths.hooks}/on-active.sh` }]
4200
3732
  }
4201
3733
  ],
4202
3734
  PostToolUse: [
@@ -4232,7 +3764,7 @@ var init_settings = __esm(() => {
4232
3764
 
4233
3765
  // src/lib/completions.ts
4234
3766
  import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync9 } from "fs";
4235
- import { join as join12 } from "path";
3767
+ import { join as join13 } from "path";
4236
3768
  function bashCompletion() {
4237
3769
  return `# bertrand bash completion
4238
3770
  _bertrand() {
@@ -4262,11 +3794,11 @@ function fishCompletion() {
4262
3794
  `;
4263
3795
  }
4264
3796
  function generateCompletions() {
4265
- const dir = join12(paths.root, "completions");
3797
+ const dir = join13(paths.root, "completions");
4266
3798
  mkdirSync9(dir, { recursive: true });
4267
- writeFileSync7(join12(dir, "bertrand.bash"), bashCompletion());
4268
- writeFileSync7(join12(dir, "_bertrand"), zshCompletion());
4269
- writeFileSync7(join12(dir, "bertrand.fish"), fishCompletion());
3799
+ writeFileSync7(join13(dir, "bertrand.bash"), bashCompletion());
3800
+ writeFileSync7(join13(dir, "_bertrand"), zshCompletion());
3801
+ writeFileSync7(join13(dir, "bertrand.fish"), fishCompletion());
4270
3802
  console.log(`Shell completions written to ${dir}`);
4271
3803
  console.log(" Add to your shell config:");
4272
3804
  console.log(` bash: source ${dir}/bertrand.bash`);
@@ -4297,7 +3829,7 @@ var init_completions = __esm(() => {
4297
3829
  var exports_init = {};
4298
3830
  import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, chmodSync as chmodSync3 } from "fs";
4299
3831
  import { execSync as execSync2 } from "child_process";
4300
- import { join as join13 } from "path";
3832
+ import { join as join14 } from "path";
4301
3833
  function detectTerminal() {
4302
3834
  try {
4303
3835
  execSync2("which wsh", { stdio: "ignore" });
@@ -4312,8 +3844,8 @@ function resolveBin() {
4312
3844
  return onPath;
4313
3845
  const entry = process.argv[1];
4314
3846
  if (entry && SOURCE_ENTRY.test(entry)) {
4315
- const launcherDir = join13(paths.root, "bin");
4316
- const launcher = join13(launcherDir, "bertrand-dev");
3847
+ const launcherDir = join14(paths.root, "bin");
3848
+ const launcher = join14(launcherDir, "bertrand-dev");
4317
3849
  mkdirSync10(launcherDir, { recursive: true });
4318
3850
  writeFileSync8(launcher, `#!/usr/bin/env bash
4319
3851
  exec ${process.execPath} ${JSON.stringify(entry)} "$@"
@@ -4520,31 +4052,8 @@ function extractSummary(row) {
4520
4052
  const answers = meta.answers;
4521
4053
  return answers ? Object.values(answers).join(", ") : "";
4522
4054
  }
4523
- case "permission.request":
4524
- case "permission.resolve": {
4525
- const tool = meta.tool ?? "";
4526
- const detail = meta.detail ?? "";
4527
- return detail ? `${tool}: ${detail}` : tool;
4528
- }
4529
- case "gh.pr.created": {
4530
- const title = meta.pr_title ?? "";
4531
- const url = meta.pr_url ?? "";
4532
- return title || url;
4533
- }
4534
- case "gh.pr.merged":
4535
- return meta.branch ?? "";
4536
- case "worktree.entered":
4537
- return meta.branch ?? "";
4538
- case "linear.issue.read":
4539
- return meta.issue_title ?? "";
4540
- case "notion.page.read":
4541
- return meta.page_title ?? "";
4542
- case "vercel.deploy":
4543
- return meta.project_name ?? "";
4544
4055
  case "user.prompt":
4545
4056
  return meta.prompt ?? "";
4546
- case "context.snapshot":
4547
- return meta.remaining_pct ? `${meta.remaining_pct}% remaining` : "";
4548
4057
  case "session.recap":
4549
4058
  return meta.recap ?? "";
4550
4059
  default:
@@ -4574,26 +4083,13 @@ function enrichAll(rows) {
4574
4083
  var catalog, DEFAULT_INFO;
4575
4084
  var init_catalog = __esm(() => {
4576
4085
  catalog = {
4577
- "session.started": { label: "started", category: "lifecycle", color: 34, detailColor: 245, skip: false },
4578
- "session.resumed": { label: "resumed", category: "lifecycle", color: 34, detailColor: 245, skip: false },
4579
- "session.end": { label: "ended", category: "lifecycle", color: 245, detailColor: 245, skip: false },
4580
4086
  "claude.started": { label: "claude started", category: "lifecycle", color: 35, detailColor: 245, skip: false },
4581
4087
  "claude.ended": { label: "claude ended", category: "lifecycle", color: 35, detailColor: 245, skip: false },
4582
4088
  "claude.discarded": { label: "discarded", category: "lifecycle", color: 245, detailColor: 245, skip: false },
4583
4089
  "session.waiting": { label: "waiting", category: "interaction", color: 33, detailColor: 245, skip: false },
4584
4090
  "session.answered": { label: "answered", category: "interaction", color: 36, detailColor: 245, skip: false },
4585
- "permission.request": { label: "permission", category: "work", color: 214, detailColor: 245, skip: false },
4586
- "permission.resolve": { label: "allowed", category: "work", color: 214, detailColor: 245, skip: false },
4587
- "worktree.entered": { label: "worktree", category: "lifecycle", color: 35, detailColor: 245, skip: false },
4588
- "worktree.exited": { label: "worktree exited", category: "lifecycle", color: 35, detailColor: 245, skip: false },
4589
- "gh.pr.created": { label: "PR created", category: "integration", color: 32, detailColor: 32, skip: false },
4590
- "gh.pr.merged": { label: "PR merged", category: "integration", color: 35, detailColor: 35, skip: false },
4591
- "linear.issue.read": { label: "Linear issue", category: "integration", color: 33, detailColor: 33, skip: false },
4592
- "notion.page.read": { label: "Notion page", category: "integration", color: 245, detailColor: 245, skip: false },
4593
- "vercel.deploy": { label: "deployed", category: "integration", color: 245, detailColor: 245, skip: false },
4594
4091
  "user.prompt": { label: "prompt", category: "interaction", color: 36, detailColor: 245, skip: false },
4595
- "context.snapshot": { label: "context", category: "context", color: 245, detailColor: 245, skip: true },
4596
- "tool.used": { label: "tool", category: "work", color: 214, detailColor: 245, skip: true },
4092
+ "tool.used": { label: "tool", category: "work", color: 214, detailColor: 245, skip: false },
4597
4093
  "tool.work": { label: "tool work", category: "work", color: 214, detailColor: 245, skip: false },
4598
4094
  "session.recap": { label: "session recap", category: "lifecycle", color: 33, detailColor: 245, skip: false },
4599
4095
  "assistant.message": { label: "claude", category: "interaction", color: 39, detailColor: 245, skip: false },
@@ -4654,8 +4150,6 @@ function collapsePermissions(events2) {
4654
4150
  continue;
4655
4151
  const toolCounts = new Map;
4656
4152
  for (const pev of batch) {
4657
- if (pev.event !== "permission.request" && pev.event !== "tool.used")
4658
- continue;
4659
4153
  const meta = pev.meta;
4660
4154
  const tool = meta?.tool ?? "unknown";
4661
4155
  toolCounts.set(tool, (toolCounts.get(tool) ?? 0) + 1);
@@ -4717,8 +4211,6 @@ var ROLLUP_EVENTS;
4717
4211
  var init_compact = __esm(() => {
4718
4212
  init_catalog();
4719
4213
  ROLLUP_EVENTS = new Set([
4720
- "permission.request",
4721
- "permission.resolve",
4722
4214
  "tool.used"
4723
4215
  ]);
4724
4216
  });
@@ -4960,14 +4452,11 @@ function getMetrics(sessionId, name, status2) {
4960
4452
  const allEvents = getEventsBySession(sessionId);
4961
4453
  const conversations2 = new Set;
4962
4454
  let interactionCount = 0;
4963
- let prCount = 0;
4964
4455
  for (const ev of allEvents) {
4965
4456
  if (ev.conversationId)
4966
4457
  conversations2.add(ev.conversationId);
4967
4458
  if (ev.event === "session.waiting" || ev.event === "session.answered")
4968
4459
  interactionCount++;
4969
- if (ev.event === "gh.pr.created")
4970
- prCount++;
4971
4460
  }
4972
4461
  return {
4973
4462
  name,
@@ -4975,7 +4464,6 @@ function getMetrics(sessionId, name, status2) {
4975
4464
  eventCount: allEvents.length,
4976
4465
  conversationCount: conversations2.size,
4977
4466
  interactionCount,
4978
- prCount,
4979
4467
  claudeWorkS: Math.round(timing.totalClaudeWorkMs / 1000),
4980
4468
  userWaitS: Math.round(timing.totalUserWaitMs / 1000),
4981
4469
  activePct: timing.activePct,
@@ -4996,9 +4484,8 @@ function renderGlobal(metrics, isJson) {
4996
4484
  claudeWorkS: acc.claudeWorkS + m.claudeWorkS,
4997
4485
  userWaitS: acc.userWaitS + m.userWaitS,
4998
4486
  conversations: acc.conversations + m.conversationCount,
4999
- prs: acc.prs + m.prCount,
5000
4487
  interactions: acc.interactions + m.interactionCount
5001
- }), { sessions: 0, active: 0, durationS: 0, claudeWorkS: 0, userWaitS: 0, conversations: 0, prs: 0, interactions: 0 });
4488
+ }), { sessions: 0, active: 0, durationS: 0, claudeWorkS: 0, userWaitS: 0, conversations: 0, interactions: 0 });
5002
4489
  const totalTracked = totals.claudeWorkS + totals.userWaitS;
5003
4490
  const globalActivePct = totalTracked > 0 ? Math.round(totals.claudeWorkS / totalTracked * 100) : 0;
5004
4491
  if (isJson) {
@@ -5015,7 +4502,6 @@ function renderGlobal(metrics, isJson) {
5015
4502
  console.log(` Claude work: ${dur(totals.claudeWorkS)} ${dim}(${pct(globalActivePct)})${reset}`);
5016
4503
  console.log(` User wait: ${dur(totals.userWaitS)} ${dim}(${pct(100 - globalActivePct)})${reset}`);
5017
4504
  console.log(` Conversations: ${totals.conversations}`);
5018
- console.log(` PRs: ${totals.prs}`);
5019
4505
  console.log(` Interactions: ${totals.interactions}`);
5020
4506
  }
5021
4507
  function renderCategory(metrics, categoryPath, isJson) {
@@ -5030,21 +4516,20 @@ function renderCategory(metrics, categoryPath, isJson) {
5030
4516
  const maxName = Math.max(...sorted.map((m) => m.name.length), 4);
5031
4517
  console.log(`${bold}${categoryPath}${reset}
5032
4518
  `);
5033
- console.log(`${dim}${"NAME".padEnd(maxName)} ${"DURATION".padEnd(8)} ${"CLAUDE".padEnd(8)} ${"WAIT".padEnd(8)} ${"ACT%".padEnd(5)} ${"CONVOS".padEnd(6)} PRS${reset}`);
4519
+ console.log(`${dim}${"NAME".padEnd(maxName)} ${"DURATION".padEnd(8)} ${"CLAUDE".padEnd(8)} ${"WAIT".padEnd(8)} ${"ACT%".padEnd(5)} CONVOS${reset}`);
5034
4520
  for (const m of sorted) {
5035
- console.log(`${m.name.padEnd(maxName)} ${dur(m.durationS).padEnd(8)} ${dur(m.claudeWorkS).padEnd(8)} ${dur(m.userWaitS).padEnd(8)} ${pct(m.activePct).padEnd(5)} ${String(m.conversationCount).padEnd(6)} ${m.prCount}`);
4521
+ console.log(`${m.name.padEnd(maxName)} ${dur(m.durationS).padEnd(8)} ${dur(m.claudeWorkS).padEnd(8)} ${dur(m.userWaitS).padEnd(8)} ${pct(m.activePct).padEnd(5)} ${m.conversationCount}`);
5036
4522
  }
5037
4523
  const totals = sorted.reduce((acc, m) => ({
5038
4524
  durationS: acc.durationS + m.durationS,
5039
4525
  claudeWorkS: acc.claudeWorkS + m.claudeWorkS,
5040
4526
  userWaitS: acc.userWaitS + m.userWaitS,
5041
- conversations: acc.conversations + m.conversationCount,
5042
- prs: acc.prs + m.prCount
5043
- }), { durationS: 0, claudeWorkS: 0, userWaitS: 0, conversations: 0, prs: 0 });
4527
+ conversations: acc.conversations + m.conversationCount
4528
+ }), { durationS: 0, claudeWorkS: 0, userWaitS: 0, conversations: 0 });
5044
4529
  const totalTracked = totals.claudeWorkS + totals.userWaitS;
5045
4530
  const totalPct = totalTracked > 0 ? Math.round(totals.claudeWorkS / totalTracked * 100) : 0;
5046
4531
  console.log(`${dim}${"\u2500".repeat(maxName + 50)}${reset}`);
5047
- console.log(`${"TOTAL".padEnd(maxName)} ${dur(totals.durationS).padEnd(8)} ${dur(totals.claudeWorkS).padEnd(8)} ${dur(totals.userWaitS).padEnd(8)} ${pct(totalPct).padEnd(5)} ${String(totals.conversations).padEnd(6)} ${totals.prs}`);
4532
+ console.log(`${"TOTAL".padEnd(maxName)} ${dur(totals.durationS).padEnd(8)} ${dur(totals.claudeWorkS).padEnd(8)} ${dur(totals.userWaitS).padEnd(8)} ${pct(totalPct).padEnd(5)} ${totals.conversations}`);
5048
4533
  }
5049
4534
  function renderSession(m, isJson) {
5050
4535
  if (isJson) {
@@ -5062,7 +4547,6 @@ function renderSession(m, isJson) {
5062
4547
  console.log(` Events: ${m.eventCount}`);
5063
4548
  console.log(` Conversations: ${m.conversationCount}`);
5064
4549
  console.log(` Interactions: ${m.interactionCount}`);
5065
- console.log(` PRs: ${m.prCount}`);
5066
4550
  }
5067
4551
  var ACTIVE_STATUSES2;
5068
4552
  var init_stats2 = __esm(() => {
@@ -5512,9 +4996,7 @@ init_router();
5512
4996
  var command = process.argv[2];
5513
4997
  var hotPath = {
5514
4998
  update: () => Promise.resolve().then(() => (init_update(), exports_update)),
5515
- snapshot: () => Promise.resolve().then(() => (init_snapshot(), exports_snapshot)),
5516
4999
  "assistant-message": () => Promise.resolve().then(() => (init_assistant_message(), exports_assistant_message)),
5517
- "recap-thinking": () => Promise.resolve().then(() => (init_recap_thinking(), exports_recap_thinking)),
5518
5000
  badge: () => Promise.resolve().then(() => (init_badge(), exports_badge)),
5519
5001
  notify: () => Promise.resolve().then(() => (init_notify(), exports_notify)),
5520
5002
  serve: () => Promise.resolve().then(() => (init_serve(), exports_serve)),
@@ -5532,9 +5014,7 @@ if (command && command in hotPath) {
5532
5014
  Promise.resolve().then(() => (init_backfill_stats(), exports_backfill_stats)),
5533
5015
  Promise.resolve().then(() => (init_archive(), exports_archive)),
5534
5016
  Promise.resolve().then(() => (init_update(), exports_update)),
5535
- Promise.resolve().then(() => (init_snapshot(), exports_snapshot)),
5536
5017
  Promise.resolve().then(() => (init_assistant_message(), exports_assistant_message)),
5537
- Promise.resolve().then(() => (init_recap_thinking(), exports_recap_thinking)),
5538
5018
  Promise.resolve().then(() => (init_serve(), exports_serve)),
5539
5019
  Promise.resolve().then(() => (init_badge(), exports_badge)),
5540
5020
  Promise.resolve().then(() => (init_notify(), exports_notify)),