Skip to content

Commit e3eca40

Browse files
committed
feat: add session export cli command
opencode session export [sessionID] export session transcript to file Positionals: sessionID session id to export Options: -o, --output output file path -f, --format output format [string] [choices: "markdown", "json"] [default: "markdown"] Fixes: #5426 Signed-off-by: Christian Stewart <[email protected]>
1 parent 8c05eb2 commit e3eca40

File tree

1 file changed

+135
-1
lines changed

1 file changed

+135
-1
lines changed

packages/opencode/src/cli/cmd/session.ts

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { Locale } from "../../util/locale"
77
import { Flag } from "../../flag/flag"
88
import { EOL } from "os"
99
import path from "path"
10+
import * as prompts from "@clack/prompts"
11+
import { formatTranscript } from "./tui/util/transcript"
1012

1113
function pagerCmd(): string[] {
1214
const lessOptions = ["-R", "-S"]
@@ -38,7 +40,7 @@ function pagerCmd(): string[] {
3840
export const SessionCommand = cmd({
3941
command: "session",
4042
describe: "manage sessions",
41-
builder: (yargs: Argv) => yargs.command(SessionListCommand).demandCommand(),
43+
builder: (yargs: Argv) => yargs.command(SessionListCommand).command(SessionExportCommand).demandCommand(),
4244
async handler() {},
4345
})
4446

@@ -133,3 +135,135 @@ function formatSessionJSON(sessions: Session.Info[]): string {
133135
}))
134136
return JSON.stringify(jsonData, null, 2)
135137
}
138+
139+
export const SessionExportCommand = cmd({
140+
command: "export [sessionID]",
141+
describe: "export session transcript to file",
142+
builder: (yargs: Argv) => {
143+
return yargs
144+
.positional("sessionID", {
145+
describe: "session id to export",
146+
type: "string",
147+
})
148+
.option("output", {
149+
alias: "o",
150+
describe: "output file path",
151+
type: "string",
152+
})
153+
.option("format", {
154+
alias: "f",
155+
describe: "output format",
156+
type: "string",
157+
choices: ["markdown", "json"],
158+
default: "markdown",
159+
})
160+
.option("thinking", {
161+
describe: "include thinking/reasoning blocks",
162+
type: "boolean",
163+
default: true,
164+
})
165+
.option("tool-details", {
166+
describe: "include tool input/output details",
167+
type: "boolean",
168+
default: true,
169+
})
170+
.option("assistant-metadata", {
171+
describe: "include assistant metadata (agent, model, duration)",
172+
type: "boolean",
173+
default: true,
174+
})
175+
},
176+
handler: async (args) => {
177+
await bootstrap(process.cwd(), async () => {
178+
let sessionID = args.sessionID
179+
180+
if (!sessionID) {
181+
prompts.intro("Export session", {
182+
output: process.stderr,
183+
})
184+
185+
const sessions = []
186+
for await (const session of Session.list()) {
187+
sessions.push(session)
188+
}
189+
190+
if (sessions.length === 0) {
191+
prompts.log.error("No sessions found", {
192+
output: process.stderr,
193+
})
194+
prompts.outro("Done", {
195+
output: process.stderr,
196+
})
197+
return
198+
}
199+
200+
sessions.sort((a, b) => b.time.updated - a.time.updated)
201+
202+
const selectedSession = await prompts.autocomplete({
203+
message: "Select session to export",
204+
maxItems: 10,
205+
options: sessions.map((session) => ({
206+
label: session.title,
207+
value: session.id,
208+
hint: `${new Date(session.time.updated).toLocaleString()}${session.id.slice(-8)}`,
209+
})),
210+
output: process.stderr,
211+
})
212+
213+
if (prompts.isCancel(selectedSession)) {
214+
throw new UI.CancelledError()
215+
}
216+
217+
sessionID = selectedSession as string
218+
}
219+
220+
const sessionInfo = await Session.get(sessionID!)
221+
222+
let content: string
223+
let defaultExtension: string
224+
225+
const sessionMessages = await Session.messages({ sessionID: sessionID! })
226+
227+
if (args.format === "json") {
228+
const exportData = {
229+
info: sessionInfo,
230+
messages: sessionMessages.map((msg) => ({
231+
info: msg.info,
232+
parts: msg.parts,
233+
})),
234+
}
235+
content = JSON.stringify(exportData, null, 2)
236+
defaultExtension = "json"
237+
} else {
238+
content = formatTranscript(sessionInfo, sessionMessages, {
239+
thinking: args.thinking,
240+
toolDetails: args.toolDetails,
241+
assistantMetadata: args.assistantMetadata,
242+
})
243+
defaultExtension = "md"
244+
}
245+
246+
const outputPath = await (async () => {
247+
if (args.output) return args.output
248+
const defaultFilename = `session-${sessionInfo.id.slice(0, 8)}.${defaultExtension}`
249+
const filenameInput = await prompts.text({
250+
message: "Export filename",
251+
defaultValue: defaultFilename,
252+
output: process.stderr,
253+
})
254+
255+
if (prompts.isCancel(filenameInput)) {
256+
throw new UI.CancelledError()
257+
}
258+
259+
return filenameInput.trim()
260+
})()
261+
262+
await Bun.write(outputPath, content)
263+
264+
prompts.outro(`Session exported to ${outputPath}`, {
265+
output: process.stderr,
266+
})
267+
})
268+
},
269+
})

0 commit comments

Comments
 (0)