bertrand 0.20.0 → 0.21.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
@@ -79,13 +79,14 @@ Shows a picker: start a fresh Claude conversation, or resume one of the prior co
79
79
  bertrand list
80
80
  ```
81
81
 
82
- Interactive picker showing all sessions with status badges.
82
+ Interactive picker showing all sessions with status badges. Add `--project <slug>` to list sessions from another project without switching the active one.
83
83
 
84
84
  ### Other commands
85
85
 
86
86
  | Command | Purpose |
87
87
  |---|---|
88
- | `bertrand log <session>` | Print the timeline event log for a session. |
88
+ | `bertrand log` | List sessions in the active project. Add `--project <slug>` to scope to a different project. |
89
+ | `bertrand log <session>` | Print the timeline event log for a session. Supports `--json` (includes `project: { slug, name }` for agent consumption) and `--project <slug>` for cross-project reads. |
89
90
  | `bertrand stats <session>` | Print materialized stats (duration, work/wait split, lines changed). |
90
91
  | `bertrand archive <name>` | Archive or unarchive a session. |
91
92
  | `bertrand serve` | Start the dashboard HTTP API on `:5200`. |
package/dist/bertrand.js CHANGED
@@ -15,6 +15,7 @@ var __export = (target, all) => {
15
15
  });
16
16
  };
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = import.meta.require;
18
19
 
19
20
  // src/lib/paths.ts
20
21
  import { homedir } from "os";
@@ -25,6 +26,7 @@ var init_paths = __esm(() => {
25
26
  root: join(homedir(), BERTRAND_DIR),
26
27
  hooks: join(homedir(), BERTRAND_DIR, "hooks"),
27
28
  sessions: join(homedir(), BERTRAND_DIR, "sessions"),
29
+ runtime: join(homedir(), BERTRAND_DIR, "run"),
28
30
  db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
29
31
  syncEnv: join(homedir(), BERTRAND_DIR, "sync.env")
30
32
  };
@@ -2117,8 +2119,10 @@ async function handleSwitchProject(req) {
2117
2119
  return Response.json({ error: `Unknown project: ${slug}` }, { status: 404 });
2118
2120
  }
2119
2121
  setActiveProjectSlug(slug);
2120
- queueMicrotask(() => process.exit(0));
2121
- return Response.json({ ok: true, slug, willRestart: true });
2122
+ process.env.BERTRAND_PROJECT = slug;
2123
+ _resetActiveProjectCache();
2124
+ invalidateDbCache();
2125
+ return Response.json({ ok: true, slug });
2122
2126
  }
2123
2127
  async function handleOpen(req) {
2124
2128
  let body;
@@ -2287,6 +2291,7 @@ var init_server = __esm(() => {
2287
2291
  init_session_archive();
2288
2292
  init_registry();
2289
2293
  init_resolve();
2294
+ init_client();
2290
2295
  PORT = Number(process.env.BERTRAND_PORT ?? 5200);
2291
2296
  routes = [
2292
2297
  [/^\/api\/sessions$/, listSessions],
@@ -3450,10 +3455,8 @@ function installExitHandlers() {
3450
3455
  const onSignal = (signal) => {
3451
3456
  if (isClaudeRunning())
3452
3457
  return;
3453
- process.exit(signal === "SIGINT" ? 130 : signal === "SIGHUP" ? 129 : 143);
3458
+ process.exit(signal === "SIGHUP" ? 129 : 143);
3454
3459
  };
3455
- process.on("SIGINT", onSignal);
3456
- process.on("SIGTERM", onSignal);
3457
3460
  process.on("SIGHUP", onSignal);
3458
3461
  }
3459
3462
  async function launch(opts) {
@@ -3565,6 +3568,7 @@ function finalizeSession(sessionId, conversationId, exitCode) {
3565
3568
  emitSessionEnded({ sessionId });
3566
3569
  computeAndPersist(sessionId);
3567
3570
  stopServerIfIdle();
3571
+ triggerBackgroundPush();
3568
3572
  }
3569
3573
  var liveSession = null, exitHandlersInstalled = false;
3570
3574
  var init_session = __esm(() => {
@@ -3579,39 +3583,68 @@ var init_session = __esm(() => {
3579
3583
  init_spawn_context();
3580
3584
  init_timing();
3581
3585
  init_server_lifecycle();
3586
+ init_trigger();
3582
3587
  });
3583
3588
 
3584
3589
  // src/tui/app.tsx
3585
3590
  import { spawn as spawn4 } from "child_process";
3586
3591
  import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
3592
+ import { tmpdir } from "os";
3587
3593
  import { join as join9 } from "path";
3588
3594
  import { randomUUID as randomUUID2 } from "crypto";
3589
3595
  async function runScreen(screen, ...args) {
3590
- const tmpFile = `/tmp/bertrand-tui-${process.pid}-${Date.now()}.json`;
3596
+ const tmpFile = join9(tmpdir(), `bertrand-tui-${randomUUID2()}.json`);
3597
+ if (process.env.BERTRAND_DEBUG_TUI) {
3598
+ try {
3599
+ const { appendFileSync } = await import("fs");
3600
+ appendFileSync(process.env.BERTRAND_DEBUG_TUI, `--- parent runScreen("${screen}") at ${Date.now()} entry=${SCREEN_ENTRY}
3601
+ `);
3602
+ } catch {}
3603
+ }
3591
3604
  const child = spawn4("bun", ["run", SCREEN_ENTRY, screen, tmpFile, ...args], {
3592
- stdio: "inherit"
3593
- });
3594
- const exitCode = await new Promise((resolve) => {
3595
- child.on("exit", (code) => resolve(code ?? 1));
3605
+ stdio: "inherit",
3606
+ env: process.env
3596
3607
  });
3597
- if (exitCode !== 0) {
3598
- throw new Error(`TUI screen "${screen}" exited with code ${exitCode}`);
3608
+ const noopSignal = () => {};
3609
+ process.on("SIGINT", noopSignal);
3610
+ process.on("SIGTERM", noopSignal);
3611
+ let spawnError = null;
3612
+ try {
3613
+ const { code, signal } = await new Promise((resolve) => {
3614
+ child.on("error", (err) => {
3615
+ spawnError = err;
3616
+ resolve({ code: 1, signal: null });
3617
+ });
3618
+ child.on("exit", (c, s) => resolve({ code: c, signal: s }));
3619
+ });
3620
+ if (spawnError) {
3621
+ throw new Error(`Failed to launch TUI screen "${screen}": ${spawnError.message}`);
3622
+ }
3623
+ if (!existsSync8(tmpFile)) {
3624
+ const detail = signal ? `killed by ${signal}` : `exited with code ${code ?? "?"} without writing result`;
3625
+ throw new Error(`TUI screen "${screen}" ${detail}`);
3626
+ }
3627
+ return JSON.parse(readFileSync8(tmpFile, "utf-8"));
3628
+ } finally {
3629
+ process.removeListener("SIGINT", noopSignal);
3630
+ process.removeListener("SIGTERM", noopSignal);
3631
+ try {
3632
+ if (existsSync8(tmpFile))
3633
+ unlinkSync3(tmpFile);
3634
+ } catch {}
3599
3635
  }
3600
- const result = JSON.parse(readFileSync8(tmpFile, "utf-8"));
3601
- unlinkSync3(tmpFile);
3602
- return result;
3603
3636
  }
3604
- async function startStartupTui(skipProjectPicker, initialProjectSlug) {
3605
- return runScreen("startup", String(skipProjectPicker), initialProjectSlug);
3637
+ async function startLaunchTui() {
3638
+ return runScreen("launch");
3639
+ }
3640
+ async function startProjectPickerTui() {
3641
+ return runScreen("project-picker");
3606
3642
  }
3607
3643
  async function startExitTui(sessionId) {
3608
3644
  return runScreen("exit", sessionId);
3609
3645
  }
3610
3646
  async function startResumeTui(sessionId) {
3611
3647
  const conversations2 = getConversationsBySession(sessionId);
3612
- if (conversations2.length === 1) {
3613
- return { type: "conversation", conversationId: conversations2[0].id };
3614
- }
3615
3648
  if (conversations2.length === 0) {
3616
3649
  return { type: "new" };
3617
3650
  }
@@ -3658,19 +3691,17 @@ function shouldShowProjectPicker() {
3658
3691
  const projects = listProjects();
3659
3692
  return projects.length !== 1;
3660
3693
  }
3661
- async function startTui() {
3662
- const skipProjectPicker = !shouldShowProjectPicker();
3663
- const initialProjectSlug = getActiveProjectSlug();
3664
- const selection = await startStartupTui(skipProjectPicker, initialProjectSlug);
3694
+ function activateProject(slug) {
3695
+ setActiveProjectSlug(slug);
3665
3696
  _resetActiveProjectCache();
3697
+ }
3698
+ async function runLaunchCycle() {
3699
+ const selection = await startLaunchTui();
3666
3700
  switch (selection.type) {
3667
3701
  case "quit":
3668
3702
  return;
3669
3703
  case "create": {
3670
- const sessionId = await launch({
3671
- categoryPath: selection.categoryPath,
3672
- slug: selection.slug
3673
- });
3704
+ const sessionId = await launch(selection);
3674
3705
  await runSessionLoop(sessionId);
3675
3706
  return;
3676
3707
  }
@@ -3687,6 +3718,28 @@ async function startTui() {
3687
3718
  }
3688
3719
  }
3689
3720
  }
3721
+ async function startTui() {
3722
+ if (!shouldShowProjectPicker()) {
3723
+ await runLaunchCycle();
3724
+ return;
3725
+ }
3726
+ const projectSelection = await startProjectPickerTui();
3727
+ switch (projectSelection.type) {
3728
+ case "quit":
3729
+ return;
3730
+ case "select": {
3731
+ activateProject(projectSelection.slug);
3732
+ await runLaunchCycle();
3733
+ return;
3734
+ }
3735
+ case "create": {
3736
+ createProject({ slug: projectSelection.slug });
3737
+ activateProject(projectSelection.slug);
3738
+ await runLaunchCycle();
3739
+ return;
3740
+ }
3741
+ }
3742
+ }
3690
3743
  var SCREEN_ENTRY;
3691
3744
  var init_app = __esm(() => {
3692
3745
  init_sessions();
@@ -3694,6 +3747,7 @@ var init_app = __esm(() => {
3694
3747
  init_session_archive();
3695
3748
  init_session();
3696
3749
  init_registry();
3750
+ init_create();
3697
3751
  init_resolve();
3698
3752
  SCREEN_ENTRY = (() => {
3699
3753
  const built = join9(import.meta.dir, "run-screen.js");
@@ -3755,21 +3809,33 @@ var init_recovery = __esm(() => {
3755
3809
 
3756
3810
  // src/cli/commands/launch.ts
3757
3811
  var exports_launch = {};
3812
+ function reportFatal(err) {
3813
+ const message = err instanceof Error ? err.message : String(err);
3814
+ console.error(`bertrand: ${message}`);
3815
+ if (process.env.BERTRAND_DEBUG && err instanceof Error && err.stack) {
3816
+ console.error(err.stack);
3817
+ }
3818
+ process.exit(1);
3819
+ }
3758
3820
  var init_launch = __esm(() => {
3759
3821
  init_router();
3760
3822
  init_app();
3761
3823
  init_session();
3762
3824
  init_recovery();
3763
3825
  register("launch", async (args) => {
3764
- recoverStaleSessions();
3765
- const sessionName = args[0];
3766
- if (sessionName) {
3767
- const { categoryPath, slug } = parseSessionName(sessionName);
3768
- const sessionId = await launch({ categoryPath, slug });
3769
- await runSessionLoop(sessionId);
3770
- return;
3826
+ try {
3827
+ recoverStaleSessions();
3828
+ const sessionName = args[0];
3829
+ if (sessionName) {
3830
+ const { categoryPath, slug } = parseSessionName(sessionName);
3831
+ const sessionId = await launch({ categoryPath, slug });
3832
+ await runSessionLoop(sessionId);
3833
+ return;
3834
+ }
3835
+ await startTui();
3836
+ } catch (err) {
3837
+ reportFatal(err);
3771
3838
  }
3772
- await startTui();
3773
3839
  });
3774
3840
  });
3775
3841
 
@@ -3777,7 +3843,7 @@ var init_launch = __esm(() => {
3777
3843
  function quietHelper(bin) {
3778
3844
  return `bq() { ${bin} "$@" 2>/dev/null || true; }`;
3779
3845
  }
3780
- function waitingScript(bin) {
3846
+ function waitingScript(bin, runtimeDir) {
3781
3847
  return `#!/usr/bin/env bash
