Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
c2fd0b1
feat(db): add agent API schema and migrations
richiemcilroy Jul 18, 2026
a4d45ed
feat(email): add Resend idempotency key support
richiemcilroy Jul 18, 2026
769a6ca
feat(domain): add Agent API domain types
richiemcilroy Jul 18, 2026
9297f86
feat(backend): add AgentAuth service
richiemcilroy Jul 18, 2026
e7ab79e
feat(backend): add AgentManagement service
richiemcilroy Jul 18, 2026
423490c
feat(web): add agent auth utilities
richiemcilroy Jul 18, 2026
202b743
feat(web): add agent token exchange utilities
richiemcilroy Jul 18, 2026
1177ecc
feat(web): add agent access grant helper
richiemcilroy Jul 18, 2026
f9d0fb1
feat(web): add agent API response helpers
richiemcilroy Jul 18, 2026
73c60e8
feat(web): add agent write operations
richiemcilroy Jul 18, 2026
66d965b
feat(web): add agent management library
richiemcilroy Jul 18, 2026
5cf1a20
feat(web): add agent server runtime wiring
richiemcilroy Jul 18, 2026
8c6ef58
feat(web): add agent API cleanup utilities
richiemcilroy Jul 18, 2026
1e05af4
feat(web): add agent API rate limit identifiers
richiemcilroy Jul 18, 2026
2d1490d
feat(web): add agent cap operation workflow
richiemcilroy Jul 18, 2026
f853010
feat(web): track agent operations in Loom import workflow
richiemcilroy Jul 18, 2026
544b257
fix(web): update video processing recovery tests for Loom import
richiemcilroy Jul 18, 2026
f06574d
feat(web): add agent API v1 route handler
richiemcilroy Jul 18, 2026
c38d53b
feat(web): add CLI OAuth authorize page
richiemcilroy Jul 18, 2026
f0fb855
feat(web): add CLI OAuth complete page
richiemcilroy Jul 18, 2026
5bb0993
feat(web): support agent Google Drive OAuth redirects
richiemcilroy Jul 18, 2026
8e78533
feat(web): add agent API cleanup cron job
richiemcilroy Jul 18, 2026
74e141c
test(web): add agent API fixtures and core unit tests
richiemcilroy Jul 18, 2026
143c008
test(web): add agent API contract and handler tests
richiemcilroy Jul 18, 2026
fd60c94
test(web): add agent API local e2e test
richiemcilroy Jul 18, 2026
425c47a
build(cli): add agent API CLI dependencies
richiemcilroy Jul 18, 2026
e5097d8
feat(cli): add atomic write and confirmation helpers
richiemcilroy Jul 18, 2026
19e9072
feat(cli): add agent OAuth authentication module
richiemcilroy Jul 18, 2026
6bc3207
feat(cli): add agent HTTP client
richiemcilroy Jul 18, 2026
8ec9d87
feat(cli): extend credentials storage for agent tokens
richiemcilroy Jul 18, 2026
3c30fd4
feat(cli): add account commands
richiemcilroy Jul 18, 2026
21c70c9
feat(cli): add caps commands
richiemcilroy Jul 18, 2026
e5f7abe
feat(cli): add library commands
richiemcilroy Jul 18, 2026
c4b4373
feat(cli): extend upload for agent API
richiemcilroy Jul 18, 2026
ff2a04a
feat(cli): add jobs commands
richiemcilroy Jul 18, 2026
b7b0214
feat(cli): add agents management commands
richiemcilroy Jul 18, 2026
8d0426a
feat(cli): add analytics commands
richiemcilroy Jul 18, 2026
1e956ef
feat(cli): add notifications commands
richiemcilroy Jul 18, 2026
4a7d1f2
feat(cli): add organizations commands
richiemcilroy Jul 18, 2026
ef35647
feat(cli): add developers commands
richiemcilroy Jul 18, 2026
03528de
feat(cli): add MCP server integration
richiemcilroy Jul 18, 2026
a9dbcdc
feat(cli): update CLI guide for agent commands
richiemcilroy Jul 18, 2026
00bf8f9
feat(cli): wire agent commands in main entrypoint
richiemcilroy Jul 18, 2026
9c78741
docs(cli): update Cap skill documentation
richiemcilroy Jul 18, 2026
0bc3838
test(cli): add agent API CLI tests
richiemcilroy Jul 18, 2026
ecfd2d3
chore: update Cargo.lock for CLI dependencies
richiemcilroy Jul 18, 2026
c8061de
fix: track agent credential usage
richiemcilroy Jul 19, 2026
ea1dc7d
fix: respond to invalid auth callbacks
richiemcilroy Jul 19, 2026
f598d75
fix: escape agent library search patterns
richiemcilroy Jul 19, 2026
d66351e
fix: clean up revoked agent credentials
richiemcilroy Jul 19, 2026
1e79ae9
fix: preserve idempotency across token rotation
richiemcilroy Jul 19, 2026
9b26d82
fix: allow CLI auth on self-hosted deployments
richiemcilroy Jul 19, 2026
dc8f876
fix: expire completed agent operations
richiemcilroy Jul 19, 2026
0e6dbe5
fix: allow tombstoned organization name reuse
richiemcilroy Jul 19, 2026
60d30ea
perf: bound folder deletion traversal
richiemcilroy Jul 19, 2026
13b8c56
fix: validate developer app logo URLs
richiemcilroy Jul 19, 2026
3c15503
fix: gate unix-only credential import
richiemcilroy Jul 19, 2026
0793d9a
fix: address agent API code scanning alerts
richiemcilroy Jul 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
633 changes: 458 additions & 175 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion apps/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ cap-cli-install = { path = "../../crates/cli-install" }
scap-targets = { path = "../../crates/scap-targets" }
serde = { workspace = true }
serde_json = "1.0.133"
tokio = { workspace = true, features = ["signal"] }
tokio = { workspace = true, features = ["io-util", "net", "signal"] }
tokio-util = { version = "0.7", features = ["io"] }
uuid = { version = "1.11.1", features = ["v4"] }
ffmpeg = { workspace = true }
Expand All @@ -30,6 +30,14 @@ futures = { workspace = true }
dirs = "6.0.0"
image = "0.25.2"
chrono = "0.4.31"
base64 = "0.22.1"
keyring = "4.1.5"
open = "5.3.6"
rmcp = { version = "2.2.0", features = ["transport-io"] }
rpassword = "7.5.4"
sha2 = "0.10.9"
toml_edit = "0.23.7"
url = "2.5.7"
tracing.workspace = true
tracing-subscriber = "0.3.19"
cpal = { workspace = true }
Expand All @@ -43,6 +51,7 @@ libc = "0.2"
[target.'cfg(windows)'.dependencies]
windows = { workspace = true, features = [
"Win32_Foundation",
"Win32_Storage_FileSystem",
"Win32_System_Threading",
] }

