@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,162 @@
1
+ /**
2
+ * CLI error taxonomy. Every expected failure surfaces as a `CliError` so the
3
+ * runner can print one actionable line instead of a viem/zod stack dump, and so
4
+ * a script can branch on the exit code.
5
+ */
6
+ export type CliExitCode =
7
+ /** Bad flags, missing arguments, unparseable amounts. */
8
+ | 2
9
+ /** Configuration refused: wrong chain, missing key, mainnet target. */
10
+ | 3
11
+ /** The operation itself failed (RPC, registry, escrow revert, HTTP). */
12
+ | 4;
13
+
14
+ export class CliError extends Error {
15
+ readonly exitCode: CliExitCode;
16
+ /** Optional next step printed under the message in human mode. */
17
+ readonly hint: string | undefined;
18
+
19
+ constructor(
20
+ message: string,
21
+ options: {
22
+ readonly exitCode?: CliExitCode;
23
+ readonly hint?: string;
24
+ readonly cause?: unknown;
25
+ } = {},
26
+ ) {
27
+ super(message, options.cause === undefined ? {} : { cause: options.cause });
28
+ this.name = "CliError";
29
+ this.exitCode = options.exitCode ?? 4;
30
+ this.hint = options.hint;
31
+ }
32
+ }
33
+
34
+ /** Usage error: the caller typed something the CLI cannot act on. */
35
+ export function usageError(message: string, hint?: string): CliError {
36
+ return new CliError(message, {
37
+ exitCode: 2,
38
+ ...(hint === undefined ? {} : { hint }),
39
+ });
40
+ }
41
+
42
+ /** Configuration error: the environment/flags describe a target we refuse. */
43
+ export function configError(message: string, hint?: string): CliError {
44
+ return new CliError(message, {
45
+ exitCode: 3,
46
+ ...(hint === undefined ? {} : { hint }),
47
+ });
48
+ }
49
+
50
+ /**
51
+ * viem's generic tail. Each of these is true and each carries zero information
52
+ * once an outer link has already named the revert:
53
+ *
54
+ * NothingToReclaim() — …this will not change on retry:
55
+ * The contract function "reclaim" reverted.:
56
+ * Execution reverted for an unknown reason.
57
+ *
58
+ * The message names the reason and then says the reason is unknown. Matched
59
+ * against a WHOLE link, so a message that merely contains one of these phrases
60
+ * alongside real detail is never dropped.
61
+ */
62
+ const VIEM_BOILERPLATE: readonly RegExp[] = [
63
+ /^The contract function "[^"]*" reverted\.?$/,
64
+ /^Execution reverted for an unknown reason\.?$/,
65
+ /^Execution reverted\.?$/,
66
+ /^Execution reverted\.? \(the revert payload is not a readable reason string\)$/,
67
+ /^An unknown RPC error occurred\.?$/,
68
+ ];
69
+
70
+ /** Does this link name a concrete revert — a custom error, reason or panic? */
71
+ function namesTheRevert(line: string): boolean {
72
+ return (
73
+ /\b[A-Z][A-Za-z0-9]*\([^)]*\)/.test(line) ||
74
+ /\breverted:/.test(line) ||
75
+ /\bpanicked with code\b/.test(line)
76
+ );
77
+ }
78
+
79
+ function isBoilerplate(line: string): boolean {
80
+ return VIEM_BOILERPLATE.some((pattern) => pattern.test(line));
81
+ }
82
+
83
+ /**
84
+ * Unwrap the most useful message from an arbitrary thrown value.
85
+ *
86
+ * Boilerplate links are dropped once something in the chain has already named
87
+ * the revert; when nothing has, ONE is kept, because "it reverted" is then the
88
+ * only fact available and losing it would leave a bare "close failed".
89
+ */
90
+ export function describeError(error: unknown): string {
91
+ if (error instanceof Error) {
92
+ const chain: string[] = [];
93
+ for (let current: unknown = error, depth = 0; depth < 4; depth++) {
94
+ if (!(current instanceof Error)) break;
95
+ const line = presentMessage(current.message.split("\n")[0] ?? "");
96
+ if (line.length > 0 && !chain.includes(line)) {
97
+ chain.push(line);
98
+ }
99
+ current = current.cause;
100
+ }
101
+ return stripRedundantBoilerplate(chain).join(": ");
102
+ }
103
+ return presentMessage(String(error));
104
+ }
105
+
106
+ /** Exported for the test that pins the shape of the chain, not the wording. */
107
+ export function stripRedundantBoilerplate(
108
+ chain: readonly string[],
109
+ ): readonly string[] {
110
+ const decoded = chain.some(
111
+ (line) => namesTheRevert(line) && !isBoilerplate(line),
112
+ );
113
+ let keptBoilerplate = false;
114
+ const kept = chain.filter((line, index) => {
115
+ if (!isBoilerplate(line)) return true;
116
+ // The first link is the thrown error's own message: never dropped, or a
117
+ // failure whose entire message is boilerplate would print as nothing.
118
+ if (index === 0) return true;
119
+ if (decoded || keptBoilerplate) return false;
120
+ keptBoilerplate = true;
121
+ return true;
122
+ });
123
+ return kept.length > 0 ? kept : chain;
124
+ }
125
+
126
+ /**
127
+ * Internal SDK method names that mean nothing to someone running a CLI. The
128
+ * message is otherwise good, so the name is rewritten rather than the whole line
129
+ * discarded. `getStreamV2` in particular is an SDK METHOD, not a contract
130
+ * function — printing it invites a reader to go looking for it on the escrow.
131
+ */
132
+ const INTERNAL_NAMES: readonly (readonly [RegExp, string])[] = [
133
+ [/\bgetStreamV2 failed for stream\b/g, "could not read stream"],
134
+ [/\bgetStream failed for stream\b/g, "could not read stream"],
135
+ ];
136
+
137
+ /**
138
+ * Tidy one message line for display.
139
+ *
140
+ * viem renders an undecodable revert payload as `Execution reverted with
141
+ * reason: <the raw bytes as text>`, which arrives as mojibake glued to the end
142
+ * of an otherwise excellent error ("… Execution reverted with reason: ׃.x."). A
143
+ * revert reason that is not printable ASCII carries no information for a reader,
144
+ * so the unreadable tail is dropped and the useful prefix kept. Legitimate
145
+ * reasons — which are ASCII strings the contract author wrote — are untouched.
146
+ */
147
+ export function presentMessage(raw: string): string {
148
+ let message = raw.trim();
149
+ for (const [pattern, replacement] of INTERNAL_NAMES) {
150
+ message = message.replace(pattern, replacement);
151
+ }
152
+ message = message.replace(
153
+ /(Execution reverted with reason:)\s*(.*)$/,
154
+ (whole, prefix: string, reason: string) => {
155
+ const trimmed = reason.trim();
156
+ if (trimmed.length >= 3 && /^[\x20-\x7e]+$/.test(trimmed)) return whole;
157
+ return `${prefix.replace(/ with reason:$/, ".")} (the revert payload is not a readable reason string)`;
158
+ },
159
+ );
160
+ // Strip control characters wherever they survived: they corrupt a terminal.
161
+ return message.replace(/[\u0000-\u001f\u007f-\u009f]/g, "").trim();
162
+ }
@@ -0,0 +1,92 @@
1
+ import type { StreamV2 } from "@absol-labs/sdk";
2
+
3
+ import { formatDuration } from "./amounts.js";
4
+ import type { CliReadClient } from "./deps.js";
5
+
6
+ /**
7
+ * The buyer's no-proof escape hatch, as the CLI reasons about it.
8
+ *
9
+ * Every OTHER route the CLI has to a buyer's money goes through
10
+ * `reclaimWithProof`, which needs a Merkle proof fetched from the oracle. So the
11
+ * whole proof-based path is only as available as the oracle is.
12
+ * `StreamEscrowV2.reclaimUnverified` is the path that is not: buyer-only, no
13
+ * proof, no signer quorum, no guardian. It is gated on one thing —
14
+ *
15
+ * block.timestamp >= expiresAt + escapeGraceSeconds
16
+ *
17
+ * — and `escapeGraceSeconds` is a public immutable on the escrow, so readiness
18
+ * is fully computable before a single wei of gas is spent.
19
+ *
20
+ * Note which timestamp the escrow uses: `expiresAt`, NEVER `closedAt`. Closing a
21
+ * stream early does not bring the escape hatch forward, and the CLI must not
22
+ * imply that it does.
23
+ */
24
+ export interface EscapeHatchWindow {
25
+ /** `escapeGraceSeconds` as read from the escrow. */
26
+ readonly graceSeconds: number;
27
+ /** The exact unix second at which `reclaimUnverified` stops reverting. */
28
+ readonly readyAt: number;
29
+ readonly ready: boolean;
30
+ /** Seconds left until `readyAt`; `0` once the window is open. */
31
+ readonly secondsRemaining: number;
32
+ }
33
+
34
+ export function escapeHatchWindow(
35
+ stream: Pick<StreamV2, "expiresAt">,
36
+ graceSeconds: number,
37
+ nowSeconds: number,
38
+ ): EscapeHatchWindow {
39
+ const readyAt = stream.expiresAt + graceSeconds;
40
+ const secondsRemaining = readyAt - nowSeconds;
41
+ return {
42
+ graceSeconds,
43
+ readyAt,
44
+ ready: secondsRemaining <= 0,
45
+ secondsRemaining: secondsRemaining > 0 ? secondsRemaining : 0,
46
+ };
47
+ }
48
+
49
+ /** `2026-09-17T09:15:00.000Z (1789…)`, or `-` for an unset timestamp. */
50
+ export function formatTimestamp(seconds: number): string {
51
+ if (seconds <= 0) return "-";
52
+ return `${new Date(seconds * 1000).toISOString()} (${seconds})`;
53
+ }
54
+
55
+ /** One line describing where the escape hatch stands, for humans. */
56
+ export function describeEscapeHatch(window: EscapeHatchWindow): string {
57
+ return window.ready
58
+ ? `open since ${formatTimestamp(window.readyAt)}`
59
+ : `opens ${formatTimestamp(window.readyAt)} — in ${formatDuration(
60
+ window.secondsRemaining,
61
+ )}`;
62
+ }
63
+
64
+ export interface ChainNow {
65
+ readonly seconds: number;
66
+ /** `local` means the chain would not answer and the caller's clock was used. */
67
+ readonly source: "chain" | "local";
68
+ }
69
+
70
+ /**
71
+ * The time the ESCROW will compare against, not the time this machine thinks it
72
+ * is. `reclaimUnverified` reverts on `block.timestamp`, so a readiness answer
73
+ * computed from a skewed laptop clock could tell a buyer to spend gas on a
74
+ * guaranteed revert — or tell them to wait when the hatch is already open.
75
+ *
76
+ * A failed read is not fatal: the caller's clock is the fallback, and the
77
+ * `source` field is reported so the difference is never hidden.
78
+ */
79
+ export async function readChainNow(
80
+ read: Pick<CliReadClient, "latestBlockTimestamp">,
81
+ fallbackSeconds: number,
82
+ ): Promise<ChainNow> {
83
+ try {
84
+ const seconds = await read.latestBlockTimestamp();
85
+ if (Number.isFinite(seconds) && seconds > 0) {
86
+ return { seconds, source: "chain" };
87
+ }
88
+ } catch {
89
+ // fall through to the caller's clock
90
+ }
91
+ return { seconds: fallbackSeconds, source: "local" };
92
+ }
@@ -0,0 +1,27 @@
1
+ export { runCli, renderHelp, COMMANDS, type RunCliOptions } from "./run.js";
2
+ export { parseArgs, type ParsedArgs } from "./args.js";
3
+ export {
4
+ TESTNET_CHAIN_IDS,
5
+ assertTestnetChainId,
6
+ assertTestnetRpc,
7
+ resolveConfig,
8
+ type CliConfig,
9
+ } from "./config.js";
10
+ export { CliError, describeError } from "./errors.js";
11
+ export {
12
+ defaultCliDependencies,
13
+ type CliDependencies,
14
+ type CliReadClient,
15
+ type CliWriteClient,
16
+ type OpenedStream,
17
+ type StreamOpenedQuery,
18
+ } from "./deps.js";
19
+ export {
20
+ CDP_ENV_VARS,
21
+ PRIVATE_KEY_ENV,
22
+ optionalCliWallet,
23
+ resolveCliWallet,
24
+ type CliWallet,
25
+ type CliWalletSource,
26
+ } from "./wallet.js";
27
+ export type { CommandContext, CommandSpec } from "./context.js";
@@ -0,0 +1,77 @@
1
+ import { RegistryUnavailableError } from "../discovery/registry.js";
2
+
3
+ import { CliError } from "./errors.js";
4
+ import type { CommandContext } from "./context.js";
5
+ import type { ServiceListing } from "./deps.js";
6
+
7
+ /**
8
+ * Finds the verified listing for a `serviceRef`.
9
+ *
10
+ * The registry is an untrusted transport: `discoverServices` re-derives every
11
+ * `serviceRef` from the signed record and recovers the operator signature, so a
12
+ * compromised registry can neither invent an operator to pay nor redirect an
13
+ * invocation URL. That verification is the reason the CLI routes through
14
+ * discovery instead of taking a `--url`.
15
+ */
16
+ export async function resolveListing(
17
+ context: CommandContext,
18
+ serviceRef: `0x${string}`,
19
+ options: { readonly required: boolean },
20
+ ): Promise<ServiceListing | null> {
21
+ const { config, out, deps } = context;
22
+ let services;
23
+ try {
24
+ services = (
25
+ await deps.discover({
26
+ registryUrl: config.registryUrl,
27
+ logger: { warn: (...parts) => out.note(parts.map(String).join(" ")) },
28
+ })
29
+ ).services;
30
+ } catch (error) {
31
+ if (error instanceof RegistryUnavailableError && !options.required) {
32
+ out.note(
33
+ `warning: the registry at ${config.registryUrl} is unreachable; continuing without a verified listing.`,
34
+ );
35
+ return null;
36
+ }
37
+ throw new CliError(
38
+ `the service registry at ${config.registryUrl} could not be read`,
39
+ { exitCode: 4, cause: error },
40
+ );
41
+ }
42
+
43
+ const wanted = serviceRef.toLowerCase();
44
+ const match = services.find(
45
+ (service) => service.serviceRef.toLowerCase() === wanted,
46
+ );
47
+ if (match === undefined) {
48
+ if (!options.required) return null;
49
+ throw new CliError(
50
+ `no verified listing in the registry matches serviceRef ${serviceRef}`,
51
+ {
52
+ exitCode: 4,
53
+ hint: "run `metrik discover` to see what is listed. A listing that fails signature verification is dropped, so it will not appear here.",
54
+ },
55
+ );
56
+ }
57
+ if (match.accessUrl === null) {
58
+ if (!options.required) return null;
59
+ throw new CliError(
60
+ `listing ${serviceRef} carries no signed invocation URL`,
61
+ {
62
+ exitCode: 4,
63
+ hint: "the CLI refuses to call an origin that the operator did not sign.",
64
+ },
65
+ );
66
+ }
67
+
68
+ return {
69
+ serviceRef: match.serviceRef,
70
+ operator: match.operator,
71
+ publicUrl: match.publicUrl,
72
+ access: match.access,
73
+ accessUrl: match.accessUrl,
74
+ signed: match.signed,
75
+ category: match.category,
76
+ };
77
+ }
@@ -0,0 +1,126 @@
1
+ import type { Account } from "viem";
2
+
3
+ import {
4
+ parseServerMandateCapsEnv,
5
+ signServerMandate,
6
+ DEFAULT_SERVER_MANDATE_TTL_SECONDS,
7
+ type ServerMandateCaps,
8
+ } from "../mandates/env.js";
9
+ import type { SignedSpendMandate } from "../mandates/mandate.js";
10
+
11
+ /**
12
+ * Spend mandates in the CLI.
13
+ *
14
+ * Every fund-moving path in this package goes through `checkMandate()` — that is
15
+ * a golden rule of the agent layer, and the CLI is not exempt. The CLI's twist
16
+ * is that the person typing the command IS the mandate owner: they hold the key
17
+ * and they just stated the exact spend on the command line. So the mandate is
18
+ * self-signed from the same wallet that funds the stream, exactly as the MCP
19
+ * server does (`signServerMandate`).
20
+ *
21
+ * Two sources, in priority order:
22
+ *
23
+ * 1. `METRIK_AGENT_MANDATE_*` in the environment — an explicit standing ceiling.
24
+ * If the requested hire exceeds it, `checkMandate` DENIES it. This is the
25
+ * path to use when a CLI runs unattended under a policy.
26
+ * 2. No env caps configured — the mandate is sized to exactly this hire and
27
+ * scoped to exactly this operator. It authorizes the command that was typed
28
+ * and nothing wider.
29
+ *
30
+ * Neither path is an unbounded mandate.
31
+ */
32
+ export interface RequestedSpend {
33
+ readonly operator: `0x${string}`;
34
+ readonly budgetUsdc: bigint;
35
+ readonly ratePerSecondUsdc: bigint;
36
+ readonly durationSeconds: number;
37
+ }
38
+
39
+ export interface CliMandate {
40
+ readonly signed: SignedSpendMandate;
41
+ /** Where the caps came from, for the human-readable output. */
42
+ readonly source: "env" | "request";
43
+ }
44
+
45
+ /**
46
+ * The caps this hire would be authorized under, WITHOUT signing anything. Split
47
+ * out so `open --dry-run` can report the mandate it would use even when there is
48
+ * no key to sign it with.
49
+ */
50
+ export function cliMandateCaps(
51
+ request: RequestedSpend,
52
+ env: NodeJS.ProcessEnv,
53
+ ): { readonly caps: ServerMandateCaps; readonly source: "env" | "request" } {
54
+ const envCaps = parseServerMandateCapsEnv(env);
55
+ if (envCaps !== null) return { caps: envCaps, source: "env" };
56
+ return {
57
+ caps: {
58
+ maxPerStreamUsdc: request.budgetUsdc,
59
+ maxTotalUsdc: request.budgetUsdc,
60
+ maxRatePerSecondUsdc: request.ratePerSecondUsdc,
61
+ maxDurationSeconds: request.durationSeconds,
62
+ allowedOperators: [request.operator],
63
+ ttlSeconds: DEFAULT_SERVER_MANDATE_TTL_SECONDS,
64
+ },
65
+ source: "request",
66
+ };
67
+ }
68
+
69
+ export async function signCliMandate(
70
+ account: Account,
71
+ request: RequestedSpend,
72
+ options: { readonly chainId: number; readonly env: NodeJS.ProcessEnv },
73
+ ): Promise<CliMandate> {
74
+ const { caps, source } = cliMandateCaps(request, options.env);
75
+ const signed = await signServerMandate(account, caps, {
76
+ chainId: options.chainId,
77
+ });
78
+ return { signed, source };
79
+ }
80
+
81
+ /**
82
+ * The operator's claim is not a SPEND: it redeems value the stream already
83
+ * earned under an oracle-verified checkpoint, and the escrow pays it to the
84
+ * stream's operator and nobody else. But `checkMandate()` gates every
85
+ * fund-moving path in this package without exception, so the claim carries a
86
+ * mandate too. The caps below are placeholders that satisfy the schema against a
87
+ * zero-valued request; the real gate is the escrow's own `NotOperator()`.
88
+ */
89
+ export async function signClaimMandate(
90
+ account: Account,
91
+ chainId: number,
92
+ ): Promise<SignedSpendMandate> {
93
+ return await signPlaceholderMandate(account, chainId);
94
+ }
95
+
96
+ /**
97
+ * Close and reclaim are BUYER-RECOVERY actions: they stop payment or return
98
+ * unspent funds to the buyer, and `VerifiedStreamAgentClient` authorizes them on
99
+ * the mandate owner's signature alone — never on caps or expiry, so a lapsed
100
+ * mandate can never strand a buyer's own money. The caps below are therefore
101
+ * placeholders that satisfy the schema; they gate nothing.
102
+ */
103
+ export async function signRecoveryMandate(
104
+ account: Account,
105
+ chainId: number,
106
+ ): Promise<SignedSpendMandate> {
107
+ return await signPlaceholderMandate(account, chainId);
108
+ }
109
+
110
+ async function signPlaceholderMandate(
111
+ account: Account,
112
+ chainId: number,
113
+ ): Promise<SignedSpendMandate> {
114
+ return await signServerMandate(
115
+ account,
116
+ {
117
+ maxPerStreamUsdc: 1n,
118
+ maxTotalUsdc: 1n,
119
+ maxRatePerSecondUsdc: 1n,
120
+ maxDurationSeconds: 1,
121
+ allowedOperators: [],
122
+ ttlSeconds: DEFAULT_SERVER_MANDATE_TTL_SECONDS,
123
+ },
124
+ { chainId },
125
+ );
126
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Output shape. Human-readable by default; `--json` emits one JSON object on
3
+ * stdout per command so a script can pipe it into `jq` without parsing prose.
4
+ *
5
+ * Warnings and progress go to stderr in BOTH modes, so `--json` stdout stays a
6
+ * single parseable document even when discovery drops a row or a reclaim
7
+ * retries.
8
+ */
9
+ export interface CliStreams {
10
+ readonly stdout: (line: string) => void;
11
+ readonly stderr: (line: string) => void;
12
+ }
13
+
14
+ export interface Printer extends CliStreams {
15
+ readonly json: boolean;
16
+ /** Human-readable body. Suppressed entirely in `--json` mode. */
17
+ readonly print: (line: string) => void;
18
+ /** Key/value block, aligned. Suppressed in `--json` mode. */
19
+ readonly table: (rows: readonly (readonly [string, string])[]) => void;
20
+ /** Progress / warnings. Always stderr, always shown. */
21
+ readonly note: (line: string) => void;
22
+ /** The machine-readable result. Emitted only in `--json` mode. */
23
+ readonly result: (value: unknown) => void;
24
+ /**
25
+ * One COMPACT JSON object on a single stdout line — JSON Lines. Used by
26
+ * commands that emit a record per tick (`watch`), where every line must parse
27
+ * standalone so a consumer can react as each one lands. `result` is the
28
+ * pretty-printed single-document form and is wrong for a stream: a multi-line
29
+ * object means not one of its lines parses on its own.
30
+ */
31
+ readonly resultLine: (value: unknown) => void;
32
+ }
33
+
34
+ export function createPrinter(streams: CliStreams, json: boolean): Printer {
35
+ return {
36
+ ...streams,
37
+ json,
38
+ print(line) {
39
+ if (!json) streams.stdout(line);
40
+ },
41
+ table(rows) {
42
+ if (json) return;
43
+ const width = rows.reduce((max, [key]) => Math.max(max, key.length), 0);
44
+ for (const [key, value] of rows) {
45
+ streams.stdout(` ${key.padEnd(width)} ${value}`);
46
+ }
47
+ },
48
+ note(line) {
49
+ streams.stderr(line);
50
+ },
51
+ result(value) {
52
+ if (json) streams.stdout(JSON.stringify(value, jsonReplacer, 2));
53
+ },
54
+ resultLine(value) {
55
+ if (json) streams.stdout(JSON.stringify(value, jsonReplacer));
56
+ },
57
+ };
58
+ }
59
+
60
+ /** bigints are the natural amount type here; JSON has no bigint. */
61
+ function jsonReplacer(_key: string, value: unknown): unknown {
62
+ return typeof value === "bigint" ? value.toString() : value;
63
+ }
@@ -0,0 +1,88 @@
1
+ import { formatUsdc } from "./amounts.js";
2
+ import { CliError } from "./errors.js";
3
+ import type { CliReadClient } from "./deps.js";
4
+
5
+ /** Where a Base Sepolia test wallet gets its gas. */
6
+ export const BASE_SEPOLIA_FAUCET_URL =
7
+ "https://www.alchemy.com/faucets/base-sepolia";
8
+
9
+ /** Where a Base Sepolia test wallet gets the USDC it escrows. */
10
+ export const CIRCLE_USDC_FAUCET_URL = "https://faucet.circle.com";
11
+
12
+ /**
13
+ * Refuse to broadcast from a wallet that cannot pay for gas.
14
+ *
15
+ * Without this, the first thing an unfunded wallet sees is the node's own
16
+ * phrasing — `USDC approve transaction failed: Execution reverted with reason:
17
+ * gas required exceeds allowance (0)` — which reads like a USDC allowance
18
+ * problem and sends the reader looking at approvals. The actual fact is "this
19
+ * address holds no ETH", and it is knowable with one read before anything is
20
+ * signed.
21
+ *
22
+ * Deliberately only a ZERO check, not an estimate: predicting the gas a hire
23
+ * will cost means simulating it, and a wallet with a little ETH that still runs
24
+ * short should get the real revert rather than a guess from this CLI.
25
+ */
26
+ export async function assertCanPayGas(
27
+ read: CliReadClient,
28
+ address: `0x${string}`,
29
+ ): Promise<void> {
30
+ let balance: bigint;
31
+ try {
32
+ balance = await read.nativeBalance(address);
33
+ } catch {
34
+ // A balance read is a courtesy, not a gate. If the RPC will not answer it,
35
+ // let the command proceed and fail with the real error.
36
+ return;
37
+ }
38
+ if (balance > 0n) return;
39
+
40
+ throw new CliError(
41
+ `wallet ${address} has 0 ETH on Base Sepolia, so it cannot pay for gas`,
42
+ {
43
+ exitCode: 3,
44
+ hint: `fund it at ${BASE_SEPOLIA_FAUCET_URL}, then re-run. (Gas is ETH; the stream itself is escrowed in USDC, which is a separate balance.)`,
45
+ },
46
+ );
47
+ }
48
+
49
+ /**
50
+ * Refuse to broadcast a hire the wallet cannot fund.
51
+ *
52
+ * The sibling of `assertCanPayGas`, for the OTHER balance. Gas is ETH and the
53
+ * deposit is USDC: they are separate balances on separate tokens, so a wallet
54
+ * that passes the gas check can still hold no USDC at all. Without this the
55
+ * `open` path sends the USDC approval FIRST — which succeeds, because approving
56
+ * an amount you do not hold is legal — and then reverts inside `transferFrom`,
57
+ * having already spent gas to learn something that was one `balanceOf` away.
58
+ *
59
+ * Deliberately NOT an allowance check. The CLI's own open path grants the
60
+ * bounded approval it needs (`approveTxHash` in the result), so a missing
61
+ * allowance is a step of the command, not a precondition for it; gating on it
62
+ * would refuse a hire that was about to work. Balance is the fact the CLI
63
+ * cannot fix for you.
64
+ */
65
+ export async function assertCanFundStream(
66
+ read: CliReadClient,
67
+ address: `0x${string}`,
68
+ budgetUsdc: bigint,
69
+ ): Promise<void> {
70
+ let balance: bigint;
71
+ try {
72
+ balance = await read.usdcBalance(address);
73
+ } catch {
74
+ // Same courtesy rule as the gas check: an RPC that will not answer a
75
+ // balance read must not become this command's failure mode. Proceed and let
76
+ // the real error surface.
77
+ return;
78
+ }
79
+ if (balance >= budgetUsdc) return;
80
+
81
+ throw new CliError(
82
+ `wallet ${address} holds ${formatUsdc(balance)} but --budget is ${formatUsdc(budgetUsdc)}`,
83
+ {
84
+ exitCode: 3,
85
+ hint: `the deposit is escrowed in USDC — fund the wallet at ${CIRCLE_USDC_FAUCET_URL} (Base Sepolia), then re-run. (Gas is a separate ETH balance.)`,
86
+ },
87
+ );
88
+ }