@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 +26 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +161 -0
- package/package.json +29 -3
package/README.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @oathbuild/cli
|
|
2
2
|
|
|
3
|
-
|
|
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
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|