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
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## OpenAPI to CLI (ocli)

`openapi-to-cli` (short `ocli`) is a TypeScript CLI that turns any HTTP API described by an OpenAPI/Swagger spec into a set of CLI commands — at runtime, without code generation.
`openapi-to-cli` (short `ocli`) is a TypeScript CLI that turns any HTTP API described by an OpenAPI, Swagger, or OpenRPC spec into a set of CLI commands — at runtime, without code generation.

```bash
npm install -g openapi-to-cli
Expand Down Expand Up @@ -63,6 +63,35 @@ ocli commands -p other --query "send message"

`--profile` (short `-p`) overrides the profile selected by `ocli use` for this invocation only. It works for both dynamic API commands and `ocli commands`. Place it anywhere after the command name. When omitted, the profile set via `ocli use` is used (falling back to `default`).

### OpenRPC and JSON-RPC APIs

`ocli` also accepts OpenRPC JSON or YAML documents through the existing `--openapi-spec` option. It creates one command for each documented RPC method and exposes named RPC parameters as command flags.

```bash
ocli profiles add widgets \
--api-base-url https://api.example.com/rpc \
--openapi-spec ./openrpc.json \
--api-bearer-token "$TOKEN"

ocli use widgets
ocli getWidget --widgetId widget-7
```

OpenRPC commands always send an HTTP `POST` request with `Content-Type: application/json`. `ocli` builds the JSON-RPC 2.0 envelope automatically:

```json
{
"jsonrpc": "2.0",
"method": "getWidget",
"params": {
"widgetId": "widget-7"
},
"id": 1
}
```

The profile's `--api-base-url` remains the request target. If it is empty, `ocli` falls back to the first server URL in the OpenRPC document.

### Authentication and custom headers

A profile stores up to three credentials, set with `ocli profiles add` (or `ocli onboard`):
Expand Down Expand Up @@ -107,13 +136,14 @@ npx openapi-to-cli onboard \

### Broader spec support

`ocli` now handles a wider range of real-world OpenAPI and Swagger documents:
`ocli` now handles a wider range of real-world API descriptions:

- OAS 3 `requestBody` for JSON payloads
- Swagger 2 `body` and `formData` parameters
- path-level parameters inherited by operations
- local `$ref` references for parameters and request bodies
- header and cookie parameters in generated commands
- OpenRPC methods with named parameters, including local schema references

In practice this improves compatibility with APIs that define inputs outside simple path/query parameters, especially for `POST`, `PUT`, and `PATCH` operations.

Expand Down Expand Up @@ -222,6 +252,7 @@ Note: MCP+Search Compact (search → get_schema → call) is the fairest compari
| Regex command search | ✅ | ❌ | ❌ | ❌ |
| Per-profile endpoint filtering | ✅ | ✅ | ❌ | ❌ |
| OpenAPI/Swagger (JSON + YAML) | ✅ | ✅ | ✅ | ❌ |
| OpenRPC (JSON + YAML) | ✅ | ? | ? | ? |
| MCP server support | ❌ | ✅ (HTTP/SSE/stdio) | ❌ | ❌ |
| GraphQL support | ❌ | ✅ (introspection) | ❌ | ❌ |
| Spec caching | ✅ | ✅ (1h TTL) | ❌ | ❌ |
Expand Down
13 changes: 13 additions & 0 deletions examples/skill-ocli-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ ocli repos_get --profile github --owner octocat --repo Hello-World
ocli commands -p github --query "list pull requests"
```

## OpenRPC APIs

`ocli` accepts OpenRPC JSON or YAML documents through `--openapi-spec` too. It creates one command per RPC method and automatically sends a JSON-RPC 2.0 envelope. Pass documented named RPC parameters as regular flags:

```bash
ocli profiles add widgets \
--api-base-url https://api.example.com/rpc \
--openapi-spec ./openrpc.json

ocli getWidget --widgetId widget-7
```

## Tips

- All responses are JSON — pipe through `jq` for filtering
Expand All @@ -80,4 +92,5 @@ ocli commands -p github --query "list pull requests"
- Undeclared flags are rejected with `Unknown option: --x`, so copy names exactly from `--help` (including a leading `$`)
- Use `ocli commands` to list all available commands
- Use `--profile <name>` (or `-p <name>`) to switch profile for a single call without running `ocli use`
- For OpenRPC, pass documented RPC parameters as flags. `ocli` adds `jsonrpc`, `method`, and `id` to the request body.
````
18 changes: 15 additions & 3 deletions skills/ocli-api/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
---
name: ocli-api
description: Turn any OpenAPI/Swagger API into CLI commands and call them. Search endpoints with BM25, check parameters, execute — no MCP server needed.
description: Turn any OpenAPI, Swagger, or OpenRPC API into CLI commands and call them. Search endpoints with BM25, check parameters, execute — no MCP server needed.
version: 1.0.0
user-invocable: true
disable-model-invocation: false
metadata: {"openclaw":{"emoji":"🔌","requires":{"bins":["ocli"],"env":[]}}}
homepage: https://github.com/EvilFreelancer/openapi-to-cli
---

# ocli — OpenAPI to CLI
# ocli — API specification to CLI

Call any HTTP API described by an OpenAPI/Swagger spec as CLI commands.
Call any HTTP API described by an OpenAPI, Swagger, or OpenRPC spec as CLI commands.
No MCP server, no code generation, no JSON schemas in context.

## When to use

