Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ agent usage # usage dashboard for the period
agent analytics reviewer --account-type bot # which apps review the most PRs
agent analytics pr --days 30 # PR volume/trend with human vs bot splits
agent analytics review --repo my-service # review totals + top reviewers
agent ping # check authenticated /v1 connectivity
agent ping # check authenticated API connectivity
```

Every command shown is singular. The plural spelling of each (`agent assets`,
Expand All @@ -117,7 +117,7 @@ left out of `--help`. See
argument, flag, and help-text conventions.

Most commands accept `--json` to print the raw API response. The CLI talks to
the public `/v1` REST API. Point it at a different instance durably with
the public REST API. Point it at a different instance durably with
`agent host` (below), or per-invocation with `ELLIPSIS_API_BASE_URL` (or the
legacy `ELLIPSIS_API_BASE`).

Expand Down Expand Up @@ -184,7 +184,7 @@ npm run compile # single-binary build (bun)
- `scripts/smoke-local.sh` is a **fully-automated** end-to-end check against a
local `docker compose` backend. It drives the device-code login itself —
scraping the verification code and approving it headlessly through the
running `public_api` container — then exercises the authenticated `/v1` calls
running `public_api` container — then exercises the authenticated API calls
with a throwaway config dir. One command, no manual approval:

```sh
Expand All @@ -193,7 +193,7 @@ npm run compile # single-binary build (bun)
```

- `scripts/smoke.sh` is the manual variant for any backend (incl. staging/prod):
it drives login and the `/v1` calls but waits for you to approve in the
it drives login and the API calls but waits for you to approve in the
dashboard. See its header for the approval options.

```sh
Expand Down Expand Up @@ -233,7 +233,7 @@ scoped to that one repo only — no account-wide PAT involved.

### Status

