# Goke

> goke is a zero-dependency, type-safe CLI framework for TypeScript. CAC replacement with Standard Schema support (Zod, Valibot, ArkType). Use goke when building CLI tools — it handles commands, subcommands, options, type coercion, help generation, and more. Schema-based options give you automatic type inference, coercion from strings, and help text generation. Pair with `@goke/mcp` to generate CLI commands automatically from an MCP server. ALWAYS read this skill when a repo uses goke for its CLI.

- Skill: `remorses/goke` (Agent Skill)
- Install (CLI): `npx skillmds@latest add remorses/goke`
- Raw SKILL.md: https://api.skillmd.com/api/skills/remorses/goke/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: remorses (https://skillmd.com/u/remorses)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/remorses/goke

---


# goke

Fetch the full README from GitHub and read it before using goke:

```bash
curl -L https://raw.githubusercontent.com/remorses/goke/main/README.md
```

> Read the README in full every time you use goke.
>
> Important: never use `head` or `tail` to truncate it. Read the full README instead.

## Install

```bash
npm install goke # or bun, pnpm, etc
```

## Quick Notes

- Core APIs: `cli.option`, `cli.use`, `cli.section`, `cli.version`, `cli.help`, `cli.completions`, `cli.parse`
- Prefer injected `{ fs, console, process }` over globals
- Use relative paths with injected `fs`; if a helper needs current-cwd semantics, pass injected `process.cwd` into that helper
- For JustBash compatibility tests, import the existing CLI from app code instead of defining a new CLI inside the test
- **Never install `picocolors`, `chalk`, `kleur`, or any color library.** Use `import { colors } from 'goke'` instead. It's a vendored picocolors with zero extra dependencies.
- Use [`@goke/mcp`](https://github.com/remorses/goke/tree/main/mcp) to generate CLI commands from an MCP server. `addMcpCommands({ cli, getMcpUrl, loadCache, saveCache })` discovers tools and registers a typed command for each one.

The README is the source of truth for rules, examples, testing patterns, JustBash integration, and API details.

## Reporting goke bugs

If you find a bug in goke, **always open a GitHub issue with the `gh` CLI**. Do not only mention it in chat.

```bash
gh issue create --repo remorses/goke --title "Short bug title" --body "$(cat <<'EOF'
**Model:** grok-4.6
**Harness:** OpenCode / Kimaki

**What happened**
Describe the unexpected behavior.

**Relevant goke code**
Paste the CLI definition or goke API usage from this package that triggered the bug.

**Reproduction**
Minimal example that fails:

```ts
import { goke } from 'goke'

const cli = goke('mycli')
cli.command('deploy', 'Deploy').action(() => {})
cli.parse()
```
EOF
)"
```

Always include in the issue body:

- the **model** used (for example `grok-4.6`, `claude-opus-4-6`)
- the **harness** (for example OpenCode, Claude Code, Cursor, Codex)
- the **relevant goke code** from this package that hit the bug
- a **minimal reproduction** example

If the README or this skill mentions a `goke` export that is missing from the installed package, upgrade `goke` to latest first before adding workarounds or custom local detection code:

```bash
pnpm update goke --latest
```

Use the project package manager for the repo you are editing. After upgrading, re-check the export from the installed package and continue with the documented API.

## Terminal Colors

**Never install a separate color library.** goke vendors picocolors and exports it as `colors`:

```ts
import { colors } from 'goke'

console.log(colors.green('success'))
console.log(colors.red('error'))
console.log(colors.bold(colors.cyan('info')))
```

Available formatters: `bold`, `dim`, `italic`, `underline`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `gray`, `bgRed`, `bgGreen`, etc. Color support is auto-detected.

## Agent Detection

goke exports `isAgent`, `agent`, `agentInfo`, and `detectAgent()` from `goke/src/agents.ts`. Use `isAgent` to detect if the CLI is running inside an AI coding agent and skip interactive prompts or prefer structured output.

```ts
import { isAgent, agent } from 'goke'

if (isAgent) {
  // skip clack prompts, output YAML/JSON instead of interactive UI
}
```

`isAgent` is for **skipping interactive prompts that have a CLI flag alternative** (e.g. `--env staging` instead of a clack select). Use it to require the flag and fail with a usage hint instead of hanging on stdin:

```ts
if (!env) {
  if (isAgent || !process.stdin.isTTY) {
    console.error('Missing --env. Usage: deploy --env staging|production')
    process.exit(1)
  }
  // ... clack prompt fallback
}
```

**Never use `isAgent` to block commands that have no flag alternative**, like device-flow login. Those commands don't read stdin at all; they open a browser, print a code, and poll. The only real requirement is a TTY for spinner output. Use `!process.stdin.isTTY` or `!process.stdout.isTTY` alone for those:

```ts
// login command — no flag alternative, agent can run it in tuistory
if (!process.stdin.isTTY) {
  console.error('Login requires a terminal. Run in tuistory or tmux.')
  process.exit(1)
}
```

Agents can run login commands in a PTY via tuistory and complete the browser flow with playwriter. Blocking them with `isAgent` makes login impossible from agent contexts.

Supported agents: `cursor`, `claude`, `devin`, `replit`, `gemini`, `codex`, `auggie`, `opencode`, `kiro`, `goose`, `pi`. Set `AI_AGENT` env var to override.

## Long-Running Interactive Commands

Commands that start a browser/device login flow need to work for both interactive users and AI agents. goke's built-in **daemon** pattern (`ctx.daemon`) handles this: the command forks itself into a background process that waits for browser approval, while the foreground returns immediately so agents aren't blocked.

Use `isAgent` to branch between agent mode (start daemon, return immediately) and interactive mode (block until login completes). Do not fail in non-TTY shells; use the daemon instead.

```ts
import { goke, isAgent, openInBrowser } from 'goke'

cli
  .command('login', 'Authenticate with browser login')
  .action(async (options, ctx) => {
    if (ctx.daemon.isDaemon) {
      // ── DAEMON: create the URL, hand it to the foreground, then wait ──
      const flow = await startOAuthFlow({ /* ... */ })
      ctx.daemon.publishStartupMessage(`Authorize: ${flow.authorizationUrl}`)
      ctx.daemon.ready()
      const result = await flow.waitForApproval()
      if (result.success) saveAuth(result)
      return // daemon exits, PID file is cleaned up
    }

    // ── CLIENT: decide foreground vs background ──
    if (isAgent) {
      // Agent mode: return after the daemon publishes its authorization URL
      await ctx.daemon.start({
        waitForStartup: true,
        timeoutMs: 10 * 60 * 1000,
      })
      ctx.console.log('Login running in background.')
      ctx.console.log('After approving in browser, verify with: mycli me')
      return
    }

    // Interactive mode: attach to daemon, see all its logs and errors in real time
    await ctx.daemon.start({ attach: true, timeoutMs: 10 * 60 * 1000 })
    ctx.console.log('Login successful!')
  })
```

Use `waitForStartup: true` when the daemon creates an OAuth URL, device code, or other value that the agent must see before the foreground command returns. Publish ordered messages with `publishStartupMessage(message, { stream: 'stdout' | 'stderr' })`, then call `ready()`. Detached stdio remains ignored. Attached mode still receives the messages directly and waits for the daemon to exit.

Add a `me` command (exits 0 if logged in, 1 if not) so agents can poll for completion. Use `ctx.daemon.forCommand('login')` to check the login daemon status from other commands. See the **Background Daemons** section in the goke README for the full pattern, including `env` passthrough and PID file safety.

## Shell Completions

**Always add `.completions()` next to `.help()` in every CLI.** This gives users Tab completion for free.

```ts
cli.help()
cli.completions()
cli.parse()
```

### How it works

The shell calls the CLI binary on every Tab press with a hidden `--get-goke-completions` flag. The binary inspects registered commands and options, prints matching candidates to stdout, and exits. No static completion file to regenerate when commands change.

### README section for new CLIs

Every new CLI should include a **Shell Completions** section in its README. Add it after the main usage docs:

````md
## Shell Completions

Enable Tab completion for your shell:

```bash
mycli completions install
```

Restart your shell (or run `autoload -Uz compinit && compinit` for zsh). Then Tab works:

```bash
mycli <TAB>          # shows all commands
mycli dep<TAB>       # completes to "deploy"
mycli deploy --<TAB> # shows available options
```

Completions stay up-to-date automatically. To remove:

```bash
mycli completions uninstall
```
````

Replace `mycli` with the actual CLI name.

### Available completions commands

`.completions()` registers three subcommands:

- `completions install` — finds a writable shell completion directory and writes the shim script
- `completions uninstall` — removes installed completion files
- `completions script` — prints the raw script to stdout (for `eval` or piping)

All three accept `--shell zsh` or `--shell bash` to override auto-detection.

## openInBrowser is async

`openInBrowser` returns a `Promise<void>` and **must be awaited**. Without `await`, the process may exit before the browser opens. In non-TTY environments it writes the URL to stderr, keeping stdout clean for JSON parsing.

```ts
import { openInBrowser } from 'goke'

await openInBrowser('https://example.com/dashboard')
```

## Command Descriptions and Examples

**Use backtick formatting in descriptions** for flags and command references (e.g. `` `--status` ``, `` `mycli deploy` ``). Plain text flag names won't render as code in generated docs.

**Use `.example()` for usage examples, not the description string.** `generateDocs()` auto-wraps `.example()` strings in fenced `` ```sh `` code blocks. Examples in the description render as plain text without syntax highlighting.

```ts
cli.command(
  'query <sql>',
  dedent`
    Run a SQL query. Add \`--json\` for the raw JSON envelope.
  `,
)
  .example('mycli query "SELECT * FROM users" -p my-app')
  .example('mycli query "SELECT * FROM users FORMAT CSV" > out.csv')
```

## Command Naming Conventions

**ALWAYS read existing commands before adding a new one.** Scan the CLI for option names, verbs, and noun patterns already in use. New commands must stay consistent with what exists.

### Consistent option names

Options that do the same thing across different commands must use the same name. For example, if `list` commands already use `--limit` to cap results, never introduce `--max` or `--count` for the same purpose in a new command. Grep the codebase for similar options before picking a name.

### CRUD-style spaced commands

Prefer spaced subcommands that read like `noun verb`:

```
project list
project add
project remove
```

Pick **singular or plural** for the noun and stick with it across the entire CLI. If `project list` exists, don't add `projects add`.

**Always group namespaced commands with `.section()`.** Commands that share a parent word (`get pods`, `get services`, `auth login`) must sit under a named heading in root help. Call `cli.section('Get')` before registering that group. Use `.command(...).section('Name')` when one command belongs in a different group.

```ts
cli.section('Get')
cli.command('get pods', 'List pods')
cli.command('get services', 'List services')

cli.section('Describe')
cli.command('describe pod <name>', 'Describe a pod')
```

### Consistent verbs

Choose one verb per action and reuse it everywhere:

| Action | Pick one | Not both |
|--------|----------|----------|
| Create | `add` or `create` | not both |
| Delete | `remove` or `delete` | not both |
| Show   | `show` or `get` | not both |
| Update | `update` or `edit` | not both |

Check which verbs the CLI already uses and match them. If existing commands use `add`, every new "create something" command should also use `add`.

### Never mix different ID kinds in positionals

All positionals of a command must identify the same kind of thing. Never give the first positional a different meaning than the rest (e.g. `events <recordingId> [...eventIds]`): a call like `events 1 4 7` is unreadable because the first number silently refers to a different entity. Move the parent/scoping ID to an optional flag with a sensible default and keep positionals homogeneous:

```ts
cli
  .command('recorder events [...eventIds]', 'Print recorded events')
  .option('-r, --recording <id>', 'Recording ID (defaults to the latest recording)')
```

## Prefer Optional Flags Over Required Flags

**Never make a flag required when it can be optional with an interactive fallback.** Required flags force users to read `--help` before they can run anything. Optional flags let them run the bare command and discover options progressively through prompts.

The pattern: make the flag optional, and when the user omits it, show a `clack.select` prompt in TTY mode or exit with a clear error in non-TTY mode.

```ts
cli
  .command('deploy', 'Deploy the app')
  .option(
    '--env [env]',
    z.enum(['staging', 'production']).optional().describe('Target environment'),
  )
  .action(async (options) => {
    let env = options.env

    if (!env) {
      if (!process.stdin.isTTY) {
        console.error('Missing --env. Usage: deploy --env staging|production')
        process.exit(1)
      }

      const choice = await clack.select({
        message: 'Which environment?',
        options: [
          { value: 'staging', label: 'Staging' },
          { value: 'production', label: 'Production', hint: 'requires approval' },
        ],
      })
      if (clack.isCancel(choice)) {
        process.exit(0)
      }
      env = choice
    }

    // env is now guaranteed to be defined
  })
```

This applies to every flag that has a finite set of valid values. If you can enumerate the choices, make it a select prompt. The non-TTY error message must show the exact flag name and valid values so agents and CI scripts can self-correct.

**Bad** — forces users to know the flag upfront:

```ts
.option('--env <env>', z.enum(['staging', 'production']).describe('Target environment'))
```

**Good** — users can just run `deploy` and get prompted:

```ts
.option('--env [env]', z.enum(['staging', 'production']).optional().describe('Target environment'))
```

For flags with free-form string values (not enums), use `clack.text` instead of `clack.select`:

```ts
if (!options.name) {
  if (!process.stdin.isTTY) {
    console.error('Missing --name. Usage: create --name "my-project"')
    process.exit(1)
  }

  const name = await clack.text({ message: 'Project name' })
  if (clack.isCancel(name)) process.exit(0)
  options.name = name
}
```

## Error Messages with Recovery Hints

**Every error message should include a hint showing how to fix or rerun the command.** When a CLI exits with an error, the user (or agent) should be able to copy-paste a corrected command immediately instead of reading `--help`.

```ts
console.error('Failed to detect local port.')
console.error('If your server prints the port in an unusual format, specify it explicitly:')
console.error(`  mycli serve -p <port> -- ${shellQuote(args)}`)
process.exit(1)
```

Include the user's original arguments in the suggested command when possible, so the hint is directly copy-pasteable. For timeouts, show how long it waited so the user knows whether to wait longer or try a different approach.

## Interactive Prompts with @clack/prompts

Use `@clack/prompts` for interactive CLI prompts like `select`, `confirm`, and text input.

```bash
npm install @clack/prompts
```

```ts
import * as clack from '@clack/prompts'

const method = await clack.select({
  message: 'Choose authentication method',
  options: [
    { value: 'google', label: 'Google', hint: 'opens browser for OAuth' },
    { value: 'imap', label: 'Other', hint: 'IMAP/SMTP with password' },
  ],
})
if (clack.isCancel(method)) {
  process.exit(0)
}

const confirmed = await clack.confirm({
  message: 'Delete this item?',
  initialValue: false,
})
if (clack.isCancel(confirmed) || !confirmed) {
  process.exit(0)
}
```

Always guard clack prompts with `process.stdin.isTTY`. Agents and CI often run with non-TTY stdin, so interactive prompts must fall back to explicit CLI options instead of hanging.

### Select prompts

When a command shows a `select` prompt in TTY mode, always add a matching CLI option so agents can pass the choice directly.

```ts
cli
  .command('login', 'Authenticate')
  .option(
    '--method <method>',
    z.enum(['google', 'imap']).optional().describe('Authentication method'),
  )
  .action(async (options) => {
    let method = options.method

    if (!method) {
      if (!process.stdin.isTTY) {
        console.error('Run non-interactively with: zele login --method google|imap')
        process.exit(1)
      }

      const choice = await clack.select({
        message: 'Choose authentication method',
        options: [
          { value: 'google', label: 'Google', hint: 'opens browser for OAuth' },
          { value: 'imap', label: 'Other', hint: 'IMAP/SMTP with password' },
        ],
      })
      if (clack.isCancel(choice)) {
        process.exit(0)
      }
      method = choice
    }

    if (method === 'imap') {
      return
    }
  })
```

### Confirm prompts

For destructive confirmations, add a `--force` flag and exit with a clear error in non-TTY mode when it is missing.

```ts
cli
  .command('delete <id>', 'Delete an item')
  .option('--force', 'Skip confirmation')
  .action(async (id, options) => {
    if (!options.force) {
      if (!process.stdin.isTTY) {
        console.error('Use --force to delete non-interactively')
        process.exit(1)
      }

      const confirmed = await clack.confirm({
        message: `Delete ${id}?`,
        initialValue: false,
      })
      if (clack.isCancel(confirmed) || !confirmed) {
        return
      }
    }
  })
```

## Remote Server Auth & Config

CLIs that talk to a remote server must support multiple server URLs so users can self-host, use a preview/staging environment, or point to localhost during development. Auth tokens and other per-server state live in a JSON config file keyed by API URL.

**Never log config file paths** (like `~/.myapp/config.json` or `~/.myapp/auth.json`) in CLI output. Agents read CLI output and will try to `cat` or parse these files directly, bypassing the CLI's own commands. Log success/failure messages without revealing internal storage paths.

### Config file location

Store config at `~/.cliname/config.json`. The directory is named after the CLI binary. Use `os.homedir()` to resolve `~`.

### Config structure

The config is an object keyed by server URL. Each entry holds auth tokens and any other per-server state. The CLI reads/writes only the entry matching the current `--api-url`.

```ts
// ~/.cliname/config.json
{
  "https://api.cliname.com": {
    "accessToken": "tok_abc123",
    "refreshToken": "rt_xyz789",
    "expiresAt": "2026-08-01T00:00:00Z"
  },
  "https://staging.cliname.com": {
    "accessToken": "tok_staging_456"
  },
  "http://localhost:3000": {
    "accessToken": "tok_dev_789"
  }
}
```

### Global `--api-url` option

Register `--api-url` as a global option with a default pointing to the production hosted service. The `.use()` middleware resolves the final URL from the flag, env var, or default, then **writes it back to `process.env`**. All other code just reads `process.env.CLINAME_API_URL` instead of threading `options.apiUrl` through every function call. This avoids type-safety issues since global options aren't visible in command action types.

```ts
import { goke } from 'goke'
import { z } from 'zod'

const DEFAULT_API_URL = 'https://api.cliname.com'

const cli = goke('cliname')

cli.option(
  '--api-url [url]',
  z.string().url().optional().describe('Server URL'),
)

cli.use((options) => {
  const apiUrl = (
    options.apiUrl
    || process.env.CLINAME_API_URL
    || DEFAULT_API_URL
  ).replace(/\/+$/, '') // normalize: strip trailing slash so config keys are consistent

  process.env.CLINAME_API_URL = apiUrl
})
```

After this middleware runs, any module can call `getApiUrl()` without receiving it as a parameter:

```ts
export function getApiUrl(): string {
  return process.env.CLINAME_API_URL!
}
```

### Reading and writing config

Config helpers take the injected `fs` from the action context as an object argument. This keeps them portable across normal Node.js runs and JustBash sandboxes.

```ts
import path from 'node:path'
import os from 'node:os'
import type { GokeFs } from 'goke'

const CONFIG_DIR = path.join(os.homedir(), '.cliname')
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json')

interface ServerConfig {
  accessToken?: string
  refreshToken?: string
  expiresAt?: string
}

type Config = Record<string, ServerConfig>

async function loadConfig({ fs }: { fs: GokeFs }): Promise<Config> {
  try {
    return JSON.parse(await fs.readFile(CONFIG_PATH, 'utf-8'))
  } catch {
    return {}
  }
}

async function saveConfig({ fs, config }: { fs: GokeFs; config: Config }) {
  await fs.mkdir(CONFIG_DIR, { recursive: true })
  await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n')
}

async function getServerConfig({ fs }: { fs: GokeFs }): Promise<ServerConfig> {
  const config = await loadConfig({ fs })
  return config[getApiUrl()] ?? {}
}

async function setServerConfig({ fs, data }: { fs: GokeFs; data: ServerConfig }) {
  const apiUrl = getApiUrl()
  const config = await loadConfig({ fs })
  config[apiUrl] = { ...config[apiUrl], ...data }
  await saveConfig({ fs, config })
}
```

### Using it in commands

Commands pass the injected `{ fs }` to config helpers and read the API URL via `getApiUrl()`.

```ts
cli
  .command('login', 'Authenticate with the server')
  .action(async (_options, { fs, console }) => {
    const apiUrl = getApiUrl()
    const token = await doLogin(apiUrl)
    await setServerConfig({ fs, data: { accessToken: token } })
    console.log(`Logged in to ${apiUrl}`)
  })

cli
  .command('status', 'Show current config')
  .action(async (_options, { fs, console }) => {
    const apiUrl = getApiUrl()
    const server = await getServerConfig({ fs })
    console.log(`Server: ${apiUrl}`)
    console.log(`Authenticated: ${server.accessToken ? 'yes' : 'no'}`)
  })

cli
  .command('logout', 'Clear auth for current server')
  .action(async (_options, { fs, console }) => {
    const apiUrl = getApiUrl()
    const config = await loadConfig({ fs })
    delete config[apiUrl]
    await saveConfig({ fs, config })
    console.log(`Logged out from ${apiUrl}`)
  })
```

### Why key by URL

- Users can be logged in to production and staging simultaneously
- Self-hosters get isolated auth without conflicting with the hosted service
- Developers can point to `http://localhost:3000` during development without losing their production token
- Switching servers is just `--api-url` or setting an env var; no re-login needed if the server was used before
```

## MCP CLIs (`@goke/mcp`)

Use `addMcpCommands` from `@goke/mcp`. Do not hide `--help` behind a token. Put token steps on a `config` command. Never write `isSetupCmd` or a first-run prompt that blocks no-args.

Use `getMcpUrl` plus `getHeaders` for HTTP Bearer tokens. Use `getMcpTransport` for stdio or any custom transport.

```ts
await addMcpCommands({
  cli,
  getMcpUrl: () => MCP_URL,
  getHeaders: () => {
    const token = process.env.TOKEN || loadConfig().token
    if (!token) return
    return { Authorization: `Bearer ${token}` }
  },
  loadCache: () => loadConfig().cache,
  saveCache: (cache) => saveConfig({ cache }),
})
```

## Writing a Skill for a goke CLI

When creating a SKILL.md for a CLI built with goke, keep it **thin**. The skill is a bridge between the agent and the canonical docs, not a copy of them.

### Structure

1. **Always start with `--help`.** The CLI self-documents commands, options, and examples via goke's help generator. Make agents read it first, in full, every session. Add a "Read the help first" section with `mycli --help` and a rule to never truncate with `head`, `tail`, or `sed -n`.

2. **Point at canonical docs.** If the CLI has a docs site with `llms-full.txt` (e.g. built with Holocron), tell agents to `curl -s https://mycli.dev/llms-full.txt` as the single source of truth. For OSS repos without a docs site, curl the raw README from GitHub: `curl -s https://raw.githubusercontent.com/owner/repo/main/README.md`. Add a "never truncate" rule next to the curl command. Optionally list 3-4 individual page URLs for the most common quick lookups.

3. **Install and auth.** Just the commands, one per line, minimal prose. Start with `which mycli` so agents check before installing. Then `npm install -g mycli`, `mycli auth check`, `mycli auth login`, and `mycli auth set --key` for CI.

4. **Gotchas.** Agent-specific knowledge that is not obvious from `--help` or the docs. Things like browser flows that need user interaction, remote vs local file path restrictions, implicit defaults that cause silent failures, and subscription requirements.

5. **MCP alternative.** If the CLI has an MCP server, include the one-liner install command.

### What NOT to put in the skill

- **Command reference** — already in `--help` output
- **Workflow examples** — belong in the docs site (quickstart, guides)
- **Option descriptions** — already in Zod `.describe()` strings that goke renders in help
- **Output format details** — covered by `--help` and docs

If a section in the skill duplicates content from `--help` or the docs, delete it. The agent will fetch the canonical source.