Expand Down
129 changes: 67 additions & 62 deletions apps/cli/skill/cap/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,99 @@
---
name: cap
description: >-
Record the screen, capture screenshots, and create shareable video links from the command line using
the Cap CLI. Use when the user wants to record a screen demo or bug repro, capture a screenshot of a
screen/window, produce a Loom-style shareable video link, or automate/script screen recording. Requires
the `cap` command to be installed (Cap Desktop, https://cap.so).
Use Cap from the command line or local MCP to record, export, upload, manage the user's library, read
transcripts and AI output, collaborate, administer organizations, storage, billing and developer apps,
or run Cap without the web dashboard. Requires the `cap` command from Cap Desktop (https://cap.so).
---

# Cap CLI

`cap` is a command-line screen recorder built to be driven by agents. Every command takes `--json` for
machine-readable output on stdout; stderr stays human-readable; failures exit non-zero and, in `--json`
mode, print a final object/event with an `error` field.

**`cap guide --json` is the authoritative contract** (output convention, env vars, exit codes, and every
command's output mode + event tags). Prefer it over guessing, and `cap <command> --help` for flags.

## First: check it's available
Start by reading the installed CLI's authoritative contract:

```sh
cap doctor --json
cap guide --json
```

- If `cap` is not found, it isn't installed. Tell the user to install Cap Desktop from
https://cap.so/download, then enable the CLI (Settings → Command Line), or run
`curl -fsSL https://cap.so/install-cli.sh | sh` (Windows: `irm https://cap.so/install-cli.ps1 | iex`).
- Read `captureReady` and `permissions.screenRecording` from the output. On macOS, recording fails until
Screen Recording permission is granted in System Settings.
Use `--json` for machine-readable output. Treat stdout as authoritative, stderr as diagnostics, exit code
`1` as a runtime failure, and exit code `2` as invalid usage. Do not guess output schemas that are available
from `cap guide --json` or `cap <command> --help`.

## Discover capture targets
## Cap library

```sh
cap targets --json # screens, windows, cameras, mics
```
Authorize once with `cap auth login --json`. Login defaults to the least-privileged `creator` profile; request
`admin` or `full` only when the task requires it. Use `cap auth status --json` to verify the credential with the
server. For headless use, `CAP_AGENT_TOKEN` overrides the OS-stored credential. Existing Cap Desktop and
`CAP_API_KEY` credentials remain supported.

Use a screen's `id` (or a window's `id`) for `--screen`/`--window`, a camera's `deviceId` for `--camera`,
and a mic's `name` for `--mic`.
Use `cap caps list --json` for discovery, `cap caps get <id-or-url> --json` for lightweight metadata and
capabilities, and `cap caps context <id-or-url> --json` for title, AI title, summary, chapters, transcript,
comments, reactions, views, sharing, permissions, and processing state.

## Record (background lifecycle — the usual agent pattern)
`cap caps wait <id-or-url> --for transcript|ai|all --json` only observes existing processing. Reads and
waits must never be described as starting transcription, AI generation, or any paid work.

When you don't know the duration in advance: start detached, do the work, then stop.
Stream large content to disk:

```sh
cap record start --screen <id> --json --detach # -> {"type":"started","recordingId","pid","path"}
# ... perform the actions to capture ...
cap record stop --id <recordingId> --json # -> {"type":"stopped","path","recordingMetaExists":true}
cap record status --json # list active sessions
cap caps transcript <id-or-url> --format text --output transcript.txt --json
cap caps download <id-or-url> --output recording.mp4 --json
```

A `stopped` event is only a complete recording when `recordingMetaExists` is `true`.
For `PASSWORD_REQUIRED`, ask the user to run `cap caps unlock <id-or-url>` in a secure terminal. Never request a
password in an agent prompt, argument, JSON object, log, or MCP tool call.

Fixed-length alternative (no detach): `cap record start --screen <id> --duration 10 --json`.
Before any mutation, show the exact proposed action and obtain explicit user confirmation. This includes
posting or deleting content, changing metadata or visibility, starting paid processing, replacing transcripts,
moving or deleting Caps, changing membership or storage, creating checkout links, and rotating credentials.
Pass `--yes` or MCP `confirmed=true` only after that confirmation. Reads and waits do not require confirmation.

## Screenshot
## Dashboard-independent management

```sh
cap screenshot --screen <id> --path shot.png --json # -> {"path","width","height"}
```
Use `cap account`, `cap organizations`, `cap library`, `cap notifications`, `cap analytics`, `cap developers`,
and `cap jobs` for account, organization, sharing, storage, billing, analytics, developer, and asynchronous
operation workflows. Inspect each command's current arguments with `--help`; do not reconstruct them from this
skill.

## Export and share
Organization invites deliver email by default. Use `cap organizations invite add ... --no-email` only when the
user explicitly wants a link-only invite, and report the returned `emailDelivery` state.

```sh
cap project validate <path.cap> --json # confirm the recording is complete
cap export <path.cap> --output out.mp4 --json # render (here --format means container: mp4|gif|mov)
cap upload out.mp4 --json # -> {"type":"uploaded","id","link"}
```
Some secure or local-file actions are intentionally CLI-only: Cap passwords, S3 credentials, profile and
organization images, and newly issued developer credentials. Never ask the user to paste those values into
chat or an MCP call. Ask them to run the exact secure terminal command. S3 accepts hidden prompts or
`--credentials-stdin`, never credential arguments.

`cap upload` authenticates automatically by reusing the user's Cap Desktop login — check with
`cap auth status --json`. If it reports `authenticated:false`, tell the user to sign into Cap Desktop,
or set `CAP_API_KEY` (a Cap auth key from Settings) for headless use. `cap upload <path.cap> --export
--json` exports then uploads in one step.
Stripe checkout, the billing portal, Google Drive authorization, and `cap account referrals` return a URL and
may open a browser. These are focused provider handoffs, not a dependency on the Cap dashboard. The agent may
continue after the user completes the handoff and should re-read state to verify the result.

## Automations
Use `cap jobs wait <operationId> --json` for durable operations. Do not infer success from an accepted or queued
response. Warn that `cap account sign-out-all` revokes every device and agent session, including the current CLI
credential.

Automations are `trigger -> (conditions) -> actions` rules authored in Cap Desktop (Settings →
Automations) and shared with the CLI. After `cap screenshot`, a `cap record` finish, and `cap upload`,
the CLI evaluates the matching rules and runs their actions (save to a folder, run a command, send a
webhook, export, etc.).
Use `cap caps import loom <url> --organization <id> --yes --json` for Loom imports. Organization managers may
add `--owner-email` and `--space` to perform CSV-style migrations one durable, idempotent row at a time. Wait for
the returned operation before treating the import as complete.

```sh
cap automations list --json # the rules the CLI will honor
```
## Local MCP

`cap mcp serve` is a stdio server; its stdout is protocol-only. Prefer MCP for structured reads, safe writes,
resource links, and browser-handoff URLs. Fall back to the CLI for secure-input and local-file actions.

Install MCP or this skill for one explicitly selected agent with
`cap agents install --target <agent> --component skill|mcp|all --dry-run`. Review the preview, then rerun
interactively or with `--yes`. Never install into every detected agent automatically.

Desktop-only actions (copy to clipboard, OCR, notifications, open editor) are skipped on the CLI; all
others run. `cap doctor --json` reports the configured rule count under `automations`.
## Recording and sharing

## Conventions to rely on
Check readiness with `cap doctor --json` and discover devices with `cap targets --json`. For an unknown
duration, use the detached lifecycle:

```sh
cap record start --screen <id> --detach --json
cap record stop --id <recordingId> --json
cap project validate <path.cap> --json
cap export <path.cap> --output out.mp4 --json
cap upload out.mp4 --json
```

- Add `--json` to any command; it overrides each command's `--format`.
- Detect failure with a single check: the process exits non-zero and the JSON carries an `error` field.
- `record` and `export` stream newline-delimited JSON (NDJSON) events; everything else returns one object.
- `doctor`, `project validate`, and `recordings list` are reports — branch on their fields
(`ok`/`captureReady`, `valid`), not just the exit code.
A stopped recording is complete only when `recordingMetaExists` is `true`.
180 changes: 180 additions & 0 deletions apps/cli/src/account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use std::path::PathBuf;

use clap::{ArgGroup, Args, Subcommand};
use reqwest::Method;
use serde_json::{Map, Value, json};

use crate::{
OutputFormat, agent_client, caps::AgentClient, confirmation, credentials, resolve_format,
};

#[derive(Args)]
pub struct AccountArgs {
#[command(subcommand)]
command: AccountCommands,
}

#[derive(Subcommand)]
enum AccountCommands {
Get(FormatArgs),
Update(AccountUpdateArgs),
Image(AccountImageArgs),
Referrals(AccountReferralsArgs),
SignOutAll(AccountSignOutAllArgs),
}

#[derive(Args)]
struct FormatArgs {
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
}

#[derive(Args)]
#[command(group(ArgGroup::new("name_action").args(["name", "clear_name"])))]
#[command(group(ArgGroup::new("last_name_action").args(["last_name", "clear_last_name"])))]
#[command(group(ArgGroup::new("organization_action").args(["default_organization", "clear_default_organization"])))]
struct AccountUpdateArgs {
#[arg(long)]
name: Option<String>,
#[arg(long)]
clear_name: bool,
#[arg(long)]
last_name: Option<String>,
#[arg(long)]
clear_last_name: bool,
#[arg(long)]
default_organization: Option<String>,
#[arg(long)]
clear_default_organization: bool,
#[arg(long)]
yes: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
}

#[derive(Args)]
struct AccountImageArgs {
#[command(subcommand)]
command: AccountImageCommands,
}

#[derive(Subcommand)]
enum AccountImageCommands {
Set(AccountImageSetArgs),
Remove(AccountImageRemoveArgs),
}

#[derive(Args)]
struct AccountImageSetArgs {
image: PathBuf,
#[arg(long)]
yes: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
}

#[derive(Args)]
struct AccountImageRemoveArgs {
#[arg(long)]
yes: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
}

#[derive(Args)]
struct AccountSignOutAllArgs {
#[arg(long)]
yes: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
}

#[derive(Args)]
struct AccountReferralsArgs {
#[arg(long)]
no_open: bool,
#[arg(long)]
yes: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
format: OutputFormat,
}

impl AccountArgs {
pub async fn run(self, global_json: bool) -> Result<(), String> {
match self.command {
AccountCommands::Get(args) => agent_client::read("/me", global_json, args.format).await,
AccountCommands::Update(args) => {
let mut body = Map::new();
if let Some(name) = args.name {
body.insert("name".to_string(), Value::String(name));
} else if args.clear_name {
body.insert("name".to_string(), Value::Null);
}
if let Some(last_name) = args.last_name {
body.insert("lastName".to_string(), Value::String(last_name));
} else if args.clear_last_name {
body.insert("lastName".to_string(), Value::Null);
}
if let Some(organization_id) = args.default_organization {
body.insert(
"defaultOrganizationId".to_string(),
Value::String(organization_id),
);
} else if args.clear_default_organization {
body.insert("defaultOrganizationId".to_string(), Value::Null);
}
if body.is_empty() {
return Err("Provide at least one account update".to_string());
}
confirmation::require(args.yes, "Update the Cap account")?;
agent_client::mutate(Method::PATCH, "/me", &json!(body), global_json, args.format)
.await
}
AccountCommands::Image(args) => match args.command {
AccountImageCommands::Set(args) => {
confirmation::require(args.yes, "Update the Cap profile image")?;
let body = agent_client::image_payload(&args.image)?;
agent_client::mutate_confirmed(
Method::PUT,
"/me/image",
&body,
global_json,
args.format,
)
.await
}
AccountImageCommands::Remove(args) => {
confirmation::require(args.yes, "Remove the Cap profile image")?;
agent_client::mutate_confirmed(
Method::DELETE,
"/me/image",
&json!({}),
global_json,
args.format,
)
.await
}
},
AccountCommands::Referrals(args) => {
confirmation::require(args.yes, "Open the Cap referral portal")?;
let client = AgentClient::from_credentials().map_err(|error| error.to_string())?;
let value = client
.mutate_json_confirmed(Method::POST, "/me/referrals", &json!({}))
.await
.map_err(|error| error.to_string())?;
agent_client::open_browser_action(&value, args.no_open);
agent_client::print_value(&value, resolve_format(global_json, args.format))
}
AccountCommands::SignOutAll(args) => {
confirmation::require(args.yes, "Sign out every Cap device and agent")?;
let client = AgentClient::from_credentials().map_err(|error| error.to_string())?;
let value = client
.mutate_json_confirmed(Method::POST, "/me/sign-out-all", &json!({}))
.await
.map_err(|error| error.to_string())?;
credentials::delete_agent()?;
agent_client::print_value(&value, resolve_format(global_json, args.format))
}
}
}
}
Loading
Loading