@@ -7,6 +7,8 @@ import { UI } from "../ui"
77import { Locale } from "../../util/locale"
88import { Flag } from "../../flag/flag"
99import { EOL } from "os"
10+ import * as prompts from "@clack/prompts"
11+ import { formatTranscript } from "./tui/util/transcript"
1012
1113function pagerCmd ( ) : string [ ] {
1214 const lessOptions = [ "-R" , "-S" ]
@@ -38,7 +40,7 @@ function pagerCmd(): string[] {
3840export 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