Skip to content

Commit 0eb0ab2

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: anomalyco#5426 Signed-off-by: Christian Stewart <[email protected]>
1 parent dd44a80 commit 0eb0ab2

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
@@ -8,6 +8,8 @@ import { Locale } from "../../util/locale"
88
import { Flag } from "../../flag/flag"
99
import { EOL } from "os"
1010
import path from "path"
11+
import * as prompts from "@clack/prompts"
12+
import { formatTranscript } from "./tui/util/transcript"
1113

1214
function pagerCmd(): string[] {
1315
const lessOptions = ["-R", "-S"]
@@ -39,7 +41,7 @@ function pagerCmd(): string[] {
3941
export const SessionCommand = cmd({
4042
command: "session",
4143
describe: "manage sessions",
42-
builder: (yargs: Argv) => yargs.command(SessionListCommand).demandCommand(),
44+
builder: (yargs: Argv) => yargs.command(SessionListCommand).command(SessionExportCommand).demandCommand(),
4345
async handler() {},
4446
})
4547

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

0 commit comments

Comments
 (0)