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 +83 -603
- package/dist/dashboard/assets/index-CCHVGoKS.js +654 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/migrations/0005_drop_pr_count.sql +1 -0
- package/dist/migrations/0006_drop_worktrees_last_question.sql +2 -0
- package/dist/migrations/meta/_journal.json +14 -0
- package/dist/run-screen.js +0 -15
- package/package.json +1 -1
- package/dist/dashboard/assets/index-SB5vH7Ec.js +0 -684
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
}
|
|
1371
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
2168
|
-
|
|
1830
|
+
join8(import.meta.dir, "dashboard"),
|
|
1831
|
+
join8(import.meta.dir, "..", "dashboard")
|
|
2169
1832
|
];
|
|
2170
1833
|
for (const dir of candidates) {
|
|
2171
|
-
if (existsSync6(
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
3755
|
-
return existsSync8(built) ? built :
|
|
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
|
-
#
|
|
3882
|
-
#
|
|
3883
|
-
#
|
|
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,
|
|
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
|
-
#
|
|
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.
|
|
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.
|
|
4017
|
-
#
|
|
4018
|
-
#
|
|
4019
|
-
#
|
|
4020
|
-
#
|
|
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
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
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
|
|
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
|
|
3656
|
+
bq update --session-id "$sid" --event session.paused
|
|
4118
3657
|
|
|
4119
|
-
# Final
|
|
4120
|
-
#
|
|
4121
|
-
#
|
|
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
|
|
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 =
|
|
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
|
|
4166
|
-
import { homedir as
|
|
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 =
|
|
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
|
|
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 =
|
|
3797
|
+
const dir = join13(paths.root, "completions");
|
|
4266
3798
|
mkdirSync9(dir, { recursive: true });
|
|
4267
|
-
writeFileSync7(
|
|
4268
|
-
writeFileSync7(
|
|
4269
|
-
writeFileSync7(
|
|
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
|
|
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 =
|
|
4316
|
-
const launcher =
|
|
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
|
-
"
|
|
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,
|
|
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)}
|
|
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)} ${
|
|
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
|
-
|
|
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)} ${
|
|
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)),
|