Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

204 lines
5.5 KiB
Go

// ABOUTME: `session export <id>` subcommand — streams the raw source
// ABOUTME: JSONL file for a locally-synced session. Local-only by
// ABOUTME: design; bypasses the SessionService layer.
package main
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"go.kenn.io/agentsview/internal/config"
"go.kenn.io/agentsview/internal/parser"
)
func newSessionExportCommand() *cobra.Command {
return &cobra.Command{
Use: "export <id>",
Short: "Stream the raw source JSONL for a session (local only)",
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("server") {
return fmt.Errorf(
"session export: local-only command; --server not supported",
)
}
if err := rejectFormatFlags(
cmd, "session export", "raw bytes",
); err != nil {
return err
}
if pgReadRequested(cmd) {
return fmt.Errorf(
"session export: local-only command; --pg not supported",
)
}
cfg, err := config.LoadPFlags(cmd.Flags())
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
d, err := openReadOnlyDB(cfg)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf(
"session not in local archive: %s", args[0],
)
}
return fmt.Errorf("open local archive: %w", err)
}
defer d.Close()
id, err := resolveSessionID(cmd.Context(), d, args[0])
if err != nil {
return err
}
if id == "" {
return fmt.Errorf(
"session not in local archive: %s", args[0],
)
}
session, err := d.GetSession(cmd.Context(), id)
if err != nil {
return err
}
if session == nil {
return fmt.Errorf(
"session not in local archive: %s", args[0],
)
}
storedPath := d.GetSessionFilePath(id)
if storedPath == "" {
return fmt.Errorf(
"source file not found for session %s", id,
)
}
// Aider stores many repo runs in one Markdown history file,
// with sessions keyed by a <history>#<idx> virtual path.
// Export only the selected run, not sibling runs from the
// same repository.
if historyPath, idx, ok :=
parser.ParseAiderVirtualPath(storedPath); ok {
rawID, ok := rawAiderSessionID(id)
if !ok {
return fmt.Errorf(
"stale aider source for session %s: invalid aider session id",
id,
)
}
if got, ok := parser.AiderRawIDAt(historyPath, idx); !ok || got != rawID {
if _, statErr := os.Stat(historyPath); statErr != nil {
if os.IsNotExist(statErr) {
return fmt.Errorf(
"source file not found: %s", historyPath,
)
}
return statErr
}
resolved, found := parser.AiderVirtualPathForRawID(
historyPath, rawID,
)
if !found {
return fmt.Errorf(
"stale aider source for session %s: %s no longer contains the archived run",
id, historyPath,
)
}
historyPath, idx, _ = parser.ParseAiderVirtualPath(resolved)
}
err := parser.WriteAiderRunMarkdown(
cmd.OutOrStdout(), historyPath, idx,
)
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf(
"source file not found: %s", historyPath,
)
}
return err
}
// A Visual Studio Copilot trace file holds spans for several
// conversations, so streaming the whole file would disclose
// unrelated conversations. Filter to the requested conversation.
if tracePath, conversationID, ok :=
parser.SplitVisualStudioCopilotVirtualPath(storedPath); ok {
err := parser.WriteVisualStudioCopilotConversationJSONL(
cmd.OutOrStdout(), tracePath, conversationID,
)
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf(
"source file not found: %s", tracePath,
)
}
return err
}
if dbPath, sessionID, ok := parser.SplitWindsurfVirtualPath(storedPath); ok {
err := parser.WriteWindsurfSessionJSON(
cmd.OutOrStdout(), dbPath, sessionID,
)
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf(
"source file not found: %s", dbPath,
)
}
return err
}
if session.Agent == string(parser.AgentHermes) &&
filepath.Base(parser.ResolveSourceFilePath(storedPath)) == "state.db" {
rawSessionID := session.SourceSessionID
if rawSessionID == "" {
rawSessionID, _ = rawHermesSessionID(id)
}
err := parser.WriteHermesSessionJSONL(
cmd.OutOrStdout(),
storedPath,
cfg.AgentDirs[parser.AgentHermes],
rawSessionID,
)
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf(
"source file not found for session %s", id,
)
}
return err
}
path := parser.ResolveSourceFilePath(storedPath)
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf(
"source file not found: %s", path,
)
}
return err
}
defer f.Close()
_, err = io.Copy(cmd.OutOrStdout(), f)
return err
},
}
}
func rawAiderSessionID(sessionID string) (string, bool) {
def, ok := parser.AgentByPrefix(sessionID)
if !ok || def.Type != parser.AgentAider {
return "", false
}
_, rawID := parser.StripHostPrefix(sessionID)
rawID = strings.TrimPrefix(rawID, def.IDPrefix)
return rawID, rawID != ""
}
func rawHermesSessionID(sessionID string) (string, bool) {
def, ok := parser.AgentByPrefix(sessionID)
if !ok || def.Type != parser.AgentHermes {
return "", false
}
_, rawID := parser.StripHostPrefix(sessionID)
rawID = strings.TrimPrefix(rawID, def.IDPrefix)
return rawID, rawID != ""
}