Skip to content

Commit 094ff00

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 2ce81a9 commit 094ff00

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 { UI } from "../ui"
77
import { Locale } from "../../util/locale"
88
import { Flag } from "../../flag/flag"
99
import { EOL } from "os"
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

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

0 commit comments

Comments
 (0)