From ca0c1632eac9132310251251919a65586564aa9d Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 12 Aug 2025 19:00:01 -0400 Subject: [PATCH] Add export command to CLI tool --- AGENTS.md | 4 ++ packages/opencode/src/cli/cmd/export.ts | 76 +++++++++++++++++++++++++ packages/opencode/src/index.ts | 2 + 3 files changed, 82 insertions(+) create mode 100644 packages/opencode/src/cli/cmd/export.ts diff --git a/AGENTS.md b/AGENTS.md index cca69e4b6..507cfea53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,3 +10,7 @@ - AVOID `let` statements - PREFER single word variable names where possible - Use as many bun apis as possible like Bun.file() + +## Debugging + +- To test opencode in the `packages/opencode` directory you can run `bun dev` diff --git a/packages/opencode/src/cli/cmd/export.ts b/packages/opencode/src/cli/cmd/export.ts new file mode 100644 index 000000000..60c60f543 --- /dev/null +++ b/packages/opencode/src/cli/cmd/export.ts @@ -0,0 +1,76 @@ +import type { Argv } from "yargs" +import { Session } from "../../session" +import { cmd } from "./cmd" +import { bootstrap } from "../bootstrap" +import { UI } from "../ui" +import * as prompts from "@clack/prompts" + +export const ExportCommand = cmd({ + command: "export [sessionID]", + describe: "export session data as JSON", + builder: (yargs: Argv) => { + return yargs.positional("sessionID", { + describe: "session id to export", + type: "string", + }) + }, + handler: async (args) => { + await bootstrap({ cwd: process.cwd() }, async () => { + let sessionID = args.sessionID + + if (!sessionID) { + UI.empty() + prompts.intro("Export session") + + const sessions = [] + for await (const session of Session.list()) { + sessions.push(session) + } + + if (sessions.length === 0) { + prompts.log.error("No sessions found") + prompts.outro("Done") + return + } + + sessions.sort((a, b) => b.time.updated - a.time.updated) + + const selectedSession = await prompts.autocomplete({ + message: "Select session to export", + maxItems: 10, + options: sessions.map((session) => ({ + label: session.title, + value: session.id, + hint: `${new Date(session.time.updated).toLocaleString()} • ${session.id.slice(-8)}`, + })), + }) + + if (prompts.isCancel(selectedSession)) { + throw new UI.CancelledError() + } + + sessionID = selectedSession as string + + prompts.outro("Exporting session...") + } + + try { + const sessionInfo = await Session.get(sessionID!) + const messages = await Session.messages(sessionID!) + + const exportData = { + info: sessionInfo, + messages: messages.map((msg) => ({ + info: msg.info, + parts: msg.parts, + })), + } + + console.log(JSON.stringify(exportData, null, 2)) + } catch (error) { + UI.error(`Session not found: ${sessionID!}`) + process.exit(1) + } + }) + }, +}) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 868c96ba0..f401f8ab2 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -18,6 +18,7 @@ import { DebugCommand } from "./cli/cmd/debug" import { StatsCommand } from "./cli/cmd/stats" import { McpCommand } from "./cli/cmd/mcp" import { GithubCommand } from "./cli/cmd/github" +import { ExportCommand } from "./cli/cmd/export" const cancel = new AbortController() @@ -80,6 +81,7 @@ const cli = yargs(hideBin(process.argv)) .command(ServeCommand) .command(ModelsCommand) .command(StatsCommand) + .command(ExportCommand) .command(GithubCommand) .fail((msg) => { if (msg.startsWith("Unknown argument") || msg.startsWith("Not enough non-option arguments")) {