Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ curl -fsSL https://berthd.app/install | sh
It downloads `berthd` from the [latest release](https://github.com/cosscom/shipyard/releases/latest),
checks it against the release's checksums, installs it for that user (no
root), starts it as a systemd user service (launchd on macOS), installs the
hooks that let Claude Code, Codex and Cursor report their state
hooks that let Claude Code, Codex, Cursor, Grok and others report their state
(`--no-integrations` skips them), and prints a pairing link. The link works
once, for ten minutes. Run it again to upgrade in place; `sh -s -- --help`
lists the options.
Expand Down
4 changes: 3 additions & 1 deletion app/src/components/agent-glyph.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AsteriskIcon, CheckIcon, HexagonIcon, SparkleIcon, SquareTerminalIcon, TerminalIcon } from "lucide-react";
import { AsteriskIcon, CheckIcon, HexagonIcon, SparkleIcon, SquareTerminalIcon, TerminalIcon, ZapIcon } from "lucide-react";

import { Tip } from "@/components/tip";
import type { SessionState } from "@/lib/derive";
Expand Down Expand Up @@ -26,6 +26,8 @@ export function AgentIcon({ agent, className, muted }: { agent?: string; classNa
return <span className={cn("shrink-0 font-semibold text-xs leading-none", tone("text-[#f472b6]"), className)}>π</span>;
case "opencode":
return <SquareTerminalIcon className={cn(cls, tone("text-foreground/80"))} />;
case "grok":
return <ZapIcon className={cn(cls, tone("text-foreground/80"))} strokeWidth={2.25} />;
default:
return <TerminalIcon className={cn(cls, tone("text-muted-foreground"))} />;
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/agent-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface IntegrationsReport {
}

// The commands Shipyard has hooks for, by the tool they install for.
const TOOLS: Record<string, string> = { claude: "claude", codex: "codex", "cursor-agent": "cursor", cursor: "cursor" };
const TOOLS: Record<string, string> = { claude: "claude", codex: "codex", "cursor-agent": "cursor", cursor: "cursor", grok: "grok" };

// hookToolFor is the tool an agent preset ID or command line needs hooks for.
export function hookToolFor(agentOrCommand: string): string | undefined {
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/derive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test("a preset session's first prompt is read back from its command", () => {
assert.equal(firstPrompt({ preset: "claude", command: `claude 'it'\\''s "done"
next line'` }), `it's "done"\nnext line`);
assert.equal(firstPrompt({ preset: "opencode", command: "opencode --prompt 'add a health check'" }), "add a health check");
assert.equal(firstPrompt({ preset: "grok", command: "grok --model grok-build 'fix the bug'" }), "fix the bug");
assert.equal(firstPrompt({ preset: "claude", command: "claude --resume 0d1e 'go on'" }), "go on");
// No prompt, or a command berth didn't write.
assert.equal(firstPrompt({ preset: "claude", command: "claude --model haiku" }), undefined);
Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Location, Session, Stats, Worktree } from "@/lib/api";
// read as parked until a reply or a later time as ready (Idle).
export type SessionState = "ready" | "running" | "waiting" | "finished" | "exited" | "idle";

export const AGENTS = ["claude", "codex", "opencode", "gemini", "pi", "omp", "cursor-agent"];
export const AGENTS = ["claude", "codex", "opencode", "gemini", "pi", "omp", "cursor-agent", "grok"];

export function agentOf(s: Session): string | undefined {
// A service's terminal runs no agent, whatever its command.
Expand Down Expand Up @@ -161,7 +161,7 @@ export function sortedWorktrees(loc: Location): Worktree[] {
return [...(loc.worktrees ?? [])].sort((a, b) => Number(!!b.main) - Number(!!a.main) || a.name.localeCompare(b.name));
}

const labels: Record<string, string> = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", pi: "Pi", omp: "oh-my-pi", "cursor-agent": "Cursor Agent", cursor: "Cursor Agent" };
const labels: Record<string, string> = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", pi: "Pi", omp: "oh-my-pi", "cursor-agent": "Cursor Agent", cursor: "Cursor Agent", grok: "Grok CLI" };

// agentLabel is an agent's product name: "claude" → "Claude Code".
export const agentLabel = (a: string) => labels[a] ?? a.charAt(0).toUpperCase() + a.slice(1);
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/mock-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const MOCK_AGENTS: AgentChoice[] = [
{ id: "codex", name: "Codex", command: "codex", offered: true, install: "codex 0.160.1 from github.com/openai/codex/releases → ~/.local/bin/codex", verified: "pinned to 0.160.1 and checked against its sha256" },
{ id: "cursor", name: "Cursor Agent", command: "cursor-agent", offered: true, install: "curl -fsS https://cursor.com/install | bash", verified: "Cursor's installer, over HTTPS; Cursor publishes no checksums for it" },
{ id: "opencode", name: "OpenCode", command: "opencode", offered: true, install: "curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path", verified: "OpenCode's installer, over HTTPS; it publishes no checksums for it" },
{ id: "grok", name: "Grok CLI", command: "grok", offered: true, install: "curl -fsSL https://x.ai/cli/install.sh | bash", verified: "xAI's installer, over HTTPS; xAI publishes no checksums for it" },
{
id: "gemini",
name: "Gemini CLI",
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/mock-team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ const GITHUB_STEP: PlanStep = {
commands: ["gh auth status || gh auth login --hostname github.com --git-protocol https --web", "gh auth setup-git", "# a device code: you enter it at github.com/login/device; this computer's sign-in is never copied"],
};

const AGENT_NAMES: Record<string, string> = { claude: "Claude Code", codex: "Codex", cursor: "Cursor Agent", opencode: "OpenCode" };
const AGENT_NAMES: Record<string, string> = { claude: "Claude Code", codex: "Codex", cursor: "Cursor Agent", opencode: "OpenCode", grok: "Grok CLI" };
const agentsStep = (ids: string[]): PlanStep => {
const names = ids.map((id) => AGENT_NAMES[id] ?? id);
return {
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/session-open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { activateTab, findSession, focusPane, openTab, refOf, removePane, select
// that worktree, and never takes the keyboard from them; otherwise a
// notification offers to open it.

const agentNames: Record<string, string> = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", omp: "oh-my-pi", cursor: "Cursor" };
const agentNames: Record<string, string> = { claude: "Claude Code", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", omp: "oh-my-pi", cursor: "Cursor", grok: "Grok CLI" };

function agentLabel(box: string, agent?: string): string {
if (!agent) return "A terminal";
Expand Down
2 changes: 1 addition & 1 deletion app/src/views/automations/flows/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const TRIGGER_PHRASE: Record<string, string> = {
"box.upgraded": "the box is upgraded",
};

const AGENT_NAME: Record<string, string> = { claude: "Claude", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", omp: "oh-my-pi", cursor: "Cursor" };
const AGENT_NAME: Record<string, string> = { claude: "Claude", codex: "Codex", opencode: "OpenCode", gemini: "Gemini", omp: "oh-my-pi", cursor: "Cursor", grok: "Grok" };
export const agentName = (id?: string) => (id ? (AGENT_NAME[id] ?? id) : "an agent");

export const GITHUB_ONS: { on: GitHubOn; label: string; phrase: string; fields: string[] }[] = [
Expand Down
4 changes: 2 additions & 2 deletions app/src/views/onboarding/guided-install.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ function AgentPicker({ choices, value, onChange, installed = [] }: { choices: Ag
);
}

const ORDER = ["claude", "codex", "cursor", "opencode", "gemini"];
const ORDER = ["claude", "codex", "cursor", "opencode", "grok", "gemini"];
const order = (id: string) => (ORDER.indexOf(id) + 1 || 99) as number;

function PlanRow({ step, n, bundledTmux }: { step: InstallPlanStep; n: number; bundledTmux: boolean }) {
Expand Down Expand Up @@ -1031,7 +1031,7 @@ function Banner({ run, ready, readyLabel, onReady, onBack, agents }: { run: Inst
}

export function agentNames(ids: string[]) {
const n = ids.map((id) => ({ claude: "Claude Code", codex: "Codex", cursor: "Cursor Agent", opencode: "OpenCode" })[id] ?? id);
const n = ids.map((id) => ({ claude: "Claude Code", codex: "Codex", cursor: "Cursor Agent", opencode: "OpenCode", grok: "Grok CLI" })[id] ?? id);
return n.length <= 1 ? (n[0] ?? "") : `${n.slice(0, -1).join(", ")} and ${n[n.length - 1]}`;
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/views/onboarding/quick-install.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export function InlineAgents({ value, onChange, disabled }: { value: string[]; o
);
}

const ORDER = ["claude", "codex", "cursor", "opencode"];
const ORDER = ["claude", "codex", "cursor", "opencode", "grok", "gemini"];
const order = (id: string) => (ORDER.indexOf(id) + 1 || 99) as number;
const busyHint = (v: string[]) => (v.length === 0 ? <span className="text-muted-foreground">none: add them later in Settings</span> : null);

Expand Down
2 changes: 1 addition & 1 deletion app/src/views/project/agents-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function AgentsSection({ repo, draft, setDraft, box }: { repo: RepoConfig
<Section
id="agents"
title="Agents"
description="How agents start in this repo. A preset with a built-in's id (claude, codex, opencode, gemini, cursor) replaces it here."
description="How agents start in this repo. A preset with a built-in's id (claude, codex, opencode, gemini, cursor, grok) replaces it here."
actions={
<Button size="xs" variant="ghost" onClick={() => setOwn([...own, { id: ids.includes("claude") ? `agent-${own.length + 1}` : "claude", name: "", command: "" }])}>
<PlusIcon />
Expand Down
2 changes: 2 additions & 0 deletions app/src/views/settings/agents-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const INSTALL: Record<string, string> = {
gemini: "npm install -g @google/gemini-cli",
cursor: "curl https://cursor.com/install -fsS | bash",
omp: "bun install -g @oh-my-pi/pi-coding-agent",
grok: "curl -fsSL https://x.ai/cli/install.sh | bash",
};

const BUILTIN: Pick<AgentPreset, "id" | "name">[] = [
Expand All @@ -42,6 +43,7 @@ const BUILTIN: Pick<AgentPreset, "id" | "name">[] = [
{ id: "gemini", name: "Gemini CLI" },
{ id: "cursor", name: "Cursor Agent" },
{ id: "omp", name: "oh-my-pi" },
{ id: "grok", name: "Grok CLI" },
];

export function AgentsSection() {
Expand Down
2 changes: 1 addition & 1 deletion cmd/berth/addssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func addSSHSteps(l laptop, args []string) error {
identity := fs.String("identity", "", "an SSH private key file to log in with (as ssh -i)")
trustKey := fs.String("trust-host-key", "", "trust the box's host key if its fingerprint is this SHA256:… (a new box only; a changed key is never trusted)")
noIntegrations := fs.Bool("no-integrations", false, "don't install hooks and skills for the agent CLIs on the box")
agentList := fs.String("agents", strings.Join(agentcli.Defaults(), ","), "agent CLIs to install on the box: claude, codex, cursor, opencode, or none")
agentList := fs.String("agents", strings.Join(agentcli.Defaults(), ","), "agent CLIs to install on the box: claude, codex, cursor, opencode, grok, or none")
yes := fs.Bool("yes", false, "ask nothing: don't wait for Enter, and stop with the command to run where sudo would ask for a password")
guidedFlag := fs.Bool("guided", false, "show the whole plan first, wait for Enter, and run every step in one terminal; without it, steps run on their own and only one that needs sudo's password asks")
from := fs.String("from", "", "start from this step (connect, berthd, linger, tools, agents, integrations, pair); the ones before it are kept")
Expand Down
2 changes: 1 addition & 1 deletion cmd/berthd/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/cosscom/shipyard/internal/service"
)

const agentsUsage = "usage: berthd agents install [--integrations] [--markers] claude|codex|cursor|opencode ... | berthd agents list [--json]"
const agentsUsage = "usage: berthd agents install [--integrations] [--markers] claude|codex|cursor|opencode|grok ... | berthd agents list [--json]"

// agents installs agent CLIs here, as this user, into ~/.local/bin, with no
// sudo: `berthd agents install claude codex`. The guided install runs it on
Expand Down
4 changes: 2 additions & 2 deletions cmd/berthd/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func TestLogTail(t *testing.T) {
// finds, and the advice for the others, are integrations' tests.)
func TestInstallSetsUpIntegrationsForAgentsOnThisBox(t *testing.T) {
home, path := t.TempDir(), t.TempDir()
for _, tool := range []string{"claude", "codex", "cursor-agent"} {
for _, tool := range []string{"claude", "codex", "cursor-agent", "grok"} {
os.WriteFile(filepath.Join(path, tool), []byte("#!/bin/sh\n"), 0o755)
}
t.Setenv("HOME", home)
Expand All @@ -137,7 +137,7 @@ func TestInstallSetsUpIntegrationsForAgentsOnThisBox(t *testing.T) {
if err != nil || !strings.Contains(string(settings), "/home/alex/.local/bin/berthd hook claude Stop") {
t.Fatalf("settings.json = %s, %v", settings, err)
}
for _, want := range []string{"Claude Code: hooks and 7 skills in ~/.claude", "hooks added in", "Codex: hooks in ~/.codex; 7 skills in", "notify added in", "Cursor: hooks added"} {
for _, want := range []string{"Claude Code: hooks and 7 skills in ~/.claude", "hooks added in", "Codex: hooks in ~/.codex; 7 skills in", "notify added in", "Cursor: hooks added", "Grok CLI: hooks added"} {
if !strings.Contains(out.String(), want) {
t.Errorf("output missing %q:\n%s", want, out.String())
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/berthd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const usage = `berthd — the berth daemon for a development box
(--check: only say whether it can read now)
berthd mcp A stdio MCP server of berth's tools for agents on this box
(integrations install adds it to Claude, Codex and Gemini)
berthd agents install [--integrations] [--markers] claude|codex|cursor|opencode ...
berthd agents install [--integrations] [--markers] claude|codex|cursor|opencode|grok ...
Install agent CLIs into ~/.local/bin, without sudo (each is
skipped when it is already here)
berthd agents list [--json] Which agent CLIs are here, and how to add the others
Expand Down
10 changes: 10 additions & 0 deletions docs/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ again.

## Unreleased

- **Grok CLI.** `grok` (xAI Grok Build) is a built-in agent preset: first
prompt as the last argument, `--model` and `--effort` as on the CLI.
`berthd integrations install grok` writes session-state hooks to
`~/.grok/hooks/berth.json`. Skills and MCP are not installed for Grok
yet. The guided install and `berthd agents install grok` run xAI's own
installer (`curl -fsSL https://x.ai/cli/install.sh | bash`) and link
`~/.grok/bin/grok` into `~/.local/bin`. See
[First agent](/getting-started/first-agent) and
[agent integrations](/guides/agent-integrations).

- **A box with a full hostname is named by its first label.** A cloud VM
whose hostname is `devbox.europe-north1-a.c.example.internal` called itself
by all of it: the phone and notifications showed the whole name, and it
Expand Down
5 changes: 3 additions & 2 deletions docs/concepts/sessions-and-agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ announces `task.created`.

An agent is a preset: an id, a name and a command. The box offers each
built-in whose command it finds on its `PATH` (ids `claude`, `codex`,
`opencode`, `gemini`, `omp` (oh-my-pi), and `cursor`, which runs
`opencode`, `gemini`, `omp` (oh-my-pi), `grok`, and `cursor`, which runs
`cursor-agent`), plus any a
repository's config adds or replaces. `berth agents BOX` lists the
built-ins the box has; a repository's own presets come with its location
Expand All @@ -122,7 +122,7 @@ A first prompt is passed as the command's last argument, or after its
Shipyard knows what an agent is doing from the agent's own hooks, which
`berthd install` sets up for the agent CLIs it finds on the box (later,
`berthd integrations install claude`, or `codex`, `cursor`, `gemini`,
`opencode`, `omp`, `all`; an upgrade brings hooks Shipyard installed before up to
`opencode`, `omp`, `grok`, `all`; an upgrade brings hooks Shipyard installed before up to
date):

| State | In the app | Meaning | From |
Expand Down Expand Up @@ -244,6 +244,7 @@ screen shows.
| Gemini CLI | ✓ | ✓ | ✓ | ✓ | hooks in `~/.gemini/settings.json` |
| OpenCode | | ✓ | ✓ | ✓ | a plugin in `~/.config/opencode/plugin/berth.js` |
| oh-my-pi | ✓ | ✓ | ✓ | ✓ | an extension in `~/.omp/agent/extensions/berth.ts` |
| Grok CLI | ✓ | ✓ | ✓ | ✓ | hooks in `~/.grok/hooks/berth.json` |
| anything else | screen | screen | screen | screen | the terminal |

The box lists these as `adapters` in `GET /v1/info`, with `capabilities`
Expand Down
13 changes: 8 additions & 5 deletions docs/getting-started/add-a-box.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,9 @@ that truly needs `sudo`'s password on the box, and only for that step.
### In the app

**Add a box › Or let Shipyard set it up over SSH** takes `user@host`, with
**Agents** right below it: Claude Code is ticked; Codex, Cursor Agent and
OpenCode are there to add, and your choice is remembered for the next box.
**Agents** right below it: Claude Code is ticked; Codex, Cursor Agent,
OpenCode and Grok CLI are there to add, and your choice is remembered for
the next box.
**Install and pair** on a machine [on your tailnet](#on-your-tailnet) has the
same choice. **Set up** opens a compact dialog with a short checklist:
Connect, Install berthd, tmux and git, the agents, Agent integrations, Pair.
Expand Down Expand Up @@ -351,8 +352,10 @@ What each step does:
- Codex, its release binary from
[github.com/openai/codex](https://github.com/openai/codex/releases),
pinned to a version and checked against its sha256;
- Cursor Agent and OpenCode with their own installers (over HTTPS; neither
publishes checksums for them).
- Cursor Agent, OpenCode and Grok CLI with their own installers (over
HTTPS; none of them publish checksums). Grok's is xAI's
(`curl -fsSL https://x.ai/cli/install.sh | bash`), which puts the
binary in `~/.grok/bin`; Shipyard links it into `~/.local/bin`.

Gemini CLI isn't offered: it installs with npm and needs Node.js 20 or
newer, which Shipyard doesn't install. Install Node, then
Expand Down Expand Up @@ -399,7 +402,7 @@ Options:
| Flag | What it does |
| --- | --- |
| `--name N` | What to call the box in Shipyard (default: its hostname). It becomes part of URLs like `3000.N.localhost`. |
| `--agents LIST` | The agent CLIs to install: `claude`, `codex`, `cursor`, `opencode`, comma-separated, or `none` (default `claude`). |
| `--agents LIST` | The agent CLIs to install: `claude`, `codex`, `cursor`, `opencode`, `grok`, comma-separated, or `none` (default `claude`). |
| `--guided` | Show the whole plan first, wait for Enter, and run every step in one terminal (what Team setup does). Without it, steps run on their own and only one that needs `sudo`'s password asks. |
| `--yes` | Ask nothing, and stop with the command to run where `sudo` would ask for a password. |
| `--from STEP` | Start from a step (`berthd`, `linger`, `tools`, `agents`, `integrations`, `pair`), keeping the ones before it. It still connects first. |
Expand Down
3 changes: 2 additions & 1 deletion docs/getting-started/first-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ my-box**). Or run, from the laptop:
ssh me@my-box '~/.local/bin/berthd integrations install claude'
```

Use `codex`, `cursor` or `all` for other agents. For Codex this sets
Use `codex`, `cursor`, `grok` or `all` for other agents. For Codex this sets
`notify` in `~/.codex/config.toml`, unless you already use it for something
else; then it prints the line to change yourself. The full path matters: a
command over SSH may not have `~/.local/bin`, where berthd lives, on its
Expand Down Expand Up @@ -63,6 +63,7 @@ The built-in agents, offered when their command is installed on the box:
| `opencode` | OpenCode | `opencode --prompt …` |
| `gemini` | Gemini CLI | `gemini -i …` |
| `cursor` | Cursor Agent | `cursor-agent` |
| `grok` | Grok CLI | `grok` |

A repository can add its own or replace these in
[`.berth/config.json`](/reference/config#agents).
Expand Down
Loading