diff --git a/README.md b/README.md index 95ce95a..8376c55 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ tracking, and an analytics dashboard to monitor and compare agents side-by-side. - ⚡ **Zero config** — no setup required; `vp run ` 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 @@ -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) @@ -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)) @@ -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 ``` diff --git a/docs/agents/index.md b/docs/agents/index.md index 49f99e0..4fd4b9b 100644 --- a/docs/agents/index.md +++ b/docs/agents/index.md @@ -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`. @@ -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 @@ -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: @@ -315,6 +317,7 @@ Task mode applies a finite timeout by default: **2 hours**. Override it per task | `codex` | `codex exec ""` | | `auggie` | `auggie --print ""` | | `tau` | `tau -p ""` | +| `jcode` | `jcode run ""` | Other agents error with a clear message; support can be added by setting `headless_prefix` on their `AgentSpec`. @@ -718,3 +721,72 @@ vp task create tau "Summarize the README" are mounted at `/config/.agents/skills/` 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 `) | +| `/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/` 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. diff --git a/docs/configuration.md b/docs/configuration.md index 1361994..2394f93 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 @@ -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`) | diff --git a/docs/index.md b/docs/index.md index 9b9e5e3..d314605 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/docs/quickstart.md b/docs/quickstart.md index 89ce791..1d267ed 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -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 diff --git a/src/vibepod/commands/run.py b/src/vibepod/commands/run.py index 9bae2fb..87efcf1 100644 --- a/src/vibepod/commands/run.py +++ b/src/vibepod/commands/run.py @@ -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. @@ -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 [] diff --git a/src/vibepod/constants.py b/src/vibepod/constants.py index e988e88..7e9a25a 100644 --- a/src/vibepod/constants.py +++ b/src/vibepod/constants.py @@ -29,6 +29,7 @@ "pi", "agy", "tau", + "jcode", ) AGENT_SHORTCUTS: dict[str, str] = { @@ -41,6 +42,7 @@ "x": "codex", "n": "agy", "t": "tau", + "j": "jcode", } AGENT_ALIASES: dict[str, str] = { @@ -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", @@ -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", diff --git a/src/vibepod/core/agents.py b/src/vibepod/core/agents.py index 3e05dbc..c244d54 100644 --- a/src/vibepod/core/agents.py +++ b/src/vibepod/core/agents.py @@ -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"}, + headless_prefix=["run"], + ), } _SHORTCUT_BY_AGENT = {agent: shortcut for shortcut, agent in AGENT_SHORTCUTS.items()} diff --git a/src/vibepod/core/config.py b/src/vibepod/core/config.py index cc28c53..3ec14b8 100644 --- a/src/vibepod/core/config.py +++ b/src/vibepod/core/config.py @@ -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, diff --git a/tests/test_agents.py b/tests/test_agents.py index 14b3e1a..cb45b9b 100644 --- a/tests/test_agents.py +++ b/tests/test_agents.py @@ -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") @@ -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" @@ -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" diff --git a/tests/test_cli.py b/tests/test_cli.py index c274267..31738e3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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} diff --git a/tests/test_constants.py b/tests/test_constants.py index 7da84ce..3f3e2cd 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -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", ): @@ -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" @@ -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" diff --git a/tests/test_run.py b/tests/test_run.py index 927d854..30ed14a 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -87,7 +87,7 @@ def test_agent_extra_volumes_for_auggie(tmp_path: Path) -> None: def test_agent_extra_volumes_for_other_agents(tmp_path: Path) -> None: config_dir = tmp_path / "agents" / "claude" # Agents without explicit volume mappings return empty - for agent in ("claude", "gemini", "codex", "devstral", "pi", "agy", "tau"): + for agent in ("claude", "gemini", "codex", "devstral", "pi", "agy", "tau", "jcode"): assert run_cmd._agent_extra_volumes(agent, config_dir) == [] @@ -220,6 +220,28 @@ def test_skills_mounts_for_tau_use_agents_skills_path( ] +def test_skills_mounts_for_jcode_use_agents_skills_path( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + local_root = tmp_path / "local-skills" + user_root = tmp_path / "user-skills" + skill_dir = local_root / "installed" / "example" + skill_dir.mkdir(parents=True) + user_root.mkdir() + (local_root / "skills-lock.json").write_text( + json.dumps({"skills": {"example": {"path": "installed/example"}}}), + encoding="utf-8", + ) + (user_root / "skills-lock.json").write_text(json.dumps({"skills": {}}), encoding="utf-8") + + monkeypatch.setattr(skills_engine, "local_skills_dir", lambda workspace: local_root) + monkeypatch.setattr(skills_engine, "user_skills_dir", lambda: user_root) + + assert run_cmd._skills_mounts_for_agent("jcode", tmp_path) == [ + (str(skill_dir.resolve()), "/config/.agents/skills/example", "ro") + ] + + def test_run_agent_supports_duplicate_host_mounts(tmp_path: Path) -> None: class _FakeContainers: def __init__(self) -> None: diff --git a/tests/test_task_cmd.py b/tests/test_task_cmd.py index 64a25a0..2b67baa 100644 --- a/tests/test_task_cmd.py +++ b/tests/test_task_cmd.py @@ -109,6 +109,7 @@ def test_headless_prefix_set_for_supported_agents() -> None: assert AGENT_SPECS["codex"].headless_prefix == ["exec"] assert AGENT_SPECS["auggie"].headless_prefix == ["--print"] assert AGENT_SPECS["tau"].headless_prefix == ["-p"] + assert AGENT_SPECS["jcode"].headless_prefix == ["run"] def test_headless_prefix_none_for_unsupported_agents() -> None: