Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ All notable changes to **basou** are recorded here. The project follows
action are skipped. Imports reuse the existing `session import` pipeline,
so path sanitization and id minting are unchanged. Re-running an import is
idempotent: a transcript already imported is skipped rather than duplicated.
Imported sessions are labelled with a human summary
(`claude-code <date>: <n> commands, <m> files`) instead of the raw session
id, so they read as content in `basou session list` and the handoff.
- `claude-code-import` session `source.kind` (additive enum value) to
distinguish transcript-derived sessions from live `claude-code-adapter`
runs and Basou-format `import`s.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ describe("claudeTranscriptToImportPayload", () => {
expect(payload.session.related_files).toEqual([`${CWD}/a.ts`, `${CWD}/b.ts`]);
// The transcript's own sessionId becomes the source external_id (dedup key).
expect(payload.session.source.external_id).toBe("abc-123");
// The label is a human-readable summary (date + counts), not an opaque id.
expect(payload.session.label).toBe("claude-code 2026-05-10: 1 command, 2 files");

const types = payload.events.map((e) => e.type);
expect(types).toEqual([
Expand Down Expand Up @@ -177,7 +179,9 @@ describe("claudeTranscriptToImportPayload", () => {
expect(payload).not.toBeNull();
if (payload === null) return;
expect(payload.session.source.external_id).toBe("from-option");
expect(payload.session.label).toContain("from-option");
// The id lives in source.external_id; the label is a content summary, not the id.
expect(payload.session.label).not.toContain("from-option");
expect(payload.session.label).toMatch(/^claude-code \d{4}-\d{2}-\d{2}: \d+ command/);
});

it("returns null when no observable command / file action exists", () => {
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/adapters/claude-code/transcript-importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,15 @@ export function claudeTranscriptToImportPayload(
];

const externalId = options.externalId ?? claudeSessionId;
const label =
externalId !== undefined ? `claude-code import ${externalId}` : "claude-code import";
// Human-readable label: when + how much, so the session reads as content in
// `basou session list` / handoff rather than an opaque id. The source id is
// kept structurally in `source.external_id` (not the label), and paths are
// deliberately excluded — the label is NOT path-sanitized downstream, so a
// raw file path here would leak an operator-private prefix.
const commandCount = derived.reduce((n, e) => (e.type === "command_executed" ? n + 1 : n), 0);
const fileCount = relatedFiles.size;
const date = minTs.slice(0, 10);
const label = `claude-code ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}, ${fileCount} ${fileCount === 1 ? "file" : "files"}`;

const payload: SessionImportPayload = {
schema_version: "0.1.0",
Expand Down