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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ tracking, and an analytics dashboard to monitor and compare agents side-by-side.

- ⚡ **Zero config** — no setup required; `vp run <agent>` just works. Optional YAML for custom configuration
- 🐳 **Isolated agents** — each agent runs in its own Docker or Podman container
- 🔀 **Unified interface** — one CLI for Claude, Gemini, Codex, Devstral/Vibe, Copilot, Auggie, Pi, Agy, Tau & more
- 🔀 **Unified interface** — one CLI for Claude, Gemini, Codex, Devstral/Vibe, Copilot, Auggie, Pi, Agy, Tau, Jcode & more
- 🧩 **Skills** — install reusable prompt recipes per-project or per-user with `vp skills add`
- 🧱 **Project overlays** — commit a `FROM`-less Dockerfile fragment in `.vibepod/overlay/` and VibePod auto-builds a cached, content-addressed image layer on top of the agent's base image ([docs](https://vibepod.dev/docs/overlays/))
- 📊 **Local analytics dashboard** — track usage and HTTP traffic per agent, plus token metrics
Expand Down Expand Up @@ -70,6 +70,7 @@ Use `--ikwid` to append each agent's auto-approval / permission-skip flag when s
| `opencode` | Not supported |
| `auggie` | Not supported |
| `tau` | Not supported |
| `jcode` | Not supported |

![VibePod CLI preview](https://raw.githubusercontent.com/VibePod/vibepod-cli/main/docs/assets/preview.png)

Expand Down Expand Up @@ -133,6 +134,7 @@ Current defaults:
- `pi` -> `vibepod/pi:latest`
- `agy` -> `vibepod/agy:latest`
- `tau` -> `vibepod/tau:latest`
- `jcode` -> `vibepod/jcode:latest`
- `datasette` -> `vibepod/datasette:latest`
- `proxy` -> `vibepod/proxy:latest` ([repo](https://github.com/VibePod/vibepod-proxy))

Expand All @@ -152,6 +154,7 @@ VP_IMAGE_CODEX=vibepod/codex:latest vp run codex
VP_IMAGE_PI=vibepod/pi:latest vp run pi
VP_IMAGE_AGY=vibepod/agy:latest vp run agy
VP_IMAGE_TAU=vibepod/tau:latest vp run tau
VP_IMAGE_JCODE=vibepod/jcode:latest vp run jcode
VP_DATASETTE_IMAGE=vibepod/datasette:latest vp logs start
VP_SKILLS_ENGINE_IMAGE=vibepod/skills-engine:latest vp skills list
```
Expand Down
74 changes: 73 additions & 1 deletion docs/agents/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ VibePod manages each agent as a Docker or Podman container. Credentials and conf
| `pi` | Earendil | `vp pi` | `vibepod/pi:latest` |
| `agy` (Antigravity) | Google | `vp n` | `vibepod/agy:latest` |
| `tau` | Hugging Face | `vp t` | `vibepod/tau:latest` |
| `jcode` | 1jehuang | `vp j` | `vibepod/jcode:latest` |

Alias note: `vp run vibe` resolves to `vp run devstral`.

Expand Down Expand Up @@ -76,7 +77,7 @@ agents:

## Image customization workflows

VibePod has a fixed set of supported agent IDs (`claude`, `gemini`, `opencode`, `devstral`, `auggie`, `copilot`, `codex`, `pi`, `agy`, `tau`). The CLI also supports the alias `vibe`, which resolves to `devstral`. Image customization means changing the image used for one of those IDs.
VibePod has a fixed set of supported agent IDs (`claude`, `gemini`, `opencode`, `devstral`, `auggie`, `copilot`, `codex`, `pi`, `agy`, `tau`, `jcode`). The CLI also supports the alias `vibe`, which resolves to `devstral`. Image customization means changing the image used for one of those IDs.

### 1. Extend an existing image for an agent

Expand Down Expand Up @@ -203,6 +204,7 @@ Use `--ikwid` to enable each agent's built-in auto-approval / permission-skip mo
| `opencode` | Not supported |
| `auggie` | Not supported |
| `tau` | Not supported |
| `jcode` | Not supported |

Example:

Expand Down Expand Up @@ -315,6 +317,7 @@ Task mode applies a finite timeout by default: **2 hours**. Override it per task
| `codex` | `codex exec "<prompt>"` |
| `auggie` | `auggie --print "<prompt>"` |
| `tau` | `tau -p "<prompt>"` |
| `jcode` | `jcode run "<prompt>"` |

Other agents error with a clear message; support can be added by setting `headless_prefix` on their `AgentSpec`.

Expand Down Expand Up @@ -718,3 +721,72 @@ vp task create tau "Summarize the README"
are mounted at `/config/.agents/skills/<id>` and picked up automatically.

Tau has no auto-approval flag, so `--ikwid` is not supported for it.

### Jcode (1jehuang)

```bash
vp run jcode # or: vp j
```

Jcode is a resource-focused terminal coding agent written in Rust, distributed
as a single static binary. It supports Anthropic, OpenAI (including the Codex
subscription), Gemini, GitHub Copilot, OpenRouter, Azure, Ollama, LM Studio,
and custom OpenAI-compatible endpoints. Credentials and configuration are
persisted under `~/.config/vibepod/agents/jcode/`, mounted at `/config` inside
the container, where Jcode finds them as `~/.jcode/` and `~/.config/jcode/`:

| Path in container | Contents |
|---|---|
| `/config/.jcode/config.toml` | Main configuration (display, providers, timeouts) |
| `/config/.jcode/auth.json` | Claude/account credentials written by `jcode login` (OpenAI tokens go to `openai-auth.json`) |
| `/config/.jcode/mcp.json` | Global MCP server configuration |
| `/config/.jcode/sessions/` | Session history (resume with `jcode --resume <name>`) |
| `/config/.config/jcode/` | Provider env files for custom endpoints (API keys) |

**Authentication.** Run `jcode login` inside the container:

```bash
vp run jcode -- login --provider claude # Anthropic OAuth / subscription
vp run jcode -- login --provider anthropic-api # Anthropic API key
vp run jcode -- login --provider openai --no-browser # headless: prints the auth URL
```

OAuth-style providers support `--no-browser` (alias `--headless`) so the auth
URL is printed instead of a browser being launched — open the URL on the host,
then paste the callback URL (or code) back into the container prompt, since the
container's localhost callback listener is not reachable from the host browser.
Saved credentials survive container restarts because they live in the
persisted mount.

API keys can also be injected instead of `jcode login`:

```yaml
agents:
jcode:
env:
ANTHROPIC_API_KEY: sk-ant-...
OPENAI_API_KEY: sk-...
```

Or per run: `vp run jcode -e ANTHROPIC_API_KEY=sk-ant-...`.

**Proxy and TLS.** Jcode's HTTP client (reqwest with rustls) honors
`HTTP_PROXY`/`HTTPS_PROXY` and loads extra CA certificates from
`SSL_CERT_FILE`, both of which VibePod sets when the proxy is enabled, so
traffic routes through `vibepod-proxy` with the mounted mitmproxy CA.

**Non-interactive mode.** Jcode's `run` subcommand works with both `vp run`
and task mode:

```bash
vp run jcode run "explain this repo"
vp task create jcode "Summarize the README"
```

**Skills.** Jcode scans `~/.agents/skills/`, so skills installed via
`vp skills` are mounted at `/config/.agents/skills/<id>` and picked up
automatically.

VibePod sets `JCODE_NO_AUTO_UPDATE=1` so the pinned binary in the image is
never self-updated at runtime. Jcode has no auto-approval flag (its safety
system asks for permission in-session), so `--ikwid` is not supported for it.
8 changes: 8 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ agents:
volumes: []
init: []

jcode:
enabled: true
image: vibepod/jcode:latest
env: {}
volumes: []
init: []

# Connect agents to a local or remote LLM server (Ollama, vLLM, etc.)
llm:
enabled: false
Expand Down Expand Up @@ -173,6 +180,7 @@ Each agent image can be overridden individually:
| `VP_IMAGE_PI` | pi |
| `VP_IMAGE_AGY` | agy |
| `VP_IMAGE_TAU` | tau |
| `VP_IMAGE_JCODE` | jcode |
| `VP_DATASETTE_IMAGE` | datasette (logs UI) |
| `VP_PROXY_IMAGE` | proxy |
| `VP_SKILLS_ENGINE_IMAGE` | skills-engine (used by `vp skills`) |
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ VibePod (`vp`) lets you run any supported AI coding agent in an isolated Docker
| `pi` | Earendil | `vp pi` |
| `agy` (Antigravity) | Google | `vp n` |
| `tau` | Hugging Face | `vp t` |
| `jcode` | 1jehuang | `vp j` |

## Next Steps

Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Press **Ctrl+C** to stop the container when you are done.

You can start agents with the full agent command. Most agents also have a single-letter
shortcut; Pi uses `vp pi` because `vp p` is assigned to Copilot, Agy uses
`vp n`, and Tau uses `vp t`.
`vp n`, Tau uses `vp t`, and Jcode uses `vp j`.

```bash
vp claude # full name
Expand Down
3 changes: 2 additions & 1 deletion src/vibepod/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def _agent_skill_paths(agent: str) -> list[str]:
- opencode reads ~/.agents/skills/ (also ~/.claude/skills/, ~/.config/opencode/skills/)
- auggie reads ~/.agents/skills/ (also ~/.augment/skills/, ~/.claude/skills/)
- tau reads ~/.agents/skills/ (also ~/.tau/skills/)
- jcode reads ~/.agents/skills/ (also ~/.jcode/skills/)

Gemini wraps skills inside an extension manifest and would need a generated
gemini-extension.json — handled separately when we add that support.
Expand All @@ -128,7 +129,7 @@ def _agent_skill_paths(agent: str) -> list[str]:
return ["/claude/skills"]
if agent == "pi":
return ["/config/.pi/agent/skills"]
if agent in ("codex", "opencode", "auggie", "tau"):
if agent in ("codex", "opencode", "auggie", "tau", "jcode"):
return ["/config/.agents/skills"]
return []

Expand Down
7 changes: 7 additions & 0 deletions src/vibepod/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"pi",
"agy",
"tau",
"jcode",
)

AGENT_SHORTCUTS: dict[str, str] = {
Expand All @@ -41,6 +42,7 @@
"x": "codex",
"n": "agy",
"t": "tau",
"j": "jcode",
}

AGENT_ALIASES: dict[str, str] = {
Expand All @@ -59,6 +61,7 @@
"VP_IMAGE_PI",
"VP_IMAGE_AGY",
"VP_IMAGE_TAU",
"VP_IMAGE_JCODE",
"VP_DATASETTE_IMAGE",
"VP_PROXY_IMAGE",
"VP_SKILLS_ENGINE_IMAGE",
Expand Down Expand Up @@ -120,6 +123,10 @@ def get_default_images() -> dict[str, str]:
"VP_IMAGE_TAU",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/tau:latest",
),
"jcode": os.environ.get(
"VP_IMAGE_JCODE",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/jcode:latest",
),
"datasette": os.environ.get(
"VP_DATASETTE_IMAGE",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/datasette:latest",
Expand Down
13 changes: 13 additions & 0 deletions src/vibepod/core/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,19 @@ class AgentSpec:
{"HOME": "/config", "TAU_NO_UPDATE_CHECK": "1"},
headless_prefix=["-p"],
),
"jcode": AgentSpec(
"jcode",
"1jehuang",
DEFAULT_IMAGES["jcode"],
"jcode",
["jcode"],
"/config",
# jcode resolves ~/.jcode (sessions, auth, config.toml, mcp.json) via
# $HOME and its provider env files via XDG config (~/.config/jcode),
# so pointing HOME at the persisted /config mount covers both.
{"HOME": "/config", "JCODE_NO_AUTO_UPDATE": "1"},
Comment thread
nezhar marked this conversation as resolved.
headless_prefix=["run"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mount installed skills for Jcode tasks

When vp task create jcode ... is used with installed project or user skills, task.create builds extra_volumes only from agent_extra_volumes and never invokes the skills-mount logic added for Jcode in run.py. Consequently, the new task-mode path launches successfully but lacks /config/.agents/skills/<id>, so Jcode cannot discover those skills despite the documented support; task creation should add the same resolved skill mounts as vp run.

Useful? React with 👍 / 👎.

),
}

_SHORTCUT_BY_AGENT = {agent: shortcut for shortcut, agent in AGENT_SHORTCUTS.items()}
Expand Down
8 changes: 8 additions & 0 deletions src/vibepod/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ def _default_config() -> dict[str, Any]:
"volumes": [],
"init": [],
},
"jcode": {
"enabled": True,
"image": DEFAULT_IMAGES["jcode"],
"auto_pull": None,
"env": {},
"volumes": [],
"init": [],
},
},
"logging": {
"enabled": True,
Expand Down
27 changes: 25 additions & 2 deletions tests/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ def test_tau_spec_matches_container_contract() -> None:
assert spec.headless_prefix == ["-p"]


def test_jcode_spec_matches_container_contract() -> None:
spec = get_agent_spec("jcode")
assert spec.id == "jcode"
assert spec.provider == "1jehuang"
assert spec.image == DEFAULT_IMAGES["jcode"]
assert spec.config_subdir == "jcode"
assert spec.command == ["jcode"]
assert spec.config_mount_path == "/config"
assert spec.extra_env["HOME"] == "/config"
assert spec.extra_env["JCODE_NO_AUTO_UPDATE"] == "1"
assert spec.headless_prefix == ["run"]


def test_get_agent_spec_unknown() -> None:
with pytest.raises(ValueError):
get_agent_spec("unknown")
Expand Down Expand Up @@ -129,7 +142,7 @@ def test_gemini_spec_runs_via_node_wrapper() -> None:


def test_unsupported_agents_have_no_ikwid_args() -> None:
for agent in ("opencode", "auggie", "tau"):
for agent in ("opencode", "auggie", "tau", "jcode"):
spec = get_agent_spec(agent)
assert spec.ikwid_args is None, f"{agent} should not have ikwid_args"

Expand Down Expand Up @@ -169,7 +182,17 @@ def test_codex_spec_has_llm_env_map() -> None:


def test_agents_without_llm_env_map() -> None:
for agent in ("gemini", "opencode", "devstral", "auggie", "copilot", "pi", "agy", "tau"):
for agent in (
"gemini",
"opencode",
"devstral",
"auggie",
"copilot",
"pi",
"agy",
"tau",
"jcode",
):
spec = get_agent_spec(agent)
assert spec.llm_env_map is None, f"{agent} should not have llm_env_map"

Expand Down
15 changes: 15 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ def _fake_run(agent=None, **kwargs) -> None: # noqa: ANN001, ANN003, ARG001
assert called["passthrough"] == []


def test_jcode_shortcut_runs_jcode(monkeypatch) -> None:
called: dict[str, object] = {"agent": None, "passthrough": None}

def _fake_run(agent=None, **kwargs) -> None: # noqa: ANN001, ANN003, ARG001
called["agent"] = agent
called["passthrough"] = list(kwargs.get("passthrough_args") or [])

monkeypatch.setattr(run_cmd, "run", _fake_run)

result = runner.invoke(app, ["j"])
assert result.exit_code == 0
assert called["agent"] == "jcode"
assert called["passthrough"] == []


def test_copilot_shortcut_still_runs_copilot(monkeypatch) -> None:
called: dict[str, object] = {"agent": None, "passthrough": None}

Expand Down
10 changes: 10 additions & 0 deletions tests/test_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def test_default_images_match_documented_registry_defaults(monkeypatch) -> None:
"VP_IMAGE_PI",
"VP_IMAGE_AGY",
"VP_IMAGE_TAU",
"VP_IMAGE_JCODE",
"VP_DATASETTE_IMAGE",
"VP_PROXY_IMAGE",
):
Expand All @@ -35,6 +36,7 @@ def test_default_images_match_documented_registry_defaults(monkeypatch) -> None:
assert images["pi"] == "vibepod/pi:latest"
assert images["agy"] == "vibepod/agy:latest"
assert images["tau"] == "vibepod/tau:latest"
assert images["jcode"] == "vibepod/jcode:latest"
assert images["datasette"] == "vibepod/datasette:latest"
assert images["proxy"] == "vibepod/proxy:latest"

Expand All @@ -61,3 +63,11 @@ def test_tau_image_override(monkeypatch) -> None:
images = get_default_images()

assert images["tau"] == "example/tau:dev"


def test_jcode_image_override(monkeypatch) -> None:
monkeypatch.setenv("VP_IMAGE_JCODE", "example/jcode:dev")

images = get_default_images()

assert images["jcode"] == "example/jcode:dev"
Loading
Loading