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
22 changes: 22 additions & 0 deletions CODING_HARNESSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Current OpenClaw requires Node `>=24.16.0 <25` or `>=26.1.0`; Pi requires Node `
| Harness | Status | What is unique |
| --- | --- | --- |
| [Bloom CLI](https://github.com/Ilm-Alan/bloom-cli) | **Available now** — `polli harness bloom on` | Creates a dedicated key for Bloom's existing Pollinations integration. |
| [Codex](https://github.com/openai/codex) via [Codex Router](https://github.com/duolahypercho/codex-router) | **Available now** — `polli harness codex on` | Drives the router's own provider commands (pinned commit, v0.6.0); the dedicated key lives in the router's protected credential store. |
| [Claude Code](https://claude.com/claude-code) via [Claude Code Router](https://github.com/musistudio/claude-code-router) | **Available now** — `polli harness claude-code on` | Semi-automatic: starts the pinned router (3.1.1) and opens its management UI with exact values, then verifies read-only. |
| [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) | **Available now** — `polli harness dsh on` | Adds the Pollinations provider, hosted Pollinations MCP, and Polli skill. Uses `deepseek/deepseek-v4-flash` by default. Its official launch uses `npx`, so no separate global DSH installation is required. |
| [OpenCode](https://opencode.ai) | **Available now** — `polli harness opencode on` | Uses the existing [Pollinations OpenCode plugin](https://github.com/fkom13/opencode-pollinations-plugin) for models, media tools, usage, and quests. Defaults to `openai/gpt-5.4-nano`. |
| [OpenClaw](https://github.com/openclaw/openclaw) | **Available now** — `polli harness openclaw on` | Adds the Pollinations provider, a dedicated key, and the Polli skill, pulling models from the live catalog. Defaults to `moonshotai/kimi-k2.6`. |
Expand All @@ -58,6 +60,26 @@ bloom

Bloom already uses Pollinations for its models. `on` creates a dedicated key and stores it in `~/.bloom/.env` (or `$BLOOM_HOME/.env`); `off` restores the previous file or removes only that key if the file changed later.

## Codex (Codex Router)

```bash
npx @pollinations/cli@latest harness codex on
polli harness codex status
polli harness codex off
```

`on` requires the Codex CLI. It clones [Codex Router](https://github.com/duolahypercho/codex-router) into `~/.pollinations/harnesses/codex/codex-router` pinned to commit `5e1b49e` (v0.6.0) and refuses to drive any other version. It then registers the `pollinations` generic provider exclusively through the router's own commands (`providers generic add/enable`, `curate-models`), mints a dedicated child key, and publishes it into the router's documented protected credential file (`generic-provider-credentials/pollinations.key`, temp-write + rename, `0o600`) — verified afterwards with `providers generic credential pollinations status`. The curated catalog is the live first-party tool-calling model list; choose the active model with `--model <id>`, and add `--smoke` for one billable request proving quota works. An interrupted `on` is reconciled from a journal on the next run. `off` removes only the models, credential, and provider it created — a provider that existed before (or was enabled before) is kept in its previous state — and revokes the child key. Fully quit and reopen Codex afterwards.

## Claude Code (Claude Code Router)

```bash
npx @pollinations/cli@latest harness claude-code on
polli harness claude-code status
polli harness claude-code off
```

`on` requires Claude Code 2.x. It installs [Claude Code Router](https://github.com/musistudio/claude-code-router) pinned to `@musistudio/claude-code-router@3.1.1` if missing, starts the service, then opens the management UI (`http://127.0.0.1:3458`) with exact values: provider `pollinations` → `https://gen.pollinations.ai/v1` (OpenAI Chat) plus a freshly minted child key (shown once), and a Claude Code agent profile on that provider with the chosen model. CCR's only supported way to create providers is that UI, so polli finishes by watching `config.sqlite` **read-only** until the new provider and profile verify (ownership is proven by a pre-state snapshot taken before the intent). Interrupting is safe: re-run `on` to continue, `status` shows the lifecycle state (`awaiting-provider`, `awaiting-profile`, `key-valid`, ...). `off` asks you to delete the two entries in the UI, verifies their absence read-only, then revokes the child key; it never writes to `config.sqlite`. Launch Claude Code through the profile with `ccr <profile-name-or-id> cli`.

## DeepSeek Harness

```bash
Expand Down
17 changes: 17 additions & 0 deletions packages/polli-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ restores the backup.
```bash
polli harness --help # supported harnesses
polli harness bloom on # creates a dedicated key for Bloom CLI
polli harness codex on # Codex via Codex Router (pinned v0.6.0)
polli harness claude-code on # Claude Code via Claude Code Router (pinned 3.1.1)
polli harness dsh on # DeepSeek Harness → Pollinations
polli harness dsh on --model moonshotai/kimi-k2.6
polli harness dsh on --no-mcp # skip MCP tool configuration
Expand All @@ -157,6 +159,21 @@ and Polli CLI skill globally under `$DSH_HOME` (default `~/.dsh`). OpenCode uses
its official plugin; OpenClaw uses `openclaw.json`, while Pi and Prime Agent use
their native `models.json` provider support.

