bertrand 0.19.0 → 0.20.0
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 +514 -179
- package/dist/dashboard/assets/{index-ofrZyO9k.js → index-DgvcDkSY.js} +101 -101
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/sw.js +1 -1
- package/dist/run-screen.js +464 -308
- package/package.json +1 -1
package/dist/bertrand.js
CHANGED
|
@@ -752,6 +752,7 @@ function openDb(dbPath) {
|
|
|
752
752
|
return cached;
|
|
753
753
|
mkdirSync4(dirname(dbPath), { recursive: true });
|
|
754
754
|
const sqlite = new Database(dbPath);
|
|
755
|
+
sqlite.exec("PRAGMA busy_timeout = 5000");
|
|
755
756
|
sqlite.exec("PRAGMA journal_mode = WAL");
|
|
756
757
|
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
757
758
|
sqlite.exec("PRAGMA synchronous = NORMAL");
|
|
@@ -761,6 +762,10 @@ function openDb(dbPath) {
|
|
|
761
762
|
if (!_migrated.has(dbPath)) {
|
|
762
763
|
try {
|
|
763
764
|
migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
|
|
765
|
+
if (!hasSessionsTable(sqlite)) {
|
|
766
|
+
sqlite.exec("DROP TABLE IF EXISTS __drizzle_migrations");
|
|
767
|
+
migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
|
|
768
|
+
}
|
|
764
769
|
} catch (err) {
|
|
765
770
|
sqlite.close();
|
|
766
771
|
throw err;
|
|
@@ -770,6 +775,10 @@ function openDb(dbPath) {
|
|
|
770
775
|
_cache.set(dbPath, db);
|
|
771
776
|
return db;
|
|
772
777
|
}
|
|
778
|
+
function hasSessionsTable(sqlite) {
|
|
779
|
+
const row = sqlite.query("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'").get();
|
|
780
|
+
return row !== null;
|
|
781
|
+
}
|
|
773
782
|
var MIGRATIONS_FOLDER, _cache, _migrated, _testDb = null;
|
|
774
783
|
var init_client = __esm(() => {
|
|
775
784
|
init_resolve();
|
|
@@ -833,6 +842,28 @@ var init_sessions = __esm(() => {
|
|
|
833
842
|
init_id();
|
|
834
843
|
});
|
|
835
844
|
|
|
845
|
+
// src/db/queries/conversations.ts
|
|
846
|
+
import { eq as eq2, and as and2, desc, sql as sql3 } from "drizzle-orm";
|
|
847
|
+
function createConversation(opts) {
|
|
848
|
+
return getDb().insert(conversations).values(opts).returning().get();
|
|
849
|
+
}
|
|
850
|
+
function getConversation(id) {
|
|
851
|
+
return getDb().select().from(conversations).where(eq2(conversations.id, id)).get();
|
|
852
|
+
}
|
|
853
|
+
function getConversationsBySession(sessionId) {
|
|
854
|
+
return getDb().select().from(conversations).where(and2(eq2(conversations.sessionId, sessionId), eq2(conversations.discarded, false))).orderBy(desc(conversations.startedAt)).all();
|
|
855
|
+
}
|
|
856
|
+
function endConversation(id) {
|
|
857
|
+
return getDb().update(conversations).set({ endedAt: sql3`(datetime('now'))` }).where(eq2(conversations.id, id)).returning().get();
|
|
858
|
+
}
|
|
859
|
+
function updateLastQuestion(id, question) {
|
|
860
|
+
return getDb().update(conversations).set({ lastQuestion: question }).where(eq2(conversations.id, id)).returning().get();
|
|
861
|
+
}
|
|
862
|
+
var init_conversations = __esm(() => {
|
|
863
|
+
init_client();
|
|
864
|
+
init_schema();
|
|
865
|
+
});
|
|
866
|
+
|
|
836
867
|
// src/lib/markdown.ts
|
|
837
868
|
function normalizeMarkdown(input) {
|
|
838
869
|
return input.replace(/\r\n?/g, `
|
|
@@ -882,7 +913,7 @@ function normalizeAnsweredMeta(meta) {
|
|
|
882
913
|
}
|
|
883
914
|
|
|
884
915
|
// src/db/queries/events.ts
|
|
885
|
-
import { eq as
|
|
916
|
+
import { eq as eq3, and as and3, desc as desc2 } from "drizzle-orm";
|
|
886
917
|
function insertEvent(opts) {
|
|
887
918
|
return getDb().insert(events).values({
|
|
888
919
|
sessionId: opts.sessionId,
|
|
@@ -893,17 +924,27 @@ function insertEvent(opts) {
|
|
|
893
924
|
}).returning().get();
|
|
894
925
|
}
|
|
895
926
|
function getEventsBySession(sessionId) {
|
|
896
|
-
return getDb().select().from(events).where(
|
|
927
|
+
return getDb().select().from(events).where(eq3(events.sessionId, sessionId)).orderBy(events.createdAt, events.id).all();
|
|
897
928
|
}
|
|
898
929
|
function getEventsByType(sessionId, eventType) {
|
|
899
|
-
return getDb().select().from(events).where(
|
|
930
|
+
return getDb().select().from(events).where(and3(eq3(events.sessionId, sessionId), eq3(events.event, eventType))).orderBy(events.createdAt).all();
|
|
931
|
+
}
|
|
932
|
+
function getLatestEventOfType(sessionId, eventType, conversationId) {
|
|
933
|
+
const conditions = [
|
|
934
|
+
eq3(events.sessionId, sessionId),
|
|
935
|
+
eq3(events.event, eventType)
|
|
936
|
+
];
|
|
937
|
+
if (conversationId) {
|
|
938
|
+
conditions.push(eq3(events.conversationId, conversationId));
|
|
939
|
+
}
|
|
940
|
+
return getDb().select().from(events).where(and3(...conditions)).orderBy(desc2(events.createdAt)).limit(1).get();
|
|
900
941
|
}
|
|
901
942
|
function getLatestRecaps() {
|
|
902
943
|
const rows = getDb().select({
|
|
903
944
|
sessionId: events.sessionId,
|
|
904
945
|
meta: events.meta,
|
|
905
946
|
createdAt: events.createdAt
|
|
906
|
-
}).from(events).where(
|
|
947
|
+
}).from(events).where(eq3(events.event, "session.recap")).orderBy(desc2(events.createdAt)).all();
|
|
907
948
|
const result = {};
|
|
908
949
|
for (const row of rows) {
|
|
909
950
|
if (result[row.sessionId])
|
|
@@ -921,26 +962,231 @@ var init_events = __esm(() => {
|
|
|
921
962
|
init_schema();
|
|
922
963
|
});
|
|
923
964
|
|
|
924
|
-
// src/db/
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
965
|
+
// src/db/events/emit.ts
|
|
966
|
+
function emitSessionStarted(args) {
|
|
967
|
+
return insertEvent({
|
|
968
|
+
sessionId: args.sessionId,
|
|
969
|
+
conversationId: args.conversationId,
|
|
970
|
+
event: "session.started",
|
|
971
|
+
meta: {
|
|
972
|
+
category_path: args.categoryPath,
|
|
973
|
+
session_name: args.sessionName,
|
|
974
|
+
session_slug: args.sessionSlug,
|
|
975
|
+
labels: args.labels,
|
|
976
|
+
summary: args.summary ?? null
|
|
977
|
+
}
|
|
978
|
+
});
|
|
928
979
|
}
|
|
929
|
-
function
|
|
930
|
-
return
|
|
980
|
+
function emitSessionResumed(args) {
|
|
981
|
+
return insertEvent({
|
|
982
|
+
sessionId: args.sessionId,
|
|
983
|
+
conversationId: args.conversationId,
|
|
984
|
+
event: "session.resumed",
|
|
985
|
+
meta: { claude_id: args.conversationId }
|
|
986
|
+
});
|
|
931
987
|
}
|
|
932
|
-
function
|
|
933
|
-
return
|
|
988
|
+
function emitSessionPaused(args) {
|
|
989
|
+
return insertEvent({
|
|
990
|
+
sessionId: args.sessionId,
|
|
991
|
+
conversationId: args.conversationId,
|
|
992
|
+
event: "session.paused",
|
|
993
|
+
meta: { claude_id: args.conversationId }
|
|
994
|
+
});
|
|
934
995
|
}
|
|
935
|
-
function
|
|
936
|
-
return
|
|
996
|
+
function emitSessionPausedByRecovery(args) {
|
|
997
|
+
return insertEvent({
|
|
998
|
+
sessionId: args.sessionId,
|
|
999
|
+
event: "session.paused",
|
|
1000
|
+
summary: "Recovered from stale state (process not found)",
|
|
1001
|
+
meta: { stale_pid: args.stalePid }
|
|
1002
|
+
});
|
|
937
1003
|
}
|
|
938
|
-
function
|
|
939
|
-
return
|
|
1004
|
+
function emitSessionEnded(args) {
|
|
1005
|
+
return insertEvent({
|
|
1006
|
+
sessionId: args.sessionId,
|
|
1007
|
+
event: "session.end"
|
|
1008
|
+
});
|
|
940
1009
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1010
|
+
function emitClaudeStarted(args) {
|
|
1011
|
+
return insertEvent({
|
|
1012
|
+
sessionId: args.sessionId,
|
|
1013
|
+
conversationId: args.conversationId,
|
|
1014
|
+
event: "claude.started",
|
|
1015
|
+
meta: {
|
|
1016
|
+
claude_id: args.conversationId,
|
|
1017
|
+
model: args.model,
|
|
1018
|
+
claude_version: args.claudeVersion,
|
|
1019
|
+
git: args.git,
|
|
1020
|
+
cwd: args.cwd
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
function emitClaudeEnded(args) {
|
|
1025
|
+
return insertEvent({
|
|
1026
|
+
sessionId: args.sessionId,
|
|
1027
|
+
conversationId: args.conversationId,
|
|
1028
|
+
event: "claude.ended",
|
|
1029
|
+
meta: { claude_id: args.conversationId, exit_code: args.exitCode }
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
function emitUserPrompted(args) {
|
|
1033
|
+
return insertEvent({
|
|
1034
|
+
sessionId: args.sessionId,
|
|
1035
|
+
conversationId: args.conversationId,
|
|
1036
|
+
event: "user.prompt",
|
|
1037
|
+
meta: { prompt: args.prompt, claude_id: args.conversationId }
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
function emitSessionWaiting(args) {
|
|
1041
|
+
return insertEvent({
|
|
1042
|
+
sessionId: args.sessionId,
|
|
1043
|
+
conversationId: args.conversationId,
|
|
1044
|
+
event: "session.waiting",
|
|
1045
|
+
summary: args.question,
|
|
1046
|
+
meta: { question: args.question, claude_id: args.conversationId }
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
function emitSessionAnswered(args) {
|
|
1050
|
+
const joinedAnswers = Object.values(args.answers).map((v) => String(v)).join(", ") || undefined;
|
|
1051
|
+
return insertEvent({
|
|
1052
|
+
sessionId: args.sessionId,
|
|
1053
|
+
conversationId: args.conversationId,
|
|
1054
|
+
event: "session.answered",
|
|
1055
|
+
summary: joinedAnswers,
|
|
1056
|
+
meta: {
|
|
1057
|
+
answers: args.answers,
|
|
1058
|
+
annotations: args.annotations ?? {},
|
|
1059
|
+
questions: args.questions ?? [],
|
|
1060
|
+
claude_id: args.conversationId
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
function emitSessionRecap(args) {
|
|
1065
|
+
return insertEvent({
|
|
1066
|
+
sessionId: args.sessionId,
|
|
1067
|
+
conversationId: args.conversationId,
|
|
1068
|
+
event: "session.recap",
|
|
1069
|
+
summary: args.recap.slice(0, 200),
|
|
1070
|
+
meta: { recap: args.recap, claude_id: args.conversationId }
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
function emitPermissionRequested(args) {
|
|
1074
|
+
return insertEvent({
|
|
1075
|
+
sessionId: args.sessionId,
|
|
1076
|
+
conversationId: args.conversationId,
|
|
1077
|
+
event: "permission.request",
|
|
1078
|
+
meta: {
|
|
1079
|
+
tool: args.tool,
|
|
1080
|
+
detail: args.detail,
|
|
1081
|
+
claude_id: args.conversationId
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
function emitPermissionResolved(args) {
|
|
1086
|
+
return insertEvent({
|
|
1087
|
+
sessionId: args.sessionId,
|
|
1088
|
+
conversationId: args.conversationId,
|
|
1089
|
+
event: "permission.resolve",
|
|
1090
|
+
meta: {
|
|
1091
|
+
tool: args.tool,
|
|
1092
|
+
detail: args.detail,
|
|
1093
|
+
outcome: args.outcome,
|
|
1094
|
+
claude_id: args.conversationId
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
function emitToolUsed(args) {
|
|
1099
|
+
const summary = formatToolSummary(args.tool, args.detail);
|
|
1100
|
+
return insertEvent({
|
|
1101
|
+
sessionId: args.sessionId,
|
|
1102
|
+
conversationId: args.conversationId,
|
|
1103
|
+
event: "tool.used",
|
|
1104
|
+
summary,
|
|
1105
|
+
meta: {
|
|
1106
|
+
tool: args.tool,
|
|
1107
|
+
detail: args.detail,
|
|
1108
|
+
outcome: args.outcome,
|
|
1109
|
+
claude_id: args.conversationId
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
function formatToolSummary(tool, detail) {
|
|
1114
|
+
if (!detail)
|
|
1115
|
+
return tool;
|
|
1116
|
+
switch (tool) {
|
|
1117
|
+
case "Bash":
|
|
1118
|
+
return `ran \`${detail.slice(0, 120)}\``;
|
|
1119
|
+
case "Read":
|
|
1120
|
+
return `read ${detail}`;
|
|
1121
|
+
case "Glob":
|
|
1122
|
+
case "Grep":
|
|
1123
|
+
return `${tool.toLowerCase()} ${detail}`;
|
|
1124
|
+
case "TodoWrite":
|
|
1125
|
+
return "updated todos";
|
|
1126
|
+
case "WebFetch":
|
|
1127
|
+
return `fetched ${detail}`;
|
|
1128
|
+
case "WebSearch":
|
|
1129
|
+
return `searched: ${detail}`;
|
|
1130
|
+
default:
|
|
1131
|
+
return `${tool}: ${detail}`;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function emitToolApplied(args) {
|
|
1135
|
+
return insertEvent({
|
|
1136
|
+
sessionId: args.sessionId,
|
|
1137
|
+
conversationId: args.conversationId,
|
|
1138
|
+
event: "tool.applied",
|
|
1139
|
+
summary: args.summary,
|
|
1140
|
+
meta: {
|
|
1141
|
+
permissions: args.permissions,
|
|
1142
|
+
outcome: "applied",
|
|
1143
|
+
claude_id: args.conversationId
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
function emitAssistantMessage(args) {
|
|
1148
|
+
return insertEvent({
|
|
1149
|
+
sessionId: args.sessionId,
|
|
1150
|
+
conversationId: args.conversationId,
|
|
1151
|
+
event: "assistant.message",
|
|
1152
|
+
summary: args.summary,
|
|
1153
|
+
meta: {
|
|
1154
|
+
model: args.model,
|
|
1155
|
+
text: args.text,
|
|
1156
|
+
thinkingBlocks: args.thinkingBlocks,
|
|
1157
|
+
thinkingBytes: args.thinkingBytes,
|
|
1158
|
+
claude_id: args.conversationId
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
function emitAssistantRecap(args) {
|
|
1163
|
+
return insertEvent({
|
|
1164
|
+
sessionId: args.sessionId,
|
|
1165
|
+
conversationId: args.conversationId,
|
|
1166
|
+
event: "assistant.recap",
|
|
1167
|
+
summary: args.recap.length > 80 ? `${args.recap.slice(0, 77)}...` : args.recap,
|
|
1168
|
+
meta: { recap: args.recap, claude_id: args.conversationId }
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
function emitContextSnapshot(args) {
|
|
1172
|
+
return insertEvent({
|
|
1173
|
+
sessionId: args.sessionId,
|
|
1174
|
+
conversationId: args.conversationId,
|
|
1175
|
+
event: "context.snapshot",
|
|
1176
|
+
summary: `${args.remainingPct}% remaining`,
|
|
1177
|
+
meta: {
|
|
1178
|
+
model: args.model,
|
|
1179
|
+
input_tokens: String(args.inputTokens),
|
|
1180
|
+
cache_creation_tokens: String(args.cacheCreationTokens),
|
|
1181
|
+
cache_read_tokens: String(args.cacheReadTokens),
|
|
1182
|
+
context_window_tokens: String(args.totalContextTokens),
|
|
1183
|
+
remaining_pct: String(args.remainingPct),
|
|
1184
|
+
claude_id: args.conversationId
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
var init_emit = __esm(() => {
|
|
1189
|
+
init_events();
|
|
944
1190
|
});
|
|
945
1191
|
|
|
946
1192
|
// src/cli/commands/update.ts
|
|
@@ -955,13 +1201,87 @@ function shouldIgnoreStatusFlip(newStatus, sessionPid) {
|
|
|
955
1201
|
return false;
|
|
956
1202
|
return sessionPid === null;
|
|
957
1203
|
}
|
|
1204
|
+
function dispatchHookEvent(event, ctx) {
|
|
1205
|
+
const { sessionId, conversationId, meta, summary } = ctx;
|
|
1206
|
+
switch (event) {
|
|
1207
|
+
case "user.prompt":
|
|
1208
|
+
emitUserPrompted({
|
|
1209
|
+
sessionId,
|
|
1210
|
+
conversationId,
|
|
1211
|
+
prompt: String(meta.prompt ?? "")
|
|
1212
|
+
});
|
|
1213
|
+
return true;
|
|
1214
|
+
case "session.waiting":
|
|
1215
|
+
emitSessionWaiting({
|
|
1216
|
+
sessionId,
|
|
1217
|
+
conversationId,
|
|
1218
|
+
question: String(meta.question ?? "Waiting for input")
|
|
1219
|
+
});
|
|
1220
|
+
return true;
|
|
1221
|
+
case "session.answered":
|
|
1222
|
+
emitSessionAnswered({
|
|
1223
|
+
sessionId,
|
|
1224
|
+
conversationId,
|
|
1225
|
+
answers: meta.answers ?? {},
|
|
1226
|
+
annotations: meta.annotations ?? {},
|
|
1227
|
+
questions: meta.questions ?? []
|
|
1228
|
+
});
|
|
1229
|
+
return true;
|
|
1230
|
+
case "session.recap":
|
|
1231
|
+
emitSessionRecap({
|
|
1232
|
+
sessionId,
|
|
1233
|
+
conversationId,
|
|
1234
|
+
recap: String(meta.recap ?? "")
|
|
1235
|
+
});
|
|
1236
|
+
return true;
|
|
1237
|
+
case "session.paused":
|
|
1238
|
+
emitSessionPaused({ sessionId, conversationId });
|
|
1239
|
+
return true;
|
|
1240
|
+
case "permission.request":
|
|
1241
|
+
emitPermissionRequested({
|
|
1242
|
+
sessionId,
|
|
1243
|
+
conversationId,
|
|
1244
|
+
tool: String(meta.tool ?? ""),
|
|
1245
|
+
detail: String(meta.detail ?? "")
|
|
1246
|
+
});
|
|
1247
|
+
return true;
|
|
1248
|
+
case "permission.resolve":
|
|
1249
|
+
emitPermissionResolved({
|
|
1250
|
+
sessionId,
|
|
1251
|
+
conversationId,
|
|
1252
|
+
tool: String(meta.tool ?? ""),
|
|
1253
|
+
detail: String(meta.detail ?? ""),
|
|
1254
|
+
outcome: meta.outcome === "denied" ? "denied" : "approved"
|
|
1255
|
+
});
|
|
1256
|
+
return true;
|
|
1257
|
+
case "tool.applied":
|
|
1258
|
+
emitToolApplied({
|
|
1259
|
+
sessionId,
|
|
1260
|
+
conversationId,
|
|
1261
|
+
summary: summary ?? "edited a file",
|
|
1262
|
+
permissions: meta.permissions ?? []
|
|
1263
|
+
});
|
|
1264
|
+
return true;
|
|
1265
|
+
case "tool.used":
|
|
1266
|
+
emitToolUsed({
|
|
1267
|
+
sessionId,
|
|
1268
|
+
conversationId,
|
|
1269
|
+
tool: String(meta.tool ?? "Unknown"),
|
|
1270
|
+
detail: String(meta.detail ?? ""),
|
|
1271
|
+
outcome: meta.outcome === "approved" ? "approved" : "auto"
|
|
1272
|
+
});
|
|
1273
|
+
return true;
|
|
1274
|
+
default:
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
958
1278
|
var EVENT_STATUS_MAP;
|
|
959
1279
|
var init_update = __esm(() => {
|
|
960
1280
|
init_router();
|
|
961
1281
|
init_sessions();
|
|
962
|
-
init_events();
|
|
963
1282
|
init_conversations();
|
|
964
1283
|
init_trigger();
|
|
1284
|
+
init_emit();
|
|
965
1285
|
EVENT_STATUS_MAP = {
|
|
966
1286
|
"session.waiting": "waiting",
|
|
967
1287
|
"session.answered": "active",
|
|
@@ -1006,7 +1326,7 @@ var init_update = __esm(() => {
|
|
|
1006
1326
|
return;
|
|
1007
1327
|
}
|
|
1008
1328
|
const ignoreStatusFlip = shouldIgnoreStatusFlip(newStatus, session.pid);
|
|
1009
|
-
let meta;
|
|
1329
|
+
let meta = {};
|
|
1010
1330
|
if (metaJson) {
|
|
1011
1331
|
try {
|
|
1012
1332
|
meta = JSON.parse(metaJson);
|
|
@@ -1017,15 +1337,15 @@ var init_update = __esm(() => {
|
|
|
1017
1337
|
}
|
|
1018
1338
|
const rawConvoId = meta?.claude_id || process.env.BERTRAND_CLAUDE_ID || undefined;
|
|
1019
1339
|
const conversationId = rawConvoId && getConversation(rawConvoId) ? rawConvoId : undefined;
|
|
1020
|
-
const
|
|
1021
|
-
const joinedAnswers = answersObj ? Object.values(answersObj).join(", ") || undefined : undefined;
|
|
1022
|
-
insertEvent({
|
|
1340
|
+
const dispatched = dispatchHookEvent(event, {
|
|
1023
1341
|
sessionId,
|
|
1024
1342
|
conversationId,
|
|
1025
|
-
|
|
1026
|
-
summary: summaryArg
|
|
1027
|
-
meta
|
|
1343
|
+
meta,
|
|
1344
|
+
summary: summaryArg
|
|
1028
1345
|
});
|
|
1346
|
+
if (!dispatched) {
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1029
1349
|
if (newStatus && !ignoreStatusFlip) {
|
|
1030
1350
|
updateSessionStatus(sessionId, newStatus);
|
|
1031
1351
|
}
|
|
@@ -1164,7 +1484,7 @@ var init_snapshot = __esm(() => {
|
|
|
1164
1484
|
init_router();
|
|
1165
1485
|
init_sessions();
|
|
1166
1486
|
init_conversations();
|
|
1167
|
-
|
|
1487
|
+
init_emit();
|
|
1168
1488
|
init_transcript();
|
|
1169
1489
|
register("snapshot", async (args) => {
|
|
1170
1490
|
let sessionId = "";
|
|
@@ -1197,20 +1517,15 @@ var init_snapshot = __esm(() => {
|
|
|
1197
1517
|
if (!snapshot)
|
|
1198
1518
|
return;
|
|
1199
1519
|
const convoId = conversationId && getConversation(conversationId) ? conversationId : undefined;
|
|
1200
|
-
|
|
1520
|
+
emitContextSnapshot({
|
|
1201
1521
|
sessionId,
|
|
1202
1522
|
conversationId: convoId,
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
cache_read_tokens: String(snapshot.cacheReadTokens),
|
|
1210
|
-
context_window_tokens: String(snapshot.totalContextTokens),
|
|
1211
|
-
remaining_pct: String(snapshot.remainingPct),
|
|
1212
|
-
claude_id: convoId
|
|
1213
|
-
}
|
|
1523
|
+
model: snapshot.model,
|
|
1524
|
+
inputTokens: snapshot.inputTokens,
|
|
1525
|
+
cacheCreationTokens: snapshot.cacheCreationTokens,
|
|
1526
|
+
cacheReadTokens: snapshot.cacheReadTokens,
|
|
1527
|
+
totalContextTokens: snapshot.totalContextTokens,
|
|
1528
|
+
remainingPct: snapshot.remainingPct
|
|
1214
1529
|
});
|
|
1215
1530
|
});
|
|
1216
1531
|
});
|
|
@@ -1223,14 +1538,16 @@ function summarize(text2) {
|
|
|
1223
1538
|
const trimmed = firstLine.trim();
|
|
1224
1539
|
return trimmed.length > 80 ? `${trimmed.slice(0, 77)}...` : trimmed;
|
|
1225
1540
|
}
|
|
1226
|
-
var
|
|
1541
|
+
var RECAP_RE, RECAP_TAG_GLOBAL;
|
|
1227
1542
|
var init_assistant_message = __esm(() => {
|
|
1228
1543
|
init_router();
|
|
1229
1544
|
init_sessions();
|
|
1230
1545
|
init_conversations();
|
|
1231
1546
|
init_events();
|
|
1547
|
+
init_emit();
|
|
1232
1548
|
init_transcript();
|
|
1233
|
-
|
|
1549
|
+
RECAP_RE = /<recap>([\s\S]*?)<\/recap>/i;
|
|
1550
|
+
RECAP_TAG_GLOBAL = /<recap>[\s\S]*?<\/recap>/gi;
|
|
1234
1551
|
register("assistant-message", async (args) => {
|
|
1235
1552
|
let sessionId = "";
|
|
1236
1553
|
let transcriptPath = "";
|
|
@@ -1261,34 +1578,50 @@ var init_assistant_message = __esm(() => {
|
|
|
1261
1578
|
const turn = getLatestAssistantTurn(transcriptPath);
|
|
1262
1579
|
if (!turn)
|
|
1263
1580
|
return;
|
|
1264
|
-
const
|
|
1581
|
+
const fullText = turn.text;
|
|
1582
|
+
const textSansRecap = fullText.replace(RECAP_TAG_GLOBAL, "").trim();
|
|
1265
1583
|
const convoId = conversationId && getConversation(conversationId) ? conversationId : undefined;
|
|
1266
|
-
|
|
1267
|
-
sessionId,
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1584
|
+
if (textSansRecap || turn.thinkingBlocks > 0) {
|
|
1585
|
+
const latestMsg = getLatestEventOfType(sessionId, "assistant.message", convoId);
|
|
1586
|
+
const latestText = latestMsg?.meta?.text;
|
|
1587
|
+
if (latestText !== textSansRecap) {
|
|
1588
|
+
emitAssistantMessage({
|
|
1589
|
+
sessionId,
|
|
1590
|
+
conversationId: convoId,
|
|
1591
|
+
text: textSansRecap,
|
|
1592
|
+
model: turn.model,
|
|
1593
|
+
thinkingBlocks: turn.thinkingBlocks,
|
|
1594
|
+
thinkingBytes: turn.thinkingBytes,
|
|
1595
|
+
summary: textSansRecap ? summarize(textSansRecap) : "thinking only"
|
|
1596
|
+
});
|
|
1277
1597
|
}
|
|
1278
|
-
}
|
|
1598
|
+
}
|
|
1599
|
+
const recapMatch = fullText.match(RECAP_RE);
|
|
1600
|
+
const recap = recapMatch?.[1]?.trim();
|
|
1601
|
+
if (recap) {
|
|
1602
|
+
const latestRecap = getLatestEventOfType(sessionId, "assistant.recap", convoId);
|
|
1603
|
+
const latestRecapText = latestRecap?.meta?.recap;
|
|
1604
|
+
if (latestRecapText !== recap) {
|
|
1605
|
+
emitAssistantRecap({
|
|
1606
|
+
sessionId,
|
|
1607
|
+
conversationId: convoId,
|
|
1608
|
+
recap
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1279
1612
|
});
|
|
1280
1613
|
});
|
|
1281
1614
|
|
|
1282
1615
|
// src/cli/commands/recap-thinking.ts
|
|
1283
1616
|
var exports_recap_thinking = {};
|
|
1284
|
-
var
|
|
1617
|
+
var RECAP_RE2;
|
|
1285
1618
|
var init_recap_thinking = __esm(() => {
|
|
1286
1619
|
init_router();
|
|
1287
1620
|
init_sessions();
|
|
1288
1621
|
init_conversations();
|
|
1289
|
-
|
|
1622
|
+
init_emit();
|
|
1290
1623
|
init_transcript();
|
|
1291
|
-
|
|
1624
|
+
RECAP_RE2 = /<recap>([\s\S]*?)<\/recap>/i;
|
|
1292
1625
|
register("recap-thinking", async (args) => {
|
|
1293
1626
|
let sessionId = "";
|
|
1294
1627
|
let transcriptPath = "";
|
|
@@ -1316,17 +1649,15 @@ var init_recap_thinking = __esm(() => {
|
|
|
1316
1649
|
const turn = getLatestAssistantTurn(transcriptPath);
|
|
1317
1650
|
if (!turn?.text)
|
|
1318
1651
|
return;
|
|
1319
|
-
const match = turn.text.match(
|
|
1652
|
+
const match = turn.text.match(RECAP_RE2);
|
|
1320
1653
|
const recap = match?.[1]?.trim();
|
|
1321
1654
|
if (!recap)
|
|
1322
1655
|
return;
|
|
1323
1656
|
const convoId = conversationId && getConversation(conversationId) ? conversationId : undefined;
|
|
1324
|
-
|
|
1657
|
+
emitAssistantRecap({
|
|
1325
1658
|
sessionId,
|
|
1326
1659
|
conversationId: convoId,
|
|
1327
|
-
|
|
1328
|
-
summary: recap.length > 80 ? `${recap.slice(0, 77)}...` : recap,
|
|
1329
|
-
meta: { recap, claude_id: convoId }
|
|
1660
|
+
recap
|
|
1330
1661
|
});
|
|
1331
1662
|
});
|
|
1332
1663
|
});
|
|
@@ -1646,6 +1977,14 @@ function aggregateToolUsage(sessionId) {
|
|
|
1646
1977
|
continue;
|
|
1647
1978
|
counts[tool] = (counts[tool] ?? 0) + 1;
|
|
1648
1979
|
}
|
|
1980
|
+
const used = getEventsByType(sessionId, "tool.used");
|
|
1981
|
+
for (const ev of used) {
|
|
1982
|
+
const meta = ev.meta;
|
|
1983
|
+
const tool = meta?.tool;
|
|
1984
|
+
if (!tool)
|
|
1985
|
+
continue;
|
|
1986
|
+
counts[tool] = (counts[tool] ?? 0) + 1;
|
|
1987
|
+
}
|
|
1649
1988
|
return counts;
|
|
1650
1989
|
}
|
|
1651
1990
|
function contextTokenStats(sessionId) {
|
|
@@ -2280,6 +2619,7 @@ function runMigrations(dbPath) {
|
|
|
2280
2619
|
const target = dbPath ?? resolveActiveProject().db;
|
|
2281
2620
|
mkdirSync5(dirname3(target), { recursive: true });
|
|
2282
2621
|
const sqlite = new Database3(target);
|
|
2622
|
+
sqlite.exec("PRAGMA busy_timeout = 5000");
|
|
2283
2623
|
sqlite.exec("PRAGMA journal_mode = WAL");
|
|
2284
2624
|
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
2285
2625
|
const db = drizzle2(sqlite);
|
|
@@ -3145,29 +3485,22 @@ async function launch(opts) {
|
|
|
3145
3485
|
await ensureServerStarted();
|
|
3146
3486
|
const spawnContext = await captureSpawnContext();
|
|
3147
3487
|
const sessionName = `${opts.categoryPath}/${opts.slug}`;
|
|
3148
|
-
|
|
3488
|
+
emitSessionStarted({
|
|
3149
3489
|
sessionId: session.id,
|
|
3150
3490
|
conversationId: claudeId,
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
labels: opts.labelNames ?? [],
|
|
3157
|
-
summary: session.summary ?? null
|
|
3158
|
-
}
|
|
3491
|
+
categoryPath: opts.categoryPath,
|
|
3492
|
+
sessionName: opts.name ?? opts.slug,
|
|
3493
|
+
sessionSlug: opts.slug,
|
|
3494
|
+
labels: opts.labelNames ?? [],
|
|
3495
|
+
summary: session.summary ?? null
|
|
3159
3496
|
});
|
|
3160
|
-
|
|
3497
|
+
emitClaudeStarted({
|
|
3161
3498
|
sessionId: session.id,
|
|
3162
3499
|
conversationId: claudeId,
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
claude_version: spawnContext.claudeVersion,
|
|
3168
|
-
git: spawnContext.git,
|
|
3169
|
-
cwd: spawnContext.cwd
|
|
3170
|
-
}
|
|
3500
|
+
model: spawnContext.model,
|
|
3501
|
+
claudeVersion: spawnContext.claudeVersion,
|
|
3502
|
+
git: spawnContext.git,
|
|
3503
|
+
cwd: spawnContext.cwd
|
|
3171
3504
|
});
|
|
3172
3505
|
const siblingContext = buildSiblingContext(categoryId, opts.categoryPath, session.id);
|
|
3173
3506
|
const contract = buildContract(sessionName, siblingContext);
|
|
@@ -3191,11 +3524,9 @@ async function resume(opts) {
|
|
|
3191
3524
|
liveSession = { sessionId: session.id, claudeId: opts.conversationId };
|
|
3192
3525
|
installExitHandlers();
|
|
3193
3526
|
await ensureServerStarted();
|
|
3194
|
-
|
|
3527
|
+
emitSessionResumed({
|
|
3195
3528
|
sessionId: session.id,
|
|
3196
|
-
conversationId: opts.conversationId
|
|
3197
|
-
event: "session.resumed",
|
|
3198
|
-
meta: { claude_id: opts.conversationId }
|
|
3529
|
+
conversationId: opts.conversationId
|
|
3199
3530
|
});
|
|
3200
3531
|
const categoryPath = category?.path ?? "";
|
|
3201
3532
|
const siblingContext = buildSiblingContext(session.categoryId, categoryPath, session.id);
|
|
@@ -3219,11 +3550,10 @@ function finalizeSession(sessionId, conversationId, exitCode) {
|
|
|
3219
3550
|
if (conversationExists) {
|
|
3220
3551
|
endConversation(conversationId);
|
|
3221
3552
|
}
|
|
3222
|
-
|
|
3553
|
+
emitClaudeEnded({
|
|
3223
3554
|
sessionId,
|
|
3224
3555
|
conversationId: safeConversationId,
|
|
3225
|
-
|
|
3226
|
-
meta: { claude_id: conversationId, exit_code: exitCode }
|
|
3556
|
+
exitCode
|
|
3227
3557
|
});
|
|
3228
3558
|
updateSession(sessionId, {
|
|
3229
3559
|
status: "paused",
|
|
@@ -3232,10 +3562,7 @@ function finalizeSession(sessionId, conversationId, exitCode) {
|
|
|
3232
3562
|
});
|
|
3233
3563
|
if (liveSession?.sessionId === sessionId)
|
|
3234
3564
|
liveSession = null;
|
|
3235
|
-
|
|
3236
|
-
sessionId,
|
|
3237
|
-
event: "session.end"
|
|
3238
|
-
});
|
|
3565
|
+
emitSessionEnded({ sessionId });
|
|
3239
3566
|
computeAndPersist(sessionId);
|
|
3240
3567
|
stopServerIfIdle();
|
|
3241
3568
|
}
|
|
@@ -3243,7 +3570,7 @@ var liveSession = null, exitHandlersInstalled = false;
|
|
|
3243
3570
|
var init_session = __esm(() => {
|
|
3244
3571
|
init_sessions();
|
|
3245
3572
|
init_conversations();
|
|
3246
|
-
|
|
3573
|
+
init_emit();
|
|
3247
3574
|
init_categories();
|
|
3248
3575
|
init_labels();
|
|
3249
3576
|
init_template2();
|
|
@@ -3274,11 +3601,8 @@ async function runScreen(screen, ...args) {
|
|
|
3274
3601
|
unlinkSync3(tmpFile);
|
|
3275
3602
|
return result;
|
|
3276
3603
|
}
|
|
3277
|
-
async function
|
|
3278
|
-
return runScreen("
|
|
3279
|
-
}
|
|
3280
|
-
async function startProjectPickerTui() {
|
|
3281
|
-
return runScreen("project-picker");
|
|
3604
|
+
async function startStartupTui(skipProjectPicker, initialProjectSlug) {
|
|
3605
|
+
return runScreen("startup", String(skipProjectPicker), initialProjectSlug);
|
|
3282
3606
|
}
|
|
3283
3607
|
async function startExitTui(sessionId) {
|
|
3284
3608
|
return runScreen("exit", sessionId);
|
|
@@ -3334,17 +3658,19 @@ function shouldShowProjectPicker() {
|
|
|
3334
3658
|
const projects = listProjects();
|
|
3335
3659
|
return projects.length !== 1;
|
|
3336
3660
|
}
|
|
3337
|
-
function
|
|
3338
|
-
|
|
3661
|
+
async function startTui() {
|
|
3662
|
+
const skipProjectPicker = !shouldShowProjectPicker();
|
|
3663
|
+
const initialProjectSlug = getActiveProjectSlug();
|
|
3664
|
+
const selection = await startStartupTui(skipProjectPicker, initialProjectSlug);
|
|
3339
3665
|
_resetActiveProjectCache();
|
|
3340
|
-
}
|
|
3341
|
-
async function runLaunchCycle() {
|
|
3342
|
-
const selection = await startLaunchTui();
|
|
3343
3666
|
switch (selection.type) {
|
|
3344
3667
|
case "quit":
|
|
3345
3668
|
return;
|
|
3346
3669
|
case "create": {
|
|
3347
|
-
const sessionId = await launch(
|
|
3670
|
+
const sessionId = await launch({
|
|
3671
|
+
categoryPath: selection.categoryPath,
|
|
3672
|
+
slug: selection.slug
|
|
3673
|
+
});
|
|
3348
3674
|
await runSessionLoop(sessionId);
|
|
3349
3675
|
return;
|
|
3350
3676
|
}
|
|
@@ -3361,28 +3687,6 @@ async function runLaunchCycle() {
|
|
|
3361
3687
|
}
|
|
3362
3688
|
}
|
|
3363
3689
|
}
|
|
3364
|
-
async function startTui() {
|
|
3365
|
-
if (!shouldShowProjectPicker()) {
|
|
3366
|
-
await runLaunchCycle();
|
|
3367
|
-
return;
|
|
3368
|
-
}
|
|
3369
|
-
const projectSelection = await startProjectPickerTui();
|
|
3370
|
-
switch (projectSelection.type) {
|
|
3371
|
-
case "quit":
|
|
3372
|
-
return;
|
|
3373
|
-
case "select": {
|
|
3374
|
-
activateProject(projectSelection.slug);
|
|
3375
|
-
await runLaunchCycle();
|
|
3376
|
-
return;
|
|
3377
|
-
}
|
|
3378
|
-
case "create": {
|
|
3379
|
-
createProject({ slug: projectSelection.slug });
|
|
3380
|
-
activateProject(projectSelection.slug);
|
|
3381
|
-
await runLaunchCycle();
|
|
3382
|
-
return;
|
|
3383
|
-
}
|
|
3384
|
-
}
|
|
3385
|
-
}
|
|
3386
3690
|
var SCREEN_ENTRY;
|
|
3387
3691
|
var init_app = __esm(() => {
|
|
3388
3692
|
init_sessions();
|
|
@@ -3390,7 +3694,6 @@ var init_app = __esm(() => {
|
|
|
3390
3694
|
init_session_archive();
|
|
3391
3695
|
init_session();
|
|
3392
3696
|
init_registry();
|
|
3393
|
-
init_create();
|
|
3394
3697
|
init_resolve();
|
|
3395
3698
|
SCREEN_ENTRY = (() => {
|
|
3396
3699
|
const built = join9(import.meta.dir, "run-screen.js");
|
|
@@ -3436,11 +3739,9 @@ function recoverStaleSessions() {
|
|
|
3436
3739
|
status: "paused",
|
|
3437
3740
|
pid: null
|
|
3438
3741
|
});
|
|
3439
|
-
|
|
3742
|
+
emitSessionPausedByRecovery({
|
|
3440
3743
|
sessionId: session.id,
|
|
3441
|
-
|
|
3442
|
-
summary: "Recovered from stale state (process not found)",
|
|
3443
|
-
meta: { stale_pid: session.pid }
|
|
3744
|
+
stalePid: session.pid
|
|
3444
3745
|
});
|
|
3445
3746
|
recovered++;
|
|
3446
3747
|
}
|
|
@@ -3449,7 +3750,7 @@ function recoverStaleSessions() {
|
|
|
3449
3750
|
}
|
|
3450
3751
|
var init_recovery = __esm(() => {
|
|
3451
3752
|
init_sessions();
|
|
3452
|
-
|
|
3753
|
+
init_emit();
|
|
3453
3754
|
});
|
|
3454
3755
|
|
|
3455
3756
|
// src/cli/commands/launch.ts
|
|
@@ -3473,10 +3774,13 @@ var init_launch = __esm(() => {
|
|
|
3473
3774
|
});
|
|
3474
3775
|
|
|
3475
3776
|
// src/hooks/scripts.ts
|
|
3777
|
+
function quietHelper(bin) {
|
|
3778
|
+
return `bq() { ${bin} "$@" 2>/dev/null || true; }`;
|
|
3779
|
+
}
|
|
3476
3780
|
function waitingScript(bin) {
|
|
3477
|
-
const BIN = bin;
|
|
3478
3781
|
return `#!/usr/bin/env bash
|
|
3479
3782
|
# Hook: PreToolUse AskUserQuestion \u2192 enforce multiSelect, mark session as waiting
|
|
3783
|
+
${quietHelper(bin)}
|
|
3480
3784
|
sid="\${BERTRAND_SESSION:-}"
|
|
3481
3785
|
[ -z "$sid" ] && exit 0
|
|
3482
3786
|
|
|
@@ -3500,23 +3804,26 @@ question="$(printf '%s' "$input" | grep -o '"question":"[^"]*"' | head -1 | cut
|
|
|
3500
3804
|
# Clear working debounce marker so next resume\u2192working transition fires
|
|
3501
3805
|
rm -f "/tmp/bertrand-working-$sid"
|
|
3502
3806
|
|
|
3503
|
-
|
|
3807
|
+
bq update --session-id "$sid" --event session.waiting --meta "$(jq -n --arg q "$question" --arg cid "$cid" '{question:$q, claude_id:$cid}')"
|
|
3504
3808
|
|
|
3505
|
-
# Context snapshot \u2014 extract transcript path and capture token usage
|
|
3809
|
+
# Context snapshot \u2014 extract transcript path and capture token usage.
|
|
3810
|
+
# assistant-message captures the latest turn's text + recap tag in one pass;
|
|
3811
|
+
# replaces the older recap-thinking call which only grabbed the recap. Dedup
|
|
3812
|
+
# inside the command makes it idempotent vs the matching Stop-time capture,
|
|
3813
|
+
# so the same turn never lands twice.
|
|
3506
3814
|
tpath="$(printf '%s' "$input" | grep -o '"transcript_path":"[^"]*"' | cut -d'"' -f4)"
|
|
3507
3815
|
if [ -n "$tpath" ]; then
|
|
3508
|
-
|
|
3509
|
-
|
|
3816
|
+
bq snapshot --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
|
|
3817
|
+
bq assistant-message --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
|
|
3510
3818
|
fi
|
|
3511
3819
|
|
|
3512
3820
|
# Badge + notify in background \u2014 terminal UI doesn't need to block Claude
|
|
3513
|
-
|
|
3514
|
-
|
|
3821
|
+
bq badge message-question --color '#e0b956' --priority 20 --beep &
|
|
3822
|
+
bq notify bertrand "$question" &
|
|
3515
3823
|
wait
|
|
3516
3824
|
`;
|
|
3517
3825
|
}
|
|
3518
3826
|
function answeredScript(bin) {
|
|
3519
|
-
const BIN = bin;
|
|
3520
3827
|
return `#!/usr/bin/env bash
|
|
3521
3828
|
# Hook: PostToolUse AskUserQuestion \u2192 mark session as active
|
|
3522
3829
|
#
|
|
@@ -3524,6 +3831,7 @@ function answeredScript(bin) {
|
|
|
3524
3831
|
# Claude Code so the agent halts immediately instead of taking another turn.
|
|
3525
3832
|
# This is the mechanical enforcement of the contract's loop-exit rule \u2014 the
|
|
3526
3833
|
# contract prose is a soft hint, this JSON is the guarantee.
|
|
3834
|
+
${quietHelper(bin)}
|
|
3527
3835
|
sid="\${BERTRAND_SESSION:-}"
|
|
3528
3836
|
[ -z "$sid" ] && exit 0
|
|
3529
3837
|
|
|
@@ -3545,9 +3853,9 @@ meta="$(printf '%s' "$input" | jq --arg cid "$cid" '
|
|
|
3545
3853
|
# Concatenate all answer values into a single string for the Done-for-now check.
|
|
3546
3854
|
done_check="$(printf '%s' "$meta" | jq -r '.answers | to_entries | map(.value | tostring) | join(" ")' 2>/dev/null)"
|
|
3547
3855
|
|
|
3548
|
-
|
|
3856
|
+
bq update --session-id "$sid" --event session.answered --meta "$meta"
|
|
3549
3857
|
|
|
3550
|
-
|
|
3858
|
+
bq badge --clear &
|
|
3551
3859
|
|
|
3552
3860
|
# Halt the agent loop if the user signaled Done for now. The Stop hook
|
|
3553
3861
|
# (on-done.sh) will fire afterwards and mark the session as paused.
|
|
@@ -3560,7 +3868,7 @@ if printf '%s' "$done_check" | grep -q "Done for now"; then
|
|
|
3560
3868
|
[.questions[]?.options[]? | select(.label == "Done for now") | .description] | first // empty
|
|
3561
3869
|
' 2>/dev/null)"
|
|
3562
3870
|
if [ -n "$recap" ]; then
|
|
3563
|
-
|
|
3871
|
+
bq update --session-id "$sid" --event session.recap --meta "$(jq -n --arg recap "$recap" --arg cid "$cid" '{recap:$recap, claude_id:$cid}')"
|
|
3564
3872
|
fi
|
|
3565
3873
|
|
|
3566
3874
|
printf '{"continue": false, "stopReason": "User selected Done for now"}\\n'
|
|
@@ -3570,9 +3878,9 @@ wait
|
|
|
3570
3878
|
`;
|
|
3571
3879
|
}
|
|
3572
3880
|
function activeScript(bin) {
|
|
3573
|
-
const BIN = bin;
|
|
3574
3881
|
return `#!/usr/bin/env bash
|
|
3575
3882
|
# Hook: PreToolUse (catch-all) \u2192 flip waiting to active
|
|
3883
|
+
${quietHelper(bin)}
|
|
3576
3884
|
sid="\${BERTRAND_SESSION:-}"
|
|
3577
3885
|
[ -z "$sid" ] && exit 0
|
|
3578
3886
|
|
|
@@ -3592,13 +3900,13 @@ ${EXTRACT_TOOL}
|
|
|
3592
3900
|
|
|
3593
3901
|
touch "$marker"
|
|
3594
3902
|
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
3595
|
-
|
|
3903
|
+
bq update --session-id "$sid" --event session.active --meta "$(jq -n --arg cid "$cid" '{claude_id:$cid}')"
|
|
3596
3904
|
`;
|
|
3597
3905
|
}
|
|
3598
3906
|
function permissionWaitScript(bin) {
|
|
3599
|
-
const BIN = bin;
|
|
3600
3907
|
return `#!/usr/bin/env bash
|
|
3601
3908
|
# Hook: PermissionRequest \u2192 mark pending, emit permission.request
|
|
3909
|
+
${quietHelper(bin)}
|
|
3602
3910
|
sid="\${BERTRAND_SESSION:-}"
|
|
3603
3911
|
[ -z "$sid" ] && exit 0
|
|
3604
3912
|
|
|
@@ -3617,38 +3925,45 @@ case "$tool" in
|
|
|
3617
3925
|
esac
|
|
3618
3926
|
|
|
3619
3927
|
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
3620
|
-
|
|
3928
|
+
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}')"
|
|
3621
3929
|
|
|
3622
3930
|
# Badge + notify in background
|
|
3623
|
-
|
|
3624
|
-
|
|
3931
|
+
bq badge bell-exclamation --color '#ff6b35' --priority 25 --beep &
|
|
3932
|
+
bq notify bertrand "Needs permission: $tool" &
|
|
3625
3933
|
wait
|
|
3626
3934
|
`;
|
|
3627
3935
|
}
|
|
3628
3936
|
function permissionDoneScript(bin) {
|
|
3629
|
-
const BIN = bin;
|
|
3630
3937
|
return `#!/usr/bin/env bash
|
|
3631
3938
|
# Hook: PostToolUse (catch-all)
|
|
3632
3939
|
#
|
|
3633
|
-
#
|
|
3634
|
-
# 1. Edit/Write/MultiEdit
|
|
3635
|
-
#
|
|
3636
|
-
#
|
|
3637
|
-
# 2.
|
|
3638
|
-
#
|
|
3639
|
-
#
|
|
3940
|
+
# Captures every tool call Claude makes. Three event flows:
|
|
3941
|
+
# 1. Edit/Write/MultiEdit \u2192 tool.applied with diff payload. Keeps the
|
|
3942
|
+
# existing dashboard diff-renderer happy and is the only place we get
|
|
3943
|
+
# old_string/new_string on auto-approved edits.
|
|
3944
|
+
# 2. Tools that went through a permission prompt \u2192 permission.resolve.
|
|
3945
|
+
# The PermissionRequest hook set a marker; we clear it and log the
|
|
3946
|
+
# approval. (Denials never reach PostToolUse, so absence-of-resolve
|
|
3947
|
+
# means the user said no.)
|
|
3948
|
+
# 3. Everything else (auto-approved Bash/Read/Grep/Glob/etc.) \u2192 tool.used
|
|
3949
|
+
# with outcome:"auto". Previously this case dropped the call entirely;
|
|
3950
|
+
# now Claude's read-only / shell activity shows up in the timeline.
|
|
3951
|
+
${quietHelper(bin)}
|
|
3640
3952
|
sid="\${BERTRAND_SESSION:-}"
|
|
3641
3953
|
[ -z "$sid" ] && exit 0
|
|
3642
3954
|
|
|
3643
3955
|
input="$(cat)"
|
|
3644
3956
|
${EXTRACT_TOOL}
|
|
3645
3957
|
|
|
3958
|
+
# Don't double-log: AskUserQuestion has its own waiting/answered events
|
|
3959
|
+
[ "$tool" = "AskUserQuestion" ] && exit 0
|
|
3960
|
+
|
|
3646
3961
|
marker="/tmp/bertrand-perm-pending-$sid"
|
|
3647
3962
|
had_marker=0
|
|
3648
3963
|
if [ -f "$marker" ]; then
|
|
3649
3964
|
had_marker=1
|
|
3650
3965
|
rm -f "$marker"
|
|
3651
|
-
|
|
3966
|
+
bq badge --clear &
|
|
3652
3967
|
fi
|
|
3653
3968
|
|
|
3654
3969
|
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
@@ -3660,10 +3975,6 @@ case "$tool" in
|
|
|
3660
3975
|
Write) summary="wrote a file" ;;
|
|
3661
3976
|
*) summary="edited a file" ;;
|
|
3662
3977
|
esac
|
|
3663
|
-
# Single jq pass: build meta.permissions[] with diff data so the dashboard renders
|
|
3664
|
-
# via the existing WorkContent path (same shape as collapsed permission events).
|
|
3665
|
-
# Emit camelCase keys (oldStr/newStr/edits) directly so WorkContent reads
|
|
3666
|
-
# meta.permissions[] without going through transforms.ts's snake\u2192camel adapter.
|
|
3667
3978
|
meta="$(printf '%s' "$input" | jq --arg t "$tool" --arg d "$detail" --arg cid "$cid" '
|
|
3668
3979
|
{
|
|
3669
3980
|
permissions: [
|
|
@@ -3676,28 +3987,40 @@ case "$tool" in
|
|
|
3676
3987
|
claude_id: $cid
|
|
3677
3988
|
}
|
|
3678
3989
|
')"
|
|
3679
|
-
|
|
3990
|
+
bq update --session-id "$sid" --event tool.applied --summary "$summary" --meta "$meta"
|
|
3680
3991
|
wait
|
|
3681
3992
|
exit 0
|
|
3682
3993
|
;;
|
|
3683
3994
|
esac
|
|
3684
3995
|
|
|
3685
|
-
#
|
|
3686
|
-
|
|
3687
|
-
|
|
3996
|
+
# Extract a tool-shaped detail for the timeline summary. Bash gets the
|
|
3997
|
+
# command, file tools get the path; everything else falls back to a generic
|
|
3998
|
+
# label inside the emit helper.
|
|
3688
3999
|
detail=""
|
|
3689
4000
|
case "$tool" in
|
|
3690
4001
|
Bash) detail="$(printf '%s' "$input" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-1000)" ;;
|
|
4002
|
+
Read|NotebookRead) detail="$(printf '%s' "$input" | grep -o '"file_path":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-1000)" ;;
|
|
4003
|
+
Glob) detail="$(printf '%s' "$input" | grep -o '"pattern":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-200)" ;;
|
|
4004
|
+
Grep) detail="$(printf '%s' "$input" | grep -o '"pattern":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-200)" ;;
|
|
4005
|
+
WebFetch) detail="$(printf '%s' "$input" | grep -o '"url":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-300)" ;;
|
|
4006
|
+
WebSearch) detail="$(printf '%s' "$input" | grep -o '"query":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-200)" ;;
|
|
3691
4007
|
esac
|
|
3692
4008
|
|
|
3693
|
-
|
|
4009
|
+
if [ "$had_marker" = "1" ]; then
|
|
4010
|
+
# Prompted-then-approved path. Keep permission.resolve for back-compat
|
|
4011
|
+
# rendering; downstream consumers can migrate to tool.used at their pace.
|
|
4012
|
+
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}')"
|
|
4013
|
+
else
|
|
4014
|
+
# Auto-approved path. Without tool.used, these calls were invisible.
|
|
4015
|
+
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}')"
|
|
4016
|
+
fi
|
|
3694
4017
|
wait
|
|
3695
4018
|
`;
|
|
3696
4019
|
}
|
|
3697
4020
|
function userPromptScript(bin) {
|
|
3698
|
-
const BIN = bin;
|
|
3699
4021
|
return `#!/usr/bin/env bash
|
|
3700
4022
|
# Hook: UserPromptSubmit \u2192 record user free-text prompt
|
|
4023
|
+
${quietHelper(bin)}
|
|
3701
4024
|
sid="\${BERTRAND_SESSION:-}"
|
|
3702
4025
|
[ -z "$sid" ] && exit 0
|
|
3703
4026
|
|
|
@@ -3707,28 +4030,31 @@ cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
|
3707
4030
|
meta="$(printf '%s' "$input" | jq --arg cid "$cid" '{prompt: (.prompt // ""), claude_id: $cid}')"
|
|
3708
4031
|
[ -z "$meta" ] && exit 0
|
|
3709
4032
|
|
|
3710
|
-
|
|
4033
|
+
bq update --session-id "$sid" --event user.prompt --meta "$meta"
|
|
3711
4034
|
`;
|
|
3712
4035
|
}
|
|
3713
4036
|
function doneScript(bin) {
|
|
3714
|
-
const BIN = bin;
|
|
3715
4037
|
return `#!/usr/bin/env bash
|
|
3716
4038
|
# Hook: Stop \u2192 mark session as paused
|
|
4039
|
+
${quietHelper(bin)}
|
|
3717
4040
|
sid="\${BERTRAND_SESSION:-}"
|
|
3718
4041
|
[ -z "$sid" ] && exit 0
|
|
3719
4042
|
|
|
3720
4043
|
input="$(cat)"
|
|
3721
4044
|
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
3722
|
-
|
|
4045
|
+
bq update --session-id "$sid" --event session.paused --meta "$(jq -n --arg cid "$cid" '{claude_id:$cid}')"
|
|
3723
4046
|
|
|
3724
|
-
# Final
|
|
4047
|
+
# Final transcript reads. assistant-message dedups against the most-recent
|
|
4048
|
+
# AskUQ-time capture, so a Done-for-now exit (no work between AskUQ and Stop)
|
|
4049
|
+
# lands zero new events here; intermediate Stops with fresh assistant output
|
|
4050
|
+
# do land new ones.
|
|
3725
4051
|
tpath="$(printf '%s' "$input" | grep -o '"transcript_path":"[^"]*"' | cut -d'"' -f4)"
|
|
3726
4052
|
if [ -n "$tpath" ]; then
|
|
3727
|
-
|
|
3728
|
-
|
|
4053
|
+
bq snapshot --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
|
|
4054
|
+
bq assistant-message --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
|
|
3729
4055
|
fi
|
|
3730
4056
|
|
|
3731
|
-
|
|
4057
|
+
bq badge check --color '#58c142' --priority 10
|
|
3732
4058
|
`;
|
|
3733
4059
|
}
|
|
3734
4060
|
var EXTRACT_TOOL = `tool="$(printf '%s' "$input" | grep -o '"tool_name":"[^"]*"' | cut -d'"' -f4)"`, HOOK_SCRIPTS;
|
|
@@ -4151,8 +4477,11 @@ var init_catalog = __esm(() => {
|
|
|
4151
4477
|
"vercel.deploy": { label: "deployed", category: "integration", color: 245, detailColor: 245, skip: false },
|
|
4152
4478
|
"user.prompt": { label: "prompt", category: "interaction", color: 36, detailColor: 245, skip: false },
|
|
4153
4479
|
"context.snapshot": { label: "context", category: "context", color: 245, detailColor: 245, skip: true },
|
|
4480
|
+
"tool.used": { label: "tool", category: "work", color: 214, detailColor: 245, skip: true },
|
|
4154
4481
|
"tool.work": { label: "tool work", category: "work", color: 214, detailColor: 245, skip: false },
|
|
4155
|
-
"session.recap": { label: "session recap", category: "lifecycle", color: 33, detailColor: 245, skip: false }
|
|
4482
|
+
"session.recap": { label: "session recap", category: "lifecycle", color: 33, detailColor: 245, skip: false },
|
|
4483
|
+
"assistant.message": { label: "claude", category: "interaction", color: 39, detailColor: 245, skip: false },
|
|
4484
|
+
"assistant.recap": { label: "recap", category: "interaction", color: 39, detailColor: 245, skip: false }
|
|
4156
4485
|
};
|
|
4157
4486
|
DEFAULT_INFO = {
|
|
4158
4487
|
label: "unknown",
|
|
@@ -4192,7 +4521,7 @@ function collapsePermissions(events2) {
|
|
|
4192
4521
|
let i = 0;
|
|
4193
4522
|
while (i < events2.length) {
|
|
4194
4523
|
const ev = events2[i];
|
|
4195
|
-
if (
|
|
4524
|
+
if (!ROLLUP_EVENTS.has(ev.event)) {
|
|
4196
4525
|
result.push(ev);
|
|
4197
4526
|
i++;
|
|
4198
4527
|
continue;
|
|
@@ -4200,7 +4529,7 @@ function collapsePermissions(events2) {
|
|
|
4200
4529
|
const batch = [];
|
|
4201
4530
|
while (i < events2.length) {
|
|
4202
4531
|
const current = events2[i];
|
|
4203
|
-
if (
|
|
4532
|
+
if (!ROLLUP_EVENTS.has(current.event))
|
|
4204
4533
|
break;
|
|
4205
4534
|
batch.push(current);
|
|
4206
4535
|
i++;
|
|
@@ -4209,7 +4538,7 @@ function collapsePermissions(events2) {
|
|
|
4209
4538
|
continue;
|
|
4210
4539
|
const toolCounts = new Map;
|
|
4211
4540
|
for (const pev of batch) {
|
|
4212
|
-
if (pev.event !== "permission.request")
|
|
4541
|
+
if (pev.event !== "permission.request" && pev.event !== "tool.used")
|
|
4213
4542
|
continue;
|
|
4214
4543
|
const meta = pev.meta;
|
|
4215
4544
|
const tool = meta?.tool ?? "unknown";
|
|
@@ -4268,8 +4597,14 @@ function filterSkipped(events2) {
|
|
|
4268
4597
|
function compact(events2) {
|
|
4269
4598
|
return deduplicate(collapsePermissions(repairQAPairs(filterSkipped(events2))));
|
|
4270
4599
|
}
|
|
4600
|
+
var ROLLUP_EVENTS;
|
|
4271
4601
|
var init_compact = __esm(() => {
|
|
4272
4602
|
init_catalog();
|
|
4603
|
+
ROLLUP_EVENTS = new Set([
|
|
4604
|
+
"permission.request",
|
|
4605
|
+
"permission.resolve",
|
|
4606
|
+
"tool.used"
|
|
4607
|
+
]);
|
|
4273
4608
|
});
|
|
4274
4609
|
|
|
4275
4610
|
// src/cli/commands/log.ts
|