@mandate.md/cli 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/dist/index.js +493 -0
- package/package.json +21 -0
- package/src/__tests__/credentials.test.ts +83 -0
- package/src/__tests__/event.test.ts +73 -0
- package/src/__tests__/login.test.ts +76 -0
- package/src/__tests__/status.test.ts +128 -0
- package/src/__tests__/transfer.test.ts +99 -0
- package/src/__tests__/validate.test.ts +200 -0
- package/src/commands/activate.ts +41 -0
- package/src/commands/approve.ts +39 -0
- package/src/commands/event.ts +25 -0
- package/src/commands/login.ts +55 -0
- package/src/commands/status.ts +28 -0
- package/src/commands/transfer.ts +111 -0
- package/src/commands/types.ts +10 -0
- package/src/commands/validate.ts +101 -0
- package/src/commands/whoami.ts +17 -0
- package/src/credentials.ts +48 -0
- package/src/index.ts +61 -0
- package/src/middleware.ts +14 -0
- package/src/vars.ts +10 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +9 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Cli } from "incur";
|
|
5
|
+
|
|
6
|
+
// src/vars.ts
|
|
7
|
+
import { z } from "incur";
|
|
8
|
+
var cliVars = z.object({
|
|
9
|
+
client: z.custom(),
|
|
10
|
+
credentials: z.custom()
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// src/middleware.ts
|
|
14
|
+
import { middleware } from "incur";
|
|
15
|
+
import { MandateClient } from "@mandate.md/sdk";
|
|
16
|
+
|
|
17
|
+
// src/credentials.ts
|
|
18
|
+
import * as fs from "fs";
|
|
19
|
+
import * as path from "path";
|
|
20
|
+
import * as os from "os";
|
|
21
|
+
function credentialsPath() {
|
|
22
|
+
return path.join(os.homedir(), ".mandate", "credentials.json");
|
|
23
|
+
}
|
|
24
|
+
function credentialsDir() {
|
|
25
|
+
return path.join(os.homedir(), ".mandate");
|
|
26
|
+
}
|
|
27
|
+
function loadCredentials() {
|
|
28
|
+
const p = credentialsPath();
|
|
29
|
+
if (!fs.existsSync(p)) return null;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function saveCredentials(creds) {
|
|
37
|
+
const dir = credentialsDir();
|
|
38
|
+
if (!fs.existsSync(dir)) {
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
const p = credentialsPath();
|
|
42
|
+
fs.writeFileSync(p, JSON.stringify(creds, null, 2));
|
|
43
|
+
fs.chmodSync(p, 384);
|
|
44
|
+
}
|
|
45
|
+
function updateCredentials(partial) {
|
|
46
|
+
const existing = loadCredentials();
|
|
47
|
+
if (!existing) {
|
|
48
|
+
throw new Error("No existing credentials. Run: mandate login");
|
|
49
|
+
}
|
|
50
|
+
saveCredentials({ ...existing, ...partial });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/middleware.ts
|
|
54
|
+
var requireAuth = middleware((c, next) => {
|
|
55
|
+
const creds = loadCredentials();
|
|
56
|
+
if (!creds?.runtimeKey) {
|
|
57
|
+
return c.error({ code: "NOT_AUTHENTICATED", message: "No credentials. Run: mandate login" });
|
|
58
|
+
}
|
|
59
|
+
c.set("client", new MandateClient({ runtimeKey: creds.runtimeKey, baseUrl: creds.baseUrl }));
|
|
60
|
+
c.set("credentials", creds);
|
|
61
|
+
return next();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// src/commands/login.ts
|
|
65
|
+
import { z as z2 } from "incur";
|
|
66
|
+
import { MandateClient as MandateClient2 } from "@mandate.md/sdk";
|
|
67
|
+
var loginCommand = {
|
|
68
|
+
description: "Register a new agent and store credentials",
|
|
69
|
+
options: z2.object({
|
|
70
|
+
name: z2.string().describe("Agent name"),
|
|
71
|
+
address: z2.string().optional().describe("EVM address (0x...)"),
|
|
72
|
+
perTxLimit: z2.number().optional().describe("Per-transaction USD limit"),
|
|
73
|
+
dailyLimit: z2.number().optional().describe("Daily USD limit"),
|
|
74
|
+
baseUrl: z2.string().optional().describe("Mandate API base URL"),
|
|
75
|
+
chainId: z2.number().optional().describe("Chain ID (default: 84532)")
|
|
76
|
+
}),
|
|
77
|
+
alias: { perTxLimit: "p", dailyLimit: "d" },
|
|
78
|
+
examples: [
|
|
79
|
+
{ options: { name: "MyAgent", address: "0x1234567890abcdef1234567890abcdef12345678" }, description: "Register with address" },
|
|
80
|
+
{ options: { name: "MyAgent" }, description: "Register without address (set later via activate)" }
|
|
81
|
+
],
|
|
82
|
+
async run(c) {
|
|
83
|
+
const { name, address, perTxLimit, dailyLimit, baseUrl, chainId } = c.options;
|
|
84
|
+
const defaultPolicy = {};
|
|
85
|
+
if (perTxLimit !== void 0) defaultPolicy.spendLimitPerTxUsd = perTxLimit;
|
|
86
|
+
if (dailyLimit !== void 0) defaultPolicy.spendLimitPerDayUsd = dailyLimit;
|
|
87
|
+
const result = await MandateClient2.register({
|
|
88
|
+
name,
|
|
89
|
+
evmAddress: address ?? "0x0000000000000000000000000000000000000000",
|
|
90
|
+
chainId: chainId ?? 84532,
|
|
91
|
+
defaultPolicy: Object.keys(defaultPolicy).length ? defaultPolicy : void 0,
|
|
92
|
+
baseUrl
|
|
93
|
+
});
|
|
94
|
+
saveCredentials({
|
|
95
|
+
runtimeKey: result.runtimeKey,
|
|
96
|
+
agentId: result.agentId,
|
|
97
|
+
claimUrl: result.claimUrl,
|
|
98
|
+
evmAddress: result.evmAddress,
|
|
99
|
+
chainId: result.chainId,
|
|
100
|
+
baseUrl
|
|
101
|
+
});
|
|
102
|
+
const masked = result.runtimeKey.slice(0, 14) + "..." + result.runtimeKey.slice(-3);
|
|
103
|
+
return {
|
|
104
|
+
agentId: result.agentId,
|
|
105
|
+
runtimeKey: masked,
|
|
106
|
+
claimUrl: result.claimUrl,
|
|
107
|
+
evmAddress: result.evmAddress || void 0,
|
|
108
|
+
next: "Run: mandate whoami (verify) or mandate validate (first tx)"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/commands/activate.ts
|
|
114
|
+
import { z as z3 } from "incur";
|
|
115
|
+
var activateCommand = {
|
|
116
|
+
description: "Set EVM address after registration",
|
|
117
|
+
args: z3.object({
|
|
118
|
+
address: z3.string().describe("EVM address (0x...)")
|
|
119
|
+
}),
|
|
120
|
+
examples: [
|
|
121
|
+
{ args: { address: "0x1234567890abcdef1234567890abcdef12345678" }, description: "Set wallet address" }
|
|
122
|
+
],
|
|
123
|
+
async run(c) {
|
|
124
|
+
const { address } = c.args;
|
|
125
|
+
const client = c.var.client;
|
|
126
|
+
const res = await fetch(`${c.var.credentials.baseUrl ?? "https://app.mandate.md"}/api/activate`, {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: {
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
"Authorization": `Bearer ${c.var.credentials.runtimeKey}`
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({ evmAddress: address })
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
const data2 = await res.json().catch(() => ({}));
|
|
136
|
+
return c.error({ code: "ACTIVATE_FAILED", message: data2.message ?? "Activation failed" });
|
|
137
|
+
}
|
|
138
|
+
const data = await res.json();
|
|
139
|
+
updateCredentials({ evmAddress: data.evmAddress });
|
|
140
|
+
return {
|
|
141
|
+
activated: true,
|
|
142
|
+
evmAddress: data.evmAddress,
|
|
143
|
+
onboardingUrl: data.onboardingUrl,
|
|
144
|
+
next: "Run: mandate validate (start validating transactions)"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/commands/whoami.ts
|
|
150
|
+
var whoamiCommand = {
|
|
151
|
+
description: "Verify credentials and show agent info",
|
|
152
|
+
async run(c) {
|
|
153
|
+
const creds = c.var.credentials;
|
|
154
|
+
const masked = creds.runtimeKey.slice(0, 14) + "..." + creds.runtimeKey.slice(-3);
|
|
155
|
+
return {
|
|
156
|
+
agentId: creds.agentId,
|
|
157
|
+
evmAddress: creds.evmAddress ?? "not set",
|
|
158
|
+
chainId: creds.chainId ?? 84532,
|
|
159
|
+
keyPrefix: masked,
|
|
160
|
+
baseUrl: creds.baseUrl ?? "https://app.mandate.md"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/commands/validate.ts
|
|
166
|
+
import { z as z4 } from "incur";
|
|
167
|
+
import { computeIntentHash } from "@mandate.md/sdk";
|
|
168
|
+
import { PolicyBlockedError, RiskBlockedError, ApprovalRequiredError } from "@mandate.md/sdk";
|
|
169
|
+
var validateCommand = {
|
|
170
|
+
description: "Policy-check a transaction (computes intentHash automatically)",
|
|
171
|
+
options: z4.object({
|
|
172
|
+
to: z4.string().describe("Recipient address (0x...)"),
|
|
173
|
+
calldata: z4.string().optional().describe("Transaction calldata (default: 0x)"),
|
|
174
|
+
valueWei: z4.string().optional().describe("Value in wei (default: 0)"),
|
|
175
|
+
nonce: z4.number().describe("Transaction nonce"),
|
|
176
|
+
gasLimit: z4.string().describe("Gas limit"),
|
|
177
|
+
maxFeePerGas: z4.string().describe("Max fee per gas (wei)"),
|
|
178
|
+
maxPriorityFeePerGas: z4.string().describe("Max priority fee per gas (wei)"),
|
|
179
|
+
chainId: z4.number().optional().describe("Chain ID (default from credentials)"),
|
|
180
|
+
txType: z4.number().optional().describe("Transaction type (default: 2)"),
|
|
181
|
+
accessList: z4.string().optional().describe("Access list JSON string"),
|
|
182
|
+
reason: z4.string().describe("Why this transaction is being sent")
|
|
183
|
+
}),
|
|
184
|
+
examples: [
|
|
185
|
+
{
|
|
186
|
+
options: {
|
|
187
|
+
to: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
188
|
+
calldata: "0xa9059cbb000000000000000000000000",
|
|
189
|
+
nonce: 42,
|
|
190
|
+
gasLimit: "90000",
|
|
191
|
+
maxFeePerGas: "1000000000",
|
|
192
|
+
maxPriorityFeePerGas: "1000000000",
|
|
193
|
+
reason: "Invoice #127 from Alice"
|
|
194
|
+
},
|
|
195
|
+
description: "Validate an ERC20 transfer"
|
|
196
|
+
}
|
|
197
|
+
],
|
|
198
|
+
async run(c) {
|
|
199
|
+
const client = c.var.client;
|
|
200
|
+
const creds = c.var.credentials;
|
|
201
|
+
const opts = c.options;
|
|
202
|
+
const chainId = opts.chainId ?? creds.chainId ?? 84532;
|
|
203
|
+
const calldata = opts.calldata ?? "0x";
|
|
204
|
+
const valueWei = opts.valueWei ?? "0";
|
|
205
|
+
const txType = opts.txType ?? 2;
|
|
206
|
+
const accessList = opts.accessList ? JSON.parse(opts.accessList) : [];
|
|
207
|
+
const intentHash = computeIntentHash({
|
|
208
|
+
chainId,
|
|
209
|
+
nonce: opts.nonce,
|
|
210
|
+
to: opts.to,
|
|
211
|
+
calldata,
|
|
212
|
+
valueWei,
|
|
213
|
+
gasLimit: opts.gasLimit,
|
|
214
|
+
maxFeePerGas: opts.maxFeePerGas,
|
|
215
|
+
maxPriorityFeePerGas: opts.maxPriorityFeePerGas,
|
|
216
|
+
txType,
|
|
217
|
+
accessList
|
|
218
|
+
});
|
|
219
|
+
try {
|
|
220
|
+
const result = await client.validate({
|
|
221
|
+
chainId,
|
|
222
|
+
nonce: opts.nonce,
|
|
223
|
+
to: opts.to,
|
|
224
|
+
calldata,
|
|
225
|
+
valueWei,
|
|
226
|
+
gasLimit: opts.gasLimit,
|
|
227
|
+
maxFeePerGas: opts.maxFeePerGas,
|
|
228
|
+
maxPriorityFeePerGas: opts.maxPriorityFeePerGas,
|
|
229
|
+
txType,
|
|
230
|
+
accessList,
|
|
231
|
+
intentHash,
|
|
232
|
+
reason: opts.reason
|
|
233
|
+
});
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
intentId: result.intentId,
|
|
237
|
+
feedback: "\u2705 Mandate: policy check passed",
|
|
238
|
+
next: `Run: mandate event ${result.intentId} --tx-hash 0x...`
|
|
239
|
+
};
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (err instanceof PolicyBlockedError || err instanceof RiskBlockedError) {
|
|
242
|
+
return {
|
|
243
|
+
error: "POLICY_BLOCKED",
|
|
244
|
+
message: `\u{1F6AB} Mandate: blocked \u2014 ${err.message}`,
|
|
245
|
+
blockReason: err.blockReason
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (err instanceof ApprovalRequiredError) {
|
|
249
|
+
return {
|
|
250
|
+
ok: true,
|
|
251
|
+
requiresApproval: true,
|
|
252
|
+
intentId: err.intentId,
|
|
253
|
+
feedback: "\u23F3 Mandate: approval required \u2014 waiting for owner decision",
|
|
254
|
+
next: `Run: mandate approve ${err.intentId}`
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// src/commands/transfer.ts
|
|
263
|
+
import { z as z5 } from "incur";
|
|
264
|
+
import { encodeFunctionData, parseAbi } from "viem";
|
|
265
|
+
import { computeIntentHash as computeIntentHash2 } from "@mandate.md/sdk";
|
|
266
|
+
import { PolicyBlockedError as PolicyBlockedError2, RiskBlockedError as RiskBlockedError2, ApprovalRequiredError as ApprovalRequiredError2 } from "@mandate.md/sdk";
|
|
267
|
+
var erc20Abi = parseAbi(["function transfer(address to, uint256 amount) returns (bool)"]);
|
|
268
|
+
var transferCommand = {
|
|
269
|
+
description: "ERC20 transfer with automatic calldata encoding and validation",
|
|
270
|
+
options: z5.object({
|
|
271
|
+
to: z5.string().describe("Recipient address (0x...)"),
|
|
272
|
+
amount: z5.string().describe("Amount in raw token units"),
|
|
273
|
+
token: z5.string().describe("ERC20 token contract address (0x...)"),
|
|
274
|
+
reason: z5.string().describe("Why this transfer is being sent"),
|
|
275
|
+
chainId: z5.number().optional().describe("Chain ID (default from credentials)"),
|
|
276
|
+
nonce: z5.number().describe("Transaction nonce"),
|
|
277
|
+
gasLimit: z5.string().optional().describe("Gas limit (default: 65000)"),
|
|
278
|
+
maxFeePerGas: z5.string().describe("Max fee per gas (wei)"),
|
|
279
|
+
maxPriorityFeePerGas: z5.string().describe("Max priority fee per gas (wei)")
|
|
280
|
+
}),
|
|
281
|
+
examples: [
|
|
282
|
+
{
|
|
283
|
+
options: {
|
|
284
|
+
to: "0xAlice",
|
|
285
|
+
amount: "10000000",
|
|
286
|
+
token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
287
|
+
reason: "Invoice #127",
|
|
288
|
+
nonce: 42,
|
|
289
|
+
maxFeePerGas: "1000000000",
|
|
290
|
+
maxPriorityFeePerGas: "1000000000"
|
|
291
|
+
},
|
|
292
|
+
description: "Transfer 10 USDC"
|
|
293
|
+
}
|
|
294
|
+
],
|
|
295
|
+
async run(c) {
|
|
296
|
+
const client = c.var.client;
|
|
297
|
+
const creds = c.var.credentials;
|
|
298
|
+
const opts = c.options;
|
|
299
|
+
const chainId = opts.chainId ?? creds.chainId ?? 84532;
|
|
300
|
+
const gasLimit = opts.gasLimit ?? "65000";
|
|
301
|
+
const calldata = encodeFunctionData({
|
|
302
|
+
abi: erc20Abi,
|
|
303
|
+
functionName: "transfer",
|
|
304
|
+
args: [opts.to, BigInt(opts.amount)]
|
|
305
|
+
});
|
|
306
|
+
const intentHash = computeIntentHash2({
|
|
307
|
+
chainId,
|
|
308
|
+
nonce: opts.nonce,
|
|
309
|
+
to: opts.token,
|
|
310
|
+
calldata,
|
|
311
|
+
valueWei: "0",
|
|
312
|
+
gasLimit,
|
|
313
|
+
maxFeePerGas: opts.maxFeePerGas,
|
|
314
|
+
maxPriorityFeePerGas: opts.maxPriorityFeePerGas
|
|
315
|
+
});
|
|
316
|
+
try {
|
|
317
|
+
const result = await client.validate({
|
|
318
|
+
chainId,
|
|
319
|
+
nonce: opts.nonce,
|
|
320
|
+
to: opts.token,
|
|
321
|
+
calldata,
|
|
322
|
+
valueWei: "0",
|
|
323
|
+
gasLimit,
|
|
324
|
+
maxFeePerGas: opts.maxFeePerGas,
|
|
325
|
+
maxPriorityFeePerGas: opts.maxPriorityFeePerGas,
|
|
326
|
+
intentHash,
|
|
327
|
+
reason: opts.reason
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
ok: true,
|
|
331
|
+
intentId: result.intentId,
|
|
332
|
+
feedback: "\u2705 Mandate: policy check passed",
|
|
333
|
+
unsignedTx: {
|
|
334
|
+
to: opts.token,
|
|
335
|
+
calldata,
|
|
336
|
+
value: "0",
|
|
337
|
+
gasLimit,
|
|
338
|
+
maxFeePerGas: opts.maxFeePerGas,
|
|
339
|
+
maxPriorityFeePerGas: opts.maxPriorityFeePerGas,
|
|
340
|
+
nonce: opts.nonce,
|
|
341
|
+
chainId
|
|
342
|
+
},
|
|
343
|
+
next: `Run: mandate event ${result.intentId} --tx-hash 0x...`
|
|
344
|
+
};
|
|
345
|
+
} catch (err) {
|
|
346
|
+
if (err instanceof PolicyBlockedError2 || err instanceof RiskBlockedError2) {
|
|
347
|
+
return {
|
|
348
|
+
error: "POLICY_BLOCKED",
|
|
349
|
+
message: `\u{1F6AB} Mandate: blocked \u2014 ${err.message}`,
|
|
350
|
+
blockReason: err.blockReason
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (err instanceof ApprovalRequiredError2) {
|
|
354
|
+
return {
|
|
355
|
+
ok: true,
|
|
356
|
+
requiresApproval: true,
|
|
357
|
+
intentId: err.intentId,
|
|
358
|
+
feedback: "\u23F3 Mandate: approval required \u2014 waiting for owner decision",
|
|
359
|
+
next: `Run: mandate approve ${err.intentId}`
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
throw err;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// src/commands/event.ts
|
|
368
|
+
import { z as z6 } from "incur";
|
|
369
|
+
var eventCommand = {
|
|
370
|
+
description: "Post txHash after signing and broadcasting",
|
|
371
|
+
args: z6.object({
|
|
372
|
+
intentId: z6.string().describe("Intent ID from validate")
|
|
373
|
+
}),
|
|
374
|
+
options: z6.object({
|
|
375
|
+
txHash: z6.string().describe("Transaction hash (0x...)")
|
|
376
|
+
}),
|
|
377
|
+
examples: [
|
|
378
|
+
{ args: { intentId: "uuid-1" }, options: { txHash: "0xabc123" }, description: "Post transaction hash" }
|
|
379
|
+
],
|
|
380
|
+
async run(c) {
|
|
381
|
+
const client = c.var.client;
|
|
382
|
+
await client.postEvent(c.args.intentId, c.options.txHash);
|
|
383
|
+
return {
|
|
384
|
+
posted: true,
|
|
385
|
+
intentId: c.args.intentId,
|
|
386
|
+
next: `Run: mandate status ${c.args.intentId}`
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/commands/status.ts
|
|
392
|
+
import { z as z7 } from "incur";
|
|
393
|
+
var CTA = {
|
|
394
|
+
reserved: "Run: mandate event <intentId> --tx-hash 0x...",
|
|
395
|
+
approval_pending: "Run: mandate approve <intentId>",
|
|
396
|
+
broadcasted: "Run: mandate status <intentId> (poll again)"
|
|
397
|
+
};
|
|
398
|
+
var statusCommand = {
|
|
399
|
+
description: "Check intent state",
|
|
400
|
+
args: z7.object({
|
|
401
|
+
intentId: z7.string().describe("Intent ID")
|
|
402
|
+
}),
|
|
403
|
+
examples: [
|
|
404
|
+
{ args: { intentId: "uuid-1" }, description: "Check status of an intent" }
|
|
405
|
+
],
|
|
406
|
+
async run(c) {
|
|
407
|
+
const client = c.var.client;
|
|
408
|
+
const status = await client.getStatus(c.args.intentId);
|
|
409
|
+
const result = { ...status };
|
|
410
|
+
const cta = CTA[status.status];
|
|
411
|
+
if (cta) result.next = cta;
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// src/commands/approve.ts
|
|
417
|
+
import { z as z8 } from "incur";
|
|
418
|
+
var approveCommand = {
|
|
419
|
+
description: "Wait for owner approval on a pending intent",
|
|
420
|
+
args: z8.object({
|
|
421
|
+
intentId: z8.string().describe("Intent ID awaiting approval")
|
|
422
|
+
}),
|
|
423
|
+
options: z8.object({
|
|
424
|
+
timeout: z8.number().optional().describe("Timeout in seconds (default: 3600)")
|
|
425
|
+
}),
|
|
426
|
+
examples: [
|
|
427
|
+
{ args: { intentId: "uuid-1" }, description: "Wait for approval" }
|
|
428
|
+
],
|
|
429
|
+
async run(c) {
|
|
430
|
+
const client = c.var.client;
|
|
431
|
+
const timeoutMs = (c.options.timeout ?? 3600) * 1e3;
|
|
432
|
+
const status = await client.waitForApproval(c.args.intentId, {
|
|
433
|
+
timeoutMs,
|
|
434
|
+
onPoll: () => {
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
if (status.status === "approved" || status.status === "confirmed") {
|
|
438
|
+
return {
|
|
439
|
+
status: "approved",
|
|
440
|
+
intentId: c.args.intentId,
|
|
441
|
+
feedback: "\u2705 Approved \u2014 ready to broadcast",
|
|
442
|
+
next: `Run: mandate event ${c.args.intentId} --tx-hash 0x...`
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
status: status.status,
|
|
447
|
+
intentId: c.args.intentId,
|
|
448
|
+
feedback: `Intent ended with status: ${status.status}`
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// src/index.ts
|
|
454
|
+
var cli = Cli.create("mandate", {
|
|
455
|
+
version: "0.1.0",
|
|
456
|
+
description: "Non-custodial agent wallet policy layer. Validate transactions against spend limits, allowlists, and approval workflows \u2014 without ever touching private keys.",
|
|
457
|
+
vars: cliVars,
|
|
458
|
+
sync: {
|
|
459
|
+
suggestions: [
|
|
460
|
+
"register a new agent with mandate login",
|
|
461
|
+
"validate a transaction before signing",
|
|
462
|
+
"check intent status after broadcasting"
|
|
463
|
+
]
|
|
464
|
+
}
|
|
465
|
+
}).command("login", {
|
|
466
|
+
...loginCommand
|
|
467
|
+
}).command("activate", {
|
|
468
|
+
...activateCommand,
|
|
469
|
+
middleware: [requireAuth]
|
|
470
|
+
}).command("whoami", {
|
|
471
|
+
...whoamiCommand,
|
|
472
|
+
middleware: [requireAuth]
|
|
473
|
+
}).command("validate", {
|
|
474
|
+
...validateCommand,
|
|
475
|
+
middleware: [requireAuth]
|
|
476
|
+
}).command("transfer", {
|
|
477
|
+
...transferCommand,
|
|
478
|
+
middleware: [requireAuth]
|
|
479
|
+
}).command("event", {
|
|
480
|
+
...eventCommand,
|
|
481
|
+
middleware: [requireAuth]
|
|
482
|
+
}).command("status", {
|
|
483
|
+
...statusCommand,
|
|
484
|
+
middleware: [requireAuth]
|
|
485
|
+
}).command("approve", {
|
|
486
|
+
...approveCommand,
|
|
487
|
+
middleware: [requireAuth]
|
|
488
|
+
});
|
|
489
|
+
cli.serve();
|
|
490
|
+
var index_default = cli;
|
|
491
|
+
export {
|
|
492
|
+
index_default as default
|
|
493
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mandate.md/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": { "mandate": "./dist/index.js" },
|
|
6
|
+
"exports": { ".": { "import": "./dist/index.js" } },
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsup",
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@mandate.md/sdk": "workspace:*",
|
|
13
|
+
"viem": "^2.0.0",
|
|
14
|
+
"incur": "^0.3.4"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"tsup": "^8.0.0",
|
|
18
|
+
"typescript": "^5.0.0",
|
|
19
|
+
"vitest": "^2.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { loadCredentials, saveCredentials, updateCredentials } from '../credentials.js';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
|
|
7
|
+
vi.mock('node:fs');
|
|
8
|
+
vi.mock('node:os');
|
|
9
|
+
|
|
10
|
+
const CREDS_DIR = '/home/test/.mandate';
|
|
11
|
+
const CREDS_PATH = '/home/test/.mandate/credentials.json';
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.mocked(os.homedir).mockReturnValue('/home/test');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('loadCredentials', () => {
|
|
22
|
+
it('returns null when file does not exist', () => {
|
|
23
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
24
|
+
expect(loadCredentials()).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns parsed credentials when file exists', () => {
|
|
28
|
+
const creds = { runtimeKey: 'mndt_test_abc', agentId: 'uuid-1', claimUrl: 'http://x' };
|
|
29
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
30
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(creds));
|
|
31
|
+
|
|
32
|
+
const result = loadCredentials();
|
|
33
|
+
expect(result).toEqual(creds);
|
|
34
|
+
expect(fs.readFileSync).toHaveBeenCalledWith(CREDS_PATH, 'utf-8');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns null on parse error', () => {
|
|
38
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
39
|
+
vi.mocked(fs.readFileSync).mockReturnValue('not json');
|
|
40
|
+
|
|
41
|
+
expect(loadCredentials()).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('saveCredentials', () => {
|
|
46
|
+
it('creates directory and writes file with chmod 600', () => {
|
|
47
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
48
|
+
vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
|
|
49
|
+
vi.mocked(fs.writeFileSync).mockReturnValue(undefined);
|
|
50
|
+
vi.mocked(fs.chmodSync).mockReturnValue(undefined);
|
|
51
|
+
|
|
52
|
+
const creds = { runtimeKey: 'mndt_test_abc', agentId: 'uuid-1', claimUrl: 'http://x' };
|
|
53
|
+
saveCredentials(creds);
|
|
54
|
+
|
|
55
|
+
expect(fs.mkdirSync).toHaveBeenCalledWith(CREDS_DIR, { recursive: true });
|
|
56
|
+
expect(fs.writeFileSync).toHaveBeenCalledWith(CREDS_PATH, JSON.stringify(creds, null, 2));
|
|
57
|
+
expect(fs.chmodSync).toHaveBeenCalledWith(CREDS_PATH, 0o600);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('updateCredentials', () => {
|
|
62
|
+
it('merges partial into existing credentials', () => {
|
|
63
|
+
const existing = { runtimeKey: 'mndt_test_abc', agentId: 'uuid-1', claimUrl: 'http://x' };
|
|
64
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
65
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(existing));
|
|
66
|
+
vi.mocked(fs.writeFileSync).mockReturnValue(undefined);
|
|
67
|
+
vi.mocked(fs.chmodSync).mockReturnValue(undefined);
|
|
68
|
+
vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
|
|
69
|
+
vi.mocked(os.homedir).mockReturnValue('/home/test');
|
|
70
|
+
|
|
71
|
+
updateCredentials({ evmAddress: '0xabc' });
|
|
72
|
+
|
|
73
|
+
const written = JSON.parse(vi.mocked(fs.writeFileSync).mock.calls[0][1] as string);
|
|
74
|
+
expect(written.runtimeKey).toBe('mndt_test_abc');
|
|
75
|
+
expect(written.evmAddress).toBe('0xabc');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('throws when no existing credentials', () => {
|
|
79
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
80
|
+
|
|
81
|
+
expect(() => updateCredentials({ evmAddress: '0xabc' })).toThrow();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node:fs');
|
|
4
|
+
vi.mock('node:os');
|
|
5
|
+
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as os from 'node:os';
|
|
8
|
+
|
|
9
|
+
const CREDS = {
|
|
10
|
+
runtimeKey: 'mndt_test_abc123',
|
|
11
|
+
agentId: 'uuid-1',
|
|
12
|
+
claimUrl: 'http://x',
|
|
13
|
+
chainId: 84532,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.restoreAllMocks();
|
|
18
|
+
vi.mocked(os.homedir).mockReturnValue('/home/test');
|
|
19
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
20
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(CREDS));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('mandate event', () => {
|
|
24
|
+
it('posts txHash and returns success', async () => {
|
|
25
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
26
|
+
ok: true,
|
|
27
|
+
status: 200,
|
|
28
|
+
json: () => Promise.resolve({ status: 'broadcasted' }),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const { default: cli } = await import('../index.js');
|
|
32
|
+
|
|
33
|
+
let output = '';
|
|
34
|
+
await cli.serve(['event', 'intent-1', '--tx-hash', '0xdeadbeef'], {
|
|
35
|
+
stdout(s: string) { output += s; },
|
|
36
|
+
exit() {},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(output).toContain('posted');
|
|
40
|
+
expect(output).toContain('true');
|
|
41
|
+
expect(output).toContain('intent-1');
|
|
42
|
+
expect(output).toContain('mandate status');
|
|
43
|
+
|
|
44
|
+
vi.unstubAllGlobals();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('sends correct POST request', async () => {
|
|
48
|
+
const fetchSpy = vi.fn().mockResolvedValue({
|
|
49
|
+
ok: true,
|
|
50
|
+
status: 200,
|
|
51
|
+
json: () => Promise.resolve({ status: 'broadcasted' }),
|
|
52
|
+
});
|
|
53
|
+
vi.stubGlobal('fetch', fetchSpy);
|
|
54
|
+
|
|
55
|
+
const { default: cli } = await import('../index.js');
|
|
56
|
+
|
|
57
|
+
await cli.serve(['event', 'intent-1', '--tx-hash', '0xdeadbeef'], {
|
|
58
|
+
stdout() {},
|
|
59
|
+
exit() {},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Verify the POST to /api/intents/intent-1/events
|
|
63
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
64
|
+
expect.stringContaining('/api/intents/intent-1/events'),
|
|
65
|
+
expect.objectContaining({ method: 'POST' }),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
|
|
69
|
+
expect(body.txHash).toBe('0xdeadbeef');
|
|
70
|
+
|
|
71
|
+
vi.unstubAllGlobals();
|
|
72
|
+
});
|
|
73
|
+
});
|