@absol-labs/agent 0.9.0 → 0.10.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.
Files changed (216) hide show
  1. package/README.md +77 -3
  2. package/dist/cli/amounts.d.ts +26 -0
  3. package/dist/cli/amounts.d.ts.map +1 -0
  4. package/dist/cli/amounts.js +71 -0
  5. package/dist/cli/amounts.js.map +1 -0
  6. package/dist/cli/args.d.ts +32 -0
  7. package/dist/cli/args.d.ts.map +1 -0
  8. package/dist/cli/args.js +119 -0
  9. package/dist/cli/args.js.map +1 -0
  10. package/dist/cli/attesters.d.ts +51 -0
  11. package/dist/cli/attesters.d.ts.map +1 -0
  12. package/dist/cli/attesters.js +58 -0
  13. package/dist/cli/attesters.js.map +1 -0
  14. package/dist/cli/bin.d.ts +3 -0
  15. package/dist/cli/bin.d.ts.map +1 -0
  16. package/dist/cli/bin.js +5 -0
  17. package/dist/cli/bin.js.map +1 -0
  18. package/dist/cli/commands/claim.d.ts +13 -0
  19. package/dist/cli/commands/claim.d.ts.map +1 -0
  20. package/dist/cli/commands/claim.js +86 -0
  21. package/dist/cli/commands/claim.js.map +1 -0
  22. package/dist/cli/commands/close.d.ts +3 -0
  23. package/dist/cli/commands/close.d.ts.map +1 -0
  24. package/dist/cli/commands/close.js +43 -0
  25. package/dist/cli/commands/close.js.map +1 -0
  26. package/dist/cli/commands/discover.d.ts +3 -0
  27. package/dist/cli/commands/discover.d.ts.map +1 -0
  28. package/dist/cli/commands/discover.js +183 -0
  29. package/dist/cli/commands/discover.js.map +1 -0
  30. package/dist/cli/commands/invoke.d.ts +3 -0
  31. package/dist/cli/commands/invoke.d.ts.map +1 -0
  32. package/dist/cli/commands/invoke.js +141 -0
  33. package/dist/cli/commands/invoke.js.map +1 -0
  34. package/dist/cli/commands/mcp.d.ts +3 -0
  35. package/dist/cli/commands/mcp.d.ts.map +1 -0
  36. package/dist/cli/commands/mcp.js +113 -0
  37. package/dist/cli/commands/mcp.js.map +1 -0
  38. package/dist/cli/commands/open.d.ts +3 -0
  39. package/dist/cli/commands/open.d.ts.map +1 -0
  40. package/dist/cli/commands/open.js +279 -0
  41. package/dist/cli/commands/open.js.map +1 -0
  42. package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
  43. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
  44. package/dist/cli/commands/reclaim-unverified.js +164 -0
  45. package/dist/cli/commands/reclaim-unverified.js.map +1 -0
  46. package/dist/cli/commands/reclaim.d.ts +33 -0
  47. package/dist/cli/commands/reclaim.d.ts.map +1 -0
  48. package/dist/cli/commands/reclaim.js +297 -0
  49. package/dist/cli/commands/reclaim.js.map +1 -0
  50. package/dist/cli/commands/status.d.ts +3 -0
  51. package/dist/cli/commands/status.d.ts.map +1 -0
  52. package/dist/cli/commands/status.js +43 -0
  53. package/dist/cli/commands/status.js.map +1 -0
  54. package/dist/cli/commands/streams.d.ts +3 -0
  55. package/dist/cli/commands/streams.d.ts.map +1 -0
  56. package/dist/cli/commands/streams.js +257 -0
  57. package/dist/cli/commands/streams.js.map +1 -0
  58. package/dist/cli/commands/watch.d.ts +3 -0
  59. package/dist/cli/commands/watch.d.ts.map +1 -0
  60. package/dist/cli/commands/watch.js +55 -0
  61. package/dist/cli/commands/watch.js.map +1 -0
  62. package/dist/cli/config.d.ts +61 -0
  63. package/dist/cli/config.d.ts.map +1 -0
  64. package/dist/cli/config.js +140 -0
  65. package/dist/cli/config.js.map +1 -0
  66. package/dist/cli/context.d.ts +29 -0
  67. package/dist/cli/context.d.ts.map +1 -0
  68. package/dist/cli/context.js +2 -0
  69. package/dist/cli/context.js.map +1 -0
  70. package/dist/cli/deps.d.ts +145 -0
  71. package/dist/cli/deps.d.ts.map +1 -0
  72. package/dist/cli/deps.js +227 -0
  73. package/dist/cli/deps.js.map +1 -0
  74. package/dist/cli/descriptor.d.ts +61 -0
  75. package/dist/cli/descriptor.d.ts.map +1 -0
  76. package/dist/cli/descriptor.js +78 -0
  77. package/dist/cli/descriptor.js.map +1 -0
  78. package/dist/cli/errors.d.ts +48 -0
  79. package/dist/cli/errors.d.ts.map +1 -0
  80. package/dist/cli/errors.js +129 -0
  81. package/dist/cli/errors.js.map +1 -0
  82. package/dist/cli/escape-hatch.d.ts +50 -0
  83. package/dist/cli/escape-hatch.d.ts.map +1 -0
  84. package/dist/cli/escape-hatch.js +45 -0
  85. package/dist/cli/escape-hatch.js.map +1 -0
  86. package/dist/cli/index.d.ts +8 -0
  87. package/dist/cli/index.d.ts.map +1 -0
  88. package/dist/cli/index.js +7 -0
  89. package/dist/cli/index.js.map +1 -0
  90. package/dist/cli/listings.d.ts +15 -0
  91. package/dist/cli/listings.d.ts.map +1 -0
  92. package/dist/cli/listings.js +56 -0
  93. package/dist/cli/listings.js.map +1 -0
  94. package/dist/cli/mandate.d.ts +66 -0
  95. package/dist/cli/mandate.d.ts.map +1 -0
  96. package/dist/cli/mandate.js +61 -0
  97. package/dist/cli/mandate.js.map +1 -0
  98. package/dist/cli/output.d.ts +33 -0
  99. package/dist/cli/output.d.ts.map +1 -0
  100. package/dist/cli/output.js +34 -0
  101. package/dist/cli/output.js.map +1 -0
  102. package/dist/cli/preflight.d.ts +38 -0
  103. package/dist/cli/preflight.d.ts.map +1 -0
  104. package/dist/cli/preflight.js +72 -0
  105. package/dist/cli/preflight.js.map +1 -0
  106. package/dist/cli/progress.d.ts +36 -0
  107. package/dist/cli/progress.d.ts.map +1 -0
  108. package/dist/cli/progress.js +40 -0
  109. package/dist/cli/progress.js.map +1 -0
  110. package/dist/cli/run.d.ts +25 -0
  111. package/dist/cli/run.d.ts.map +1 -0
  112. package/dist/cli/run.js +173 -0
  113. package/dist/cli/run.js.map +1 -0
  114. package/dist/cli/scan-range.d.ts +55 -0
  115. package/dist/cli/scan-range.d.ts.map +1 -0
  116. package/dist/cli/scan-range.js +57 -0
  117. package/dist/cli/scan-range.js.map +1 -0
  118. package/dist/cli/status.d.ts +90 -0
  119. package/dist/cli/status.d.ts.map +1 -0
  120. package/dist/cli/status.js +252 -0
  121. package/dist/cli/status.js.map +1 -0
  122. package/dist/cli/timeout.d.ts +30 -0
  123. package/dist/cli/timeout.d.ts.map +1 -0
  124. package/dist/cli/timeout.js +55 -0
  125. package/dist/cli/timeout.js.map +1 -0
  126. package/dist/cli/version.d.ts +12 -0
  127. package/dist/cli/version.d.ts.map +1 -0
  128. package/dist/cli/version.js +22 -0
  129. package/dist/cli/version.js.map +1 -0
  130. package/dist/cli/wallet.d.ts +95 -0
  131. package/dist/cli/wallet.d.ts.map +1 -0
  132. package/dist/cli/wallet.js +169 -0
  133. package/dist/cli/wallet.js.map +1 -0
  134. package/dist/frameworks/agentkit.d.ts.map +1 -1
  135. package/dist/frameworks/agentkit.js +8 -4
  136. package/dist/frameworks/agentkit.js.map +1 -1
  137. package/dist/frameworks/crewai.d.ts +10 -0
  138. package/dist/frameworks/crewai.d.ts.map +1 -1
  139. package/dist/frameworks/crewai.js +12 -2
  140. package/dist/frameworks/crewai.js.map +1 -1
  141. package/dist/frameworks/eliza.d.ts.map +1 -1
  142. package/dist/frameworks/eliza.js +14 -3
  143. package/dist/frameworks/eliza.js.map +1 -1
  144. package/dist/frameworks/langchain.d.ts.map +1 -1
  145. package/dist/frameworks/langchain.js +14 -3
  146. package/dist/frameworks/langchain.js.map +1 -1
  147. package/dist/gateway/server-entry.d.ts +1 -0
  148. package/dist/gateway/server-entry.js +1 -0
  149. package/dist/gateway/server-entry.js.map +1 -1
  150. package/dist/index.d.ts +1 -1
  151. package/dist/index.d.ts.map +1 -1
  152. package/dist/index.js +1 -1
  153. package/dist/index.js.map +1 -1
  154. package/dist/mcp/http-server.d.ts +1 -0
  155. package/dist/mcp/http-server.js +1 -0
  156. package/dist/mcp/http-server.js.map +1 -1
  157. package/dist/mcp/server.d.ts.map +1 -1
  158. package/dist/mcp/server.js +46 -6
  159. package/dist/mcp/server.js.map +1 -1
  160. package/dist/mcp/stdio.d.ts +1 -0
  161. package/dist/mcp/stdio.js +1 -0
  162. package/dist/mcp/stdio.js.map +1 -1
  163. package/dist/sdk/client.d.ts +44 -3
  164. package/dist/sdk/client.d.ts.map +1 -1
  165. package/dist/sdk/client.js +38 -3
  166. package/dist/sdk/client.js.map +1 -1
  167. package/docs/agent-layer.md +2 -1
  168. package/docs/cli.md +569 -0
  169. package/docs/crewai.md +6 -2
  170. package/docs/eliza.md +6 -1
  171. package/docs/mcp-hosted.md +3 -2
  172. package/docs/quickstart.md +18 -5
  173. package/package.json +17 -5
  174. package/src/cli/amounts.ts +102 -0
  175. package/src/cli/args.ts +150 -0
  176. package/src/cli/attesters.ts +86 -0
  177. package/src/cli/bin.ts +5 -0
  178. package/src/cli/commands/claim.ts +100 -0
  179. package/src/cli/commands/close.ts +53 -0
  180. package/src/cli/commands/discover.ts +228 -0
  181. package/src/cli/commands/invoke.ts +177 -0
  182. package/src/cli/commands/mcp.ts +135 -0
  183. package/src/cli/commands/open.ts +331 -0
  184. package/src/cli/commands/reclaim-unverified.ts +206 -0
  185. package/src/cli/commands/reclaim.ts +360 -0
  186. package/src/cli/commands/status.ts +56 -0
  187. package/src/cli/commands/streams.ts +344 -0
  188. package/src/cli/commands/watch.ts +70 -0
  189. package/src/cli/config.ts +208 -0
  190. package/src/cli/context.ts +30 -0
  191. package/src/cli/deps.ts +437 -0
  192. package/src/cli/descriptor.ts +133 -0
  193. package/src/cli/errors.ts +162 -0
  194. package/src/cli/escape-hatch.ts +92 -0
  195. package/src/cli/index.ts +27 -0
  196. package/src/cli/listings.ts +77 -0
  197. package/src/cli/mandate.ts +126 -0
  198. package/src/cli/output.ts +63 -0
  199. package/src/cli/preflight.ts +88 -0
  200. package/src/cli/progress.ts +75 -0
  201. package/src/cli/run.ts +209 -0
  202. package/src/cli/scan-range.ts +76 -0
  203. package/src/cli/status.ts +381 -0
  204. package/src/cli/timeout.ts +64 -0
  205. package/src/cli/version.ts +21 -0
  206. package/src/cli/wallet.ts +241 -0
  207. package/src/frameworks/agentkit.ts +8 -3
  208. package/src/frameworks/crewai.ts +13 -2
  209. package/src/frameworks/eliza.ts +14 -3
  210. package/src/frameworks/langchain.ts +14 -3
  211. package/src/gateway/server-entry.ts +1 -0
  212. package/src/index.ts +2 -0
  213. package/src/mcp/http-server.ts +1 -0
  214. package/src/mcp/server.ts +51 -6
  215. package/src/mcp/stdio.ts +1 -0
  216. package/src/sdk/client.ts +71 -4
