diff --git a/Cargo.toml b/Cargo.toml index 03ab938..21fda4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ tempfile = "3" pre-release-hook = ["git", "cliff", "-o", "CHANGELOG.md", "--tag", "{{version}}" ] publish = false pre-release-replacements = [ - { file = "skills/hotdata-cli/SKILL.md", search = "^version: .+", replace = "version: {{version}}", exactly = 1 }, + { file = "skills/hotdata/SKILL.md", search = "^version: .+", replace = "version: {{version}}", exactly = 1 }, { file = "README.md", search = "version-[0-9.]+-blue", replace = "version-{{version}}-blue", exactly = 1 }, ] diff --git a/README.md b/README.md index 7c7b965..68fe988 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,8 @@ API key priority (lowest to highest): config file → `HOTDATA_API_KEY` env var | `indexes` | `list`, `create` | Manage indexes on a table | | `results` | `list` | Retrieve stored query results | | `jobs` | `list` | Manage background jobs | -| `skills` | `install`, `status` | Manage the hotdata-cli agent skill | +| `sessions` | `list`, `new`, `set`, `read`, `update`, `run` | Manage work sessions | +| `skills` | `install`, `status` | Manage the hotdata agent skill | ## Global options @@ -225,6 +226,28 @@ hotdata jobs [--workspace-id ] [--format table|json|yaml] - `--job-type` accepts: `data_refresh_table`, `data_refresh_connection`, `create_index`. - `--status` accepts: `pending`, `running`, `succeeded`, `partially_succeeded`, `failed`. +## Sessions + +Sessions group related CLI activity (queries, dataset operations, etc.) under a single context. + +```sh +hotdata sessions list [-w ] [-o table|json|yaml] +hotdata sessions [-w ] [-o table|json|yaml] +hotdata sessions new [--name "My Session"] [-o table|json|yaml] +hotdata sessions set [] +hotdata sessions read +hotdata sessions update [] [--name "New Name"] [--markdown "..."] [-o table|json|yaml] +hotdata sessions run [args...] +hotdata sessions run [args...] +``` + +- `list` shows all sessions with a `*` marker on the active one. +- `new` creates a session and sets it as active. +- `set` switches the active session. Omit the ID to clear the active session. +- `read` prints the markdown content of the current session. +- `update` modifies the name or markdown of a session (defaults to the active session). +- `run` runs a command with the hotdata CLI sandboxed in a session. Creates a new session unless a session ID is provided before `run`. Useful for launching an agent that can only access session data. Nesting sessions is not allowed. + ## Configuration Config is stored at `~/.hotdata/config.yml` keyed by profile (default: `default`). diff --git a/dist-workspace.toml b/dist-workspace.toml index d99e1bb..607ec00 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -26,4 +26,4 @@ build = ["tar", "-czf", "skills.tar.gz", "skills/"] [[dist.extra-artifacts]] artifacts = ["SKILL.md"] -build = ["cp", "skills/hotdata-cli/SKILL.md", "SKILL.md"] +build = ["cp", "skills/hotdata/SKILL.md", "SKILL.md"] diff --git a/skills/hotdata-cli/SKILL.md b/skills/hotdata/SKILL.md similarity index 79% rename from skills/hotdata-cli/SKILL.md rename to skills/hotdata/SKILL.md index 6a2ab26..0d88e66 100644 --- a/skills/hotdata-cli/SKILL.md +++ b/skills/hotdata/SKILL.md @@ -1,6 +1,6 @@ --- -name: hotdata-cli -description: Use this skill when the user wants to run hotdata CLI commands, query the Hotdata API, list workspaces, list connections, create connections, list tables, manage datasets, execute SQL queries, manage saved queries, search tables, manage indexes, or interact with the hotdata service. Activate when the user says "run hotdata", "query hotdata", "list workspaces", "list connections", "create a connection", "list tables", "list datasets", "create a dataset", "upload a dataset", "execute a query", "search a table", "list indexes", "create an index", "list saved queries", "run a saved query", or asks you to use the hotdata CLI. +name: hotdata +description: Use this skill when the user wants to run hotdata CLI commands, query the Hotdata API, list workspaces, list connections, create connections, list tables, manage datasets, execute SQL queries, manage saved queries, search tables, manage indexes, manage sessions, or interact with the hotdata service. Activate when the user says "run hotdata", "query hotdata", "list workspaces", "list connections", "create a connection", "list tables", "list datasets", "create a dataset", "upload a dataset", "execute a query", "search a table", "list indexes", "create an index", "list saved queries", "run a saved query", "list sessions", "create a session", "run a session", or asks you to use the hotdata CLI. version: 0.1.8 --- @@ -276,6 +276,69 @@ hotdata auth status # Check current auth status hotdata auth logout # Remove saved auth for the default profile ``` +### Sessions + +Sessions are for **ad-hoc, exploratory work** that does not need to be long-lived. They group related CLI activity (queries, dataset operations, etc.) under a single context so it can be tracked and cleaned up together. **Datasets created inside a session are tied to that session and will be removed when the session ends.** If you need data to persist beyond the session, create datasets outside of a session context. + +> **IMPORTANT: If `HOTDATA_SESSION` is set in the environment, you are inside an active session. NEVER attempt to unset, override, or work around this variable. Do not clear it, do not start a new session, do not run `sessions run` or `sessions new` or `sessions set`. All your work should be attributed to the current session. Attempting to nest or escape a session will fail with an error.** + +``` +hotdata sessions list [-w ] [-o table|json|yaml] +hotdata sessions [-w ] [-o table|json|yaml] +hotdata sessions new [--name "Session Name"] [-o table|json|yaml] +hotdata sessions set [] +hotdata sessions read +hotdata sessions update [] [--name "New Name"] [--markdown "..."] [-o table|json|yaml] +hotdata sessions run [args...] +hotdata sessions run [args...] +``` + +- `list` shows all sessions with a `*` marker on the active one. +- `new` creates a session and sets it as active. Blocked inside an existing session. +- `set` switches the active session. Omit the ID to clear. Blocked inside an existing session. +- `read` prints the markdown content of the current session. Use this to retrieve session state at the start of work or between steps. +- `update` modifies a session's name or markdown. Defaults to the active session if no ID is given. The `--markdown` field is for writing details about the work being done in the session — observations, intermediate findings, next steps, etc. This state persists for the life of the session and is the primary way to record context that should survive across commands or agent invocations within the session. +- `run` launches a command with `HOTDATA_SESSION` and `HOTDATA_WORKSPACE` set in the child process environment. Creates a new session unless a session ID is provided before `run`. Blocked inside an existing session. +- When inside a session (HOTDATA_SESSION is set), all API requests automatically include the session ID — no extra flags needed. + +#### Example: Building a data model in a session + +Use a session to explore tables and iteratively build a model description in the session markdown. + +1. Start a session: + ``` + hotdata sessions new --name "Model: sales pipeline" + ``` +2. Inspect tables and columns: + ``` + hotdata tables list --connection-id + ``` +3. Run exploratory queries to understand relationships, cardinality, and key columns: + ``` + hotdata query "SELECT DISTINCT status FROM sales.public.deals LIMIT 20" + hotdata query "SELECT count(*), count(DISTINCT account_id) FROM sales.public.deals" + ``` +4. Write findings into the session markdown as you go: + ``` + hotdata sessions update --markdown "## sales pipeline model + + ### deals (sales.public.deals) + - PK: id + - FK: account_id -> accounts.id + - status: open | won | lost + - ~50k rows, one row per deal + + ### accounts (sales.public.accounts) + - PK: id + - name, industry, created_at + - ~12k rows, one row per company + + ### TODO + - check how line_items joins to deals + - confirm revenue column semantics" + ``` +5. Continue exploring and update the markdown as the model takes shape. The markdown is the living artifact — when the session ends, its content captures what was learned. + Other commands (not covered in detail above): `hotdata connections new` (interactive connection wizard), `hotdata skills install|status`, `hotdata completions `. ## Workflow: Running a Query diff --git a/skills/hotdata-cli/references/DATA_MODEL.template.md b/skills/hotdata/references/DATA_MODEL.template.md similarity index 93% rename from skills/hotdata-cli/references/DATA_MODEL.template.md rename to skills/hotdata/references/DATA_MODEL.template.md index a2b1526..b9fb25a 100644 --- a/skills/hotdata-cli/references/DATA_MODEL.template.md +++ b/skills/hotdata/references/DATA_MODEL.template.md @@ -2,7 +2,7 @@ > Copy this file to your **project** directory (e.g. `./DATA_MODEL.md`, `./data_model.md`, or `./docs/DATA_MODEL.md`). > Do not commit workspace-specific content into agent skill folders. -> For a **full** build (per-table detail, connector enrichment, index summary), follow [MODEL_BUILD.md](MODEL_BUILD.md) from the installed skill’s `references/` (or this repo’s `skills/hotdata-cli/references/`). Relative links to `MODEL_BUILD.md` below work only while this file lives next to those references; in your project, open that path separately if the link 404s. +> For a **full** build (per-table detail, connector enrichment, index summary), follow [MODEL_BUILD.md](MODEL_BUILD.md) from the installed skill’s `references/` (or this repo’s `skills/hotdata/references/`). Relative links to `MODEL_BUILD.md` below work only while this file lives next to those references; in your project, open that path separately if the link 404s. **Workspace (Hotdata):** `` **Last catalog refresh:** `` diff --git a/skills/hotdata-cli/references/MODEL_BUILD.md b/skills/hotdata/references/MODEL_BUILD.md similarity index 100% rename from skills/hotdata-cli/references/MODEL_BUILD.md rename to skills/hotdata/references/MODEL_BUILD.md diff --git a/skills/hotdata-cli/references/WORKFLOWS.md b/skills/hotdata/references/WORKFLOWS.md similarity index 100% rename from skills/hotdata-cli/references/WORKFLOWS.md rename to skills/hotdata/references/WORKFLOWS.md diff --git a/src/command.rs b/src/command.rs index 1390c89..62d56c6 100644 --- a/src/command.rs +++ b/src/command.rs @@ -75,7 +75,7 @@ pub enum Commands { command: TablesCommands, }, - /// Manage the hotdata-cli agent skill + /// Manage the hotdata agent skill Skills { #[command(subcommand)] command: SkillCommands, @@ -446,13 +446,13 @@ pub enum ConnectionsCommands { #[derive(Subcommand)] pub enum SkillCommands { - /// Install or update the hotdata-cli skill into agent directories + /// Install or update the hotdata skill into agent directories Install { /// Install into the current project directory instead of globally #[arg(long)] project: bool, }, - /// Show the installation status of the hotdata-cli skill + /// Show the installation status of the hotdata skill Status, } @@ -597,6 +597,9 @@ pub enum SessionsCommands { output: String, }, + /// Print the markdown content of the current session + Read, + /// Set the active session (omit ID to clear) Set { /// Session ID to set as active (omit to clear) diff --git a/src/main.rs b/src/main.rs index 98cd98b..bf0f84e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -358,6 +358,20 @@ fn main() { } } } + Some(SessionsCommands::Read) => { + let session_id = id.or_else(|| { + std::env::var("HOTDATA_SESSION").ok() + }).or_else(|| { + config::load("default").ok().and_then(|p| p.session) + }); + match session_id { + Some(sid) => sessions::read(&sid, &workspace_id), + None => { + eprintln!("error: no active session. Use 'sessions new' or 'sessions set '."); + std::process::exit(1); + } + } + } Some(SessionsCommands::Set { id: set_id }) => { sessions::set(set_id.as_deref(), &workspace_id) } diff --git a/src/sessions.rs b/src/sessions.rs index 7c290d8..3f5ce4f 100644 --- a/src/sessions.rs +++ b/src/sessions.rs @@ -78,6 +78,17 @@ pub fn get(session_id: &str, workspace_id: &str, format: &str) { } } +pub fn read(session_id: &str, workspace_id: &str) { + let api = ApiClient::new(Some(workspace_id)); + let path = format!("/sessions/{session_id}"); + let body: DetailResponse = api.get(&path); + if body.session.markdown.is_empty() { + eprintln!("{}", "Session markdown is empty.".dark_grey()); + } else { + print!("{}", body.session.markdown); + } +} + fn check_session_lock() { if std::env::var("HOTDATA_SESSION").is_ok() || find_session_run_ancestor().is_some() { eprintln!("error: session is locked"); diff --git a/src/skill.rs b/src/skill.rs index cf34d91..a3a6d87 100644 --- a/src/skill.rs +++ b/src/skill.rs @@ -5,11 +5,11 @@ use std::fs; use std::path::PathBuf; const REPO: &str = "hotdata-dev/hotdata-cli"; -const SKILL_NAME: &str = "hotdata-cli"; +const SKILL_NAME: &str = "hotdata"; const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Agent root directories to check for symlink installation. -/// If the root dir exists, we create /skills/hotdata-cli -> ~/.agents/skills/hotdata-cli +/// If the root dir exists, we create /skills/hotdata -> ~/.agents/skills/hotdata const AGENT_ROOTS: &[&str] = &[".claude", ".pi"]; fn home_dir() -> PathBuf { @@ -19,13 +19,13 @@ fn home_dir() -> PathBuf { .to_path_buf() } -/// The canonical install location: ~/.agents/skills/hotdata-cli -/// Source of truth: ~/.hotdata/skills/hotdata-cli +/// The canonical install location: ~/.agents/skills/hotdata +/// Source of truth: ~/.hotdata/skills/hotdata fn skill_store_path() -> PathBuf { home_dir().join(".hotdata").join("skills").join(SKILL_NAME) } -/// Canonical agents layer: ~/.agents/skills/hotdata-cli +/// Canonical agents layer: ~/.agents/skills/hotdata fn agents_skill_path() -> PathBuf { home_dir().join(".agents").join("skills").join(SKILL_NAME) } @@ -185,11 +185,11 @@ fn ensure_symlinks() -> Vec<(String, PathBuf, Result)> { let agents_path = agents_skill_path(); let mut results = Vec::new(); - // First: ~/.agents/skills/hotdata-cli -> ~/.hotdata/skills/hotdata-cli + // First: ~/.agents/skills/hotdata -> ~/.hotdata/skills/hotdata let agents_result = ensure_symlink_or_copy(&store_path, &agents_path); results.push(("~/.agents".to_string(), agents_path.clone(), agents_result)); - // Then: each detected agent root -> ~/.agents/skills/hotdata-cli + // Then: each detected agent root -> ~/.agents/skills/hotdata for (root, link_path) in detected_agent_skill_paths() { let result = ensure_symlink_or_copy(&agents_path, &link_path); results.push((format!("~/{root}"), link_path, result)); @@ -228,7 +228,7 @@ pub fn install_project() { let cwd = std::env::current_dir().expect("could not determine current directory"); let project_agents = cwd.join(".agents").join("skills").join(SKILL_NAME); - // Always copy (not symlink) from store to .agents/skills/hotdata-cli + // Always copy (not symlink) from store to .agents/skills/hotdata if project_agents.exists() { fs::remove_dir_all(&project_agents).unwrap_or_else(|e| { eprintln!( @@ -257,7 +257,7 @@ pub fn install_project() { ); println!("{:<20}{}", "Location:", rel_agents.display().to_string().cyan()); - // For .claude and .pi in cwd: symlink (fallback copy) from .agents/skills/hotdata-cli + // For .claude and .pi in cwd: symlink (fallback copy) from .agents/skills/hotdata for root in AGENT_ROOTS { let root_path = cwd.join(root); if root_path.exists() { @@ -290,7 +290,7 @@ pub fn install() { true } None => { - println!("Installing hotdata-cli skill v{current}..."); + println!("Installing hotdata skill v{current}..."); true } } @@ -305,7 +305,7 @@ pub fn install() { true } None => { - println!("Installing hotdata-cli skill v{current}..."); + println!("Installing hotdata skill v{current}..."); true } }