goosedump 0.12.27

Coding agent context data browser
// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (C) Jarkko Sakkinen 2026

use std::path::PathBuf;
use std::str::FromStr;

use argh::FromArgs;

use crate::engine::{Client, memory};

mod run;

pub(crate) use run::run;
/// A provider-qualified session target (`provider:id`).
#[derive(Debug, Clone)]
struct Target {
    provider: Client,
    id: String,
}

impl Target {
    fn qualified(&self) -> String {
        format!("{}:{}", self.provider.as_str(), self.id)
    }
}

impl FromStr for Target {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let (provider, id) = value
            .split_once(':')
            .ok_or_else(|| "session must be qualified as provider:id".to_string())?;
        if id.is_empty() {
            return Err("session id must not be empty".to_string());
        }
        Ok(Self {
            provider: provider.parse()?,
            id: id.to_string(),
        })
    }
}

/// A durable-memory ID or a provider-qualified session target.
#[derive(Debug, Clone)]
enum MemoryTarget {
    Id(String),
    Session(Target),
}

impl FromStr for MemoryTarget {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        if value.starts_with("mem_") {
            return Ok(Self::Id(value.to_string()));
        }
        Target::from_str(value)
            .map(Self::Session)
            .map_err(|_| "memory target must be mem_<id> or provider:id".to_string())
    }
}

#[derive(FromArgs)]
#[argh(description = "coding-agent session browser")]
struct GoosedumpArgs {
    #[argh(subcommand)]
    command: GoosedumpCommand,
}

#[derive(FromArgs)]
#[argh(subcommand)]
enum GoosedumpCommand {
    Session(SessionArgs),
    Memory(MemoryArgs),
}

#[derive(FromArgs)]
#[argh(subcommand, name = "session", description = "session commands")]
struct SessionArgs {
    #[argh(subcommand)]
    command: SessionCommand,
}

#[derive(FromArgs)]
#[argh(subcommand)]
enum SessionCommand {
    List(ListSessionsArgs),
    Show(ShowSessionArgs),
    Find(FindSessionArgs),
    Search(SearchSessionArgs),
    Compact(CompactSessionArgs),
    Copy(CopySessionArgs),
    Remove(RemoveSessionArgs),
}

#[derive(FromArgs)]
#[argh(subcommand, name = "list", description = "list sessions")]
struct ListSessionsArgs {
    #[argh(option, short = 'q', description = "JSON query filter")]
    query: Option<String>,

    #[argh(switch, description = "output in JSON format")]
    json: bool,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "show", description = "show a session")]
struct ShowSessionArgs {
    #[argh(positional)]
    target: Target,

    #[argh(option, short = 'e', long = "entry", description = "entry ID")]
    entries: Vec<String>,

    #[argh(switch, description = "all branches")]
    all: bool,

    #[argh(option, description = "first entry ID")]
    from: Option<String>,

    #[argh(option, description = "exclusive final entry ID")]
    before: Option<String>,

    #[argh(option, long = "as", description = "render provider")]
    render_as: Option<Client>,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "find", description = "find session messages")]
struct FindSessionArgs {
    #[argh(positional)]
    target: Target,

    #[argh(positional)]
    pattern: String,

    #[argh(option, short = 'e', long = "entry", description = "entry ID")]
    entries: Vec<String>,

    #[argh(switch, description = "all branches")]
    all: bool,

    #[argh(option, description = "first entry ID")]
    from: Option<String>,

    #[argh(option, description = "exclusive final entry ID")]
    before: Option<String>,

    #[argh(option, long = "as", description = "render provider")]
    render_as: Option<Client>,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "search", description = "search session messages")]
struct SearchSessionArgs {
    #[argh(positional)]
    target: Target,

    #[argh(positional)]
    query: String,

    #[argh(option, short = 'e', long = "entry", description = "entry ID")]
    entries: Vec<String>,

    #[argh(switch, description = "all branches")]
    all: bool,

    #[argh(option, description = "first entry ID")]
    from: Option<String>,

    #[argh(option, description = "exclusive final entry ID")]
    before: Option<String>,

    #[argh(option, description = "result page")]
    page: Option<usize>,