3782
3848
  # Hook: PreToolUse AskUserQuestion \u2192 enforce multiSelect, mark session as waiting
3783
3849
  ${quietHelper(bin)}
@@ -3802,7 +3868,7 @@ question="$(printf '%s' "$input" | grep -o '"question":"[^"]*"' | head -1 | cut
3802
3868
  [ -z "$question" ] && question="Waiting for input"
3803
3869
 
3804
3870
  # Clear working debounce marker so next resume\u2192working transition fires
3805
- rm -f "/tmp/bertrand-working-$sid"
3871
+ rm -f "${runtimeDir}/working-$sid"
3806
3872
 
3807
3873
  bq update --session-id "$sid" --event session.waiting --meta "$(jq -n --arg q "$question" --arg cid "$cid" '{question:$q, claude_id:$cid}')"
3808
3874
 
@@ -3823,7 +3889,7 @@ bq notify bertrand "$question" &
3823
3889
  wait
3824
3890
  `;
3825
3891
  }
3826
- function answeredScript(bin) {
3892
+ function answeredScript(bin, _runtimeDir) {
3827
3893
  return `#!/usr/bin/env bash
3828
3894
  # Hook: PostToolUse AskUserQuestion \u2192 mark session as active
3829
3895
  #
@@ -3877,7 +3943,7 @@ fi
3877
3943
  wait
3878
3944
  `;
3879
3945
  }
3880
- function activeScript(bin) {
3946
+ function activeScript(bin, runtimeDir) {
3881
3947
  return `#!/usr/bin/env bash
3882
3948
  # Hook: PreToolUse (catch-all) \u2192 flip waiting to active
3883
3949
  ${quietHelper(bin)}
@@ -3886,7 +3952,7 @@ sid="\${BERTRAND_SESSION:-}"
3886
3952
 
3887
3953
  # Debounce: skip if we already sent session.active within the last 5 seconds.
3888
3954
  # This avoids spawning bertrand (~31ms) on every tool call during rapid sequences.
3889
- marker="/tmp/bertrand-working-$sid"
3955
+ marker="${runtimeDir}/working-$sid"
3890
3956
  if [ -f "$marker" ]; then
3891
3957
  age=$(( $(date +%s) - $(stat -f%m "$marker" 2>/dev/null || echo 0) ))
3892
3958
  [ "$age" -lt 5 ] && exit 0
@@ -3903,7 +3969,7 @@ cid="\${BERTRAND_CLAUDE_ID:-}"
3903
3969
  bq update --session-id "$sid" --event session.active --meta "$(jq -n --arg cid "$cid" '{claude_id:$cid}')"
3904
3970
  `;
3905
3971
  }
3906
- function permissionWaitScript(bin) {
3972
+ function permissionWaitScript(bin, runtimeDir) {
3907
3973
  return `#!/usr/bin/env bash
3908
3974
  # Hook: PermissionRequest \u2192 mark pending, emit permission.request
3909
3975
  ${quietHelper(bin)}
@@ -3915,7 +3981,7 @@ ${EXTRACT_TOOL}
3915
3981
  [ "$tool" = "AskUserQuestion" ] && exit 0
3916
3982
 
3917
3983
  # Write marker so PostToolUse knows this was a real permission prompt (not auto-approved)
3918
- touch "/tmp/bertrand-perm-pending-$sid"
3984
+ touch "${runtimeDir}/perm-pending-$sid"
3919
3985
 
3920
3986
  # Extract detail from tool_input via grep (avoid jq for simple fields)
3921
3987
  detail=""
@@ -3933,7 +3999,7 @@ bq notify bertrand "Needs permission: $tool" &
3933
3999
  wait
3934
4000
  `;
3935
4001
  }
3936
- function permissionDoneScript(bin) {
4002
+ function permissionDoneScript(bin, runtimeDir) {
3937
4003
  return `#!/usr/bin/env bash
3938
4004
  # Hook: PostToolUse (catch-all)
3939
4005
  #
@@ -3958,7 +4024,7 @@ ${EXTRACT_TOOL}
3958
4024
  # Don't double-log: AskUserQuestion has its own waiting/answered events
3959
4025
  [ "$tool" = "AskUserQuestion" ] && exit 0
3960
4026
 
3961
- marker="/tmp/bertrand-perm-pending-$sid"
4027
+ marker="${runtimeDir}/perm-pending-$sid"
3962
4028
  had_marker=0
3963
4029
  if [ -f "$marker" ]; then
3964
4030
  had_marker=1
@@ -4017,7 +4083,7 @@ fi
4017
4083
  wait
4018
4084
  `;
4019
4085
  }
4020
- function userPromptScript(bin) {
4086
+ function userPromptScript(bin, _runtimeDir) {
4021
4087
  return `#!/usr/bin/env bash
4022
4088
  # Hook: UserPromptSubmit \u2192 record user free-text prompt
4023
4089
  ${quietHelper(bin)}
@@ -4033,7 +4099,7 @@ meta="$(printf '%s' "$input" | jq --arg cid "$cid" '{prompt: (.prompt // ""), cl
4033
4099
  bq update --session-id "$sid" --event user.prompt --meta "$meta"
4034
4100
  `;
4035
4101
  }
4036
- function doneScript(bin) {
4102
+ function doneScript(bin, _runtimeDir) {
4037
4103
  return `#!/usr/bin/env bash
4038
4104
  # Hook: Stop \u2192 mark session as paused
4039
4105
  ${quietHelper(bin)}
@@ -4075,9 +4141,10 @@ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5, chmodSync as
4075
4141
  import { join as join10 } from "path";
4076
4142
  function installHookScripts(bin) {
4077
4143
  mkdirSync7(paths.hooks, { recursive: true });
4144
+ mkdirSync7(paths.runtime, { recursive: true });
4078
4145
  for (const [filename, scriptFn] of Object.entries(HOOK_SCRIPTS)) {
4079
4146
  const filePath = join10(paths.hooks, filename);
4080
- writeFileSync5(filePath, scriptFn(bin));
4147
+ writeFileSync5(filePath, scriptFn(bin, paths.runtime));
4081
4148
  chmodSync2(filePath, 493);
4082
4149
  }
4083
4150
  console.log(`Installed ${Object.keys(HOOK_SCRIPTS).length} hook scripts to ${paths.hooks}`);
@@ -4266,6 +4333,7 @@ var init_init = __esm(() => {
4266
4333
  `);
4267
4334
  mkdirSync10(paths.root, { recursive: true });
4268
4335
  mkdirSync10(paths.hooks, { recursive: true });
4336
+ mkdirSync10(paths.runtime, { recursive: true });
4269
4337
  const active = resolveActiveProject();
4270
4338
  runMigrations(active.db);
4271
4339
  console.log(` Database: ${active.db}`);
@@ -4296,6 +4364,40 @@ Error: couldn't locate the bertrand binary on PATH.
4296
4364
  });
4297
4365
  });
4298
4366
 
4367
+ // src/lib/projects/cli-flag.ts
4368
+ function extractProjectFlag2(args) {
4369
+ const rest = [];
4370
+ let project;
4371
+ for (let i = 0;i < args.length; i++) {
4372
+ const a = args[i];
4373
+ if (a === "--project") {
4374
+ project = args[i + 1];
4375
+ i++;
4376
+ continue;
4377
+ }
4378
+ if (a.startsWith("--project=")) {
4379
+ project = a.slice("--project=".length);
4380
+ continue;
4381
+ }
4382
+ rest.push(a);
4383
+ }
4384
+ return { project, rest };
4385
+ }
4386
+ function applyProjectFlag(slug) {
4387
+ if (!slug)
4388
+ return;
4389
+ if (!projectExists(slug)) {
4390
+ console.error(`Unknown project: ${slug}. Run \`bertrand project list\` to see registered projects.`);
4391
+ process.exit(1);
4392
+ }
4393
+ process.env.BERTRAND_PROJECT = slug;
4394
+ _resetActiveProjectCache();
4395
+ }
4396
+ var init_cli_flag = __esm(() => {
4397
+ init_resolve();
4398
+ init_registry();
4399
+ });
4400
+
4299
4401
  // src/cli/commands/list.ts
4300
4402
  var exports_list = {};
4301
4403
  function buildRows(sessions2) {
@@ -4311,13 +4413,15 @@ function buildRows(sessions2) {
4311
4413
  });
4312
4414
  }
4313
4415
  function renderTable(rows) {
4416
+ const dim = "\x1B[2m";
4417
+ const reset = "\x1B[0m";
4418
+ const project = resolveActiveProject();
4419
+ console.log(`${dim}Project: ${project.slug} (${project.name})${reset}`);
4314
4420
  if (rows.length === 0) {
4315
4421
  console.log("No sessions found.");
4316
4422
  return;
4317
4423
  }
4318
4424
  const maxName = Math.max(...rows.map((r) => r.name.length), 4);
4319
- const dim = "\x1B[2m";
4320
- const reset = "\x1B[0m";
4321
4425
  console.log(`${dim}${" "} ${"NAME".padEnd(maxName)} ${"STATUS".padEnd(10)} ${"DURATION".padEnd(8)} ${"CONVOS".padEnd(6)} LAST ACTIVE${reset}`);
4322
4426
  for (const row of rows) {
4323
4427
  const dot = STATUS_DOTS[row.status] ?? "?";
@@ -4329,12 +4433,14 @@ function renderTable(rows) {
4329
4433
  }
4330
4434
  }
4331
4435
  function renderJson(rows) {
4436
+ const project = resolveActiveProject();
4332
4437
  const data = rows.map((r) => ({
4333
4438
  name: r.name,
4334
4439
  status: r.status,
4335
4440
  duration: r.duration,
4336
4441
  conversations: r.conversations,
4337
- updatedAt: r.updatedAt
4442
+ updatedAt: r.updatedAt,
4443
+ project: { slug: project.slug, name: project.name }
4338
4444
  }));
4339
4445
  console.log(JSON.stringify(data, null, 2));
4340
4446
  }
@@ -4345,6 +4451,8 @@ var init_list = __esm(() => {
4345
4451
  init_categories();
4346
4452
  init_stats();
4347
4453
  init_format();
4454
+ init_resolve();
4455
+ init_cli_flag();
4348
4456
  STATUS_DOTS = {
4349
4457
  active: "\x1B[32m\u25CF\x1B[0m",
4350
4458
  waiting: "\x1B[33m\u25CF\x1B[0m",
@@ -4353,10 +4461,12 @@ var init_list = __esm(() => {
4353
4461
  };
4354
4462
  alias("ls", "list");
4355
4463
  register("list", async (args) => {
4356
- const isJson = args.includes("--json");
4357
- const showAll = args.includes("--all") || args.includes("-a");
4358
- const categoryFlag = args.indexOf("--category");
4359
- const categoryPath = categoryFlag !== -1 ? args[categoryFlag + 1] : undefined;
4464
+ const { project: projectSlug, rest: argsWithoutProject } = extractProjectFlag2(args);
4465
+ applyProjectFlag(projectSlug);
4466
+ const isJson = argsWithoutProject.includes("--json");
4467
+ const showAll = argsWithoutProject.includes("--all") || argsWithoutProject.includes("-a");
4468
+ const categoryFlag = argsWithoutProject.indexOf("--category");
4469
+ const categoryPath = categoryFlag !== -1 ? argsWithoutProject[categoryFlag + 1] : undefined;
4360
4470
  let sessionRows;
4361
4471
  if (categoryPath) {
4362
4472
  const category = getCategoryByPath(categoryPath);
@@ -4616,7 +4726,9 @@ function ansi256(code, text2) {
4616
4726
  return `\x1B[38;5;${code}m${text2}${RESET}`;
4617
4727
  }
4618
4728
  function showAllSessions() {
4729
+ const project = resolveActiveProject();
4619
4730
  const rows = getAllSessions().sort((a, b) => new Date(b.session.updatedAt).getTime() - new Date(a.session.updatedAt).getTime());
4731
+ console.log(`${DIM}Project: ${project.slug} (${project.name})${RESET}`);
4620
4732
  if (rows.length === 0) {
4621
4733
  console.log("No sessions.");
4622
4734
  return;
@@ -4741,15 +4853,22 @@ function renderTimingFooter(sessionId) {
4741
4853
  lines.push(`${DIM}Duration: ${formatDuration(durationS * 1000)} \xB7 Claude: ${formatDuration(claudeWorkS * 1000)} (${activePctVal}%) \xB7 Wait: ${formatDuration(userWaitS * 1000)} (${100 - activePctVal}%)${RESET}`);
4742
4854
  return lines;
4743
4855
  }
4856
+ function renderBreadcrumb(projectSlug, projectName, sessionName) {
4857
+ const segments = sessionName.split("/").filter(Boolean);
4858
+ const trail = segments.join(` ${DIM}\u203A${RESET} `);
4859
+ return `${DIM}${projectSlug} (${projectName})${RESET} ${DIM}\xB7${RESET} ${BOLD}${trail}${RESET}`;
4860
+ }
4744
4861
  function showSessionLog(session, sessionName, isJson) {
4745
4862
  const sessionId = session.id;
4746
4863
  const rawEvents = getEventsBySession(sessionId);
4747
4864
  const enriched = enrichAll(rawEvents);
4748
4865
  const compacted = compact(enriched);
4866
+ const project = resolveActiveProject();
4749
4867
  if (isJson) {
4750
4868
  const stats = getSessionStats(sessionId);
4751
4869
  const conversations2 = getConversationsBySession(sessionId);
4752
4870
  console.log(JSON.stringify({
4871
+ project: { slug: project.slug, name: project.name },
4753
4872
  session: { ...session, name: sessionName },
4754
4873
  stats,
4755
4874
  conversations: conversations2,
@@ -4767,6 +4886,8 @@ function showSessionLog(session, sessionName, isJson) {
4767
4886
  }
4768
4887
  const segments = segmentByConversation(compacted);
4769
4888
  const allLines = [];
4889
+ allLines.push(renderBreadcrumb(project.slug, project.name, sessionName));
4890
+ allLines.push("");
4770
4891
  for (let i = 0;i < segments.length; i++) {
4771
4892
  const segLines = renderSegment(segments[i], i, segments.length);
4772
4893
  allLines.push(...segLines);
@@ -4788,6 +4909,8 @@ var init_log = __esm(() => {
4788
4909
  init_compact();
4789
4910
  init_timing();
4790
4911
  init_format();
4912
+ init_resolve();
4913
+ init_cli_flag();
4791
4914
  STATUS_DOTS2 = {
4792
4915
  active: ansi(32, "\u25CF"),
4793
4916
  waiting: ansi(33, "\u25CF"),
@@ -4795,8 +4918,10 @@ var init_log = __esm(() => {
4795
4918
  archived: `${DIM}\u25CB${RESET}`
4796
4919
  };
4797
4920
  register("log", async (args) => {
4798
- const isJson = args.includes("--json");
4799
- const filteredArgs = args.filter((a) => !a.startsWith("--"));
4921
+ const { project: projectSlug, rest: argsWithoutProject } = extractProjectFlag2(args);
4922
+ applyProjectFlag(projectSlug);
4923
+ const isJson = argsWithoutProject.includes("--json");
4924
+ const filteredArgs = argsWithoutProject.filter((a) => !a.startsWith("--"));
4800
4925
  const target = filteredArgs[0];
4801
4926
  if (!target) {
4802
4927
  showAllSessions();