bertrand 0.19.0 → 0.20.1
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 +509 -147
- package/dist/dashboard/assets/index-D_l3EPhC.css +1 -0
- package/dist/dashboard/assets/index-mg96b7q-.js +684 -0
- package/dist/dashboard/index.html +3 -3
- package/dist/run-screen.js +9 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-ofrZyO9k.js +0 -673
- package/dist/dashboard/assets/index-qVHXGTGL.css +0 -1
- package/dist/dashboard/assets/workbox-window.prod.es5-BBnX5xw4.js +0 -2
- package/dist/dashboard/manifest.webmanifest +0 -1
- package/dist/dashboard/sw.js +0 -1
- package/dist/dashboard/workbox-e4022e15.js +0 -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) {
|
|
@@ -1778,8 +2117,10 @@ async function handleSwitchProject(req) {
|
|
|
1778
2117
|
return Response.json({ error: `Unknown project: ${slug}` }, { status: 404 });
|
|
1779
2118
|
}
|
|
1780
2119
|
setActiveProjectSlug(slug);
|
|
1781
|
-
|
|
1782
|
-
|
|
2120
|
+
process.env.BERTRAND_PROJECT = slug;
|
|
2121
|
+
_resetActiveProjectCache();
|
|
2122
|
+
invalidateDbCache();
|
|
2123
|
+
return Response.json({ ok: true, slug });
|
|
1783
2124
|
}
|
|
1784
2125
|
async function handleOpen(req) {
|
|
1785
2126
|
let body;
|
|
@@ -1948,6 +2289,7 @@ var init_server = __esm(() => {
|
|
|
1948
2289
|
init_session_archive();
|
|
1949
2290
|
init_registry();
|
|
1950
2291
|
init_resolve();
|
|
2292
|
+
init_client();
|
|
1951
2293
|
PORT = Number(process.env.BERTRAND_PORT ?? 5200);
|
|
1952
2294
|
routes = [
|
|
1953
2295
|
[/^\/api\/sessions$/, listSessions],
|
|
@@ -2280,6 +2622,7 @@ function runMigrations(dbPath) {
|
|
|
2280
2622
|
const target = dbPath ?? resolveActiveProject().db;
|
|
2281
2623
|
mkdirSync5(dirname3(target), { recursive: true });
|
|
2282
2624
|
const sqlite = new Database3(target);
|
|
2625
|
+
sqlite.exec("PRAGMA busy_timeout = 5000");
|
|
2283
2626
|
sqlite.exec("PRAGMA journal_mode = WAL");
|
|
2284
2627
|
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
2285
2628
|
const db = drizzle2(sqlite);
|
|
@@ -3145,29 +3488,22 @@ async function launch(opts) {
|
|
|
3145
3488
|
await ensureServerStarted();
|
|
3146
3489
|
const spawnContext = await captureSpawnContext();
|
|
3147
3490
|
const sessionName = `${opts.categoryPath}/${opts.slug}`;
|
|
3148
|
-
|
|
3491
|
+
emitSessionStarted({
|
|
3149
3492
|
sessionId: session.id,
|
|
3150
3493
|
conversationId: claudeId,
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
labels: opts.labelNames ?? [],
|
|
3157
|
-
summary: session.summary ?? null
|
|
3158
|
-
}
|
|
3494
|
+
categoryPath: opts.categoryPath,
|
|
3495
|
+
sessionName: opts.name ?? opts.slug,
|
|
3496
|
+
sessionSlug: opts.slug,
|
|
3497
|
+
labels: opts.labelNames ?? [],
|
|
3498
|
+
summary: session.summary ?? null
|
|
3159
3499
|
});
|
|
3160
|
-
|
|
3500
|
+
emitClaudeStarted({
|
|
3161
3501
|
sessionId: session.id,
|
|
3162
3502
|
conversationId: claudeId,
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
claude_version: spawnContext.claudeVersion,
|
|
3168
|
-
git: spawnContext.git,
|
|
3169
|
-
cwd: spawnContext.cwd
|
|
3170
|
-
}
|
|
3503
|
+
model: spawnContext.model,
|
|
3504
|
+
claudeVersion: spawnContext.claudeVersion,
|
|
3505
|
+
git: spawnContext.git,
|
|
3506
|
+
cwd: spawnContext.cwd
|
|
3171
3507
|
});
|
|
3172
3508
|
const siblingContext = buildSiblingContext(categoryId, opts.categoryPath, session.id);
|
|
3173
3509
|
const contract = buildContract(sessionName, siblingContext);
|
|
@@ -3191,11 +3527,9 @@ async function resume(opts) {
|
|
|
3191
3527
|
liveSession = { sessionId: session.id, claudeId: opts.conversationId };
|
|
3192
3528
|
installExitHandlers();
|
|
3193
3529
|
await ensureServerStarted();
|
|
3194
|
-
|
|
3530
|
+
emitSessionResumed({
|
|
3195
3531
|
sessionId: session.id,
|
|
3196
|
-
conversationId: opts.conversationId
|
|
3197
|
-
event: "session.resumed",
|
|
3198
|
-
meta: { claude_id: opts.conversationId }
|
|
3532
|
+
conversationId: opts.conversationId
|
|
3199
3533
|
});
|
|
3200
3534
|
const categoryPath = category?.path ?? "";
|
|
3201
3535
|
const siblingContext = buildSiblingContext(session.categoryId, categoryPath, session.id);
|
|
@@ -3219,11 +3553,10 @@ function finalizeSession(sessionId, conversationId, exitCode) {
|
|
|
3219
3553
|
if (conversationExists) {
|
|
3220
3554
|
endConversation(conversationId);
|
|
3221
3555
|
}
|
|
3222
|
-
|
|
3556
|
+
emitClaudeEnded({
|
|
3223
3557
|
sessionId,
|
|
3224
3558
|
conversationId: safeConversationId,
|
|
3225
|
-
|
|
3226
|
-
meta: { claude_id: conversationId, exit_code: exitCode }
|
|
3559
|
+
exitCode
|
|
3227
3560
|
});
|
|
3228
3561
|
updateSession(sessionId, {
|
|
3229
3562
|
status: "paused",
|
|
@@ -3232,10 +3565,7 @@ function finalizeSession(sessionId, conversationId, exitCode) {
|
|
|
3232
3565
|
});
|
|
3233
3566
|
if (liveSession?.sessionId === sessionId)
|
|
3234
3567
|
liveSession = null;
|
|
3235
|
-
|
|
3236
|
-
sessionId,
|
|
3237
|
-
event: "session.end"
|
|
3238
|
-
});
|
|
3568
|
+
emitSessionEnded({ sessionId });
|
|
3239
3569
|
computeAndPersist(sessionId);
|
|
3240
3570
|
stopServerIfIdle();
|
|
3241
3571
|
}
|
|
@@ -3243,7 +3573,7 @@ var liveSession = null, exitHandlersInstalled = false;
|
|
|
3243
3573
|
var init_session = __esm(() => {
|
|
3244
3574
|
init_sessions();
|
|
3245
3575
|
init_conversations();
|
|
3246
|
-
|
|
3576
|
+
init_emit();
|
|
3247
3577
|
init_categories();
|
|
3248
3578
|
init_labels();
|
|
3249
3579
|
init_template2();
|
|
@@ -3436,11 +3766,9 @@ function recoverStaleSessions() {
|
|
|
3436
3766
|
status: "paused",
|
|
3437
3767
|
pid: null
|
|
3438
3768
|
});
|
|
3439
|
-
|
|
3769
|
+
emitSessionPausedByRecovery({
|
|
3440
3770
|
sessionId: session.id,
|
|
3441
|
-
|
|
3442
|
-
summary: "Recovered from stale state (process not found)",
|
|
3443
|
-
meta: { stale_pid: session.pid }
|
|
3771
|
+
stalePid: session.pid
|
|
3444
3772
|
});
|
|
3445
3773
|
recovered++;
|
|
3446
3774
|
}
|
|
@@ -3449,7 +3777,7 @@ function recoverStaleSessions() {
|
|
|
3449
3777
|
}
|
|
3450
3778
|
var init_recovery = __esm(() => {
|
|
3451
3779
|
init_sessions();
|
|
3452
|
-
|
|
3780
|
+
init_emit();
|
|
3453
3781
|
});
|
|
3454
3782
|
|
|
3455
3783
|
// src/cli/commands/launch.ts
|
|
@@ -3473,10 +3801,13 @@ var init_launch = __esm(() => {
|
|
|
3473
3801
|
});
|
|
3474
3802
|
|
|
3475
3803
|
// src/hooks/scripts.ts
|
|
3804
|
+
function quietHelper(bin) {
|
|
3805
|
+
return `bq() { ${bin} "$@" 2>/dev/null || true; }`;
|
|
3806
|
+
}
|
|
3476
3807
|
function waitingScript(bin) {
|
|
3477
|
-
const BIN = bin;
|
|
3478
3808
|
return `#!/usr/bin/env bash
|
|
3479
3809
|
# Hook: PreToolUse AskUserQuestion \u2192 enforce multiSelect, mark session as waiting
|
|
3810
|
+
${quietHelper(bin)}
|
|
3480
3811
|
sid="\${BERTRAND_SESSION:-}"
|
|
3481
3812
|
[ -z "$sid" ] && exit 0
|
|
3482
3813
|
|
|
@@ -3500,23 +3831,26 @@ question="$(printf '%s' "$input" | grep -o '"question":"[^"]*"' | head -1 | cut
|
|
|
3500
3831
|
# Clear working debounce marker so next resume\u2192working transition fires
|
|
3501
3832
|
rm -f "/tmp/bertrand-working-$sid"
|
|
3502
3833
|
|
|
3503
|
-
|
|
3834
|
+
bq update --session-id "$sid" --event session.waiting --meta "$(jq -n --arg q "$question" --arg cid "$cid" '{question:$q, claude_id:$cid}')"
|
|
3504
3835
|
|
|
3505
|
-
# Context snapshot \u2014 extract transcript path and capture token usage
|
|
3836
|
+
# Context snapshot \u2014 extract transcript path and capture token usage.
|
|
3837
|
+
# assistant-message captures the latest turn's text + recap tag in one pass;
|
|
3838
|
+
# replaces the older recap-thinking call which only grabbed the recap. Dedup
|
|
3839
|
+
# inside the command makes it idempotent vs the matching Stop-time capture,
|
|
3840
|
+
# so the same turn never lands twice.
|
|
3506
3841
|
tpath="$(printf '%s' "$input" | grep -o '"transcript_path":"[^"]*"' | cut -d'"' -f4)"
|
|
3507
3842
|
if [ -n "$tpath" ]; then
|
|
3508
|
-
|
|
3509
|
-
|
|
3843
|
+
bq snapshot --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
|
|
3844
|
+
bq assistant-message --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
|
|
3510
3845
|
fi
|
|
3511
3846
|
|
|
3512
3847
|
# Badge + notify in background \u2014 terminal UI doesn't need to block Claude
|
|
3513
|
-
|
|
3514
|
-
|
|
3848
|
+
bq badge message-question --color '#e0b956' --priority 20 --beep &
|
|
3849
|
+
bq notify bertrand "$question" &
|
|
3515
3850
|
wait
|
|
3516
3851
|
`;
|
|
3517
3852
|
}
|
|
3518
3853
|
function answeredScript(bin) {
|
|
3519
|
-
const BIN = bin;
|
|
3520
3854
|
return `#!/usr/bin/env bash
|
|
3521
3855
|
# Hook: PostToolUse AskUserQuestion \u2192 mark session as active
|
|
3522
3856
|
#
|
|
@@ -3524,6 +3858,7 @@ function answeredScript(bin) {
|
|
|
3524
3858
|
# Claude Code so the agent halts immediately instead of taking another turn.
|
|
3525
3859
|
# This is the mechanical enforcement of the contract's loop-exit rule \u2014 the
|
|
3526
3860
|
# contract prose is a soft hint, this JSON is the guarantee.
|
|
3861
|
+
${quietHelper(bin)}
|
|
3527
3862
|
sid="\${BERTRAND_SESSION:-}"
|
|
3528
3863
|
[ -z "$sid" ] && exit 0
|
|
3529
3864
|
|
|
@@ -3545,9 +3880,9 @@ meta="$(printf '%s' "$input" | jq --arg cid "$cid" '
|
|
|
3545
3880
|
# Concatenate all answer values into a single string for the Done-for-now check.
|
|
3546
3881
|
done_check="$(printf '%s' "$meta" | jq -r '.answers | to_entries | map(.value | tostring) | join(" ")' 2>/dev/null)"
|
|
3547
3882
|
|
|
3548
|
-
|
|
3883
|
+
bq update --session-id "$sid" --event session.answered --meta "$meta"
|
|
3549
3884
|
|
|
3550
|
-
|
|
3885
|
+
bq badge --clear &
|
|
3551
3886
|
|
|
3552
3887
|
# Halt the agent loop if the user signaled Done for now. The Stop hook
|
|
3553
3888
|
# (on-done.sh) will fire afterwards and mark the session as paused.
|
|
@@ -3560,7 +3895,7 @@ if printf '%s' "$done_check" | grep -q "Done for now"; then
|
|
|
3560
3895
|
[.questions[]?.options[]? | select(.label == "Done for now") | .description] | first // empty
|
|
3561
3896
|
' 2>/dev/null)"
|
|
3562
3897
|
if [ -n "$recap" ]; then
|
|
3563
|
-
|
|
3898
|
+
bq update --session-id "$sid" --event session.recap --meta "$(jq -n --arg recap "$recap" --arg cid "$cid" '{recap:$recap, claude_id:$cid}')"
|
|
3564
3899
|
fi
|
|
3565
3900
|
|
|
3566
3901
|
printf '{"continue": false, "stopReason": "User selected Done for now"}\\n'
|
|
@@ -3570,9 +3905,9 @@ wait
|
|
|
3570
3905
|
`;
|
|
3571
3906
|
}
|
|
3572
3907
|
function activeScript(bin) {
|
|
3573
|
-
const BIN = bin;
|
|
3574
3908
|
return `#!/usr/bin/env bash
|
|
3575
3909
|
# Hook: PreToolUse (catch-all) \u2192 flip waiting to active
|
|
3910
|
+
${quietHelper(bin)}
|
|
3576
3911
|
sid="\${BERTRAND_SESSION:-}"
|
|
3577
3912
|
[ -z "$sid" ] && exit 0
|
|
3578
3913
|
|
|
@@ -3592,13 +3927,13 @@ ${EXTRACT_TOOL}
|
|
|
3592
3927
|
|
|
3593
3928
|
touch "$marker"
|
|
3594
3929
|
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
3595
|
-
|
|
3930
|
+
bq update --session-id "$sid" --event session.active --meta "$(jq -n --arg cid "$cid" '{claude_id:$cid}')"
|
|
3596
3931
|
`;
|
|
3597
3932
|
}
|
|
3598
3933
|
function permissionWaitScript(bin) {
|
|
3599
|
-
const BIN = bin;
|
|
3600
3934
|
return `#!/usr/bin/env bash
|
|
3601
3935
|
# Hook: PermissionRequest \u2192 mark pending, emit permission.request
|
|
3936
|
+
${quietHelper(bin)}
|
|
3602
3937
|
sid="\${BERTRAND_SESSION:-}"
|
|
3603
3938
|
[ -z "$sid" ] && exit 0
|
|
3604
3939
|
|
|
@@ -3617,38 +3952,45 @@ case "$tool" in
|
|
|
3617
3952
|
esac
|
|
3618
3953
|
|
|
3619
3954
|
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
3620
|
-
|
|
3955
|
+
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
3956
|
|
|
3622
3957
|
# Badge + notify in background
|
|
3623
|
-
|
|
3624
|
-
|
|
3958
|
+
bq badge bell-exclamation --color '#ff6b35' --priority 25 --beep &
|
|
3959
|
+
bq notify bertrand "Needs permission: $tool" &
|
|
3625
3960
|
wait
|
|
3626
3961
|
`;
|
|
3627
3962
|
}
|
|
3628
3963
|
function permissionDoneScript(bin) {
|
|
3629
|
-
const BIN = bin;
|
|
3630
3964
|
return `#!/usr/bin/env bash
|
|
3631
3965
|
# Hook: PostToolUse (catch-all)
|
|
3632
3966
|
#
|
|
3633
|
-
#
|
|
3634
|
-
# 1. Edit/Write/MultiEdit
|
|
3635
|
-
#
|
|
3636
|
-
#
|
|
3637
|
-
# 2.
|
|
3638
|
-
#
|
|
3639
|
-
#
|
|
3967
|
+
# Captures every tool call Claude makes. Three event flows:
|
|
3968
|
+
# 1. Edit/Write/MultiEdit \u2192 tool.applied with diff payload. Keeps the
|
|
3969
|
+
# existing dashboard diff-renderer happy and is the only place we get
|
|
3970
|
+
# old_string/new_string on auto-approved edits.
|
|
3971
|
+
# 2. Tools that went through a permission prompt \u2192 permission.resolve.
|
|
3972
|
+
# The PermissionRequest hook set a marker; we clear it and log the
|
|
3973
|
+
# approval. (Denials never reach PostToolUse, so absence-of-resolve
|
|
3974
|
+
# means the user said no.)
|
|
3975
|
+
# 3. Everything else (auto-approved Bash/Read/Grep/Glob/etc.) \u2192 tool.used
|
|
3976
|
+
# with outcome:"auto". Previously this case dropped the call entirely;
|
|
3977
|
+
# now Claude's read-only / shell activity shows up in the timeline.
|
|
3978
|
+
${quietHelper(bin)}
|
|
3640
3979
|
sid="\${BERTRAND_SESSION:-}"
|
|
3641
3980
|
[ -z "$sid" ] && exit 0
|
|
3642
3981
|
|
|
3643
3982
|
input="$(cat)"
|
|
3644
3983
|
${EXTRACT_TOOL}
|
|
3645
3984
|
|
|
3985
|
+
# Don't double-log: AskUserQuestion has its own waiting/answered events
|
|
3986
|
+
[ "$tool" = "AskUserQuestion" ] && exit 0
|
|
3987
|
+
|
|
3646
3988
|
marker="/tmp/bertrand-perm-pending-$sid"
|
|
3647
3989
|
had_marker=0
|
|
3648
3990
|
if [ -f "$marker" ]; then
|
|
3649
3991
|
had_marker=1
|
|
3650
3992
|
rm -f "$marker"
|
|
3651
|
-
|
|
3993
|
+
bq badge --clear &
|
|
3652
3994
|
fi
|
|
3653
3995
|
|
|
3654
3996
|
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
@@ -3660,10 +4002,6 @@ case "$tool" in
|
|
|
3660
4002
|
Write) summary="wrote a file" ;;
|
|
3661
4003
|
*) summary="edited a file" ;;
|
|
3662
4004
|
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
4005
|
meta="$(printf '%s' "$input" | jq --arg t "$tool" --arg d "$detail" --arg cid "$cid" '
|
|
3668
4006
|
{
|
|
3669
4007
|
permissions: [
|
|
@@ -3676,28 +4014,40 @@ case "$tool" in
|
|
|
3676
4014
|
claude_id: $cid
|
|
3677
4015
|
}
|
|
3678
4016
|
')"
|
|
3679
|
-
|
|
4017
|
+
bq update --session-id "$sid" --event tool.applied --summary "$summary" --meta "$meta"
|
|
3680
4018
|
wait
|
|
3681
4019
|
exit 0
|
|
3682
4020
|
;;
|
|
3683
4021
|
esac
|
|
3684
4022
|
|
|
3685
|
-
#
|
|
3686
|
-
|
|
3687
|
-
|
|
4023
|
+
# Extract a tool-shaped detail for the timeline summary. Bash gets the
|
|
4024
|
+
# command, file tools get the path; everything else falls back to a generic
|
|
4025
|
+
# label inside the emit helper.
|
|
3688
4026
|
detail=""
|
|
3689
4027
|
case "$tool" in
|
|
3690
4028
|
Bash) detail="$(printf '%s' "$input" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-1000)" ;;
|
|
4029
|
+
Read|NotebookRead) detail="$(printf '%s' "$input" | grep -o '"file_path":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-1000)" ;;
|
|
4030
|
+
Glob) detail="$(printf '%s' "$input" | grep -o '"pattern":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-200)" ;;
|
|
4031
|
+
Grep) detail="$(printf '%s' "$input" | grep -o '"pattern":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-200)" ;;
|
|
4032
|
+
WebFetch) detail="$(printf '%s' "$input" | grep -o '"url":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-300)" ;;
|
|
4033
|
+
WebSearch) detail="$(printf '%s' "$input" | grep -o '"query":"[^"]*"' | head -1 | cut -d'"' -f4 | cut -c1-200)" ;;
|
|
3691
4034
|
esac
|
|
3692
4035
|
|
|
3693
|
-
|
|
4036
|
+
if [ "$had_marker" = "1" ]; then
|
|
4037
|
+
# Prompted-then-approved path. Keep permission.resolve for back-compat
|
|
4038
|
+
# rendering; downstream consumers can migrate to tool.used at their pace.
|
|
4039
|
+
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}')"
|
|
4040
|
+
else
|
|
4041
|
+
# Auto-approved path. Without tool.used, these calls were invisible.
|
|
4042
|
+
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}')"
|
|
4043
|
+
fi
|
|
3694
4044
|
wait
|
|
3695
4045
|
`;
|
|
3696
4046
|
}
|
|
3697
4047
|
function userPromptScript(bin) {
|
|
3698
|
-
const BIN = bin;
|
|
3699
4048
|
return `#!/usr/bin/env bash
|
|
3700
4049
|
# Hook: UserPromptSubmit \u2192 record user free-text prompt
|
|
4050
|
+
${quietHelper(bin)}
|
|
3701
4051
|
sid="\${BERTRAND_SESSION:-}"
|
|
3702
4052
|
[ -z "$sid" ] && exit 0
|
|
3703
4053
|
|
|
@@ -3707,28 +4057,31 @@ cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
|
3707
4057
|
meta="$(printf '%s' "$input" | jq --arg cid "$cid" '{prompt: (.prompt // ""), claude_id: $cid}')"
|
|
3708
4058
|
[ -z "$meta" ] && exit 0
|
|
3709
4059
|
|
|
3710
|
-
|
|
4060
|
+
bq update --session-id "$sid" --event user.prompt --meta "$meta"
|
|
3711
4061
|
`;
|
|
3712
4062
|
}
|
|
3713
4063
|
function doneScript(bin) {
|
|
3714
|
-
const BIN = bin;
|
|
3715
4064
|
return `#!/usr/bin/env bash
|
|
3716
4065
|
# Hook: Stop \u2192 mark session as paused
|
|
4066
|
+
${quietHelper(bin)}
|
|
3717
4067
|
sid="\${BERTRAND_SESSION:-}"
|
|
3718
4068
|
[ -z "$sid" ] && exit 0
|
|
3719
4069
|
|
|
3720
4070
|
input="$(cat)"
|
|
3721
4071
|
cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
3722
|
-
|
|
4072
|
+
bq update --session-id "$sid" --event session.paused --meta "$(jq -n --arg cid "$cid" '{claude_id:$cid}')"
|
|
3723
4073
|
|
|
3724
|
-
# Final
|
|
4074
|
+
# Final transcript reads. assistant-message dedups against the most-recent
|
|
4075
|
+
# AskUQ-time capture, so a Done-for-now exit (no work between AskUQ and Stop)
|
|
4076
|
+
# lands zero new events here; intermediate Stops with fresh assistant output
|
|
4077
|
+
# do land new ones.
|
|
3725
4078
|
tpath="$(printf '%s' "$input" | grep -o '"transcript_path":"[^"]*"' | cut -d'"' -f4)"
|
|
3726
4079
|
if [ -n "$tpath" ]; then
|
|
3727
|
-
|
|
3728
|
-
|
|
4080
|
+
bq snapshot --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
|
|
4081
|
+
bq assistant-message --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
|
|
3729
4082
|
fi
|
|
3730
4083
|
|
|
3731
|
-
|
|
4084
|
+
bq badge check --color '#58c142' --priority 10
|
|
3732
4085
|
`;
|
|
3733
4086
|
}
|
|
3734
4087
|
var EXTRACT_TOOL = `tool="$(printf '%s' "$input" | grep -o '"tool_name":"[^"]*"' | cut -d'"' -f4)"`, HOOK_SCRIPTS;
|
|
@@ -4151,8 +4504,11 @@ var init_catalog = __esm(() => {
|
|
|
4151
4504
|
"vercel.deploy": { label: "deployed", category: "integration", color: 245, detailColor: 245, skip: false },
|
|
4152
4505
|
"user.prompt": { label: "prompt", category: "interaction", color: 36, detailColor: 245, skip: false },
|
|
4153
4506
|
"context.snapshot": { label: "context", category: "context", color: 245, detailColor: 245, skip: true },
|
|
4507
|
+
"tool.used": { label: "tool", category: "work", color: 214, detailColor: 245, skip: true },
|
|
4154
4508
|
"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 }
|
|
4509
|
+
"session.recap": { label: "session recap", category: "lifecycle", color: 33, detailColor: 245, skip: false },
|
|
4510
|
+
"assistant.message": { label: "claude", category: "interaction", color: 39, detailColor: 245, skip: false },
|
|
4511
|
+
"assistant.recap": { label: "recap", category: "interaction", color: 39, detailColor: 245, skip: false }
|
|
4156
4512
|
};
|
|
4157
4513
|
DEFAULT_INFO = {
|
|
4158
4514
|
label: "unknown",
|
|
@@ -4192,7 +4548,7 @@ function collapsePermissions(events2) {
|
|
|
4192
4548
|
let i = 0;
|
|
4193
4549
|
while (i < events2.length) {
|
|
4194
4550
|
const ev = events2[i];
|
|
4195
|
-
if (
|
|
4551
|
+
if (!ROLLUP_EVENTS.has(ev.event)) {
|
|
4196
4552
|
result.push(ev);
|
|
4197
4553
|
i++;
|
|
4198
4554
|
continue;
|
|
@@ -4200,7 +4556,7 @@ function collapsePermissions(events2) {
|
|
|
4200
4556
|
const batch = [];
|
|
4201
4557
|
while (i < events2.length) {
|
|
4202
4558
|
const current = events2[i];
|
|
4203
|
-
if (
|
|
4559
|
+
if (!ROLLUP_EVENTS.has(current.event))
|
|
4204
4560
|
break;
|
|
4205
4561
|
batch.push(current);
|
|
4206
4562
|
i++;
|
|
@@ -4209,7 +4565,7 @@ function collapsePermissions(events2) {
|
|
|
4209
4565
|
continue;
|
|
4210
4566
|
const toolCounts = new Map;
|
|
4211
4567
|
for (const pev of batch) {
|
|
4212
|
-
if (pev.event !== "permission.request")
|
|
4568
|
+
if (pev.event !== "permission.request" && pev.event !== "tool.used")
|
|
4213
4569
|
continue;
|
|
4214
4570
|
const meta = pev.meta;
|
|
4215
4571
|
const tool = meta?.tool ?? "unknown";
|
|
@@ -4268,8 +4624,14 @@ function filterSkipped(events2) {
|
|
|
4268
4624
|
function compact(events2) {
|
|
4269
4625
|
return deduplicate(collapsePermissions(repairQAPairs(filterSkipped(events2))));
|
|
4270
4626
|
}
|
|
4627
|
+
var ROLLUP_EVENTS;
|
|
4271
4628
|
var init_compact = __esm(() => {
|
|
4272
4629
|
init_catalog();
|
|
4630
|
+
ROLLUP_EVENTS = new Set([
|
|
4631
|
+
"permission.request",
|
|
4632
|
+
"permission.resolve",
|
|
4633
|
+
"tool.used"
|
|
4634
|
+
]);
|
|
4273
4635
|
});
|
|
4274
4636
|
|
|
4275
4637
|
// src/cli/commands/log.ts
|