@oathbuild/cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
- # Temporary Holding Version
1
+ # @oathbuild/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ OATH is a Transaction Builder for autonomous agents. It builds complete agent transaction flows across MCP, x402 and onchain actions, with authorization built into the execution path.
4
+
5
+ ```bash
6
+ npm install -g @oathbuild/cli
7
+ ```
8
+
9
+ ```bash
10
+ oath init
11
+ oath create --scope scope.json
12
+ oath build workflow.json
13
+ oath simulate workflow.json
14
+ oath execute workflow.json
15
+ oath receipts oath.json
16
+ oath revoke oath.json
17
+ ```
18
+
19
+ Keys are read from the environment: `OATH_OWNER_KEY`, `OATH_AGENT_KEY` and, optionally, `OATH_RELAYER_KEY`.
20
+
21
+ ## Links
22
+
23
+ - Docs: https://oathbuild.net/docs
24
+ - Contract addresses: https://oathbuild.net/docs#/deployments
25
+ - App: https://oathbuild.net/app
26
+
27
+ MIT licensed.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ // oath — command line for the OATH SDK.
3
+ // Keys are read from the environment only (OATH_OWNER_KEY, OATH_AGENT_KEY, OATH_RELAYER_KEY)
4
+ // and are never written to disk.
5
+ import fs from 'node:fs';
6
+ import { parseArgs } from 'node:util';
7
+ import { privateKeyToAccount } from 'viem/accounts';
8
+ import { Oath, OathBuilder, Policy, Receipts, buildPlan, createOathClient, deployAccount, toJson, fromJson, OathError, BuildError } from '@oathbuild/sdk';
9
+ const CONFIG = 'oath.config.json';
10
+ const HELP = `oath <command>
11
+
12
+ init write ${CONFIG} (chain, RPC, contract addresses)
13
+ create --scope <file> create and sign an OATH [OATH_OWNER_KEY] (--out oath.json)
14
+ inspect <oath.json> scope, status and remaining budget
15
+ build <workflow.json> plan a workflow offline (--oath oath.json, --out plan.json, --max-calls n)
16
+ simulate <workflow.json> simulate every onchain bundle [OATH_AGENT_KEY] (--oath oath.json)
17
+ execute <workflow.json> execute a workflow [OATH_AGENT_KEY, optional OATH_RELAYER_KEY]
18
+ receipts <oath.json> onchain bundle executions for an OATH
19
+ revoke <oath.json> revoke an OATH [OATH_OWNER_KEY]
20
+ contracts print the configured contract addresses
21
+
22
+ Options: --rpc <url> --chain-id <n> --addresses <file> --json`;
23
+ class CliError extends Error {
24
+ }
25
+ const { values: flags, positionals } = parseArgs({
26
+ allowPositionals: true,
27
+ options: {
28
+ scope: { type: 'string' }, oath: { type: 'string', default: 'oath.json' }, out: { type: 'string' }, 'max-calls': { type: 'string' },
29
+ rpc: { type: 'string' }, 'chain-id': { type: 'string' }, addresses: { type: 'string' }, json: { type: 'boolean' }, 'commit-receipts': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
30
+ },
31
+ });
32
+ const [command, arg] = positionals;
33
+ const read = (file, what) => {
34
+ if (!file)
35
+ throw new CliError(`Missing ${what}.`);
36
+ if (!fs.existsSync(file))
37
+ throw new CliError(`${what} not found: ${file}`);
38
+ try {
39
+ return fromJson(fs.readFileSync(file, 'utf8'));
40
+ }
41
+ catch {
42
+ throw new CliError(`${file} is not valid JSON.`);
43
+ }
44
+ };
45
+ const key = (name) => {
46
+ const v = process.env[name];
47
+ if (!v)
48
+ throw new CliError(`Set ${name} in the environment.`);
49
+ return privateKeyToAccount(v);
50
+ };
51
+ const print = (human, data) => console.log(flags.json ? toJson(data, 2) : human);
52
+ const units = (v) => v.toString();
53
+ function client() {
54
+ const cfg = fs.existsSync(CONFIG) ? fromJson(fs.readFileSync(CONFIG, 'utf8')) : {};
55
+ const rpc = flags.rpc ?? cfg.rpc;
56
+ const addresses = flags.addresses ? read(flags.addresses, 'Addresses file') : cfg.addresses;
57
+ if (!rpc)
58
+ throw new CliError(`No RPC configured. Run "oath init --rpc <url> --addresses <file>" or pass --rpc.`);
59
+ return createOathClient({ chainId: Number(flags['chain-id'] ?? cfg.chainId ?? 4663), rpc, addresses });
60
+ }
61
+ function describe(plan) {
62
+ const lines = [`plan ${plan.planHash}`, ` requested ${plan.actions.length} authorized ${plan.actions.filter((a) => a.decision === 'AUTHORIZED').length} rejected ${plan.rejected.length} deduplicated ${plan.deduplicated}`,
63
+ ` transactions: baseline ${plan.baselineTransactions} → ${plan.estimatedTransactions} (removed ${plan.removed})`, '', 'groups'];
64
+ for (const g of plan.groups)
65
+ lines.push(` ${g.id.padEnd(12)} ${g.kind.padEnd(20)} ${g.actions.join(', ')}`);
66
+ if (plan.rejected.length) {
67
+ lines.push('', 'rejected');
68
+ for (const r of plan.rejected)
69
+ lines.push(` ${r.id.padEnd(16)} ${r.reason}${r.detail ? ` (${r.detail})` : ''}`);
70
+ }
71
+ return lines.join('\n');
72
+ }
73
+ const builder = (c, oath) => {
74
+ const agent = key('OATH_AGENT_KEY');
75
+ return new OathBuilder({ client: c, oath, agent, relayer: process.env.OATH_RELAYER_KEY ? key('OATH_RELAYER_KEY') : agent, maxCallsPerBundle: flags['max-calls'] ? Number(flags['max-calls']) : undefined });
76
+ };
77
+ const commands = {
78
+ async init() {
79
+ if (!flags.rpc)
80
+ throw new CliError('Pass --rpc <url>. Add --addresses <file> once the contracts are deployed.');
81
+ const cfg = { chainId: Number(flags['chain-id'] ?? 4663), rpc: flags.rpc, addresses: flags.addresses ? read(flags.addresses, 'Addresses file') : undefined };
82
+ fs.writeFileSync(CONFIG, toJson(cfg, 2));
83
+ print(`wrote ${CONFIG}`, cfg);
84
+ },
85
+ async create() {
86
+ const scope = read(flags.scope, 'Scope file (--scope)');
87
+ const owner = key('OATH_OWNER_KEY'), c = client();
88
+ const unsigned = await Oath.create(c, { ...scope, owner: owner.address });
89
+ const signed = await unsigned.sign(owner);
90
+ const out = flags.out ?? 'oath.json';
91
+ fs.writeFileSync(out, toJson(signed, 2));
92
+ print(`created ${signed.id}\n account ${signed.account}\n expires ${new Date(Number(signed.validUntil) * 1000).toISOString()}\n wrote ${out}`, signed);
93
+ },
94
+ async inspect() {
95
+ const oath = read(arg, 'OATH file'), c = client();
96
+ const [status, spend] = await Promise.all([Oath.verify(c, oath), Policy.remainingSpend(c, oath)]);
97
+ const state = status.revoked ? 'REVOKED' : status.expired ? 'EXPIRED' : status.valid ? 'ACTIVE' : 'INVALID';
98
+ print([`oath ${oath.id}`, ` status ${state}${status.validSignature ? '' : ' (signature does not verify)'}`, ` owner ${oath.owner}`, ` agent ${oath.agent}`, ` account ${oath.account}`,
99
+ ` expires ${new Date(Number(oath.validUntil) * 1000).toISOString()}`, ` targets ${oath.targets.map((t) => `${t.target} [${t.selectors.join(' ')}]`).join('\n ') || 'none'}`,
100
+ ` services ${oath.services.map((s) => `${s.serviceId} [${s.allowedActions.join(' ')}] max ${units(s.maxSpend)}`).join('\n ') || 'none'}`,
101
+ ...spend.map((s) => ` spend ${s.asset} ${units(s.spent)} of ${units(s.limit)} used, ${units(s.remaining)} remaining`)].join('\n'), { oath, status, spend });
102
+ },
103
+ // Offline: planning needs no chain and no keys.
104
+ async build() {
105
+ const actions = read(arg, 'Workflow file'), oath = read(flags.oath, 'OATH file (--oath)');
106
+ const plan = buildPlan(oath, actions, { maxCallsPerBundle: flags['max-calls'] ? Number(flags['max-calls']) : undefined });
107
+ if (flags.out)
108
+ fs.writeFileSync(flags.out, toJson(plan, 2));
109
+ print(describe(plan) + (flags.out ? `\n\nwrote ${flags.out}` : ''), plan);
110
+ },
111
+ async simulate() {
112
+ const actions = read(arg, 'Workflow file'), oath = read(flags.oath, 'OATH file (--oath)');
113
+ const plan = await builder(client(), oath).add(actions).build();
114
+ const sims = await plan.simulate();
115
+ print(describe(plan.toJSON()) + '\n\nsimulation\n' + sims.map((s) => ` ${s.group.padEnd(12)} ${s.deferred ? `deferred (${s.deferred})` : s.success ? `ok gas ${s.gasEstimate}${(s.assetDeltas ?? []).filter((d) => d.delta !== 0n).map((d) => ` ${d.asset} ${d.delta > 0n ? '+' : ''}${d.delta}`).join('')}` : `FAIL ${s.revertReason}`}`).join('\n'), { plan, simulations: sims });
116
+ if (sims.some((s) => !s.success))
117
+ process.exitCode = 1;
118
+ },
119
+ async execute() {
120
+ const actions = read(arg, 'Workflow file'), oath = read(flags.oath, 'OATH file (--oath)');
121
+ const plan = await builder(client(), oath).add(actions).build();
122
+ const result = await plan.execute({ commitReceipts: flags['commit-receipts'] });
123
+ if (flags.out)
124
+ fs.writeFileSync(flags.out, toJson({ ...result, results: [...result.results] }, 2));
125
+ print(describe(plan.toJSON()) + '\n\nexecution\n' + (result.bundles.map((b) => ` ${b.group.padEnd(12)} ${b.status.padEnd(9)} ${b.txHash ?? b.error ?? ''}`).join('\n') || ' no onchain bundles'), { ...result, results: [...result.results] });
126
+ if (result.bundles.some((b) => b.status !== 'EXECUTED'))
127
+ process.exitCode = 1;
128
+ },
129
+ async receipts() {
130
+ const oath = read(arg, 'OATH file');
131
+ const list = await Receipts.get(client(), oath);
132
+ print(list.map((r) => `${r.txHash} bundle ${r.bundleHash.slice(0, 18)}… ${r.succeeded} ok, ${r.failed} failed block ${r.blockNumber}`).join('\n') || 'no executions', list);
133
+ },
134
+ async revoke() {
135
+ const oath = read(arg, 'OATH file');
136
+ const hash = await Oath.revoke(client(), oath.id, key('OATH_OWNER_KEY'));
137
+ print(`revoked ${oath.id}\n tx ${hash}`, { id: oath.id, txHash: hash });
138
+ },
139
+ async contracts() {
140
+ const c = client();
141
+ print(Object.entries(c.addresses).map(([k, v]) => `${k.padEnd(22)} ${v}`).join('\n') + `\nchain ${c.chainId}`, { chainId: c.chainId, ...c.addresses });
142
+ },
143
+ // Not in the handoff's command list, but needed once before a first OATH can execute.
144
+ async 'deploy-account'() {
145
+ const owner = key('OATH_OWNER_KEY');
146
+ print(`account ${await deployAccount(client(), owner, owner.address)}`, {});
147
+ },
148
+ };
149
+ if (flags.help || !command) {
150
+ console.log(HELP);
151
+ process.exit(command || flags.help ? 0 : 1);
152
+ }
153
+ if (!commands[command]) {
154
+ console.error(`Unknown command "${command}".\n\n${HELP}`);
155
+ process.exit(1);
156
+ }
157
+ commands[command]().catch((err) => {
158
+ const known = err instanceof CliError || err instanceof OathError || err instanceof BuildError;
159
+ console.error(`error: ${known ? err.message : err?.shortMessage ?? err?.message ?? err}`);
160
+ process.exit(1);
161
+ });
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "@oathbuild/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "OATH command line: create, inspect, build, simulate, execute and revoke OATHs.",
5
+ "keywords": [
6
+ "oath",
7
+ "agents",
8
+ "transaction-builder",
9
+ "mcp",
10
+ "x402",
11
+ "robinhood-chain",
12
+ "eip-712"
13
+ ],
14
+ "homepage": "https://oathbuild.net/docs",
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "bin": {
18
+ "oath": "./dist/cli.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "!dist/*.tsbuildinfo",
23
+ "!dist/.tsbuildinfo"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "@oathbuild/sdk": "0.1.0",
30
+ "viem": "^2.21.0"
31
+ }
6
32
  }