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
2 changes: 1 addition & 1 deletion .changelog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Description of the change.

Supported bump levels are `patch`, `minor`, `major`, and `none`.

The release is versioned as one package, `wallet-cli`, and publishes both `tempo-wallet` and `tempo-request` from the same tag. Changelog entries may also use `tempo-wallet` or `tempo-request` in frontmatter when a change is scoped to one binary; any non-`none` bump still advances the shared version.
The release is versioned as one package, `wallet-cli`, and publishes `tempo-wallet`, `tempo-request`, `tempo-api`, and `tempo-routes` from the same tag. Changelog entries may also use `tempo-wallet` or `tempo-request` in frontmatter when a change is scoped to one binary; any non-`none` bump still advances the shared version.

## Releasing

Expand Down
5 changes: 5 additions & 0 deletions .changelog/tempo-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
wallet-cli: minor
---

Expose Tapimo's hosted API as `tempo api v1 …` and its routing endpoints as `tempo routes …`, with OpenAPI-generated help, schemas, and MCP tools.
10 changes: 7 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ on:
- "v*"
- "tempo-wallet@*"
- "tempo-request@*"
- "tempo-api@*"
- "tempo-routes@*"
workflow_dispatch:
inputs:
release_tag:
Expand Down Expand Up @@ -43,22 +45,22 @@ jobs:

if [[ "$TAG" == v* ]]; then
VERSION="${TAG#v}"
PACKAGES='["tempo-wallet","tempo-request"]'
PACKAGES='["tempo-wallet","tempo-request","tempo-api","tempo-routes"]'
RELEASE_TAG="$TAG"
elif [[ "$TAG" == *"@"* ]]; then
PACKAGE="${TAG%%@*}"
VERSION="${TAG#*@}"
RELEASE_TAG="$TAG"

