@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 +11 -0
- package/dist/bertrand.js +126 -2
- package/dist/run-screen.js +460 -741
- package/package.json +1 -1
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:
|
|
715
|
+
summary: text2 ? summarize(text2) : "thinking only",
|
|
695
716
|
meta: {
|
|
696
717
|
model: turn.model,
|
|
697
|
-
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))
|