@@ -0,0 +1,241 @@
1
+ import { privateKeyToAccount } from "viem/accounts";
2
+ import type { Account, LocalAccount } from "viem";
3
+
4
+ import { resolveAgentWallet } from "../wallet/provider.js";
5
+ import { configError } from "./errors.js";
6
+
7
+ /**
8
+ * The ONLY place the CLI resolves a signing wallet.
9
+ *
10
+ * There is deliberately NO `--private-key` flag, and no flag for any other
11
+ * credential. A secret passed on the command line lands in shell history, in
12
+ * `ps` output, and in any CI log that echoes the command; an environment
13
+ * variable does none of those. Secrets are also never echoed back: commands
14
+ * print the derived ADDRESS only.
15
+ *
16
+ * ## Resolution order (first match wins, checked in this order)
17
+ *
18
+ * 1. `METRIK_AGENT_PRIVATE_KEY` — a raw Base Sepolia test key.
19
+ * 2. The `CDP_*` / `METRIK_AGENT_CDP_*` quartet — a Coinbase CDP Server Wallet
20
+ * v2 account, resolved through this package's own `resolveAgentWallet()`.
21
+ *
22
+ * The order matches `parseAgentWalletEnv()` (the MCP server's resolution), so
23
+ * one environment configures every surface the same way. It is checked in a
24
+ * FIXED order rather than "whichever is configured", because an environment
25
+ * carrying both is ambiguous about which address funds the stream — and that
26
+ * ambiguity is a fund-moving one. When both are present the private key wins
27
+ * and `resolveCliWallet` says so on stderr rather than silently picking.
28
+ *
29
+ * ## Why CDP is wired and Privy is not
30
+ *
31
+ * CDP is the only wallet path in this package with live CI evidence
32
+ * (`scripts/e2e-cdp.ts` drives the full journey against the real Base Sepolia
33
+ * escrow), and its account signs LOCALLY — `toAccount(cdpServerAccount)` is a
34
+ * viem local account, so it drops straight into the CLI's existing
35
+ * `http(rpcUrl)` transport with no other change.
36
+ *
37
+ * Privy does not. Both Privy adapters in `src/wallet/` produce a JSON-RPC
38
+ * account with no `signTransaction`: they sign and broadcast by sending
39
+ * `eth_sendTransaction` / `eth_signTypedData_v4` to a provider, so they need the
40
+ * wallet client's TRANSPORT to be that provider rather than a plain RPC. Wiring
41
+ * that into the CLI is not a matter of reading three more variables — it
42
+ * changes the transport for every fund-moving command, and `metrik invoke`
43
+ * additionally requires a local signer (`InvokeOptions.buyer: LocalAccount`).
44
+ * `assertLocalSigner` below therefore REFUSES a non-local account with that
45
+ * fact, instead of accepting one and failing later inside viem. See
46
+ * `docs/cli.md` § Wallet.
47
+ */
48
+ export const PRIVATE_KEY_ENV = "METRIK_AGENT_PRIVATE_KEY";
49
+
50
+ /** Every environment variable the CDP path reads. Named in errors, never printed. */
51
+ export const CDP_ENV_VARS = [
52
+ "CDP_API_KEY_ID",
53
+ "CDP_API_KEY_SECRET",
54
+ "CDP_WALLET_SECRET",
55
+ "METRIK_AGENT_CDP_OWNER_NAME",
56
+ ] as const;
57
+
58
+ /**
59
+ * What the key is for, in the words of the command that needs it. `invoke`
60
+ * signs an EIP-712 invocation capability and broadcasts NOTHING; telling its
61
+ * caller "this command signs a transaction" was simply false.
62
+ */
63
+ export type KeyPurpose = "transaction" | "capability";
64
+
65
+ const PURPOSE_TEXT: Record<KeyPurpose, string> = {
66
+ transaction: "signs a transaction",
67
+ capability:
68
+ "signs an off-chain EIP-712 invocation capability (no transaction is broadcast)",
69
+ };
70
+
71
+ /** Which environment configured the signer, for output and for error text. */
72
+ export type CliWalletSource = "private-key" | "cdp";
73
+
74
+ export interface CliWallet {
75
+ readonly source: CliWalletSource;
76
+ /**
77
+ * A LOCAL signer. Every CLI path either broadcasts through `http(rpcUrl)`
78
+ * (which cannot sign) or signs EIP-712 off-chain, so a signer that delegates
79
+ * to a remote provider is refused at resolution rather than mid-command.
80
+ */
81
+ readonly account: LocalAccount;
82
+ /** One line naming the signer's origin. Safe to print: no secret in it. */
83
+ readonly description: string;
84
+ }
85
+
86
+ const WALLET_HINT =
87
+ `export ${PRIVATE_KEY_ENV}=0x… (a funded Base Sepolia test key), or configure a Coinbase CDP wallet with ` +
88
+ `${CDP_ENV_VARS.join(", ")}. Both are read from the environment only — never pass a credential as a command-line flag.`;
89
+
90
+ /**
91
+ * Resolves the signing wallet, or throws an actionable configuration error.
92
+ *
93
+ * `note` receives one line when the environment is ambiguous (both wallet
94
+ * sources configured). It is optional so `resolveCliWallet` stays usable from a
95
+ * context with no printer; commands always pass `out.note`.
96
+ */
97
+ export async function resolveCliWallet(
98
+ env: NodeJS.ProcessEnv,
99
+ options: {
100
+ readonly purpose?: KeyPurpose;
101
+ readonly note?: (line: string) => void;
102
+ } = {},
103
+ ): Promise<CliWallet> {
104
+ const wallet = await optionalCliWallet(env, options);
105
+ if (wallet === null) {
106
+ throw configError(
107
+ `this command ${PURPOSE_TEXT[options.purpose ?? "transaction"]} and needs a wallet: neither ${PRIVATE_KEY_ENV} nor the CDP variables are set`,
108
+ WALLET_HINT,
109
+ );
110
+ }
111
+ return wallet;
112
+ }
113
+
114
+ /**
115
+ * The same resolution, but "nothing configured" is not an error. Used by
116
+ * look-before-you-leap paths (`open --dry-run`, `streams --address`) that
117
+ * broadcast nothing and must therefore work for someone who does not yet have a
118
+ * funded wallet — which is exactly who a dry run is for.
119
+ *
120
+ * A wallet that is PARTIALLY configured is still an error here: "you set three
121
+ * of the four CDP variables" and "you configured no wallet" are different
122
+ * facts, and rendering the first as the second sends the reader looking in the
123
+ * wrong place.
124
+ */
125
+ export async function optionalCliWallet(
126
+ env: NodeJS.ProcessEnv,
127
+ options: { readonly note?: (line: string) => void } = {},
128
+ ): Promise<CliWallet | null> {
129
+ const privateKeyAccount = optionalCliAccount(env);
130
+ const cdpConfigured = CDP_ENV_VARS.some((name) => isSet(env[name]));
131
+
132
+ if (privateKeyAccount !== null) {
133
+ if (cdpConfigured) {
134
+ options.note?.(
135
+ `warning: both ${PRIVATE_KEY_ENV} and CDP wallet variables are set; using ${PRIVATE_KEY_ENV} (${privateKeyAccount.address}). Unset it to sign with the CDP wallet.`,
136
+ );
137
+ }
138
+ return {
139
+ source: "private-key",
140
+ account: privateKeyAccount,
141
+ description: `${privateKeyAccount.address} (from ${PRIVATE_KEY_ENV})`,
142
+ };
143
+ }
144
+
145
+ if (!cdpConfigured) return null;
146
+
147
+ const missing = CDP_ENV_VARS.filter((name) => !isSet(env[name]));
148
+ if (missing.length > 0) {
149
+ throw configError(
150
+ `the CDP wallet is partially configured: ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not set`,
151
+ `a CDP wallet needs all of ${CDP_ENV_VARS.join(", ")}. Set the missing one${missing.length === 1 ? "" : "s"}, or unset the others and use ${PRIVATE_KEY_ENV} instead.`,
152
+ );
153
+ }
154
+
155
+ // A CDP SMART account is an ERC-4337 contract account at a DIFFERENT address
156
+ // from its owner EOA, and it is reached through a bundler — which this CLI
157
+ // does not route to: every command broadcasts through `http(--rpc-url)`. So a
158
+ // smart-account environment would silently sign and spend from the OWNER,
159
+ // while the user funded the smart account. Refuse instead.
160
+ if (
161
+ isSet(env.METRIK_AGENT_CDP_SMART_ACCOUNT_NAME) ||
162
+ isTruthy(env.METRIK_AGENT_CDP_CREATE_SMART_ACCOUNT)
163
+ ) {
164
+ throw configError(
165
+ "the CLI cannot sign from a CDP smart account: METRIK_AGENT_CDP_CREATE_SMART_ACCOUNT / METRIK_AGENT_CDP_SMART_ACCOUNT_NAME is set",
166
+ "a CDP smart account is an ERC-4337 account at a different address from its owner, reached through a bundler; this CLI broadcasts through --rpc-url and would spend from the OWNER address instead. Unset those two variables to use the owner account directly, or use the SDK's account-abstraction path.",
167
+ );
168
+ }
169
+
170
+ const ownerName = env.METRIK_AGENT_CDP_OWNER_NAME as string;
171
+ let account: Account;
172
+ try {
173
+ const resolved = await resolveAgentWallet({
174
+ cdp: {
175
+ apiKeyId: env.CDP_API_KEY_ID as string,
176
+ apiKeySecret: env.CDP_API_KEY_SECRET as string,
177
+ walletSecret: env.CDP_WALLET_SECRET as string,
178
+ ownerName,
179
+ },
180
+ });
181
+ account = resolved.account;
182
+ } catch (error) {
183
+ // Reaching CDP is a network call against an authenticated API, so this is
184
+ // where a wrong key, a revoked key or an outage surfaces. Say which wallet
185
+ // failed; the underlying message carries the rest.
186
+ throw configError(
187
+ `could not resolve the CDP wallet "${ownerName}"`,
188
+ `check ${CDP_ENV_VARS.join(", ")}, and that '@coinbase/cdp-sdk' (an optional peer dependency) is installed. Underlying error: ${error instanceof Error ? error.message : String(error)}`,
189
+ );
190
+ }
191
+
192
+ return {
193
+ source: "cdp",
194
+ account: assertLocalSigner(account, "cdp"),
195
+ description: `${account.address} (CDP account "${ownerName}")`,
196
+ };
197
+ }
198
+
199
+ /**
200
+ * The raw `METRIK_AGENT_PRIVATE_KEY` read, kept separate so the wallet-source
201
+ * precedence above reads as one list rather than as nested conditionals.
202
+ */
203
+ export function optionalCliAccount(
204
+ env: NodeJS.ProcessEnv,
205
+ ): LocalAccount | null {
206
+ const raw = env[PRIVATE_KEY_ENV];
207
+ if (raw === undefined || raw.length === 0) return null;
208
+ if (!/^0x[0-9a-fA-F]{64}$/.test(raw)) {
209
+ throw configError(
210
+ `${PRIVATE_KEY_ENV} must be a 0x-prefixed 32-byte hex private key`,
211
+ );
212
+ }
213
+ return privateKeyToAccount(raw as `0x${string}`);
214
+ }
215
+
216
+ /**
217
+ * Every CLI signing path needs a LOCAL signer: fund-moving commands broadcast
218
+ * through `http(--rpc-url)`, which cannot sign, and `invoke` needs a
219
+ * `LocalAccount` to sign the EIP-712 capability. A JSON-RPC account (Privy's
220
+ * adapters) would type-check as an `Account` and then fail somewhere inside
221
+ * viem with a message about a missing method, so it is rejected here with the
222
+ * actual reason.
223
+ */
224
+ function assertLocalSigner(
225
+ account: Account,
226
+ source: CliWalletSource,
227
+ ): LocalAccount {
228
+ if (account.type === "local") return account;
229
+ throw configError(
230
+ `the ${source} wallet returned a ${account.type} account, which cannot sign locally`,
231
+ "the CLI broadcasts through --rpc-url and signs EIP-712 capabilities in-process, so it needs a local signer.",
232
+ );
233
+ }
234
+
235
+ function isSet(value: string | undefined): boolean {
236
+ return value !== undefined && value.length > 0;
237
+ }
238
+
239
+ function isTruthy(value: string | undefined): boolean {
240
+ return value === "true" || value === "1";
241
+ }
@@ -21,6 +21,7 @@ import {
21
21
  } from "../discovery/registry.js";