- You need to call a REST API (internal, cloud, SaaS)
- You have an OpenAPI or Swagger spec (URL or local file)
- You have an OpenRPC JSON or YAML document for a JSON-RPC API
- You want minimal token overhead (1 tool, ~158 tokens/turn)

## Setup (one-time)
Expand All @@ -32,6 +33,16 @@ ocli profiles add <name> \
ocli use <name>
```

For OpenRPC, use the same profile command. `ocli` builds one command per RPC method and wraps documented named parameters in a JSON-RPC 2.0 request automatically:

```bash
ocli profiles add rpc-api \
--api-base-url https://api.example.com/rpc \
--openapi-spec ./openrpc.json

ocli getWidget --widgetId widget-7
```

## Workflow

1. **Search** for the right command:
Expand Down Expand Up @@ -76,6 +87,7 @@ ocli repos_get --profile github --owner octocat --repo Hello-World
- Always search before guessing a command name.
- Always check `--help` before calling a command you haven't used before.
- Never fabricate parameter names — use the ones from `--help` output.
- For OpenRPC, pass only documented RPC parameters. Do not add `jsonrpc`, `method`, or `id`; `ocli` supplies them.
- If a command returns an error, read the response body before retrying.

## Failure handling
Expand Down
18 changes: 18 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,24 @@ function buildRequestPayload(
.filter(([key]) => !knownOptionNames.has(key))
.map(([key, value]) => [key, parseBodyFlagValue(value)] as const);

if (command.jsonRpcMethod) {
const params = Object.fromEntries([
...bodyOptions
.filter((opt) => flags[opt.name] !== undefined)
.map((opt) => [opt.name, parseBodyFlagValue(flags[opt.name])] as const),
...extraBodyEntries,
]);
return {
data: {
jsonrpc: "2.0",
method: command.jsonRpcMethod,
params,
id: 1,
},
contentType: "application/json",
};
}

if (bodyOptions.length === 1 && bodyOptions[0].name === "body" && flags.body !== undefined) {
return {
data: parseBodyFlagValue(flags.body),
Expand Down
76 changes: 76 additions & 0 deletions src/openapi-to-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface CliCommand {
requestContentType?: string;
serverUrl?: string;
serverUrlOverridesProfile?: boolean;
jsonRpcMethod?: string;
}

type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "head" | "options" | "trace";
Expand Down Expand Up @@ -86,8 +87,26 @@ interface ServerLike {
variables?: Record<string, { default?: string }>;
}

interface OpenRpcMethodLike {
name?: string;
summary?: string;
description?: string;
params?: unknown[];
}

interface OpenRpcParameterLike {
name?: string;
description?: string;
required?: boolean;
schema?: SchemaLike;
}

export class OpenapiToCommands {
buildCommands(spec: OpenapiSpecLike, profile: Profile): CliCommand[] {
if (this.isOpenRpcSpec(spec)) {
return this.buildOpenRpcCommands(spec, profile);
}

const operations = this.collectOperations(spec);
const methodsByPath: Record<string, Set<HttpMethod>> = {};

Expand All @@ -111,6 +130,63 @@ export class OpenapiToCommands {
return commands;
}

private isOpenRpcSpec(spec: OpenapiSpecLike): boolean {
return Boolean(
spec &&
typeof spec === "object" &&
typeof spec.openrpc === "string" &&
Array.isArray(spec.methods)
);
}

private buildOpenRpcCommands(spec: OpenapiSpecLike, profile: Profile): CliCommand[] {
const serverUrl = this.resolveServers(spec.servers);
const methods = spec.methods as OpenRpcMethodLike[];
const commands: CliCommand[] = [];

for (const rawMethod of methods) {
const method = this.resolveValue(rawMethod, spec) as OpenRpcMethodLike;
if (!method.name) {
continue;
}

const options = (method.params ?? [])
.map((rawParameter) => this.resolveValue(rawParameter, spec) as OpenRpcParameterLike)
.filter((parameter) => Boolean(parameter.name))
.map((parameter) => {
const schema = this.resolveSchema(parameter.schema, spec);
return {
name: parameter.name as string,
location: "body" as const,
required: Boolean(parameter.required),
schemaType: this.describeSchemaType(schema),
description: parameter.description,
...this.extractSchemaHints(schema),
};
});

commands.push({
name: method.name,
method: "post",
path: "",
options,
description: method.summary ?? method.description,
requestContentType: "application/json",
serverUrl,
serverUrlOverridesProfile: false,
jsonRpcMethod: method.name,
});
}

if (profile.commandPrefix) {
for (const command of commands) {
command.name = `${profile.commandPrefix}${command.name}`;
}
}

return commands;
}

private collectOperations(spec: OpenapiSpecLike): PathOperation[] {
const result: PathOperation[] = [];
const paths = spec.paths ?? {};
Expand Down
41 changes: 41 additions & 0 deletions tests/fixtures/openrpc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"openrpc": "1.0.0-rc1",
"info": {
"title": "Example RPC API",
"version": "1.0.0"
},
"servers": [
{
"name": "example",
"url": "https://api.example.test/rpc"
}
],
"methods": [
{
"name": "getWidget",
"description": "Get one widget",
"params": [
{
"name": "widgetId",
"description": "Widget identifier",
"required": true,
"schema": {
"$ref": "#/components/schemas/Integer"
}
}
]
},
{
"name": "listWidgets",
"description": "List widgets",
"params": []
}
],
"components": {
"schemas": {
"Integer": {
"type": "integer"
}
}
}
}
Loading
Loading