The full `/v1` REST surface (auth, sessions, session search/steps, configs,
The full public REST surface (auth, sessions, session search/steps, configs,
integration discovery, budget/usage) is wired against the live API, including
live WebSocket streaming and `session stop`.
Still pending: replacing the hand-rolled request/response types with the
Expand Down
22 changes: 11 additions & 11 deletions docs/RUN_STREAMING_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@

## 1. Background

The CLI can start runs and read their state over the public `/v1` REST API, but
The CLI can start runs and read their state over the public REST API, but
it cannot stream a run's output. `agent run get --watch` exists today and gives a
**status-level** live view by polling `GET /v1/agents/runs/{id}` until the run
**status-level** live view by polling `GET /agents/runs/{id}` until the run
reaches a terminal status (`completed`/`error`/`cancelled`/`stopped`). It shows
status transitions and the final summary — not the step-by-step output.

**Crucially, the backend already streams steps live — just not over `/v1`.** The
**Crucially, the backend already streams steps live — just not over the public API.** The
dashboard consumes a WebSocket stream; this work is about re-exposing that same
stream under `/v1` for bearer-authenticated CLI clients. The bulk of the
stream on the public API for bearer-authenticated CLI clients. The bulk of the
machinery (step model, persistence, event bus) already exists and must be reused,
not reinvented.

Expand Down Expand Up @@ -56,14 +56,14 @@ not reinvented.
back to REST status-polling when streaming is unavailable. The same flag covers
both modes — no new top-level command.

## 3. Server-side requirements (`/v1`)
## 3. Server-side requirements (public API)

**Re-export the existing stream; do not build a parallel one.** Reuse
`agent_steps`, `CCStep`, and the `AgentEventBus` exactly as the frontend stream
does. The `/v1` endpoint is a thin re-auth + re-encode of `_stream_run_loop`.
does. The public endpoint is a thin re-auth + re-encode of `_stream_run_loop`.

1. **Endpoint:** `GET /v1/agents/runs/{run_id}/stream`, upgraded to WebSocket.
2. **Auth — bearer, not ticket.** The CLI holds a `/v1` bearer token, so resolve
1. **Endpoint:** `GET /agents/runs/{run_id}/stream`, upgraded to WebSocket.
2. **Auth — bearer, not ticket.** The CLI holds a public API bearer token, so resolve
it with the same `V1Auth` path as the REST API (Authorization header on the
handshake, which non-browser clients *can* set), authorizing the run's
customer. The 60s `?ticket=` dance is a browser workaround the CLI doesn't
Expand All @@ -85,7 +85,7 @@ does. The `/v1` endpoint is a thin re-auth + re-encode of `_stream_run_loop`.

## 4. Client-side requirements (this repo)

1. `agent run get <id> --watch` opens the `/v1` stream and renders frames:
1. `agent run get <id> --watch` opens the public API stream and renders frames:
`snapshot`/`steps_append` → render each `CCStep` (assistant text + tool calls,
tool stdout/stderr, thinking if `--verbose`); `run` → status transitions;
terminal close → final summary. Exit 0 on a successful terminal status,
Expand Down Expand Up @@ -121,7 +121,7 @@ Mirror the backend's existing codes where possible:
time, sharing the `AgentStep`/`CCStep` schema with the dashboard.
- Killing the socket mid-run and reconnecting with `?since=<last step_index>`
resumes with no lost or duplicated steps and without a full re-snapshot.
- `--watch` against a backend without the `/v1` endpoint transparently falls back
- `--watch` against a backend without the stream endpoint transparently falls back
to REST status-polling and still completes.
- `--json --watch` emits valid NDJSON, one frame per line.
- Unit tests for the client frame handler, the `since` resume cursor, and the
Expand All @@ -132,7 +132,7 @@ Mirror the backend's existing codes where possible:
## 7. Out of scope

- Bidirectional control (stop/input). `run stop` is tracked separately and also
has no `/v1` endpoint yet.
has no stream endpoint yet.
- Re-architecting the transport (NOTIFY + DB-as-source-of-truth stays).
- Multiplexing multiple runs over one socket.

Expand Down
4 changes: 2 additions & 2 deletions scripts/smoke-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# this drives the whole device-code flow itself: it starts `agent login`,
# scrapes the user code, and approves it headlessly by calling the cli_auth
# service inside the running `public_api` container — then exercises the
# authenticated /v1 surface. Uses a throwaway config dir, so your real token is
# authenticated API surface. Uses a throwaway config dir, so your real token is
# never touched.
#
# Prereqs: docker compose up (public_api reachable at $ELLIPSIS_API_BASE).
Expand Down Expand Up @@ -99,7 +99,7 @@ LOGIN_PID=""
cat "$LOGIN_OUT"
echo

echo "== Authenticated /v1 calls =="
echo "== Authenticated API calls =="
run me
run budget
run usage
Expand Down
4 changes: 2 additions & 2 deletions scripts/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# Manual end-to-end smoke test of the Ellipsis CLI against a backend.
#
# Drives the device-code login flow and then exercises the authenticated /v1
# Drives the device-code login flow and then exercises the authenticated public API
# surface. Uses an isolated config dir so it never touches your real token.
#
# Usage:
Expand Down Expand Up @@ -39,7 +39,7 @@ echo
echo "== Logging in (approve the printed request, then this continues) =="
npx tsx src/cli.tsx login --no-browser

echo "== Authenticated /v1 calls =="
echo "== Authenticated API calls =="
run me
run budget
run usage
Expand Down
6 changes: 3 additions & 3 deletions skills/cli-conventions/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const asset = alsoKnownAs(

apiRoutes(
alsoKnownAs(asset.command('delete <asset-id>').description('...'), 'rm'),
'DELETE /v1/assets/{id}',
'DELETE /assets/{id}',
)
```

Expand Down Expand Up @@ -93,8 +93,8 @@ Other rules:
One line, imperative verb first, no trailing period.

- **Say what the caller gets, not which endpoint answers.** `List your stored
assets, newest first` — not `List assets (GET /v1/assets)`.
- **Routes go in the long help**, last, via `apiRoutes(cmd, 'GET /v1/...')`.
assets, newest first` — not `List assets (GET /assets)`.
- **Routes go in the long help**, last, via `apiRoutes(cmd, 'GET /...')`.
When a command also has an `addHelpText('after', ...)` usage note, chain the
note *inside* the `apiRoutes()` call so the route line still lands last.
- **Name the concept the same way every time.** The object under `agent
Expand Down
6 changes: 3 additions & 3 deletions skills/ellipsis/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ the logs of a session they do not own.
YAML.

Surfaces: the dashboard at app.ellipsis.dev, the REST API at
api.ellipsis.dev/v1, and the `agent` CLI. All three drive the same `/v1` API.
api.ellipsis.dev, and the `agent` CLI. All three drive the same API.
Pricing is usage based, the tokens and compute a session spent plus a platform
fee. There are no seats.

Expand All @@ -72,7 +72,7 @@ fee. There are no seats.
- **A task that should not block the laptop**: `agent session handoff` pushes a
snapshot of the working tree and continues the work in a cloud session.
- **Delegation from scripts or CI**: `agent session start` or
`POST /v1/sessions`. With `--watch` it streams into the log and exits nonzero
`POST /sessions`. With `--watch` it streams into the log and exits nonzero
unless the session completes, so it works as a gate.

Things teams actually build: screenshot every pull request that touches the
Expand Down Expand Up @@ -359,7 +359,7 @@ cost and latency.

## The agent CLI

One open-source binary named `agent`, a terminal client for the same `/v1` API
One open-source binary named `agent`, a terminal client for the same API
the dashboard uses. Most commands accept `--json` for the raw API response,
which makes it as comfortable for a coding agent as for a human.

Expand Down
6 changes: 3 additions & 3 deletions src/commands/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export function registerAnalytics(program: Command): void {
.description('Rank who reviews the most PRs, people and apps alike'),
'reviewers',
),
'GET /v1/analytics/metrics',
'GET /analytics/metrics',
)
.option(
'-r, --repo <owner/name>',
Expand Down Expand Up @@ -148,7 +148,7 @@ export function registerAnalytics(program: Command): void {
.description('Show pull request volume and trend, split human vs bot'),
'prs',
),
'GET /v1/analytics/pull-requests',
'GET /analytics/pull-requests',
)
.option(
'--account-type <type>',
Expand Down Expand Up @@ -211,7 +211,7 @@ export function registerAnalytics(program: Command): void {
.description('Show review totals, verdicts, and the top reviewers'),
'reviews',
),
'GET /v1/analytics/reviews',
'GET /analytics/reviews',
)
.option(
'-r, --repo <name>',
Expand Down
8 changes: 4 additions & 4 deletions src/commands/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function registerAsset(program: Command): void {
asset
.command('upload <path>')
.description('Upload a PNG and print its org-gated URL, ready to paste into a PR comment'),
'POST /v1/assets',
'POST /assets',
)
.option('--json', 'output raw JSON')
.action(async (path: string, opts: { json?: boolean }) => {
Expand All @@ -106,7 +106,7 @@ export function registerAsset(program: Command): void {
asset.command('list').description('List your stored assets, newest first'),
'ls',
),
'GET /v1/assets',
'GET /assets',
)
.option('--session <id>', 'only assets uploaded by this agent session')
.option('-l, --limit <n>', 'max results (server cap: 250)', parsePositiveInt)
Expand Down Expand Up @@ -142,7 +142,7 @@ export function registerAsset(program: Command): void {
asset
.command('get <asset-id>')
.description("Print one asset's metadata, or download its bytes with -o"),
'GET /v1/assets/{id}',
'GET /assets/{id}',
'presigned S3 GET',
)
.option('-o, --output <path>', 'write the file contents to this path')
Expand Down Expand Up @@ -170,7 +170,7 @@ export function registerAsset(program: Command): void {
.description('Delete an asset, so its link stops resolving'),
'rm',
),
'DELETE /v1/assets/{id}',
'DELETE /assets/{id}',
)
.option('--json', 'output raw JSON')
.action(async (assetId: string, opts: { json?: boolean }) => {
Expand Down
18 changes: 9 additions & 9 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function registerConfig(program: Command): void {
config.command('list').description('List your saved agent configs'),
'ls',
),
'GET /v1/configs',
'GET /configs',
)
.option('--json', 'output raw JSON')
.action(async (opts: { json?: boolean }) => {
Expand Down Expand Up @@ -59,7 +59,7 @@ export function registerConfig(program: Command): void {
config
.command('get <config-id>')
.description('Print one agent config as YAML, or as JSON with --json'),
'GET /v1/configs/{id}',
'GET /configs/{id}',
)
.option('--json', 'output raw JSON')
.action(async (configId: string, opts: { json?: boolean }) => {
Expand All @@ -85,7 +85,7 @@ export function registerConfig(program: Command): void {
config
.command('create')
.description('Create an agent config by opening a pull request that adds it to a repo'),
'POST /v1/configs',
'POST /configs',
)
.requiredOption(
'-r, --repo <name>',
Expand Down Expand Up @@ -148,11 +148,11 @@ export function registerConfig(program: Command): void {
.description('Show or set which agent config runs when a session names none'),
'defaults',
),
'GET /v1/defaults',
'GET /defaults',
)
.option('--json', 'output raw JSON')
// Bare `agent config default`: the effective default for the repo you're
// standing in, computed locally from GET /v1/defaults + the origin remote
// standing in, computed locally from GET /defaults + the origin remote
// (the same ladder session start resolves server-side).
.action(async (opts: { json?: boolean }) => {
await runAction(async () => {
Expand Down Expand Up @@ -189,7 +189,7 @@ export function registerConfig(program: Command): void {
.description('List every default that is set, account rung and per-repo rungs'),
'ls',
),
'GET /v1/defaults',
'GET /defaults',
)
.option('--json', 'output raw JSON')
// The group also defines --json (for the bare view), and commander parses
Expand Down Expand Up @@ -223,7 +223,7 @@ export function registerConfig(program: Command): void {
defaults
.command('set <config-id>')
.description('Set the account default agent config, or a repo default with --repo'),
'PUT /v1/defaults',
'PUT /defaults',
)
.option(
'-r, --repo [repository]',
Expand Down Expand Up @@ -256,7 +256,7 @@ export function registerConfig(program: Command): void {
'rm',
'delete',
),
'DELETE /v1/defaults',
'DELETE /defaults',
)
.option(
'-r, --repo [repository]',
Expand All @@ -278,7 +278,7 @@ export function registerConfig(program: Command): void {
.description(
`Scaffold a starter agent config YAML locally (default: ${DEFAULT_CONFIG_PATH})`,
),
'POST /v1/configs with --template',
'POST /configs with --template',
)
// No `-f` short: CLI-wide, `-f` means an input file (see `config create`).
.option('--force', 'overwrite the file if it already exists')
Expand Down
6 changes: 3 additions & 3 deletions src/commands/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import { canHostSessionsUi, defaultStartRequest, runSessionsUi } from '../ui/lau
// `agent session connect [sessionId]` — the terminal window into a cloud
// session (documents/eng/SESSION_IDE.md §2.6, in the ellipsis monorepo).
//
// A pure /v1 client: it renders the conversation so far from the stored
// A pure API client: it renders the conversation so far from the stored
// transcript, follows new output live over the session WebSocket, and sends
// what you type through POST /v1/sessions/{id}/messages — the same inbox that
// what you type through POST /sessions/{id}/messages — the same inbox that
// delivers webhook events to the agent's Claude Code stdin at the next turn
// boundary. It NEVER spawns or attaches a Claude Code process (a second
// writer on one CC session corrupts the transcript; the cloud worker is the
Expand Down Expand Up @@ -59,7 +59,7 @@ export function registerConnect(session: Command): void {
headless or from inside the session's own sandbox, where the id is optional.
Pass --no-input to follow read-only from a script or agent (no TTY needed).

API: GET /v1/sessions/{id}, GET /v1/sessions/{id}/records, POST /v1/sessions/{id}/messages, WS /v1/sessions/{id}/stream`,
API: GET /sessions/{id}, GET /sessions/{id}/records, POST /sessions/{id}/messages, WS /sessions/{id}/stream`,
)
.action(async (sessionId: string | undefined, opts: { records: boolean; input: boolean }) => {
await runAction(async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/commands/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function registerGithub(program: Command): void {
.description('List the repositories the GitHub installation can reach'),
'repo',
),
'GET /v1/github/repos',
'GET /github/repos',
)
.option('--json', 'output raw JSON')
.action(async (opts: { json?: boolean }) => {
Expand Down Expand Up @@ -53,7 +53,7 @@ export function registerGithub(program: Command): void {
),
'member',
),
'GET /v1/github/members',
'GET /github/members',
)
.option('--json', 'output raw JSON')
.action(async (opts: { json?: boolean }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function registerHelp(program: Command): void {
program
.command('help')
.description('Show help for a command, or ask the help agent with --interactive'),
'POST /v1/sessions with --interactive',
'POST /sessions with --interactive',
)
.argument('[command...]', 'command to show help for (e.g. `session start`)')
.option(
Expand Down
2 changes: 1 addition & 1 deletion src/commands/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function registerIntegration(program: Command): void {
.description('Show which integrations are connected, in one table'),
'integrations',
),
'GET /v1/integrations',
'GET /integrations',
)
.option('--json', 'output raw JSON')
.action(async (opts: { json?: boolean }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/linear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function registerLinear(program: Command): void {
.description('List the Linear teams, marking which have Ellipsis enabled'),
'team',
),
'GET /v1/linear/teams',
'GET /linear/teams',
)
.option('--json', 'output raw JSON')
.action(async (opts: { json?: boolean }) => {
Expand Down
Loading