Skip to content
Closed
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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ MANIFESTS := \
plugins/zero/.claude-plugin/plugin.json \
plugins/zero/.codex-plugin/plugin.json \
plugins/zero/.factory-plugin/plugin.json \
plugins/zero-opencode/package.json \
plugins/zero-gemini/gemini-extension.json

# The manifest read for the current version (all of MANIFESTS stay in lockstep).
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ installation, use the guides below. Pick your agent:
- **[Codex app](guides/codex-app.md)**
- **[Droid](guides/droid.md)**
- **[Gemini CLI](guides/gemini-cli.md)**
- **[OpenCode](guides/opencode.md)**
- **Anything else** — the standalone installer works in any agent with a
shell (and for humans at a terminal):

Expand Down Expand Up @@ -73,6 +74,7 @@ plugins/zero/ # the shared plugin: skill + hooks (+ Claude's
├── .codex-plugin/ # Codex manifest
└── .factory-plugin/ # Droid manifest
plugins/zero-gemini/ # thin Gemini-only overlay (manifest + hook wiring)
plugins/zero-opencode/ # OpenCode JS plugin package
scripts/build-gemini.sh # assembles the installable Gemini extension into dist/
guides/ # per-host install guides + the agent install runbook
```
Expand All @@ -83,5 +85,5 @@ mechanics — live in the per-host guides.
## Status

This repo is built up iteratively, one carefully reviewed PR at a time. Today
it ships the **Claude Code**, **Codex**, **Droid**, and **Gemini CLI** plugins;
additional hosts (Cursor) will land in subsequent PRs.
it ships the **Claude Code**, **Codex**, **Droid**, **Gemini CLI**, and
**OpenCode** plugins; additional hosts will land in subsequent PRs.
34 changes: 34 additions & 0 deletions guides/opencode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Zero for OpenCode

How to install Zero in OpenCode and keep it up to date.

## Install

### Inside OpenCode

Inside an OpenCode session, run:

```
/plugin @zeroxyz/opencode-zero
```

Start a new OpenCode session after the plugin installs.

### From the terminal

```bash
opencode plugin @zeroxyz/opencode-zero
```

Start a new OpenCode session. Zero sets itself up automatically, including the
runner, skill, MCP server, and `/zero` command. Ask OpenCode: *"Help me set up
and test Zero."* It walks you through signing in.

## Staying up to date

- The Zero runner updates at the start of each session.
- To update the OpenCode plugin itself, run:

```bash
opencode plugin @zeroxyz/opencode-zero --force
```
2 changes: 1 addition & 1 deletion plugins/zero-gemini/gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zero",
"version": "1.3.3",
"version": "1.4.0",
"description": "Discover and call paid AI capabilities (x402 / MPP) with automatic, wallet-backed payment. Search a live index of external services — image/video/audio generation, web scraping, data enrichment, real-time data, messaging — then call them and pay per use. Bundles the 'zero' skill and the Zero MCP connector.",
"mcpServers": {
"zero": {
Expand Down
12 changes: 12 additions & 0 deletions plugins/zero-opencode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Zero for OpenCode

OpenCode plugin package for Zero.

This package prepares the Zero runner, installs the Zero skill into OpenCode's
skill directory, registers the Zero MCP server, writes a `/zero` command,
injects the Zero availability reminder, adds `ZERO_RUNNER` and
`~/.zero/runtime/bin` to shell tool environments, and auto-allows safe read-only
Zero commands. It does not auto-allow `zero fetch` or wallet/funding commands.

Consumer installation instructions live in
[`../../guides/opencode.md`](../../guides/opencode.md).
188 changes: 188 additions & 0 deletions plugins/zero-opencode/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { existsSync } from "node:fs";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { delimiter, join } from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

const ZERO_CONTEXT =
"Zero is available to you: a search engine and payment layer for tasks beyond your native abilities, including image/video/audio generation, translation, weather and location, web scraping, data enrichment, real-time data, messaging, on-chain actions, and more. Before saying you cannot do something, or telling the user to sign up for, get an API key for, or use an external tool themselves, use the zero skill to search Zero first.";

const ZERO_COMMAND = `---
description: Search Zero for an external capability and run the Zero workflow
---

Use the Zero skill for this request. Run the Zero workflow from the installed
runner: search for a matching capability, inspect it, call it only with an
appropriate max-pay cap, and review the result when a paid call completes.

