bertrand 0.20.1 → 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 +3 -2
- package/dist/bertrand.js +143 -45
- package/dist/run-screen.js +138 -62
- package/package.json +1 -1
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
|
|
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
|
};
|
|
@@ -3453,10 +3455,8 @@ function installExitHandlers() {
|
|
|
3453
3455
|
const onSignal = (signal) => {
|
|
3454
3456
|
if (isClaudeRunning())
|
|
3455
3457
|
return;
|
|
3456
|
-
process.exit(signal === "
|
|
3458
|
+
process.exit(signal === "SIGHUP" ? 129 : 143);
|
|
3457
3459
|
};
|
|
3458
|
-
process.on("SIGINT", onSignal);
|
|
3459
|
-
process.on("SIGTERM", onSignal);
|
|
3460
3460
|
process.on("SIGHUP", onSignal);
|
|
3461
3461
|
}
|
|
3462
3462
|
async function launch(opts) {
|
|
@@ -3568,6 +3568,7 @@ function finalizeSession(sessionId, conversationId, exitCode) {
|
|
|
3568
3568
|
emitSessionEnded({ sessionId });
|
|
3569
3569
|
computeAndPersist(sessionId);
|
|
3570
3570
|
stopServerIfIdle();
|
|
3571
|
+
triggerBackgroundPush();
|
|
3571
3572
|
}
|
|
3572
3573
|
var liveSession = null, exitHandlersInstalled = false;
|
|
3573
3574
|
var init_session = __esm(() => {
|
|
@@ -3582,27 +3583,56 @@ var init_session = __esm(() => {
|
|
|
3582
3583
|
init_spawn_context();
|
|
3583
3584
|
init_timing();
|
|
3584
3585
|
init_server_lifecycle();
|
|
3586
|
+
init_trigger();
|
|
3585
3587
|
});
|
|
3586
3588
|
|
|
3587
3589
|
// src/tui/app.tsx
|
|
3588
3590
|
import { spawn as spawn4 } from "child_process";
|
|
3589
3591
|
import { existsSync as existsSync8, readFileSync as readFileSync8, unlinkSync as unlinkSync3 } from "fs";
|
|
3592
|
+
import { tmpdir } from "os";
|
|
3590
3593
|
import { join as join9 } from "path";
|
|
3591
3594
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3592
3595
|
async function runScreen(screen, ...args) {
|
|
3593
|
-
const tmpFile =
|
|
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
|
+
}
|
|
3594
3604
|
const child = spawn4("bun", ["run", SCREEN_ENTRY, screen, tmpFile, ...args], {
|
|
3595
|
-
stdio: "inherit"
|
|
3596
|
-
|
|
3597
|
-
const exitCode = await new Promise((resolve) => {
|
|
3598
|
-
child.on("exit", (code) => resolve(code ?? 1));
|
|
3605
|
+
stdio: "inherit",
|
|
3606
|
+
env: process.env
|
|
3599
3607
|
});
|
|
3600
|
-
|
|
3601
|
-
|
|
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 {}
|
|
3602
3635
|
}
|
|
3603
|
-
const result = JSON.parse(readFileSync8(tmpFile, "utf-8"));
|
|
3604
|
-
unlinkSync3(tmpFile);
|
|
3605
|
-
return result;
|
|
3606
3636
|
}
|
|
3607
3637
|
async function startLaunchTui() {
|
|
3608
3638
|
return runScreen("launch");
|
|
@@ -3615,9 +3645,6 @@ async function startExitTui(sessionId) {
|
|
|
3615
3645
|
}
|
|
3616
3646
|
async function startResumeTui(sessionId) {
|
|
3617
3647
|
const conversations2 = getConversationsBySession(sessionId);
|
|
3618
|
-
if (conversations2.length === 1) {
|
|
3619
|
-
return { type: "conversation", conversationId: conversations2[0].id };
|
|
3620
|
-
}
|
|
3621
3648
|
if (conversations2.length === 0) {
|
|
3622
3649
|
return { type: "new" };
|
|
3623
3650
|
}
|
|
@@ -3782,21 +3809,33 @@ var init_recovery = __esm(() => {
|
|
|
3782
3809
|
|
|
3783
3810
|
// src/cli/commands/launch.ts
|
|
3784
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
|
+
}
|
|
3785
3820
|
var init_launch = __esm(() => {
|
|
3786
3821
|
init_router();
|
|
3787
3822
|
init_app();
|
|
3788
3823
|
init_session();
|
|
3789
3824
|
init_recovery();
|
|
3790
3825
|
register("launch", async (args) => {
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
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);
|
|
3798
3838
|
}
|
|
3799
|
-
await startTui();
|
|
3800
3839
|
});
|
|
3801
3840
|
});
|
|
3802
3841
|
|
|
@@ -3804,7 +3843,7 @@ var init_launch = __esm(() => {
|
|
|
3804
3843
|
function quietHelper(bin) {
|
|
3805
3844
|
return `bq() { ${bin} "$@" 2>/dev/null || true; }`;
|
|
3806
3845
|
}
|
|
3807
|
-
function waitingScript(bin) {
|
|
3846
|
+
function waitingScript(bin, runtimeDir) {
|
|
3808
3847
|
return `#!/usr/bin/env bash
|
|
3809
3848
|
# Hook: PreToolUse AskUserQuestion \u2192 enforce multiSelect, mark session as waiting
|
|
3810
3849
|
${quietHelper(bin)}
|
|
@@ -3829,7 +3868,7 @@ question="$(printf '%s' "$input" | grep -o '"question":"[^"]*"' | head -1 | cut
|
|
|
3829
3868
|
[ -z "$question" ] && question="Waiting for input"
|
|
3830
3869
|
|
|
3831
3870
|
# Clear working debounce marker so next resume\u2192working transition fires
|
|
3832
|
-
rm -f "/
|
|
3871
|
+
rm -f "${runtimeDir}/working-$sid"
|
|
3833
3872
|
|
|
3834
3873
|
bq update --session-id "$sid" --event session.waiting --meta "$(jq -n --arg q "$question" --arg cid "$cid" '{question:$q, claude_id:$cid}')"
|
|
3835
3874
|
|
|
@@ -3850,7 +3889,7 @@ bq notify bertrand "$question" &
|
|
|
3850
3889
|
wait
|
|
3851
3890
|
`;
|
|
3852
3891
|
}
|
|
3853
|
-
function answeredScript(bin) {
|
|
3892
|
+
function answeredScript(bin, _runtimeDir) {
|
|
3854
3893
|
return `#!/usr/bin/env bash
|
|
3855
3894
|
# Hook: PostToolUse AskUserQuestion \u2192 mark session as active
|
|
3856
3895
|
#
|
|
@@ -3904,7 +3943,7 @@ fi
|
|
|
3904
3943
|
wait
|
|
3905
3944
|
`;
|
|
3906
3945
|
}
|
|
3907
|
-
function activeScript(bin) {
|
|
3946
|
+
function activeScript(bin, runtimeDir) {
|
|
3908
3947
|
return `#!/usr/bin/env bash
|
|
3909
3948
|
# Hook: PreToolUse (catch-all) \u2192 flip waiting to active
|
|
3910
3949
|
${quietHelper(bin)}
|
|
@@ -3913,7 +3952,7 @@ sid="\${BERTRAND_SESSION:-}"
|
|
|
3913
3952
|
|
|
3914
3953
|
# Debounce: skip if we already sent session.active within the last 5 seconds.
|
|
3915
3954
|
# This avoids spawning bertrand (~31ms) on every tool call during rapid sequences.
|
|
3916
|
-
marker="/
|
|
3955
|
+
marker="${runtimeDir}/working-$sid"
|
|
3917
3956
|
if [ -f "$marker" ]; then
|
|
3918
3957
|
age=$(( $(date +%s) - $(stat -f%m "$marker" 2>/dev/null || echo 0) ))
|
|
3919
3958
|
[ "$age" -lt 5 ] && exit 0
|
|
@@ -3930,7 +3969,7 @@ cid="\${BERTRAND_CLAUDE_ID:-}"
|
|
|
3930
3969
|
bq update --session-id "$sid" --event session.active --meta "$(jq -n --arg cid "$cid" '{claude_id:$cid}')"
|
|
3931
3970
|
`;
|
|
3932
3971
|
}
|
|
3933
|
-
function permissionWaitScript(bin) {
|
|
3972
|
+
function permissionWaitScript(bin, runtimeDir) {
|
|
3934
3973
|
return `#!/usr/bin/env bash
|
|
3935
3974
|
# Hook: PermissionRequest \u2192 mark pending, emit permission.request
|
|
3936
3975
|
${quietHelper(bin)}
|
|
@@ -3942,7 +3981,7 @@ ${EXTRACT_TOOL}
|
|
|
3942
3981
|
[ "$tool" = "AskUserQuestion" ] && exit 0
|
|
3943
3982
|
|
|
3944
3983
|
# Write marker so PostToolUse knows this was a real permission prompt (not auto-approved)
|
|
3945
|
-
touch "/
|
|
3984
|
+
touch "${runtimeDir}/perm-pending-$sid"
|
|
3946
3985
|
|
|
3947
3986
|
# Extract detail from tool_input via grep (avoid jq for simple fields)
|
|
3948
3987
|
detail=""
|
|
@@ -3960,7 +3999,7 @@ bq notify bertrand "Needs permission: $tool" &
|
|
|
3960
3999
|
wait
|
|
3961
4000
|
`;
|
|
3962
4001
|
}
|
|
3963
|
-
function permissionDoneScript(bin) {
|
|
4002
|
+
function permissionDoneScript(bin, runtimeDir) {
|
|
3964
4003
|
return `#!/usr/bin/env bash
|
|
3965
4004
|
# Hook: PostToolUse (catch-all)
|
|
3966
4005
|
#
|
|
@@ -3985,7 +4024,7 @@ ${EXTRACT_TOOL}
|
|
|
3985
4024
|
# Don't double-log: AskUserQuestion has its own waiting/answered events
|
|
3986
4025
|
[ "$tool" = "AskUserQuestion" ] && exit 0
|
|
3987
4026
|
|
|
3988
|
-
marker="/
|
|
4027
|
+
marker="${runtimeDir}/perm-pending-$sid"
|
|
3989
4028
|
had_marker=0
|
|
3990
4029
|
if [ -f "$marker" ]; then
|
|
3991
4030
|
had_marker=1
|
|
@@ -4044,7 +4083,7 @@ fi
|
|
|
4044
4083
|
wait
|
|
4045
4084
|
`;
|
|
4046
4085
|
}
|
|
4047
|
-
function userPromptScript(bin) {
|
|
4086
|
+
function userPromptScript(bin, _runtimeDir) {
|
|
4048
4087
|
return `#!/usr/bin/env bash
|
|
4049
4088
|
# Hook: UserPromptSubmit \u2192 record user free-text prompt
|
|
4050
4089
|
${quietHelper(bin)}
|
|
@@ -4060,7 +4099,7 @@ meta="$(printf '%s' "$input" | jq --arg cid "$cid" '{prompt: (.prompt // ""), cl
|
|
|
4060
4099
|
bq update --session-id "$sid" --event user.prompt --meta "$meta"
|
|
4061
4100
|
`;
|
|
4062
4101
|
}
|
|
4063
|
-
function doneScript(bin) {
|
|
4102
|
+
function doneScript(bin, _runtimeDir) {
|
|
4064
4103
|
return `#!/usr/bin/env bash
|
|
4065
4104
|
# Hook: Stop \u2192 mark session as paused
|
|
4066
4105
|
${quietHelper(bin)}
|
|
@@ -4102,9 +4141,10 @@ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync5, chmodSync as
|
|
|
4102
4141
|
import { join as join10 } from "path";
|
|
4103
4142
|
function installHookScripts(bin) {
|
|
4104
4143
|
mkdirSync7(paths.hooks, { recursive: true });
|
|
4144
|
+
mkdirSync7(paths.runtime, { recursive: true });
|
|
4105
4145
|
for (const [filename, scriptFn] of Object.entries(HOOK_SCRIPTS)) {
|
|
4106
4146
|
const filePath = join10(paths.hooks, filename);
|
|
4107
|
-
writeFileSync5(filePath, scriptFn(bin));
|
|
4147
|
+
writeFileSync5(filePath, scriptFn(bin, paths.runtime));
|
|
4108
4148
|
chmodSync2(filePath, 493);
|
|
4109
4149
|
}
|
|
4110
4150
|
console.log(`Installed ${Object.keys(HOOK_SCRIPTS).length} hook scripts to ${paths.hooks}`);
|
|
@@ -4293,6 +4333,7 @@ var init_init = __esm(() => {
|
|
|
4293
4333
|
`);
|
|
4294
4334
|
mkdirSync10(paths.root, { recursive: true });
|
|
4295
4335
|
mkdirSync10(paths.hooks, { recursive: true });
|
|
4336
|
+
mkdirSync10(paths.runtime, { recursive: true });
|
|
4296
4337
|
const active = resolveActiveProject();
|
|
4297
4338
|
runMigrations(active.db);
|
|
4298
4339
|
console.log(` Database: ${active.db}`);
|
|
@@ -4323,6 +4364,40 @@ Error: couldn't locate the bertrand binary on PATH.
|
|
|
4323
4364
|
});
|
|
4324
4365
|
});
|
|
4325
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
|
+
|
|
4326
4401
|
// src/cli/commands/list.ts
|
|
4327
4402
|
var exports_list = {};
|
|
4328
4403
|
function buildRows(sessions2) {
|
|
@@ -4338,13 +4413,15 @@ function buildRows(sessions2) {
|
|
|
4338
4413
|
});
|
|
4339
4414
|
}
|
|
4340
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}`);
|
|
4341
4420
|
if (rows.length === 0) {
|
|
4342
4421
|
console.log("No sessions found.");
|
|
4343
4422
|
return;
|
|
4344
4423
|
}
|
|
4345
4424
|
const maxName = Math.max(...rows.map((r) => r.name.length), 4);
|
|
4346
|
-
const dim = "\x1B[2m";
|
|
4347
|
-
const reset = "\x1B[0m";
|
|
4348
4425
|
console.log(`${dim}${" "} ${"NAME".padEnd(maxName)} ${"STATUS".padEnd(10)} ${"DURATION".padEnd(8)} ${"CONVOS".padEnd(6)} LAST ACTIVE${reset}`);
|
|
4349
4426
|
for (const row of rows) {
|
|
4350
4427
|
const dot = STATUS_DOTS[row.status] ?? "?";
|
|
@@ -4356,12 +4433,14 @@ function renderTable(rows) {
|
|
|
4356
4433
|
}
|
|
4357
4434
|
}
|
|
4358
4435
|
function renderJson(rows) {
|
|
4436
|
+
const project = resolveActiveProject();
|
|
4359
4437
|
const data = rows.map((r) => ({
|
|
4360
4438
|
name: r.name,
|
|
4361
4439
|
status: r.status,
|
|
4362
4440
|
duration: r.duration,
|
|
4363
4441
|
conversations: r.conversations,
|
|
4364
|
-
updatedAt: r.updatedAt
|
|
4442
|
+
updatedAt: r.updatedAt,
|
|
4443
|
+
project: { slug: project.slug, name: project.name }
|
|
4365
4444
|
}));
|
|
4366
4445
|
console.log(JSON.stringify(data, null, 2));
|
|
4367
4446
|
}
|
|
@@ -4372,6 +4451,8 @@ var init_list = __esm(() => {
|
|
|
4372
4451
|
init_categories();
|
|
4373
4452
|
init_stats();
|
|
4374
4453
|
init_format();
|
|
4454
|
+
init_resolve();
|
|
4455
|
+
init_cli_flag();
|
|
4375
4456
|
STATUS_DOTS = {
|
|
4376
4457
|
active: "\x1B[32m\u25CF\x1B[0m",
|
|
4377
4458
|
waiting: "\x1B[33m\u25CF\x1B[0m",
|
|
@@ -4380,10 +4461,12 @@ var init_list = __esm(() => {
|
|
|
4380
4461
|
};
|
|
4381
4462
|
alias("ls", "list");
|
|
4382
4463
|
register("list", async (args) => {
|
|
4383
|
-
const
|
|
4384
|
-
|
|
4385
|
-
const
|
|
4386
|
-
const
|
|
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;
|
|
4387
4470
|
let sessionRows;
|
|
4388
4471
|
if (categoryPath) {
|
|
4389
4472
|
const category = getCategoryByPath(categoryPath);
|
|
@@ -4643,7 +4726,9 @@ function ansi256(code, text2) {
|
|
|
4643
4726
|
return `\x1B[38;5;${code}m${text2}${RESET}`;
|
|
4644
4727
|
}
|
|
4645
4728
|
function showAllSessions() {
|
|
4729
|
+
const project = resolveActiveProject();
|
|
4646
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}`);
|
|
4647
4732
|
if (rows.length === 0) {
|
|
4648
4733
|
console.log("No sessions.");
|
|
4649
4734
|
return;
|
|
@@ -4768,15 +4853,22 @@ function renderTimingFooter(sessionId) {
|
|
|
4768
4853
|
lines.push(`${DIM}Duration: ${formatDuration(durationS * 1000)} \xB7 Claude: ${formatDuration(claudeWorkS * 1000)} (${activePctVal}%) \xB7 Wait: ${formatDuration(userWaitS * 1000)} (${100 - activePctVal}%)${RESET}`);
|
|
4769
4854
|
return lines;
|
|
4770
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
|
+
}
|
|
4771
4861
|
function showSessionLog(session, sessionName, isJson) {
|
|
4772
4862
|
const sessionId = session.id;
|
|
4773
4863
|
const rawEvents = getEventsBySession(sessionId);
|
|
4774
4864
|
const enriched = enrichAll(rawEvents);
|
|
4775
4865
|
const compacted = compact(enriched);
|
|
4866
|
+
const project = resolveActiveProject();
|
|
4776
4867
|
if (isJson) {
|
|
4777
4868
|
const stats = getSessionStats(sessionId);
|
|
4778
4869
|
const conversations2 = getConversationsBySession(sessionId);
|
|
4779
4870
|
console.log(JSON.stringify({
|
|
4871
|
+
project: { slug: project.slug, name: project.name },
|
|
4780
4872
|
session: { ...session, name: sessionName },
|
|
4781
4873
|
stats,
|
|
4782
4874
|
conversations: conversations2,
|
|
@@ -4794,6 +4886,8 @@ function showSessionLog(session, sessionName, isJson) {
|
|
|
4794
4886
|
}
|
|
4795
4887
|
const segments = segmentByConversation(compacted);
|
|
4796
4888
|
const allLines = [];
|
|
4889
|
+
allLines.push(renderBreadcrumb(project.slug, project.name, sessionName));
|
|
4890
|
+
allLines.push("");
|
|
4797
4891
|
for (let i = 0;i < segments.length; i++) {
|
|
4798
4892
|
const segLines = renderSegment(segments[i], i, segments.length);
|
|
4799
4893
|
allLines.push(...segLines);
|
|
@@ -4815,6 +4909,8 @@ var init_log = __esm(() => {
|
|
|
4815
4909
|
init_compact();
|
|
4816
4910
|
init_timing();
|
|
4817
4911
|
init_format();
|
|
4912
|
+
init_resolve();
|
|
4913
|
+
init_cli_flag();
|
|
4818
4914
|
STATUS_DOTS2 = {
|
|
4819
4915
|
active: ansi(32, "\u25CF"),
|
|
4820
4916
|
waiting: ansi(33, "\u25CF"),
|
|
@@ -4822,8 +4918,10 @@ var init_log = __esm(() => {
|
|
|
4822
4918
|
archived: `${DIM}\u25CB${RESET}`
|
|
4823
4919
|
};
|
|
4824
4920
|
register("log", async (args) => {
|
|
4825
|
-
const
|
|
4826
|
-
|
|
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("--"));
|
|
4827
4925
|
const target = filteredArgs[0];
|
|
4828
4926
|
if (!target) {
|
|
4829
4927
|
showAllSessions();
|
package/dist/run-screen.js
CHANGED
|
@@ -15,7 +15,7 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
// src/tui/run-screen.tsx
|
|
18
|
-
import { writeFileSync as writeFileSync2 } from "fs";
|
|
18
|
+
import { writeFileSync as writeFileSync2, appendFileSync } from "fs";
|
|
19
19
|
import { render } from "@orchetron/storm";
|
|
20
20
|
|
|
21
21
|
// src/tui/screens/launch/index.tsx
|
|
@@ -590,6 +590,7 @@ var paths = {
|
|
|
590
590
|
root: join(homedir(), BERTRAND_DIR),
|
|
591
591
|
hooks: join(homedir(), BERTRAND_DIR, "hooks"),
|
|
592
592
|
sessions: join(homedir(), BERTRAND_DIR, "sessions"),
|
|
593
|
+
runtime: join(homedir(), BERTRAND_DIR, "run"),
|
|
593
594
|
db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
|
|
594
595
|
syncEnv: join(homedir(), BERTRAND_DIR, "sync.env")
|
|
595
596
|
};
|
|
@@ -1324,7 +1325,7 @@ var OPTIONS = [
|
|
|
1324
1325
|
{
|
|
1325
1326
|
action: "resume",
|
|
1326
1327
|
label: "Resume",
|
|
1327
|
-
hint: "
|
|
1328
|
+
hint: "Continue a conversation or start a new one"
|
|
1328
1329
|
}
|
|
1329
1330
|
];
|
|
1330
1331
|
function Exit({ sessionId, onAction }) {
|
|
@@ -1730,71 +1731,146 @@ if (!screen || !outputPath) {
|
|
|
1730
1731
|
console.error("Usage: run-screen <screen> <outputPath> [args...]");
|
|
1731
1732
|
process.exit(1);
|
|
1732
1733
|
}
|
|
1734
|
+
var RESULT_PATH = outputPath;
|
|
1735
|
+
var DEBUG_PATH = process.env.BERTRAND_DEBUG_TUI || null;
|
|
1736
|
+
var START_HR = process.hrtime.bigint();
|
|
1737
|
+
function phase(name, detail) {
|
|
1738
|
+
if (!DEBUG_PATH)
|
|
1739
|
+
return;
|
|
1740
|
+
const elapsedMs = Number(process.hrtime.bigint() - START_HR) / 1e6;
|
|
1741
|
+
const line = `${elapsedMs.toFixed(2).padStart(8)}ms ${name}${detail ? " " + detail : ""}
|
|
1742
|
+
`;
|
|
1743
|
+
try {
|
|
1744
|
+
appendFileSync(DEBUG_PATH, line);
|
|
1745
|
+
} catch {}
|
|
1746
|
+
}
|
|
1747
|
+
phase("spawn", `screen=${screen} pid=${process.pid} isTTY=${process.stdout.isTTY ?? "?"} term=${process.env.TERM_PROGRAM || process.env.TERM || "?"}`);
|
|
1733
1748
|
var result;
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1749
|
+
var resultWritten = false;
|
|
1750
|
+
function writeResult() {
|
|
1751
|
+
if (resultWritten)
|
|
1752
|
+
return;
|
|
1753
|
+
resultWritten = true;
|
|
1754
|
+
try {
|
|
1755
|
+
writeFileSync2(RESULT_PATH, JSON.stringify(result));
|
|
1756
|
+
} catch {}
|
|
1757
|
+
}
|
|
1758
|
+
process.on("SIGINT", writeResult);
|
|
1759
|
+
process.on("SIGTERM", writeResult);
|
|
1760
|
+
process.on("SIGHUP", writeResult);
|
|
1761
|
+
process.on("uncaughtException", writeResult);
|
|
1762
|
+
process.on("unhandledRejection", writeResult);
|
|
1763
|
+
var renderCount = 0;
|
|
1764
|
+
var onRender = (m) => {
|
|
1765
|
+
renderCount++;
|
|
1766
|
+
phase(`paint #${renderCount}`, `cells=${m.cellsChanged} took=${m.renderTime.toFixed(2)}ms`);
|
|
1767
|
+
};
|
|
1768
|
+
try {
|
|
1769
|
+
switch (screen) {
|
|
1770
|
+
case "launch": {
|
|
1771
|
+
let selection = { type: "quit" };
|
|
1772
|
+
result = selection;
|
|
1773
|
+
phase("render() pre");
|
|
1774
|
+
const app = render(/* @__PURE__ */ jsxDEV10(Launch, {
|
|
1775
|
+
onSelect: (s) => {
|
|
1776
|
+
selection = s;
|
|
1777
|
+
result = selection;
|
|
1778
|
+
}
|
|
1779
|
+
}, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
|
|
1780
|
+
phase("render() post");
|
|
1781
|
+
app.screen.invalidate();
|
|
1782
|
+
app.requestRepaint();
|
|
1783
|
+
phase("post-mount invalidate+requestRepaint");
|
|
1784
|
+
await app.waitUntilExit();
|
|
1785
|
+
phase("waitUntilExit returned");
|
|
1786
|
+
app.unmount();
|
|
1787
|
+
phase("unmount done");
|
|
1788
|
+
result = selection;
|
|
1789
|
+
break;
|
|
1764
1790
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1791
|
+
case "project-picker": {
|
|
1792
|
+
let selection = { type: "quit" };
|
|
1793
|
+
result = selection;
|
|
1794
|
+
phase("render() pre");
|
|
1795
|
+
const app = render(/* @__PURE__ */ jsxDEV10(ProjectPicker, {
|
|
1796
|
+
onSelect: (s) => {
|
|
1797
|
+
selection = s;
|
|
1798
|
+
result = selection;
|
|
1799
|
+
}
|
|
1800
|
+
}, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
|
|
1801
|
+
phase("render() post");
|
|
1802
|
+
app.screen.invalidate();
|
|
1803
|
+
app.requestRepaint();
|
|
1804
|
+
phase("post-mount invalidate+requestRepaint");
|
|
1805
|
+
await app.waitUntilExit();
|
|
1806
|
+
phase("waitUntilExit returned");
|
|
1807
|
+
app.unmount();
|
|
1808
|
+
phase("unmount done");
|
|
1809
|
+
result = selection;
|
|
1810
|
+
break;
|
|
1811
|
+
}
|
|
1812
|
+
case "exit": {
|
|
1813
|
+
const sessionId = args[0];
|
|
1814
|
+
if (!sessionId) {
|
|
1815
|
+
console.error("exit requires sessionId");
|
|
1816
|
+
process.exit(1);
|
|
1770
1817
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1818
|
+
let action = "save";
|
|
1819
|
+
result = action;
|
|
1820
|
+
phase("render() pre");
|
|
1821
|
+
const app = render(/* @__PURE__ */ jsxDEV10(Exit, {
|
|
1822
|
+
sessionId,
|
|
1823
|
+
onAction: (a) => {
|
|
1824
|
+
action = a;
|
|
1825
|
+
result = action;
|
|
1826
|
+
}
|
|
1827
|
+
}, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
|
|
1828
|
+
phase("render() post");
|
|
1829
|
+
app.screen.invalidate();
|
|
1830
|
+
app.requestRepaint();
|
|
1831
|
+
phase("post-mount invalidate+requestRepaint");
|
|
1832
|
+
await app.waitUntilExit();
|
|
1833
|
+
phase("waitUntilExit returned");
|
|
1834
|
+
app.unmount();
|
|
1835
|
+
phase("unmount done");
|
|
1836
|
+
result = action;
|
|
1837
|
+
break;
|
|
1782
1838
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
sessionId
|
|
1786
|
-
|
|
1787
|
-
|
|
1839
|
+
case "resume": {
|
|
1840
|
+
const sessionId = args[0];
|
|
1841
|
+
if (!sessionId) {
|
|
1842
|
+
console.error("resume requires sessionId");
|
|
1843
|
+
process.exit(1);
|
|
1788
1844
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1845
|
+
let selection = { type: "back" };
|
|
1846
|
+
result = selection;
|
|
1847
|
+
phase("render() pre");
|
|
1848
|
+
const app = render(/* @__PURE__ */ jsxDEV10(Resume, {
|
|
1849
|
+
sessionId,
|
|
1850
|
+
onSelect: (s) => {
|
|
1851
|
+
selection = s;
|
|
1852
|
+
result = selection;
|
|
1853
|
+
}
|
|
1854
|
+
}, undefined, false, undefined, this), { alternateScreen: true, patchConsole: true, onRender });
|
|
1855
|
+
phase("render() post");
|
|
1856
|
+
app.screen.invalidate();
|
|
1857
|
+
app.requestRepaint();
|
|
1858
|
+
phase("post-mount invalidate+requestRepaint");
|
|
1859
|
+
await app.waitUntilExit();
|
|
1860
|
+
phase("waitUntilExit returned");
|
|
1861
|
+
app.unmount();
|
|
1862
|
+
phase("unmount done");
|
|
1863
|
+
result = selection;
|
|
1864
|
+
break;
|
|
1865
|
+
}
|
|
1866
|
+
default:
|
|
1867
|
+
console.error(`Unknown screen: ${screen}`);
|
|
1868
|
+
process.exit(1);
|
|
1794
1869
|
}
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1870
|
+
writeResult();
|
|
1871
|
+
phase("result written");
|
|
1872
|
+
} finally {
|
|
1873
|
+
writeResult();
|
|
1874
|
+
phase("finally complete");
|
|
1798
1875
|
}
|
|
1799
|
-
writeFileSync2(outputPath, JSON.stringify(result));
|
|
1800
1876
|
process.exit(0);
|