@@ -8,6 +8,8 @@ import { Locale } from "../../util/locale"
88import { Flag } from "../../flag/flag"
99import { EOL } from "os"
1010import path from "path"
11+ import * as prompts from "@clack/prompts"
12+ import { formatTranscript } from "./tui/util/transcript"
1113
1214function pagerCmd ( ) : string [ ] {
1315 const lessOptions = [ "-R" , "-S" ]
@@ -39,7 +41,7 @@ function pagerCmd(): string[] {
3941export 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