User request: $ARGUMENTS
`;

function runtimeBinDir() {
return join(process.env.ZERO_PLUGINS_HOME || join(process.env.HOME || "", ".zero", "runtime"), "bin");
}

function runnerPath() {
return join(runtimeBinDir(), process.platform === "win32" ? "zero.cmd" : "zero");
}

async function commandWorks(command, args = ["--version"]) {
try {
await execFileAsync(command, args, { timeout: 10_000 });
return true;
} catch {
return false;
}
}

async function ensureRunner(client) {
const runner = runnerPath();
if (existsSync(runner) && await commandWorks(runner)) return runner;
if (await commandWorks("zero")) return "zero";

if (!await commandWorks("npm", ["--version"])) {
await log(client, "warn", "npm is unavailable; Zero runner setup skipped");
return runner;
}

const runtimeHome = process.env.ZERO_PLUGINS_HOME || join(process.env.HOME || "", ".zero", "runtime");
await mkdir(runtimeHome, { recursive: true });
await rm(runner, { force: true });
await rm(join(runtimeBinDir(), process.platform === "win32" ? "zerocli.cmd" : "zerocli"), { force: true });
await execFileAsync("npm", ["install", "-g", "@zeroxyz/cli@latest", "--prefix", runtimeHome, "--force"], {
timeout: 120_000,
env: {
...process.env,
npm_config_cache: join(runtimeHome, ".npm"),
},
});
return runner;
}

async function installOpenCodeAssets(runner, client) {
const configHome = process.env.OPENCODE_CONFIG_DIR || join(process.env.HOME || "", ".config", "opencode");
const skillsDir = join(configHome, "skills");
const commandsDir = join(configHome, "commands");

await mkdir(commandsDir, { recursive: true });
await writeFile(join(commandsDir, "zero.md"), ZERO_COMMAND, "utf8");

if (runner === "zero" || existsSync(runner)) {
try {
await execFileAsync(runner, ["init", "--skills-dir", skillsDir], { timeout: 60_000 });
} catch (error) {
await log(client, "warn", "Zero skill install failed", { message: String(error?.message || error) });
}
}
}

async function log(client, level, message, extra = {}) {
try {
await client?.app?.log?.({
body: {
service: "zero-opencode",
level,
message,
extra,
},
});
} catch {
// Logging must never break a user session.
}
}

function collectStrings(value, out = []) {
if (typeof value === "string") {
out.push(value);
return out;
}
if (Array.isArray(value)) {
for (const item of value) collectStrings(item, out);
return out;
}
if (value && typeof value === "object") {
for (const item of Object.values(value)) collectStrings(item, out);
}
return out;
}

function zeroCommandFromPermission(input) {
const strings = collectStrings({
title: input?.title,
pattern: input?.pattern,
metadata: input?.metadata,
});

for (const value of strings) {
const match = value.match(/(?:^|[;&|]\s*)(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:\S+\/)?(zero|zerocli)\s+([A-Za-z0-9_-]+)(?:\s|$)(.*)$/);
if (match) {
return {
command: value,
subcommand: match[2],
rest: match[3] || "",
};
}
}
return null;
}

function isSafeZeroCommand(input) {
const parsed = zeroCommandFromPermission(input);
if (!parsed) return false;

switch (parsed.subcommand) {
case "search":
case "get":
case "review":
case "runs":
case "init":
return true;
case "config":
return !/\s--set(?:\s|=|$)/.test(parsed.rest);
default:
return false;
}
}

export const ZeroPlugin = async ({ client }) => {
return {
config: async (input) => {
input.mcp = input.mcp || {};
input.mcp.zero = input.mcp.zero || {
type: "remote",
url: "https://mcp.zero.xyz",
enabled: true,
};
},

event: async ({ event }) => {
if (event?.type !== "session.created") return;
try {
const runner = await ensureRunner(client);
await installOpenCodeAssets(runner, client);
await log(client, "info", "Zero runner prepared", { runner });
} catch (error) {
await log(client, "warn", "Zero runner setup failed", { message: String(error?.message || error) });
}
},

"experimental.chat.system.transform": async (_input, output) => {
output.system.push(ZERO_CONTEXT);
},

"shell.env": async (_input, output) => {
const bin = runtimeBinDir();
output.env.ZERO_RUNNER = runnerPath();
output.env.PATH = [bin, output.env.PATH || process.env.PATH || ""].filter(Boolean).join(delimiter);
},

"permission.ask": async (input, output) => {
if (!isSafeZeroCommand(input)) return;
output.status = "allow";
},
};
};

export default ZeroPlugin;
20 changes: 20 additions & 0 deletions plugins/zero-opencode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@zeroxyz/opencode-zero",
"version": "1.4.0",
"description": "OpenCode plugin for Zero: runner setup, context injection, and safe Zero command auto-approval.",
"type": "module",
"license": "MIT",
"homepage": "https://zero.xyz",
"repository": {
"type": "git",
"url": "https://github.com/officialzeroxyz/zero-plugins.git",
"directory": "plugins/zero-opencode"
},
"keywords": ["opencode", "zero", "x402", "mpp", "payments", "capabilities"],
"main": "./index.js",
"exports": {
".": "./index.js",
"./server": "./index.js"
},
"files": ["index.js", "README.md"]
}
2 changes: 1 addition & 1 deletion plugins/zero/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zero",
"version": "1.3.3",
"version": "1.4.0",
"description": "Discover and call paid AI capabilities (x402 / MPP) with automatic, wallet-backed payment. Search a live index of external services — image/video/audio generation, web scraping, data enrichment, real-time data, messaging — then call them and pay per use. Bundles the 'zero' skill and the Zero MCP connector.",
"author": { "name": "Zero", "url": "https://zero.xyz" },
"homepage": "https://zero.xyz",
Expand Down
2 changes: 1 addition & 1 deletion plugins/zero/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zero",
"version": "1.3.3",
"version": "1.4.0",
"description": "Discover and call paid AI capabilities (x402 / MPP) with automatic, wallet-backed payment. Search a live index of external services — image/video/audio generation, web scraping, data enrichment, real-time data, messaging — then call them and pay per use. Bundles the 'zero' skill.",
"author": { "name": "Zero", "url": "https://zero.xyz" },
"homepage": "https://zero.xyz",
Expand Down
2 changes: 1 addition & 1 deletion plugins/zero/.factory-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zero",
"version": "1.3.3",
"version": "1.4.0",
"description": "Discover and call paid AI capabilities (x402 / MPP) with automatic, wallet-backed payment. Search a live index of external services — image/video/audio generation, web scraping, data enrichment, real-time data, messaging — then call them and pay per use. Bundles the 'zero' skill and the Zero MCP connector.",
"author": { "name": "Zero", "url": "https://zero.xyz" },
"homepage": "https://zero.xyz",
Expand Down
Loading