diff --git a/.agents/skills/cli-design/SKILL.md b/.agents/skills/cli-design/SKILL.md new file mode 100644 index 0000000..06cde0b --- /dev/null +++ b/.agents/skills/cli-design/SKILL.md @@ -0,0 +1,433 @@ +--- +name: cli-design +displayName: CLI Design +description: "Design and build agent-first CLIs with HATEOAS JSON responses, context-protecting output, and self-documenting command trees. Use when creating new CLI tools, adding commands to existing CLIs (joelclaw, slog), or reviewing CLI design for agent-friendliness. Triggers on 'build a CLI', 'add a command', 'CLI design', 'agent-friendly output', or any task involving command-line tool creation." +version: 1.1.0 +author: Joel Hooks +tags: [joelclaw, cli, agentic, ux, json] +--- + +# Agent-First CLI Design + +CLIs in this system are **agent-first, human-distant-second**. Every command returns structured JSON that an agent can parse, act on, and follow. Humans are welcome to pipe through `jq`. + +## Core Principles + +### 1. JSON always + +Every command returns JSON. No plain text. No tables. No color codes. Agents parse JSON; they don't parse prose. + +```bash +# This is the ONLY output format +joelclaw status +# → { "ok": true, "command": "joelclaw status", "result": {...}, "next_actions": [...] } +``` + +No `--json` flag. No `--human` flag. JSON is the default and only format. + +### 2. HATEOAS — every response tells you what to do next + +Every response includes `next_actions` — an array of command **templates** the agent can run next. Templates use standard POSIX/docopt placeholder syntax: + +- `` — required argument +- `[--flag ]` — optional flag with value +- `[--flag]` — optional boolean flag +- No `params` field — literal command (run as-is) +- `params` present — template (agent fills placeholders) +- `params.*.value` — pre-filled from context (agent can override) +- `params.*.default` — value if omitted +- `params.*.enum` — valid choices + +```json +{ + "ok": true, + "command": "joelclaw send pipeline/video.download", + "result": { + "event_id": "01KHF98SKZ7RE6HC2BH8PW2HB2", + "status": "accepted" + }, + "next_actions": [ + { + "command": "joelclaw run ", + "description": "Check run status for this event", + "params": { + "run-id": { "value": "01KHF98SKZ7RE6HC2BH8PW2HB2", "description": "Run ID (ULID)" } + } + }, + { + "command": "joelclaw logs [--lines ] [--grep ] [--follow]", + "description": "View worker logs", + "params": { + "source": { "enum": ["worker", "errors", "server"], "default": "worker" } + } + }, + { + "command": "joelclaw status", + "description": "Check system health" + } + ] +} +``` + +`next_actions` are **contextual** — they change based on what just happened. A failed command suggests different next steps than a successful one. Templates are the agent's **affordances** — they show what's parameterizable, what values are valid, and what the current context pre-fills. + +### 3. Self-documenting command tree + +Agents discover commands via **two paths**: the root command (JSON tree) and `--help` (Effect CLI auto-generated). Both must be useful. + +**Root command (no args)** returns the full command tree as JSON: + + + +```json +{ + "ok": true, + "command": "joelclaw", + "result": { + "description": "JoelClaw — personal AI system CLI", + "health": { "server": {...}, "worker": {...} }, + "commands": [ + { "name": "send", "description": "Send event to Inngest", "usage": "joelclaw send -d ''" }, + { "name": "status", "description": "System status", "usage": "joelclaw status" }, + { "name": "gateway", "description": "Gateway operations", "usage": "joelclaw gateway status" } + ] + }, + "next_actions": [...] +} +``` + +**`--help` output** is auto-generated by Effect CLI from `Command.withDescription()`. Every subcommand **must** have a description — agents always call `--help` and a bare command list with no descriptions is useless. + +```typescript +// ❌ Agents see a blank command list +const status = Command.make("status", {}, () => ...) + +// ✅ Agents see what each command does +const status = Command.make("status", {}, () => ...).pipe( + Command.withDescription("Active sessions, queue depths, Redis health") +) +``` + +``` +COMMANDS + - status Active sessions, queue depths, Redis health + - diagnose [--hours integer] Layer-by-layer health check + - review [--hours integer] Recent session context +``` + +### 4. Context-protecting output + +Agents have finite context windows. CLI output must not blow them up. + +**Rules:** +- Terse by default — minimum viable output +- Auto-truncate large outputs (logs, lists) at a reasonable limit +- When truncated, include a file path to the full output +- Never dump raw logs, full transcripts, or unbounded lists + +```json +{ + "ok": true, + "command": "joelclaw logs", + "result": { + "lines": 20, + "total": 4582, + "truncated": true, + "full_output": "/var/folders/.../joelclaw-logs-abc123.log", + "entries": ["...last 20 lines..."] + }, + "next_actions": [ + { + "command": "joelclaw logs [--lines ]", + "description": "Show more log lines", + "params": { + "source": { "enum": ["worker", "errors", "server"], "default": "worker" }, + "lines": { "default": 20, "description": "Number of lines" } + } + } + ] +} +``` + +### 5. Errors suggest fixes + +When something fails, the response includes a `fix` field — plain language telling the agent what to do about it. + +```json +{ + "ok": false, + "command": "joelclaw send pipeline/video.download", + "error": { + "message": "Inngest server not responding", + "code": "SERVER_UNREACHABLE" + }, + "fix": "Start the Inngest server pod: kubectl rollout restart statefulset/inngest -n joelclaw", + "next_actions": [ + { "command": "joelclaw status", "description": "Re-check system health after fix" }, + { + "command": "kubectl get pods [--namespace ]", + "description": "Check pod status", + "params": { "ns": { "default": "joelclaw" } } + } + ] +} +``` + +## Response Envelope + +Every command uses this exact shape: + +### Success + +```typescript +{ + ok: true, + command: string, // the command that was run + result: object, // command-specific payload + next_actions: Array<{ + command: string, // command template (POSIX syntax) or literal + description: string, // what it does + params?: Record args + }> + }> +} +``` + +### Error + +```typescript +{ + ok: false, + command: string, + error: { + message: string, // what went wrong + code: string // machine-readable error code + }, + fix: string, // plain-language suggested fix + next_actions: Array<{ + command: string, // command template or literal + description: string, + params?: Record // same schema as success + }> +} +``` + +### Reference implementations + +- `joelclaw` — `~/Code/joelhooks/joelclaw/packages/cli/` (Effect CLI, operational surface) +- `slog` — system log CLI (same envelope patterns) + +Use these as the current envelope source-of-truth. + +## Implementation + +### Framework: Effect CLI (@effect/cli) + +All CLIs use `@effect/cli` with Bun. This is non-negotiable — consistency across the system matters more than framework preference. + +```typescript +import { Command, Options } from "@effect/cli" +import { NodeContext, NodeRuntime } from "@effect/platform-node" + +const send = Command.make("send", { + event: Options.text("event"), + data: Options.optional(Options.text("data").pipe(Options.withAlias("d"))), +}, ({ event, data }) => { + // ... execute, return JSON envelope +}) + +const root = Command.make("joelclaw", {}, () => { + // Root: return health + command tree +}).pipe(Command.withSubcommands([send, status, logs])) +``` + +### Binary distribution + +Build with Bun, install to `~/.bun/bin/`: + +```bash +bun build src/cli.ts --compile --outfile joelclaw +cp joelclaw ~/.bun/bin/ +``` + +### Adding a new command + +1. Define the command with `Command.make` +2. Return the standard JSON envelope (ok, command, result, next_actions) +3. Include contextual `next_actions` — what makes sense AFTER this specific command +4. Handle errors with the error envelope (ok: false, error, fix, next_actions) +5. Add to the root command's subcommands +6. Add to the root command's `commands` array in the self-documenting output +7. Rebuild and install + +## Streaming Protocol (NDJSON) — ADR-0058 + +Request-response covers the **spatial** dimension (what's the state now?). Streamed NDJSON covers the **temporal** dimension (what's happening over time?). Together they make the full system observable through one protocol. + +### When to stream + +Stream when the command involves **temporal operations** — watching, following, tailing. Not every command needs streaming. Point-in-time queries (`status`, `functions`, `runs`) stay as single envelopes. + +Streaming is activated by command semantics (`--follow`, `watch`, `gateway stream`), never by a global `--stream` flag. + +### Protocol: typed NDJSON with HATEOAS terminal + +Each line is a self-contained JSON object with a `type` discriminator. The **last line is always the standard HATEOAS envelope** (`result` or `error`). Tools that don't understand streaming read the last line and get exactly what they expect. + +``` +{"type":"start","command":"joelclaw send video/download --follow","ts":"2026-02-19T08:25:00Z"} +{"type":"step","name":"download","status":"started","ts":"..."} +{"type":"progress","name":"download","percent":45,"ts":"..."} +{"type":"step","name":"download","status":"completed","duration_ms":3200,"ts":"..."} +{"type":"step","name":"transcribe","status":"started","ts":"..."} +{"type":"log","level":"warn","message":"Large file, chunked transcription","ts":"..."} +{"type":"step","name":"transcribe","status":"completed","duration_ms":45000,"ts":"..."} +{"type":"result","ok":true,"command":"...","result":{...},"next_actions":[...]} +``` + +### Stream event types + +| Type | Meaning | Terminal? | +|------|---------|-----------| +| `start` | Stream begun, echoes command | No | +| `step` | Inngest step lifecycle (started/completed/failed) | No | +| `progress` | Progress update (percent, bytes, message) | No | +| `log` | Diagnostic message (info/warn/error level) | No | +| `event` | An Inngest event was emitted (fan-out visibility) | No | +| `result` | HATEOAS success envelope — always last | **Yes** | +| `error` | HATEOAS error envelope — always last | **Yes** | + +### TypeScript types + +```typescript +import type { NextAction } from "./response" + +type StreamEvent = + | { type: "start"; command: string; ts: string } + | { type: "step"; name: string; status: "started" | "completed" | "failed"; duration_ms?: number; error?: string; ts: string } + | { type: "progress"; name: string; percent?: number; message?: string; ts: string } + | { type: "log"; level: "info" | "warn" | "error"; message: string; ts: string } + | { type: "event"; name: string; data: unknown; ts: string } + | { type: "result"; ok: true; command: string; result: unknown; next_actions: NextAction[] } + | { type: "error"; ok: false; command: string; error: { message: string; code: string }; fix: string; next_actions: NextAction[] } +``` + +### Emitting stream events + +Use the `emit()` helper — one JSON line per call, flushed immediately: + +```typescript +import { emit, emitResult, emitError } from "../stream" + +// Progress events +emit({ type: "start", command: "joelclaw send video/download --follow", ts: new Date().toISOString() }) +emit({ type: "step", name: "download", status: "started", ts: new Date().toISOString() }) +emit({ type: "step", name: "download", status: "completed", duration_ms: 3200, ts: new Date().toISOString() }) + +// Terminal — always last +emitResult("send --follow", { videoId: "abc123" }, [ + { command: "joelclaw run abc123", description: "Inspect the completed run" }, +]) +``` + +### Redis subscription pattern + +Streaming commands subscribe to the same Redis pub/sub channels the gateway extension uses. `pushGatewayEvent()` middleware is the emission point — the CLI is just another subscriber. + +```typescript +import { streamFromRedis } from "../stream" + +// Subscribe to a channel, transform events, emit NDJSON +await streamFromRedis({ + channel: `joelclaw:notify:gateway`, + command: "joelclaw gateway stream", + transform: (event) => ({ + type: "event" as const, + name: event.type, + data: event.data, + ts: new Date().toISOString(), + }), + // Optional: end condition + until: (event) => event.type === "loop.complete", +}) +``` + +### Composable with Unix tools + +NDJSON is pipe-native. Agents and humans can filter streams: + +```bash +# Only step events +joelclaw watch | jq --unbuffered 'select(.type == "step")' + +# Only failures +joelclaw send video/download --follow | jq --unbuffered 'select(.type == "error" or (.type == "step" and .status == "failed"))' + +# Count steps +joelclaw send pipeline/run --follow | jq --unbuffered 'select(.type == "step" and .status == "completed")' | wc -l +``` + +### Agent consumption pattern + +Agents consuming streams read lines as they arrive and can make decisions mid-execution: + +1. Start the stream: `joelclaw send video/download --follow` +2. Read lines incrementally +3. React to early signals (cancel if error, escalate if slow, log progress) +4. The terminal `result`/`error` line contains `next_actions` for what to do after + +This eliminates the **polling tax** — no wasted tool calls checking "is it done yet?" + +### Cleanup + +Streaming commands hold a Redis connection. They **must**: +- Handle SIGINT/SIGTERM gracefully (disconnect Redis, emit terminal event) +- Use `connectTimeout` and `commandTimeout` to prevent hangs +- Clean up the subscription on stream end (success, error, or signal) + +## Anti-Patterns + +| Don't | Do | +|-------|-----| +| Plain text output | JSON envelope | +| Tables with ANSI colors | JSON arrays | +| `--json` flag to opt into JSON | JSON is the only format | +| Dump 10,000 lines | Truncate + file pointer | +| `Error: something went wrong` | `{ ok: false, error: {...}, fix: "..." }` | +| Undiscoverable commands | Root returns full command tree | +| Static help text | HATEOAS next_actions | +| `console.log("Success!")` | `{ ok: true, result: {...} }` | +| Exit code as the only error signal | Error in JSON + exit code | +| Require the agent to read --help | Root command self-documents | +| Subcommand with no `withDescription` | Every command gets a description for `--help` | +| Poll in a loop for temporal data | Stream NDJSON via Redis sub (ADR-0058) | +| Plain text in streaming commands | Every line is a typed JSON object | +| Hold Redis connections without cleanup | SIGINT handler + connection timeout | + +## Naming Conventions + +- Commands are **nouns or verbs**, lowercase, no hyphens: `send`, `status`, `logs`, `gateway` +- Subcommands follow naturally: `joelclaw search "query"`, `joelclaw loop start` +- Flags use `--kebab-case`: `--max-quality`, `--follow` +- Short flags for common options: `-d` for `--data`, `-f` for `--follow` +- Event names use `domain/action`: `pipeline/video.download`, `content/summarize` + +## Checklist for New Commands + +- [ ] Returns JSON envelope (ok, command, result, next_actions) +- [ ] `Command.withDescription()` set (shows in `--help`) +- [ ] Error responses include fix field +- [ ] Root command lists this command in its tree +- [ ] Output is context-safe (truncated if potentially large) +- [ ] next_actions are contextual to what just happened +- [ ] next_actions with variable parts use template syntax (``, `[--flag ]`) + `params` +- [ ] Context-specific values pre-filled via `params.*.value` +- [ ] No plain text output anywhere +- [ ] No ANSI colors or formatting +- [ ] Works when piped (no TTY detection) +- [ ] Builds and installs to ~/.bun/bin/ diff --git a/.agents/skills/cli-design/agents/openai.yaml b/.agents/skills/cli-design/agents/openai.yaml new file mode 100644 index 0000000..0aeed70 --- /dev/null +++ b/.agents/skills/cli-design/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + icon_small: "./assets/small-logo.svg" + icon_large: "./assets/large-logo.png" diff --git a/.agents/skills/cli-design/assets/large-logo.png b/.agents/skills/cli-design/assets/large-logo.png new file mode 100644 index 0000000..ec2217c Binary files /dev/null and b/.agents/skills/cli-design/assets/large-logo.png differ diff --git a/.agents/skills/cli-design/assets/small-logo.svg b/.agents/skills/cli-design/assets/small-logo.svg new file mode 100644 index 0000000..d16acd2 --- /dev/null +++ b/.agents/skills/cli-design/assets/small-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/.changeset/native-keychain-auto-reauth.md b/.changeset/native-keychain-auto-reauth.md new file mode 100644 index 0000000..bab27af --- /dev/null +++ b/.changeset/native-keychain-auto-reauth.md @@ -0,0 +1,13 @@ +--- +"@godaddy/cli": minor +--- + +Replace keytar native addon with cross-platform OS keychain (macOS security CLI, Linux secret-tool, Windows PasswordVault). No native Node addons required. + +Fix CLI error routing: validation guard no longer misclassifies AuthenticationError and NetworkError as input validation errors. + +Fix `application list` to use Relay connection syntax (edges/node) matching the updated GraphQL schema. + +Add `--scope` option to `auth login` for requesting additional OAuth scopes beyond the defaults. + +Add `--scope` option to `api` command with automatic re-authentication on 403: decodes the JWT to detect missing scopes, triggers the browser auth flow, and retries the request. diff --git a/.changeset/quiet-foxes-divide.md b/.changeset/quiet-foxes-divide.md new file mode 100644 index 0000000..7bd332d --- /dev/null +++ b/.changeset/quiet-foxes-divide.md @@ -0,0 +1,5 @@ +--- +"@godaddy/cli": patch +--- + +Fix `application deploy` by using the correct GraphQL enum casing when requesting the latest release. diff --git a/.gitignore b/.gitignore index bc0101d..219a49f 100644 --- a/.gitignore +++ b/.gitignore @@ -197,4 +197,6 @@ schema.graphql graphql-env.d.ts # Alternative package manager lockfiles -bun.lock \ No newline at end of file +bun.lock + +.pnpm-store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a2a2846 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# Agent Notes + +## Effect-First Patterns (Required) +- Command handlers must use `Command.action(...)` with `Effect` values. +- Do not use `actionAsync` anywhere. +- Prefer `Effect.gen` for multi-step flows and `Effect.sync` for pure sync handlers. +- Use `Effect.catchAll` at command boundaries to map runtime errors into structured CLI envelopes. + +## API Layer Pattern +- Public module APIs use this shape: + 1. Internal Promise implementation: `async function fooPromise(...)`. + 2. Effect API: `export function fooEffect(...) => Effect.tryPromise(...)`. + 3. Promise boundary wrapper (for compatibility): `export function foo(...) => Effect.runPromise(fooEffect(...))`. +- Keep `Effect.runPromise` usage at boundaries only (CLI entrypoint and compatibility wrappers). + +## Imports and Dependencies +- Use `import * as Effect from "effect/Effect"` directly. +- Do not reintroduce `toEffect`/`effect-interop`; wrappers are explicit per function. +- Prefer static imports of `*Effect` APIs over dynamic imports in commands. + +## Streaming / Long-Running Commands +- For streamed command output, emit: + 1. start event, + 2. progress/step events, + 3. final result event, + 4. mapped stream error event on failure. +- Keep stream callbacks best-effort and non-fatal. + +## Verification Checklist +- `pnpm exec tsc --noEmit` +- `pnpm run build` +- `pnpm test tests/integration/cli-smoke.test.ts tests/unit/application-deploy-security.test.ts tests/unit/cli/deploy-stream.test.ts` +- `rg "export async function" src` should be `0`. +- `rg "toEffect|effect-interop" src` should be `0`. + +## Migration Pitfalls Seen +- Name collisions: if a file already has a hand-written `*Effect` (example: deploy), do not route compatibility wrappers to the wrong effect signature. +- Codemods can break import blocks; run typecheck immediately after broad transforms. +- Keep command-level error emission consistent (`mapRuntimeError` + `nextActionsFor(...)`). diff --git a/CLAUDE.md b/CLAUDE.md index 73cbc0c..6374205 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,11 @@ -# CLAUDE.md +# CLAUDE.md - GoDaddy CLI This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Local Development Ports + +This is a command-line application that does not run on a network port. + # GoDaddy CLI Development Guide ## Commands diff --git a/README.md b/README.md index 8b81f1a..3d5b5e6 100644 --- a/README.md +++ b/README.md @@ -1,400 +1,101 @@ # GoDaddy CLI -A powerful command-line interface for interacting with GoDaddy's developer ecosystem. Manage your applications, handle authentication, and work with webhooks effortlessly. +Agent-first CLI for interacting with GoDaddy Developer Platform. ## Installation ```bash -# Install the CLI globally from npm npm install -g @godaddy/cli - -# Verify installation godaddy --help ``` -## Development +## Output Contract -```bash -# Watch mode for development using tsx -pnpm tsx --watch index.ts +All executable commands emit JSON envelopes: -# Quick command execution during development -pnpm tsx src/index.tsx application +```json +{"ok":true,"command":"godaddy env get","result":{"environment":"ote"},"next_actions":[...]} ``` -## Features - -- **Application Management**: Create, view, and release applications -- **Authentication**: Secure OAuth-based authentication with GoDaddy -- **Webhook Management**: List available webhook event types -- **Environment Management**: Work across different GoDaddy environments -- **Actions Management**: List and describe available application actions - -## Command Reference - -### Global Options - -```bash -godaddy --help # Display help information -godaddy --version # Display version information -godaddy -e, --env # Set target environment (ote, prod) -godaddy --debug # Enable debug logging for HTTP requests and responses +```json +{"ok":false,"command":"godaddy application info demo","error":{"message":"Application 'demo' not found","code":"NOT_FOUND"},"fix":"Use discovery commands such as: godaddy application list or godaddy actions list.","next_actions":[...]} ``` -### Environment Commands - -```bash -# List all available environments -godaddy env list - Options: - -o, --output # Output format: json or text (default: text) +`--help` remains standard CLI help text. +`--output` has been removed; all executable command paths return JSON envelopes. +Use `--pretty` to format envelopes with 2-space indentation for human readability. +Long-running operations can stream typed NDJSON events with `--follow`, ending with a terminal `result` or `error` event. -# Get current environment details -godaddy env get - Options: - -o, --output # Output format: json or text (default: text) - -# Switch to a different environment -godaddy env set # is one of: ote, prod - Options: - -o, --output # Output format: json or text (default: text) - -# View detailed environment configuration -godaddy env info [environment] # [environment] is one of: ote, prod (defaults to current) - Options: - -o, --output # Output format: json or text (default: text) -``` - -### Authentication Commands +## Root Discovery ```bash -# Login to GoDaddy Developer Platform -godaddy auth login # Opens browser for OAuth authentication - Options: - -o, --output # Output format: json or text (default: text) - -# Logout and clear stored credentials -godaddy auth logout - Options: - -o, --output # Output format: json or text (default: text) - -# Check current authentication status -godaddy auth status - Options: - -o, --output # Output format: json or text (default: text) +godaddy ``` -### Application Commands - -> **Note**: `godaddy app` can be used as a shorthand alias for `godaddy application` - -```bash -# List all applications -godaddy application list # Alias: godaddy app ls - Options: - -o, --output # Output format: json or text (default: text) - -# Show application information -godaddy application info # Shows info for the named application - Options: - -o, --output # Output format: json or text (default: text) - -# Initialize a new application -godaddy application init - Options: - --name # Application name - --description # Application description - --url # Application URL - --proxy-url # Proxy URL for API endpoints - --scopes # Authorization scopes (space-separated) - -c, --config # Path to configuration file - --environment # Environment (ote|prod) - -o, --output # Output format: json or text (default: text) +Returns environment/auth snapshots and the full command tree. -# Validate application configuration -godaddy application validate - Options: - -o, --output # Output format: json or text (default: text) +## Global Options -# Update existing application -godaddy application update - Options: - --label