@uiid/bertrand 0.10.1 → 0.11.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/README.md CHANGED
@@ -195,6 +195,17 @@ bun run db:migrate # Apply migrations to ~/.bertrand/bertrand.db
195
195
 
196
196
  The dashboard has its own `tsc -b` typecheck — run from `dashboard/`.
197
197
 
198
+ ## Releasing
199
+
200
+ Releases are driven by [release-please](https://github.com/googleapis/release-please) from conventional commits on `main`.
201
+
202
+ 1. Land commits in conventional format (`feat:`, `fix:`, `refactor:`, etc.). Hidden types — `chore`, `docs`, `test`, `ci` — don't trigger a release.
203
+ 2. The `Release Please` workflow opens or updates a release PR with the next version + `CHANGELOG.md` entries.
204
+ 3. Merging the release PR creates the git tag and a GitHub Release.
205
+ 4. The same workflow then publishes to npm with provenance (typecheck + tests run first).
206
+
207
+ The `.release-please-manifest.json` file tracks the last released version; release-please updates it automatically. Publishing relies on the `NPM_TOKEN` repo secret (npm Automation token).
208
+
198
209
  ## License
199
210
 
200
211
  MIT
package/dist/bertrand.js CHANGED
@@ -358,6 +358,24 @@ function getEventsBySession(sessionId) {
358
358
  function getEventsByType(sessionId, eventType) {
359
359
  return getDb().select().from(events).where(and2(eq2(events.sessionId, sessionId), eq2(events.event, eventType))).orderBy(events.createdAt).all();
360
360
  }
361
+ function getLatestRecaps() {
362
+ const rows = getDb().select({
363
+ sessionId: events.sessionId,
364
+ meta: events.meta,
365
+ createdAt: events.createdAt
366
+ }).from(events).where(eq2(events.event, "session.recap")).orderBy(desc(events.createdAt)).all();
367
+ const result = {};
368
+ for (const row of rows) {
369
+ if (result[row.sessionId])
370
+ continue;
371
+ const meta = row.meta;
372
+ const recap = typeof meta?.recap === "string" ? meta.recap : null;
373
+ if (!recap)
374
+ continue;
375
+ result[row.sessionId] = { recap, createdAt: row.createdAt };
376
+ }
377
+ return result;
378
+ }
361
379
  var init_events = __esm(() => {
362
380
  init_client();
363
381
  init_schema();
@@ -650,12 +668,14 @@ function summarize(text2) {
650
668
  const trimmed = firstLine.trim();
651
669
  return trimmed.length > 80 ? `${trimmed.slice(0, 77)}...` : trimmed;
652
670
  }
671
+ var RECAP_TAG_RE;
653
672
  var init_assistant_message = __esm(() => {
654
673
  init_router();
655
674
  init_sessions();
656
675
  init_conversations();
657
676
  init_events();
658
677
  init_transcript();
678
+ RECAP_TAG_RE = /<recap>[\s\S]*?<\/recap>/gi;
659
679
  register("assistant-message", async (args) => {
660
680
  let sessionId = "";
661
681
  let transcriptPath = "";
@@ -686,15 +706,16 @@ var init_assistant_message = __esm(() => {
686
706
  const turn = getLatestAssistantTurn(transcriptPath);
687
707
  if (!turn)
688
708
  return;
709
+ const text2 = turn.text.replace(RECAP_TAG_RE, "").trim();
689
710
  const convoId = conversationId && getConversation(conversationId) ? conversationId : undefined;
690
711
  insertEvent({
691
712
  sessionId,
692
713
  conversationId: convoId,
693
714
  event: "assistant.message",
694
- summary: turn.text ? summarize(turn.text) : "thinking only",
715
+ summary: text2 ? summarize(text2) : "thinking only",
695
716
  meta: {
696
717
  model: turn.model,
697
- text: turn.text,
718
+ text: text2,
698
719
  thinkingBlocks: turn.thinkingBlocks,
699
720
  thinkingBytes: turn.thinkingBytes,
700
721
  claude_id: convoId
@@ -703,6 +724,58 @@ var init_assistant_message = __esm(() => {
703
724
  });
704
725
  });
705
726
 
727
+ // src/cli/commands/recap-thinking.ts
728
+ var exports_recap_thinking = {};
729
+ var RECAP_RE;
730
+ var init_recap_thinking = __esm(() => {
731
+ init_router();
732
+ init_sessions();
733
+ init_conversations();
734
+ init_events();
735
+ init_transcript();
736
+ RECAP_RE = /<recap>([\s\S]*?)<\/recap>/i;
737
+ register("recap-thinking", async (args) => {
738
+ let sessionId = "";
739
+ let transcriptPath = "";
740
+ let conversationId = "";
741
+ for (let i = 0;i < args.length; i++) {
742
+ const arg = args[i];
743
+ const next = args[i + 1];
744
+ if (arg === "--session-id" && next) {
745
+ sessionId = next;
746
+ i++;
747
+ } else if (arg === "--transcript-path" && next) {
748
+ transcriptPath = next;
749
+ i++;
750
+ } else if (arg === "--conversation-id" && next) {
751
+ conversationId = next;
752
+ i++;
753
+ }
754
+ }
755
+ if (!sessionId || !transcriptPath) {
756
+ console.error("Usage: bertrand recap-thinking --session-id <id> --transcript-path <path> [--conversation-id <id>]");
757
+ process.exit(1);
758
+ }
759
+ if (!getSession(sessionId))
760
+ return;
761
+ const turn = getLatestAssistantTurn(transcriptPath);
762
+ if (!turn?.text)
763
+ return;
764
+ const match = turn.text.match(RECAP_RE);
765
+ const recap = match?.[1]?.trim();
766
+ if (!recap)
767
+ return;
768
+ const convoId = conversationId && getConversation(conversationId) ? conversationId : undefined;
769
+ insertEvent({
770
+ sessionId,
771
+ conversationId: convoId,
772
+ event: "assistant.recap",
773
+ summary: recap.length > 80 ? `${recap.slice(0, 77)}...` : recap,
774
+ meta: { recap, claude_id: convoId }
775
+ });
776
+ });
777
+ });
778
+
706
779
  // src/terminal/wave.ts
707
780
  import { execSync } from "child_process";
708
781
 
@@ -1139,6 +1212,21 @@ var init_server = __esm(() => {
1139
1212
  return getEventsByType(sessionId, eventType);
1140
1213
  return getEventsBySession(sessionId);
1141
1214
  }],
1215
+ [/^\/api\/stats$/, () => {
1216
+ const all = getAllSessions();
1217
+ const now = new Date().toISOString();
1218
+ const result = {};
1219
+ for (const { session } of all) {
1220
+ const isLive = session.status === "active" || session.status === "waiting";
1221
+ if (isLive) {
1222
+ result[session.id] = { sessionId: session.id, ...computeSessionStats(session.id), updatedAt: now };
1223
+ continue;
1224
+ }
1225
+ const stored = getSessionStats(session.id);
1226
+ result[session.id] = stored ?? { sessionId: session.id, ...computeSessionStats(session.id), updatedAt: now };
1227
+ }
1228
+ return result;
1229
+ }],
1142
1230
  [/^\/api\/stats\/(?<sessionId>[^/]+)$/, ({ sessionId }) => {
1143
1231
  const session = getSession(sessionId);
1144
1232
  if (!session)
@@ -1154,6 +1242,9 @@ var init_server = __esm(() => {
1154
1242
  }],
1155
1243
  [/^\/api\/engagement\/(?<sessionId>[^/]+)$/, ({ sessionId }) => {
1156
1244
  return computeEngagementStats(sessionId);
1245
+ }],
1246
+ [/^\/api\/recaps$/, () => {
1247
+ return getLatestRecaps();
1157
1248
  }]
1158
1249
  ];
1159
1250
  });
@@ -1216,6 +1307,29 @@ var init_groups = __esm(() => {
1216
1307
  init_id();
1217
1308
  });
1218
1309
 
1310
+ // src/db/queries/labels.ts
1311
+ import { eq as eq7, and as and4 } from "drizzle-orm";
1312
+ function createLabel(opts) {
1313
+ return getDb().insert(labels).values({ id: createId(), ...opts }).returning().get();
1314
+ }
1315
+ function getLabelByName(name) {
1316
+ return getDb().select().from(labels).where(eq7(labels.name, name)).get();
1317
+ }
1318
+ function getOrCreateLabelByName(name) {
1319
+ const existing = getLabelByName(name);
1320
+ if (existing)
1321
+ return existing;
1322
+ return createLabel({ name });
1323
+ }
1324
+ function addLabelToSession(sessionId, labelId) {
1325
+ return getDb().insert(sessionLabels).values({ sessionId, labelId }).onConflictDoNothing().returning().get();
1326
+ }
1327
+ var init_labels = __esm(() => {
1328
+ init_client();
1329
+ init_schema();
1330
+ init_id();
1331
+ });
1332
+
1219
1333
  // src/contract/template.md
1220
1334
  var template_default = `You are running inside bertrand, session: {sessionName}. Follow these rules strictly:
1221
1335
 
@@ -1228,6 +1342,8 @@ If the user's most recent answer to AskUserQuestion was "Done for now" (or conta
1228
1342
  Every option must be a concrete, actionable next step. No filler like "Have questions?" or "Want to learn more?" \u2014 if clarification is needed, phrase it as a specific action: "Discuss tradeoffs of X vs Y".
1229
1343
 
1230
1344
  Every AskUserQuestion call MUST use multiSelect: true. No exceptions. Single-select fires on Enter with no confirmation, which causes accidental selections when a block gains focus. multiSelect requires explicit confirmation before submitting.
1345
+
1346
+ Before each AskUserQuestion call, emit a \`<recap>...</recap>\` block in your text output. Use markdown \u2014 a short bullet list is usually the most scannable shape; a single short paragraph is fine when the turn was one cohesive thing. Keep it concise. The recap covers what happened since the previous AskUserQuestion (or session start) \u2014 what you found, decided, or did. Write the gist for someone reading the session timeline, not a process log. The dashboard renders these between AskUserQuestion events; do not use this tag for any other purpose.
1231
1347
  `;
1232
1348
  var init_template = () => {};
1233
1349
 
@@ -1385,6 +1501,10 @@ async function launch(opts) {
1385
1501
  slug: opts.slug,
1386
1502
  name: opts.name ?? opts.slug
1387
1503
  });
1504
+ for (const name of opts.labelNames ?? []) {
1505
+ const label = getOrCreateLabelByName(name);
1506
+ addLabelToSession(session.id, label.id);
1507
+ }
1388
1508
  const claudeId = randomUUID();
1389
1509
  const conversation = createConversation({
1390
1510
  id: claudeId,
@@ -1471,6 +1591,7 @@ var init_session = __esm(() => {
1471
1591
  init_conversations();
1472
1592
  init_events();
1473
1593
  init_groups();
1594
+ init_labels();
1474
1595
  init_template2();
1475
1596
  init_context();
1476
1597
  init_process();
@@ -1707,6 +1828,7 @@ ${BIN} update --session-id "$sid" --event session.waiting --meta "$(jq -n --arg
1707
1828
  tpath="$(printf '%s' "$input" | grep -o '"transcript_path":"[^"]*"' | cut -d'"' -f4)"
1708
1829
  if [ -n "$tpath" ]; then
1709
1830
  ${BIN} snapshot --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
1831
+ ${BIN} recap-thinking --session-id "$sid" --transcript-path "$tpath" --conversation-id "$cid" &
1710
1832
  fi
1711
1833
 
1712
1834
  # Badge + notify in background \u2014 terminal UI doesn't need to block Claude
@@ -2944,6 +3066,7 @@ var hotPath = {
2944
3066
  update: () => Promise.resolve().then(() => (init_update(), exports_update)),
2945
3067
  snapshot: () => Promise.resolve().then(() => (init_snapshot(), exports_snapshot)),
2946
3068
  "assistant-message": () => Promise.resolve().then(() => (init_assistant_message(), exports_assistant_message)),
3069
+ "recap-thinking": () => Promise.resolve().then(() => (init_recap_thinking(), exports_recap_thinking)),
2947
3070
  badge: () => Promise.resolve().then(() => (init_badge(), exports_badge)),
2948
3071
  notify: () => Promise.resolve().then(() => (init_notify(), exports_notify)),
2949
3072
  serve: () => Promise.resolve().then(() => (init_serve(), exports_serve))
@@ -2962,6 +3085,7 @@ if (command && command in hotPath) {
2962
3085
  Promise.resolve().then(() => (init_update(), exports_update)),
2963
3086
  Promise.resolve().then(() => (init_snapshot(), exports_snapshot)),
2964
3087
  Promise.resolve().then(() => (init_assistant_message(), exports_assistant_message)),
3088
+ Promise.resolve().then(() => (init_recap_thinking(), exports_recap_thinking)),
2965
3089
  Promise.resolve().then(() => (init_serve(), exports_serve)),
2966
3090
  Promise.resolve().then(() => (init_badge(), exports_badge)),
2967
3091
  Promise.resolve().then(() => (init_notify(), exports_notify))