if [[ "$PACKAGE" == "tempo-wallet" || "$PACKAGE" == "tempo-request" ]]; then
if [[ "$PACKAGE" == "tempo-wallet" || "$PACKAGE" == "tempo-request" || "$PACKAGE" == "tempo-api" || "$PACKAGE" == "tempo-routes" ]]; then
PACKAGES="[\"${PACKAGE}\"]"
else
echo "Unsupported package in tag: ${PACKAGE}" >&2
exit 1
fi
elif [[ "$TAG" == refs/heads/* || "$TAG" == "$GITHUB_REF" ]]; then
VERSION="0.0.0-dev.${GITHUB_RUN_NUMBER}"
PACKAGES='["tempo-wallet","tempo-request"]'
PACKAGES='["tempo-wallet","tempo-request","tempo-api","tempo-routes"]'
RELEASE_TAG="$TAG"
else
echo "Unsupported tag format: ${TAG}" >&2
Expand Down Expand Up @@ -281,6 +283,8 @@ jobs:
declare -A PKG_DESC=(
[tempo-wallet]="Manage your Tempo Wallet"
[tempo-request]="Make an HTTP request"
[tempo-api]="Explore the Tempo API"
[tempo-routes]="Access the Tempo asset routing API"
)

SIGN_ARGS=(
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,35 @@ tempo wallet sessions close https://service.mpp.tempo.xyz

## Commands

### Tempo API

`tempo api` exposes the hosted Tempo API using the same OpenAPI-generated command
tree as Tapimo. `tempo routes` is a shortcut to the `/v1/routes` endpoints.
The launcher installs the corresponding `tempo-api` and `tempo-routes` extensions
on first use, once their release is published.

```sh
tempo api --help
tempo api v1 --help
tempo routes --help
tempo routes chains --format json
tempo routes quotes --help
tempo api v1 tokens --help
```

Set `TEMPO_API_KEY` for authenticated API calls. API keys are separate from wallet
login; generated command help also lists authentication options supported by the
API schema. `TEMPO_API_URL` overrides the default `https://api.tempo.xyz` host.
Help and command schemas require access to that host's `/openapi.json`.

For agents, use `--schema --format json` on a command to inspect its inputs, or
run `tempo api --mcp` / `tempo routes --mcp` to serve the generated tools over
stdio. Commands call the API directly; they do not automatically pay MPP
challenges. API errors, including authentication and payment requirements,
return a nonzero exit status.

### Wallet

`tempo wallet` includes:

- `login`, `logout`, `refresh`, `whoami`, `keys`
Expand Down Expand Up @@ -177,7 +206,7 @@ pnpm package

## Release Artifacts

The release workflow builds standalone Linux and macOS binaries for both `tempo-wallet` and `tempo-request`. Each binary is published with a checksum, SBOM, Sigstore bundle, and GitHub attestations.
The release workflow builds standalone Linux and macOS binaries for `tempo-wallet`, `tempo-request`, `tempo-api`, and `tempo-routes`. Each binary is published with a checksum, SBOM, Sigstore bundle, and GitHub attestations.

## Security

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
"url": "git+https://github.com/tempoxyz/wallet-cli.git"
},
"bin": {
"tempo-api": "./dist/api-cli.js",
"tempo-request": "./dist/request-cli.js",
"tempo-routes": "./dist/routes-cli.js",
"tempo-wallet": "./dist/cli.js"
},
"type": "module",
Expand Down
12 changes: 9 additions & 3 deletions scripts/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ type PackageJson = {
version?: string;
};

type CliPackageName = "tempo-wallet" | "tempo-request";
const entrypoints = {
"tempo-api": "src/api-cli.ts",
"tempo-request": "src/request-cli.ts",
"tempo-routes": "src/routes-cli.ts",
"tempo-wallet": "src/cli.ts",
};
type CliPackageName = keyof typeof entrypoints;

const args = new Set(process.argv.slice(2));
const root = resolve(import.meta.dirname, "..");
Expand All @@ -22,7 +28,7 @@ const output = resolve(
root,
process.env.TEMPO_WALLET_PACKAGE_OUTPUT ?? join(outDir, `${packageName}-${suffix}`),
);
const entrypoint = packageName === "tempo-request" ? "src/request-cli.ts" : "src/cli.ts";
const entrypoint = entrypoints[packageName];

mkdirSync(outDir, { recursive: true });

Expand Down Expand Up @@ -66,7 +72,7 @@ function run(command: string, commandArgs: readonly string[]) {
function resolvePackageName(): CliPackageName {
const value = process.env.TEMPO_WALLET_PACKAGE_NAME ?? process.env.PACKAGE ?? "tempo-wallet";

if (value === "tempo-wallet" || value === "tempo-request") return value;
if (Object.hasOwn(entrypoints, value)) return value as CliPackageName;

throw new Error(`unsupported TypeScript CLI package: ${value}`);
}
Expand Down
4 changes: 4 additions & 0 deletions src/api-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node
import { serveApi } from "./api.js";

void serveApi();
62 changes: 62 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Cli, Fetch, Openapi } from "incur";

import { version } from "./shared/constants.js";

type ApiOptions = {
apiKey?: string | undefined;
url?: string | undefined;
routes?: boolean | undefined;
};

/** Creates commands from the same hosted OpenAPI schema used by Tapimo. */
export async function createApiCli(options: ApiOptions = {}) {
const url = new URL(options.url ?? process.env.TEMPO_API_URL ?? "https://api.tempo.xyz");
const apiKey = options.apiKey ?? process.env.TEMPO_API_KEY;
const spec = await Openapi.resolve(new URL("openapi.json", `${url.href.replace(/\/$/, "")}/`));
const prefix = options.routes ? "/v1/routes" : "";

// Strip the routing namespace only for discovery; the request source restores it on the wire.
const openapi = options.routes
? {
...spec,
paths: Object.fromEntries(
Object.entries(spec.paths ?? {})
.filter(([path]) => path.startsWith(`${prefix}/`))
.map(([path, value]) => [path.slice(prefix.length), value]),
),
}
: spec;
url.pathname = `${url.pathname.replace(/\/$/, "")}${prefix}`;

return Cli.create(options.routes ? "tempo routes" : "tempo api", {
version,
description: options.routes ? "Tempo asset routing API" : "Tempo API commands",
fetch: Fetch.fromRequest(url, {
headers: apiKey ? { "tempo-api-key": apiKey } : {},
redirect: "error",
}),
openapi,
openapiConfig: { mode: "namespace" },
mcp: {
instructions:
"Commands call the hosted Tempo API. Use TEMPO_API_KEY or the generated authentication options for authenticated endpoints. List endpoints paginate with cursor/nextCursor.",
tools: { exclude: ["admin_*", "gecko_*", "v1_auth_*"] },
},
});
}

/** Runs an API extension, keeping version checks independent of API availability. */
export async function serveApi(routes = false) {
try {
const argv = process.argv.slice(2);
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
process.stdout.write(`${version}\n`);
return;
}
const cli = await createApiCli({ routes });
await cli.serve(argv);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
4 changes: 4 additions & 0 deletions src/routes-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node
import { serveApi } from "./api.js";

void serveApi(true);
Loading
Loading