22
22
  import { checkMandate, type SignedSpendMandate } from "../mandates/mandate.js";
23
23
  import {
24
+ isCheckpointStream,
24
25
  MandateDeniedError,
25
26
  VerifiedStreamAgentClient,
26
27
  type AgentSdkClientFactory,
@@ -400,9 +401,13 @@ export class MetrikActionProvider extends ActionProvider<WalletProvider> {
400
401
 
401
402
  return (
402
403
  `Stream ${streamId} is ${stream.status}${paused ? " (payment halted)" : ""}. ` +
403
- `Verified accrued: ${formatUsdc(stream.accrued)} USDC of ${formatUsdc(stream.deposit)} deposited. ` +
404
- `Claimable by seller: ${formatUsdc(claimable)} USDC. Reclaimable by buyer: ${formatUsdc(reclaimable)} USDC. ` +
405
- `Consecutive failed checks: ${stream.consecutiveFailures}.`
404
+ (isCheckpointStream(stream)
405
+ ? `Verified entitlement: ${formatUsdc(stream.settledCumulative)} USDC of ` +
406
+ `${formatUsdc(stream.deposit)} deposited, of which ` +
407
+ `${formatUsdc(stream.claimedCumulative)} USDC has been claimed. `
408
+ : `Verified accrued: ${formatUsdc(stream.accrued)} USDC of ` +
409
+ `${formatUsdc(stream.deposit)} deposited. `) +
410
+ `Claimable by seller: ${formatUsdc(claimable)} USDC. Reclaimable by buyer: ${formatUsdc(reclaimable)} USDC.`
406
411
  );
407
412
  }
408
413
 
@@ -23,13 +23,24 @@ export interface CrewAiVerifiedStreamMcpConfigOptions {
23
23
  readonly cacheToolsList?: boolean;
24
24
  }
25
25
 
26
+ /**
27
+ * The default launcher: the `metrik-mcp` bin this package ships.
28
+ *
29
+ * It used to be `pnpm mcp:stdio`, which only resolves INSIDE this repository —
30
+ * a CrewAI user who installed `@absol-labs/agent` from npm got a config that
31
+ * could never start. `metrik-mcp` is on `PATH` wherever the package is
32
+ * installed (npm/pnpm link it into `node_modules/.bin`), and `npx` resolves it
33
+ * with no install at all.
34
+ */
35
+ export const CREWAI_DEFAULT_MCP_COMMAND = "metrik-mcp";
36
+
26
37
  export function createCrewAiVerifiedStreamMcpConfig(
27
38
  options: CrewAiVerifiedStreamMcpConfigOptions = {},
28
39
  ): CrewAiVerifiedStreamMcpConfig {
29
40
  return {
30
41
  transport: "stdio",
31
- command: options.command ?? "pnpm",
32
- args: options.args ?? ["mcp:stdio"],
42
+ command: options.command ?? CREWAI_DEFAULT_MCP_COMMAND,
43
+ args: options.args ?? [],
33
44
  env: options.env ?? {},
34
45
  allowedToolNames:
35
46
  options.allowedToolNames ?? CREWAI_VERIFIED_STREAM_TOOL_NAMES,
@@ -15,6 +15,7 @@ import {
15
15
  } from "../discovery/registry.js";
16
16
  import { checkMandate, type SignedSpendMandate } from "../mandates/mandate.js";
17
17
  import {
18
+ isCheckpointStream,
18
19
  type MandateAuthorizedStreamActionInput,
19
20
  type ReclaimAuthorizedStreamInput,
20
21
  type ReclaimVerifiedStreamResult,
@@ -733,9 +734,19 @@ function serializeStreamStatus(
733
734
  ...result.stream,
734
735
  deposit: result.stream.deposit.toString(),
735
736
  ratePerSecond: result.stream.ratePerSecond.toString(),
736
- accrued: result.stream.accrued.toString(),
737
- claimed: result.stream.claimed.toString(),
738
- lastSequence: result.stream.lastSequence.toString(),
737
+ // The two escrow generations report entitlement differently; emit the
738
+ // vocabulary of whichever one answered rather than inventing a shared one.
739
+ ...(isCheckpointStream(result.stream)
740
+ ? {
741
+ settledCumulative: result.stream.settledCumulative.toString(),
742
+ claimedCumulative: result.stream.claimedCumulative.toString(),
743
+ feesPaid: result.stream.feesPaid.toString(),
744
+ }
745
+ : {
746
+ accrued: result.stream.accrued.toString(),
747
+ claimed: result.stream.claimed.toString(),
748
+ lastSequence: result.stream.lastSequence.toString(),
749
+ }),
739
750
  },
740
751
  claimable: result.claimable.toString(),
741
752
  reclaimable: result.reclaimable.toString(),
@@ -9,6 +9,7 @@ import {
9
9
  } from "../discovery/registry.js";
10
10
  import { checkMandate, type SignedSpendMandate } from "../mandates/mandate.js";
11
11
  import {
12
+ isCheckpointStream,
12
13
  type MandateAuthorizedStreamActionInput,
13
14
  type ReclaimAuthorizedStreamInput,
14
15
  type ReclaimVerifiedStreamResult,
@@ -332,9 +333,19 @@ function serializeStreamStatus(
332
333
  ...result.stream,
333
334
  deposit: result.stream.deposit.toString(),
334
335
  ratePerSecond: result.stream.ratePerSecond.toString(),
335
- accrued: result.stream.accrued.toString(),
336
- claimed: result.stream.claimed.toString(),
337
- lastSequence: result.stream.lastSequence.toString(),
336
+ // The two escrow generations report entitlement differently; emit the
337
+ // vocabulary of whichever one answered rather than inventing a shared one.
338
+ ...(isCheckpointStream(result.stream)
339
+ ? {
340
+ settledCumulative: result.stream.settledCumulative.toString(),
341
+ claimedCumulative: result.stream.claimedCumulative.toString(),
342
+ feesPaid: result.stream.feesPaid.toString(),
343
+ }
344
+ : {
345
+ accrued: result.stream.accrued.toString(),
346
+ claimed: result.stream.claimed.toString(),
347
+ lastSequence: result.stream.lastSequence.toString(),
348
+ }),
338
349
  },
339
350
  claimable: result.claimable.toString(),
340
351
  reclaimable: result.reclaimable.toString(),
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { startCallerAuthGatewayServerFromEnv } from "./http-server.js";
2
3
 
3
4
  /**
package/src/index.ts CHANGED
@@ -104,6 +104,8 @@ export {
104
104
  type ReclaimAuthorizedStreamInput,
105
105
  type ReclaimVerifiedStreamOptions,
106
106
  type ReclaimVerifiedStreamResult,
107
+ isCheckpointStream,
108
+ type StreamStatusStream,
107
109
  type StreamStatusView,
108
110
  type VerifiedStreamAgentOpener,
109
111
  } from "./sdk/client.js";
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { startHostedMcpHttpServerFromEnv } from "./http.js";
2
3
 
3
4
  /**
package/src/mcp/server.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  ServerMandateConfigError,
21
21
  } from "../mandates/env.js";
22
22
  import {
23
+ isCheckpointStream,
23
24
  VerifiedStreamAgentClient,
24
25
  type ReclaimAuthorizedStreamInput,
25
26
  type ReclaimVerifiedStreamResult,
@@ -798,10 +799,34 @@ export function createVerifiedStreamMcpServer(
798
799
  };
799
800
  }
800
801
 
802
+ /** One readable line per invalid or missing environment variable. */
803
+ function describeEnvIssues(error: z.ZodError): string {
804
+ const seen = new Set<string>();
805
+ for (const issue of error.issues) {
806
+ const name = issue.path.join(".");
807
+ seen.add(
808
+ issue.code === "invalid_type" && issue.received === "undefined"
809
+ ? `${name} is not set`
810
+ : `${name} is invalid (${issue.message})`,
811
+ );
812
+ }
813
+ return [...seen].join("; ");
814
+ }
815
+
801
816
  export function parseMetrikAgentEnv(
802
817
  env: NodeJS.ProcessEnv = process.env,
803
818
  ): ParsedMetrikAgentEnv {
804
- const parsed = metrikAgentEnvSchema.parse(env);
819
+ const result = metrikAgentEnvSchema.safeParse(env);
820
+ if (!result.success) {
821
+ // `.parse()` threw a ZodError whose message is a JSON array of issue
822
+ // objects. `metrik-mcp` is started by an MCP host and fails closed on stdio,
823
+ // so that array was the ONLY thing an operator ever saw — a correct failure
824
+ // rendered as noise. Name the variables instead.
825
+ throw new Error(
826
+ `metrik-mcp is not configured: ${describeEnvIssues(result.error)}. Set METRIK_AGENT_RPC_URL (an http(s) URL), METRIK_AGENT_ESCROW and METRIK_AGENT_USDC (EVM addresses), plus a wallet (METRIK_AGENT_PRIVATE_KEY, or the CDP/Privy variables). METRIK_AGENT_CHAIN_ID defaults to Base Sepolia (${baseSepolia.id}).`,
827
+ );
828
+ }
829
+ const parsed = result.data;
805
830
  const chainId = Number(parsed.METRIK_AGENT_CHAIN_ID ?? baseSepolia.id);
806
831
  if (!Number.isInteger(chainId) || chainId <= 0) {
807
832
  throw new Error("METRIK_AGENT_CHAIN_ID must be a positive integer");
@@ -859,6 +884,11 @@ export async function createVerifiedStreamMcpServerOptionsFromEnv(
859
884
  chain: parsed.chain,
860
885
  transport: http(parsed.rpcUrl),
861
886
  escrow: parsed.escrow,
887
+ // Declare the escrow as V2 as well. Without this the SDK treats the
888
+ // client as V1 and routes claimable()/reclaimable() to the legacy ABI,
889
+ // which reverts against StreamEscrowV2 — the checkpoint escrow replaced
890
+ // those one-argument views with proof-carrying ones.
891
+ escrowV2: parsed.escrow,
862
892
  usdc: parsed.usdc,
863
893
  settlementTarget: {
864
894
  chainId: parsed.chain.id,
@@ -943,15 +973,30 @@ function serializeStreamStatus(
943
973
  operator: status.stream.operator,
944
974
  serviceRef: status.stream.serviceRef,
945
975
  deposit: status.stream.deposit.toString(),
976
+ // Informational on a checkpoint escrow: it stores the rate but computes
977
+ // no accrual. Emit the vocabulary of whichever escrow generation answered
978
+ // instead of flattening two incompatible tuples into one shape.
946
979
  ratePerSecond: status.stream.ratePerSecond.toString(),
947
- accrued: status.stream.accrued.toString(),
948
- claimed: status.stream.claimed.toString(),
949
980
  openedAt: status.stream.openedAt,
950
981
  expiresAt: status.stream.expiresAt,
951
- lastVerifiedAt: status.stream.lastVerifiedAt,
952
- lastSequence: status.stream.lastSequence.toString(),
953
- consecutiveFailures: status.stream.consecutiveFailures,
954
982
  status: status.stream.status,
983
+ ...(isCheckpointStream(status.stream)
984
+ ? {
985
+ settlement: "checkpoint" as const,
986
+ settledCumulative: status.stream.settledCumulative.toString(),
987
+ claimedCumulative: status.stream.claimedCumulative.toString(),
988
+ feesPaid: status.stream.feesPaid.toString(),
989
+ closedAt: status.stream.closedAt,
990
+ reclaimed: status.stream.reclaimed,
991
+ }
992
+ : {
993
+ settlement: "per-attestation" as const,
994
+ accrued: status.stream.accrued.toString(),
995
+ claimed: status.stream.claimed.toString(),
996
+ lastVerifiedAt: status.stream.lastVerifiedAt,
997
+ lastSequence: status.stream.lastSequence.toString(),
998
+ consecutiveFailures: status.stream.consecutiveFailures,
999
+ }),
955
1000
  },
956
1001
  claimable: status.claimable.toString(),
957
1002
  reclaimable: status.reclaimable.toString(),
package/src/mcp/stdio.ts CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { startVerifiedStreamMcpServerStdio } from "./server.js";
2
3
 
3
4
  startVerifiedStreamMcpServerStdio().catch((error) => {
package/src/sdk/client.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  type HireComputeResult,
4
4
  type StreamProofClientConfig,
5
5
  type StreamProofStream,
6
+ type StreamV2,
6
7
  } from "@absol-labs/sdk";
7
8
 
8
9
  import {
@@ -25,8 +26,23 @@ export interface OpenVerifiedStreamInput {
25
26
  readonly revokedMandateIds?: Iterable<`0x${string}`>;
26
27
  }
27
28
 
29
+ /**
30
+ * A stream as read from the configured escrow. Checkpoint escrows (V2) and the
31
+ * legacy per-attestation escrow (V1) expose incompatible tuples, so the view
32
+ * carries whichever one the client is pointed at. Narrow with
33
+ * {@link isCheckpointStream} before touching version-specific fields.
34
+ */
35
+ export type StreamStatusStream = StreamProofStream | StreamV2;
36
+
37
+ /** True when the stream came from a checkpoint-settled (V2) escrow. */
38
+ export function isCheckpointStream(
39
+ stream: StreamStatusStream,
40
+ ): stream is StreamV2 {
41
+ return "settledCumulative" in stream;
42
+ }
43
+
28
44
  export interface StreamStatusView {
29
- readonly stream: StreamProofStream;
45
+ readonly stream: StreamStatusStream;
30
46
  readonly claimable: bigint;
31
47
  readonly reclaimable: bigint;
32
48
  }
@@ -63,12 +79,26 @@ export interface AgentSdkClient {
63
79
  readonly ratePerSecond: bigint;
64
80
  readonly maxDurationSeconds: number;
65
81
  }): Promise<HireComputeResult>;
82
+ // Both decoders. The SDK deliberately makes `getStream` throw once a V2
83
+ // escrow is configured (the tuples are incompatible), so the caller must pick
84
+ // based on configuration rather than probing.
66
85
  getStream(streamId: `0x${string}`): Promise<StreamProofStream>;
86
+ getStreamV2(streamId: `0x${string}`): Promise<StreamV2>;
67
87
  claimable(streamId: `0x${string}`): Promise<bigint>;
68
88
  reclaimable(streamId: `0x${string}`): Promise<bigint>;
69
89
  claim(streamId: `0x${string}`): Promise<StreamProofTransactionResult>;
70
90
  close(streamId: `0x${string}`): Promise<StreamProofTransactionResult>;
71
91
  reclaim(streamId: `0x${string}`): Promise<StreamProofTransactionResult>;
92
+ /**
93
+ * The BUYER's no-proof escape hatch. Every other buyer path to the money
94
+ * (`reclaim` -> `reclaimWithProof`) needs a Merkle proof fetched from the
95
+ * oracle, so it is only as available as the oracle is. `reclaimUnverified`
96
+ * takes no proof, no quorum and no guardian — it is the path that exists
97
+ * precisely for the case where that dependency has failed.
98
+ */
99
+ reclaimUnverified(
100
+ streamId: `0x${string}`,
101
+ ): Promise<StreamProofTransactionResult>;
72
102
  }
73
103
 
74
104
  export type AgentSdkClientFactory = (
@@ -93,6 +123,12 @@ export class MandateDeniedError extends Error {
93
123
 
94
124
  export class VerifiedStreamAgentClient implements VerifiedStreamAgentOpener {
95
125
  private readonly sdkClient: AgentSdkClient;
126
+ /**
127
+ * Whether the configured escrow is checkpoint-settled. Set once from config:
128
+ * the SDK throws rather than guessing if the wrong decoder is used, so this
129
+ * has to be decided here instead of discovered by trial.
130
+ */
131
+ private readonly checkpointSettled: boolean;
96
132
 
97
133
  constructor(
98
134
  config: StreamProofClientConfig,
@@ -101,6 +137,15 @@ export class VerifiedStreamAgentClient implements VerifiedStreamAgentOpener {
101
137
  this.sdkClient = (options.createSdkClient ?? defaultSdkClientFactory)(
102
138
  config,
103
139
  );
140
+ this.checkpointSettled = config.escrowV2 !== undefined;
141
+ }
142
+
143
+ private async readStream(
144
+ streamId: `0x${string}`,
145
+ ): Promise<StreamStatusStream> {
146
+ return this.checkpointSettled
147
+ ? await this.sdkClient.getStreamV2(streamId)
148
+ : await this.sdkClient.getStream(streamId);
104
149
  }
105
150
 
106
151
  async openVerifiedStream(
@@ -132,7 +177,7 @@ export class VerifiedStreamAgentClient implements VerifiedStreamAgentOpener {
132
177
 
133
178
  async getStreamStatus(streamId: `0x${string}`): Promise<StreamStatusView> {
134
179
  const [stream, claimable, reclaimable] = await Promise.all([
135
- this.sdkClient.getStream(streamId),
180
+ this.readStream(streamId),
136
181
  this.sdkClient.claimable(streamId),
137
182
  this.sdkClient.reclaimable(streamId),
138
183
  ]);
@@ -166,7 +211,29 @@ export class VerifiedStreamAgentClient implements VerifiedStreamAgentOpener {
166
211
  }
167
212
 
168
213
  /**
169
- * Authorizes a BUYER-RECOVERY action (close / reclaim). These only return
214
+ * Buyer-only escape hatch: reclaim WITHOUT a checkpoint proof, available only
215
+ * once `block.timestamp >= expiresAt + escapeGraceSeconds` on the escrow.
216
+ *
217
+ * Authorized exactly like `close` / `reclaim` — on the mandate owner's
218
+ * signature alone (see {@link authorizeBuyerRecovery}) — because it only
219
+ * returns the buyer's own unspent escrow. It is a SEPARATE method rather than
220
+ * an option on {@link reclaimStream} because it is not an alternative route to
221
+ * the same outcome: `reclaimUnverified` pays the buyer
222
+ * `deposit - claimedCumulative` and permanently forfeits every operator
223
+ * voucher that was earned but not yet redeemed on-chain.
224
+ *
225
+ * Takes no `closeFirst`: the escrow gates this path on `expiresAt`, never on
226
+ * `closedAt`, so closing first would buy nothing but a second gas fee.
227
+ */
228
+ async reclaimUnverifiedStream(
229
+ input: MandateAuthorizedStreamActionInput,
230
+ ): Promise<StreamProofTransactionResult> {
231
+ await this.authorizeBuyerRecovery(input);
232
+ return await this.sdkClient.reclaimUnverified(input.streamId);
233
+ }
234
+
235
+ /**
236
+ * Authorizes a BUYER-RECOVERY action (close / reclaim / reclaimUnverified). These only return
170
237
  * unspent funds to the buyer or stop payment, so they are authorized on
171
238
  * mandate-owner signature ALONE — never gated by expiry, revocation, or caps,
172
239
  * which would otherwise let a lapsed mandate strand the buyer's own funds.
@@ -183,7 +250,7 @@ export class VerifiedStreamAgentClient implements VerifiedStreamAgentOpener {
183
250
  private async authorizeExistingStreamAction(
184
251
  input: MandateAuthorizedStreamActionInput,
185
252
  ): Promise<void> {
186
- const stream = await this.sdkClient.getStream(input.streamId);
253
+ const stream = await this.readStream(input.streamId);
187
254
  const decision = await checkMandate(
188
255
  input.signedMandate,
189
256
  {