    #[argh(option, long = "as", description = "render provider")]
    render_as: Option<Client>,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "compact", description = "compact a session")]
struct CompactSessionArgs {
    #[argh(positional)]
    target: Target,

    #[argh(option, short = 'e', long = "entry", description = "entry ID")]
    entries: Vec<String>,

    #[argh(switch, description = "all branches")]
    all: bool,

    #[argh(option, description = "first entry ID")]
    from: Option<String>,

    #[argh(option, description = "exclusive final entry ID")]
    before: Option<String>,

    #[argh(option, description = "prior summary")]
    previous_summary: Option<String>,

    #[argh(
        option,
        default = "4096",
        description = "maximum estimated summary tokens; 0 disables the limit"
    )]
    summary_max_tokens: usize,

    #[argh(option, long = "as", description = "render provider")]
    render_as: Option<Client>,

    #[argh(switch, description = "screen-readable output")]
    plain: bool,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "copy", description = "copy a session")]
struct CopySessionArgs {
    #[argh(positional)]
    target: Target,

    #[argh(option, description = "destination provider")]
    to: Client,

    #[argh(switch, description = "perform the copy")]
    yes: bool,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "remove", description = "remove a session")]
struct RemoveSessionArgs {
    #[argh(positional)]
    target: Target,

    #[argh(switch, description = "remove the session")]
    yes: bool,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "memory", description = "memory commands")]
struct MemoryArgs {
    #[argh(subcommand)]
    command: MemoryCommand,
}

#[derive(FromArgs)]
#[argh(subcommand)]
enum MemoryCommand {
    Remember(MemoryRememberArgs),
    Recall(MemoryRecallArgs),
    List(MemoryListArgs),
    Show(MemoryShowArgs),
    Forget(MemoryForgetArgs),
    Status(MemoryStatusArgs),
}

#[derive(FromArgs)]
#[argh(
    subcommand,
    name = "remember",
    description = "learn durable memory from a session"
)]
struct MemoryRememberArgs {
    #[argh(positional)]
    target: Target,

    #[argh(switch, description = "all branches")]
    all: bool,

    #[argh(switch, description = "output in JSON format")]
    json: bool,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "recall", description = "recall durable memory")]
struct MemoryRecallArgs {
    #[argh(positional)]
    query: String,

    #[argh(option, description = "project working directory")]
    project: Option<PathBuf>,

    #[argh(switch, description = "search every project")]
    all_projects: bool,

    #[argh(option, long = "type", description = "memory type")]
    memory_type: Option<memory::MemoryType>,

    #[argh(option, default = "10", description = "result limit")]
    limit: usize,

    #[argh(
        option,
        default = "2048",
        description = "maximum estimated result tokens"
    )]
    max_tokens: usize,

    #[argh(switch, description = "include superseded historical memories")]
    history: bool,

    #[argh(switch, description = "output in JSON format")]
    json: bool,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "list", description = "list durable memories")]
struct MemoryListArgs {
    #[argh(option, description = "project working directory")]
    project: Option<PathBuf>,

    #[argh(switch, description = "list every project")]
    all_projects: bool,

    #[argh(option, long = "type", description = "memory type")]
    memory_type: Option<memory::MemoryType>,

    #[argh(option, default = "20", description = "result limit")]
    limit: usize,

    #[argh(switch, description = "include superseded historical memories")]
    history: bool,

    #[argh(switch, description = "output in JSON format")]
    json: bool,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "show", description = "show one durable memory")]
struct MemoryShowArgs {
    #[argh(positional)]
    id: String,

    #[argh(switch, description = "output in JSON format")]
    json: bool,
}

#[derive(FromArgs)]
#[argh(
    subcommand,
    name = "forget",
    description = "forget memory or a remembered session"
)]
struct MemoryForgetArgs {
    #[argh(positional)]
    target: MemoryTarget,

    #[argh(switch, description = "perform the forget operation")]
    yes: bool,

    #[argh(switch, description = "output in JSON format")]
    json: bool,
}

#[derive(FromArgs)]
#[argh(subcommand, name = "status", description = "show memory status")]
struct MemoryStatusArgs {
    #[argh(switch, description = "output in JSON format")]
    json: bool,
}