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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ go install github.com/jacobcxdev/cq/cmd/cq@latest

## Usage

Start with help if you are new to cq:

```bash
cq --help # Show every command family
cq check --help # Provider quota checks
cq claude --help # Claude account commands
cq codex --help # Codex account commands
cq proxy --help # Local proxy commands
cq models --help # Model registry commands
cq agent --help # Background refresh agent commands
cq refresh --help # One-shot token refresh
```

Every command path has its own help text, including leaves such as `cq proxy pin --help`, `cq models overlay add --help`, and `cq claude login --help`.

```bash
cq # Check all providers
cq check claude codex # Check specific providers
Expand All @@ -26,6 +41,19 @@ cq --version # Print version

`check` accepts `claude`, `codex`, and `gemini` provider names.

Common command families:

| Command | Purpose |
|---------|---------|
| `cq` / `cq check` | Fetch quota usage for all or selected providers. |
| `cq claude ...` | Add, list, switch, or remove Claude accounts. |
| `cq codex ...` | Add, list, switch, or remove Codex accounts. |
| `cq gemini accounts` | Show Gemini credential discovery state. |
| `cq refresh` | Refresh stored OAuth tokens before they expire. |
| `cq agent ...` | Install or uninstall background quota refresh. |
| `cq proxy ...` | Run, inspect, install, or configure the local proxy. |
| `cq models ...` | Refresh, list, or override local model registry entries. |

### JSON availability

`cq --json` includes a provider-level `availability` object for agents and other automated consumers. Use `availability.state` and `availability.guidance` to decide whether to send new work to a provider:
Expand Down
168 changes: 168 additions & 0 deletions cmd/cq/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package main

import (
"fmt"
"io"
"strings"
)

var manualHelpByPath = map[string]string{
"refresh": `Usage: cq refresh

Refresh stored OAuth tokens for Claude and Codex before they expire.

Use this before long sessions or from the background agent. Expired Claude
accounts may require interactive reauth from a terminal.
`,
"agent": `Usage: cq agent <command>

Manage the background quota refresh LaunchAgent.

Commands:
agent install Install background refresh agent
agent uninstall Uninstall background refresh agent
`,
"agent install": `Usage: cq agent install

Install the background quota refresh LaunchAgent.

The agent periodically runs token refresh work so provider checks and proxy
routing are less likely to start from expired credentials.
`,
"agent uninstall": `Usage: cq agent uninstall

Uninstall the background quota refresh LaunchAgent.
`,
"proxy": `Usage: cq proxy <command>

Run and configure the local Claude and Codex API proxy.

Commands:
proxy start Start local Claude and Codex proxy
proxy status Show proxy health
proxy install Install proxy launch agent
proxy uninstall Uninstall proxy launch agent
proxy restart Restart proxy launch agent
proxy pin Pin Claude proxy routing
`,
"proxy start": `Usage: cq proxy start [--port PORT]

Start local Claude and Codex proxy.

Options:
--port PORT Override configured listen port for this run
`,
"proxy status": `Usage: cq proxy status [--port PORT]

Show proxy health as JSON.

Options:
--port PORT Override configured health-check port
`,
"proxy install": `Usage: cq proxy install

Install the proxy launch agent for the current user.
`,
"proxy uninstall": `Usage: cq proxy uninstall

Uninstall the proxy launch agent for the current user.
`,
"proxy restart": `Usage: cq proxy restart

Restart the proxy launch agent for the current user.
`,
"proxy pin": `Usage: cq proxy pin [--clear | <email-or-account-uuid>]

Pin Claude proxy routing to a specific account, show the current pin, or clear it.

Examples:
cq proxy pin
cq proxy pin user@example.com
cq proxy pin 550e8400-e29b-41d4-a716-446655440000
cq proxy pin --clear
`,
"models": `Usage: cq models <command>

Manage the local model registry used by the proxy, Claude Code model caches,
and Codex model cache integration.

Commands:
models list List active registry models
models refresh Refresh registry data and publish caches
models overlay Manage user model overlays
`,
"models list": `Usage: cq models list [--json] [--provider PROVIDER]

List active registry models.

Options:
--json Output JSON
--provider PROVIDER Filter by provider: anthropic or codex
`,
"models refresh": `Usage: cq models refresh

Refresh registry data and publish provider-specific caches.

This updates Codex model cache, Claude Code model capabilities, and Claude Code
picker options where those files are available.
`,
"models overlay": `Usage: cq models overlay <command>

Manage user model overlays.

Overlays let cq expose model IDs before providers publish them natively.

Commands:
overlay add Add or update user model overlay
overlay remove Remove user model overlay
overlay prune Remove overlays now supplied natively
`,
"models overlay add": `Usage: cq models overlay add --provider PROVIDER --id MODEL [--clone-from MODEL]

