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: 35 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,40 @@
# @codespar/cli — changelog

## 0.7.0 — 2026-09-11

Resource groups and the 15 meta-tools, derived from the published
surface instead of written one by one. See
[codespar/codespar-core#125](https://github.com/codespar/codespar-core/issues/125).

### Added

- Six resource command groups — `consumers`, `boletos`, `sellers`,
`mcp-servers`, `wallets`, `triggers` — covering 61 operations. Each
subcommand is one row of `API_OPERATIONS`, the table `@codespar/sdk`
generates from the served OpenAPI document: the path parameters are the
positionals, `-q/--query key=value` is repeatable, and `-i/--input` is
accepted only where the operation declares a body. Dispatch goes
through `cs.api`, so the CLI adds no HTTP of its own.
- `codespar tool <name>` — invoke any of the 15 meta-tools published in
`@codespar/types`. `--action` is checked against the tool's published
vocabulary, `--arg key=value` is typed by the published schema, and a
missing required property fails before anything is sent.
- `codespar pay` and `codespar kyc` — shorthands for `tool codespar_pay`
and `tool codespar_kyc`.
- `codespar tools meta [name]` — the published definitions: actions,
required input, closed vocabularies, full input schema.
- A coverage gate (`src/__tests__/surface-coverage.test.ts`): every
resource family of the served document needs a command or an exception
with a reason and a date, and the exception list is a ratchet that only
goes down. A second ratchet pins the ten REST paths the older commands
still build by hand, none of which the served document declares.

### Changed

- Errors from the generated REST client (`CodesparApiError`,
`TimeoutError`) print the API's message and body and exit 1, instead of
falling through to the internal-error stack trace.

## 0.6.1 — 2026-09-09

Dependency range only: `@codespar/sdk` `^0.12.0` (the generated REST
Expand Down
40 changes: 40 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ codespar spend --mandate <id> --amount 1 --agent buyer \
# ramp trade at the real rate). Omit --execute to just plan it.
codespar transfer shopper --from BRL --to USDC --amount 15000

# Every published meta-tool is invocable by name; the actions come from the
# published definition, so an unknown one is refused before anything is sent
codespar tools meta
codespar tool codespar_wallet --action balance --arg consumer_id=con_0000
codespar pay --action status --arg reference=pay_0000

# Resource groups are derived from the served OpenAPI document: each
# subcommand is one operation, its path parameters are the positionals
codespar sellers status slr_0000
codespar wallets list --query status=active
codespar triggers create --input '{"url":"https://example.test/hook","events":["payment.settled"]}'
codespar boletos list con_0000

# Manage sessions and logs
codespar sessions list
codespar logs tail --server stripe
Expand All @@ -60,6 +73,15 @@ codespar init my-agent
| `servers show <id>` | Show a server's details and tools |
| `tools list` | List tools (filter by `--server`) |
| `tools show <name>` | Show a tool's full input/output schema |
| `tools meta [name]` | The 15 published meta-tool definitions — actions, required input, vocabularies |
| `tool <name>` | Invoke any published meta-tool: `--action`, `--arg key=value`, `--input` |
| `pay` / `kyc` | Shorthand for `tool codespar_pay` / `tool codespar_kyc` |
| `consumers <sub>` | Consumers: profile, Pix keys, Pix lookups, receipts, contact verification |
| `boletos <sub>` | DDA: subscribe a document, list the boletos it receives |
| `sellers <sub>` | Sellers: onboarding status, custody, pending settlement, ledger |
| `mcp-servers <sub>` | Tenant MCP servers: register, validate, patch a tool, sweep platform fees |
| `wallets <sub>` | Wallets: balances, ledger, funding sources, execute, transfer, custody |
| `triggers <sub>` | Triggers (webhooks): endpoints, deliveries, DLQ, secret rotation, redelivery |
| `execute <tool>` | Run a single tool call in a throwaway session |
| `discover <query>` | Search the catalog for tools matching a use case |
| `mandate create` | Create a consumer mandate — the agent's allowance. `--slot CURRENCY:METHOD:CAP:PER_TX` (repeatable, e.g. `BRL:pix:50000:1500`) for a unified multi-currency wallet; per-currency caps, no FX |
Expand Down Expand Up @@ -91,6 +113,24 @@ codespar init my-agent
| `--base-url <url>` | Point at a custom API (staging, self-hosted) |
| `--project <id>` | Scope requests to a project (multi-project orgs) |

Resource-group subcommands also take `-q, --query key=value` (repeatable),
`--timeout <ms>`, and — when the operation declares a request body —
`-i, --input '<json>'` or `-f, --input-file <path>`.

## Where the commands come from

The resource groups and the meta-tool commands are not written one by one.
`codespar sellers`, `consumers`, `boletos`, `mcp-servers`, `wallets` and
`triggers` are derived from `API_OPERATIONS` — the operation table
`@codespar/sdk` generates from the served OpenAPI document — so a
subcommand is one operation, and the request behind it is the one the
document declares. `codespar tool <name>` reads the 15 shared meta-tool
definitions from `@codespar/types`: the names, the actions and the
required input are the published ones, checked before anything is sent.

A resource family with no command needs a written exception with a date
(`src/surface.ts`), and the coverage test refuses to let that list grow.

## Configuration

Resolution order (first match wins):
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codespar/cli",
"version": "0.6.2",
"version": "0.7.0",
"description": "CodeSpar CLI. The agentic OS for money movement in Latin America, from your terminal: authenticate, browse servers, mint mandates, execute tools, manage sessions, stream logs.",
"type": "module",
"bin": {
Expand Down
121 changes: 121 additions & 0 deletions packages/cli/src/__tests__/meta-tool-args.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* The `codespar tool <name>` runner validates against the published
* definition and nothing else: the property names, the required subset
* and the closed vocabularies come from `@codespar/types`. These tests
* read the same definitions the runner reads, so a vocabulary that
* changes there changes the expectation here too — no rail, action or
* property name is retyped in this file.
*/

import { describe, expect, it } from "vitest";
import { SHARED_META_TOOL_DEFINITIONS } from "@codespar/sdk";
import { buildArgs, coerceArg, requireDefinition } from "../commands/meta-tool.js";
import { metaToolActions, metaToolNames } from "../surface.js";

const pay = requireDefinition("codespar_pay");
const kyc = requireDefinition("codespar_kyc");
const wallet = requireDefinition("codespar_wallet");

describe("requireDefinition", () => {
it("resolves every published name and refuses anything else", () => {
for (const name of Object.keys(SHARED_META_TOOL_DEFINITIONS)) {
expect(requireDefinition(name).name).toBe(name);
}
expect(() => requireDefinition("codespar_made_up")).toThrow(/Unknown meta-tool/);
});

it("lists the published tools when the name is wrong", () => {
try {
requireDefinition("nope");
throw new Error("should have thrown");
} catch (err) {
for (const name of metaToolNames()) {
expect((err as Error).message).toContain(name);
}
}
});
});

describe("--action", () => {
it("accepts every action the definition publishes", () => {
for (const action of metaToolActions("codespar_wallet")) {
expect(buildArgs(wallet, undefined, [], action).action).toBe(action);
}
});

it("refuses an action outside the published vocabulary, naming it", () => {
expect(() => buildArgs(pay, undefined, [], "refund")).toThrow(
new RegExp(metaToolActions("codespar_pay").join(" \\| ")),
);
});

it("refuses --action for a tool that publishes no action property", () => {
// codespar_kyc discriminates on check_type; the message must say so
// instead of silently sending an `action` the router ignores.
expect(metaToolActions("codespar_kyc")).toEqual([]);
expect(() => buildArgs(kyc, undefined, [], "status")).toThrow(/publishes no "action"/);
});
});

describe("--arg", () => {
it("refuses a property the definition does not publish", () => {
expect(() => coerceArg(wallet, "not_a_property", "x")).toThrow(/has no property/);
});

it("types a value by the published schema", () => {
const amount = wallet.input_schema.properties.amount;
expect(amount?.type).toBe("number");
expect(coerceArg(wallet, "amount", "1500")).toBe(1500);
expect(() => coerceArg(wallet, "amount", "lots")).toThrow(/expects a number/);
});

it("takes JSON for an object-typed property", () => {
const buyer = kyc.input_schema.properties.buyer;
expect(buyer?.type).toBe("object");
expect(coerceArg(kyc, "buyer", '{"name":"Fulano"}')).toEqual({ name: "Fulano" });
expect(() => coerceArg(kyc, "buyer", "Fulano")).toThrow(/needs valid JSON/);
});

it("refuses a value outside a property's published vocabulary", () => {
const check = kyc.contract.enums?.check_type ?? [];
expect(check.length).toBeGreaterThan(0);
expect(coerceArg(kyc, "check_type", check[0]!)).toBe(check[0]);
expect(() => coerceArg(kyc, "check_type", "vibes")).toThrow(/published vocabulary/);
});

it("rejects a pair with no equals sign", () => {
expect(() => buildArgs(wallet, undefined, ["justakey"], undefined)).toThrow(/key=value/);
});
});

describe("required input", () => {
it("refuses to send when a required property is missing, and says how to pass it", () => {
expect(() => buildArgs(pay, undefined, [], undefined)).toThrow(/Nothing was sent/);
expect(() => buildArgs(pay, undefined, [], undefined)).toThrow(
new RegExp(`--action <${metaToolActions("codespar_pay").join("\\|")}>`),
);
});

it("accepts --input as the base and lets --arg and --action override it", () => {
const args = buildArgs(
wallet,
{ action: "statement", consumer_id: "con_0000" },
["consumer_id=con_1111"],
"balance",
);
expect(args).toEqual({ action: "balance", consumer_id: "con_1111" });
});

it("checks every tool's required set against an empty input", () => {
for (const name of metaToolNames()) {
const definition = requireDefinition(name);
if (definition.contract.required.length === 0) {
expect(buildArgs(definition, undefined, [], undefined)).toEqual({});
continue;
}
expect(() => buildArgs(definition, undefined, [], undefined)).toThrow(
new RegExp(`requires ${definition.contract.required.join(", ")}`),
);
}
});
});
Loading
Loading