Codex and Claude Code are driven through their community routers, pinned to
exact versions so the integration cannot drift: Codex Router
(`duolahypercho/codex-router` @ `5e1b49e`, v0.6.0) and Claude Code Router
(`@musistudio/claude-code-router@3.1.1`). `polli harness codex on` installs the
router clone, registers the `pollinations` generic provider through the
router's own commands, stores the dedicated key in the router's protected
credential file, and curates the first-party tool-calling catalog; `off`
removes only what it created. Claude Code Router only supports provider
creation through its management UI, so `polli harness claude-code on` starts
the router, opens the UI with exact values and a fresh child key, and waits
(read-only) until the new profile verifies; `off` revokes the key once the
entries are deleted in the UI. Both adapters refuse to touch providers or
installations they do not own, and `--smoke` sends one billable request after
setup to prove quota works.

See [Coding Harnesses](https://github.com/pollinations/pollinations/blob/main/CODING_HARNESSES.md) for what each profile changes and how to add one.

## Links
Expand Down
4 changes: 3 additions & 1 deletion packages/polli-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ If `polli` is not installed, run `npm i -g @pollinations/cli@latest` (provides t
| Manage prompt agents | `polli agents list` |
| Manage invite-only community models | `polli my-models list` |
| Update the CLI | `polli update` (global installs only; npx/local get instructions) |
| Connect a coding harness to Pollinations | `polli harness <bloom\|dsh\|opencode\|openclaw\|pi\|prime> on` (available adapters: `polli harness --help`) |
| Connect a coding harness to Pollinations | `polli harness <bloom\|claude-code\|codex\|dsh\|opencode\|openclaw\|pi\|prime> on` (available adapters: `polli harness --help`) |
| Machine-readable output | append `--json` to any command |

## Setup
Expand Down Expand Up @@ -212,6 +212,8 @@ polli keys revoke <id> # id comes fr
```bash
polli harness --help # supported harnesses
polli harness bloom on # create a dedicated key for Bloom CLI
polli harness codex on # Codex via Codex Router (pinned v0.6.0)
polli harness claude-code on # Claude Code via Claude Code Router (pinned 3.1.1)
polli harness dsh on # login if needed, mint key "polli-harness-dsh", write provider + default model
polli harness dsh on --model moonshotai/kimi-k2.6 # use the model ID from `polli models`
polli harness dsh on --no-mcp # configure the provider and skill without MCP tools
Expand Down
60 changes: 51 additions & 9 deletions packages/polli-cli/src/commands/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import type {
HarnessContext,
HarnessOnOptions,
} from "../harnesses/types.js";
import { fail, printInfo, printResult, printSuccess } from "../lib/output.js";
import {
fail,
printError,
printInfo,
printResult,
printSuccess,
} from "../lib/output.js";

const context = (): HarnessContext => ({ home: homedir(), env: process.env });

Expand All @@ -16,15 +22,42 @@ const OFF_MESSAGES = {
unchanged: "was not connected; nothing changed.",
};

/** Errors may carry an adapter-chosen exit code (see HarnessResult.exitCode). */
const exitWith = (message: string, error: unknown): never => {
const code =
error !== null &&
typeof error === "object" &&
"exitCode" in error &&
typeof (error as { exitCode: unknown }).exitCode === "number"
? (error as { exitCode: number }).exitCode
: undefined;
if (code === undefined || code === 1) fail(message, error);
const detail = error instanceof Error ? `: ${error.message}` : "";
printError(`${message}${detail}`);
process.exit(code);
};

const adoptExitCode = (result: { exitCode?: number; notes?: string[] }) => {
for (const note of result.notes ?? []) printInfo(note);
if (result.exitCode) process.exitCode = result.exitCode;
};

const runOn = async (harness: HarnessAdapter, options: HarnessOnOptions) => {
try {
const result = await harness.on(context(), options);
const model = result.model ? ` (model: ${result.model})` : "";
printSuccess(`${harness.label} now uses Pollinations${model}.`);
printInfo(harness.restartHint);
if (result.exitCode === 3) {
printInfo(
`${harness.label}: waiting for manual steps - finish them, then re-run \`polli harness ${harness.id} on\` or check \`polli harness ${harness.id} status\`.`,
);
} else {
const model = result.model ? ` (model: ${result.model})` : "";
printSuccess(`${harness.label} now uses Pollinations${model}.`);
printInfo(harness.restartHint);
}
printResult(result);
adoptExitCode(result);
} catch (error) {
fail(`Failed to connect ${harness.label}`, error);
exitWith(`Failed to connect ${harness.label}`, error);
}
};

Expand All @@ -33,18 +66,23 @@ const runOff = async (harness: HarnessAdapter) => {
const result = await harness.off(context());
const outcome = result.outcome ?? "unchanged";
printSuccess(`${harness.label}: ${OFF_MESSAGES[outcome]}`);
if (outcome !== "unchanged") printInfo(harness.restartHint);
if (outcome !== "unchanged" && result.exitCode !== 3) {
printInfo(harness.restartHint);
}
printResult(result);
adoptExitCode(result);
} catch (error) {
fail(`Failed to disconnect ${harness.label}`, error);
exitWith(`Failed to disconnect ${harness.label}`, error);
}
};

const runStatus = async (harness: HarnessAdapter) => {
try {
printResult(await harness.status(context()));
const result = await harness.status(context());
printResult(result);
adoptExitCode(result);
} catch (error) {
fail(`Failed to inspect ${harness.label}`, error);
exitWith(`Failed to inspect ${harness.label}`, error);
}
};

Expand All @@ -55,6 +93,10 @@ const withOnOptions = (command: Command) =>
.option(
"--no-browser",
"Print the login URL instead of opening a browser",
)
.option(
"--smoke",
"Send one billable smoke request after setup to prove quota works",
);

const harnessSubcommand = (harness: HarnessAdapter) => {
Expand Down
Loading
Loading