Add or update user model overlay.

Options:
--provider PROVIDER Provider to publish under: anthropic or codex
--id MODEL Model ID to add
--clone-from MODEL Native model ID to copy metadata from
`,
"models overlay remove": `Usage: cq models overlay remove --provider PROVIDER --id MODEL

Remove user model overlay.

Options:
--provider PROVIDER Provider to remove from: anthropic or codex
--id MODEL Model ID to remove
`,
"models overlay prune": `Usage: cq models overlay prune

Remove overlays now supplied natively by refreshed provider registries.
`,
}

func manualHelp(path []string) (string, bool) {
help, ok := manualHelpByPath[strings.Join(path, " ")]
return help, ok
}

func writeManualHelp(w io.Writer, path []string) error {
help, ok := manualHelp(path)
if !ok {
return fmt.Errorf("no help for command path: %s", strings.Join(path, " "))
}
_, err := fmt.Fprint(w, help)
return err
}

func helpRequested(args []string) bool {
for _, arg := range args {
if isHelpToken(arg) {
return true
}
}
return false
}

func isHelpToken(arg string) bool {
return arg == "--help" || arg == "-h" || arg == "help"
}
130 changes: 130 additions & 0 deletions cmd/cq/help_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package main

import (
"bytes"
"io"
"strings"
"testing"

"github.com/alecthomas/kong"
)

func TestRootHelpShowsFullCLISurface(t *testing.T) {
out := &bytes.Buffer{}
var cli CLI
kctx, err := kong.New(&cli,
append(cliKongOptions(), kong.Writers(out, io.Discard), kong.Exit(func(int) {}))...,
)
if err != nil {
t.Fatalf("kong.New: %v", err)
}

_, _ = kctx.Parse([]string{"--help"})
help := out.String()
for _, want := range []string{
"check [<providers> ...]",
"claude login",
"codex login",
"gemini accounts",
"refresh",
"agent install",
"agent uninstall",
"proxy start",
"proxy install",
"proxy uninstall",
"proxy restart",
"proxy status",
"proxy pin",
"models list",
"models refresh",
"models overlay add",
"models overlay remove",
"models overlay prune",
} {
if !strings.Contains(help, want) {
t.Fatalf("root help missing %q:\n%s", want, help)
}
}
}

func TestManualHelpTextDocumentsEachCommandPath(t *testing.T) {
for _, tt := range []struct {
name string
path []string
want []string
}{
{
name: "refresh",
path: []string{"refresh"},
want: []string{"Usage: cq refresh", "Refresh stored OAuth tokens"},
},
{
name: "agent",
path: []string{"agent"},
want: []string{"Usage: cq agent <command>", "agent install", "agent uninstall"},
},
{
name: "proxy start",
path: []string{"proxy", "start"},
want: []string{"Usage: cq proxy start [--port PORT]", "Start local Claude and Codex proxy"},
},
{
name: "proxy pin",
path: []string{"proxy", "pin"},
want: []string{"Usage: cq proxy pin [--clear | <email-or-account-uuid>]", "Pin Claude proxy routing"},
},
{
name: "models list",
path: []string{"models", "list"},
want: []string{"Usage: cq models list [--json] [--provider PROVIDER]", "List active registry models"},
},
{
name: "models overlay add",
path: []string{"models", "overlay", "add"},
want: []string{"Usage: cq models overlay add --provider PROVIDER --id MODEL [--clone-from MODEL]", "Add or update user model overlay"},
},
{
name: "models overlay remove",
path: []string{"models", "overlay", "remove"},
want: []string{"Usage: cq models overlay remove --provider PROVIDER --id MODEL", "Remove user model overlay"},
},
} {
t.Run(tt.name, func(t *testing.T) {
help, ok := manualHelp(tt.path)
if !ok {
t.Fatalf("manualHelp(%v) missing entry", tt.path)
}
for _, want := range tt.want {
if !strings.Contains(help, want) {
t.Fatalf("manualHelp(%v) missing %q:\n%s", tt.path, want, help)
}
}
})
}
}

func TestRunModelsHelpDoesNotRefresh(t *testing.T) {
_, stdout, _, deps := testModelsDeps()
refreshCalls := 0
deps.Refresh = func() error {
refreshCalls++
return nil
}

for _, args := range [][]string{
{"--help"},
{"list", "--help"},
{"overlay", "add", "--help"},
} {
stdout.Reset()
if err := runModels(args, deps); err != nil {
t.Fatalf("runModels(%v): %v", args, err)
}
if refreshCalls != 0 {
t.Fatalf("runModels(%v) called Refresh %d time(s), want 0", args, refreshCalls)
}
if !strings.Contains(stdout.String(), "Usage: cq models") {
t.Fatalf("runModels(%v) did not print models help:\n%s", args, stdout.String())
}
}
}
Loading