@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absol-labs/agent",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Metrik agent layer: x402 verified-streaming payments, an MCP server, framework tools, and spend mandates so AI agents can hire and pay verified services safely.",
5
5
  "license": "MIT",
6
6
  "author": "Absol Labs",
@@ -17,6 +17,12 @@
17
17
  "type": "module",
18
18
  "main": "dist/index.js",
19
19
  "types": "dist/index.d.ts",
20
+ "bin": {
21
+ "metrik": "dist/cli/bin.js",
22
+ "metrik-mcp": "dist/mcp/stdio.js",
23
+ "metrik-mcp-http": "dist/mcp/http-server.js",
24
+ "metrik-gateway": "dist/gateway/server-entry.js"
25
+ },
20
26
  "exports": {
21
27
  ".": {
22
28
  "types": "./dist/index.d.ts",
@@ -34,6 +40,10 @@
34
40
  "types": "./dist/frameworks/langchain.d.ts",
35
41
  "import": "./dist/frameworks/langchain.js"
36
42
  },
43
+ "./cli": {
44
+ "types": "./dist/cli/index.d.ts",
45
+ "import": "./dist/cli/index.js"
46
+ },
37
47
  "./crewai": {
38
48
  "types": "./dist/frameworks/crewai.d.ts",
39
49
  "import": "./dist/frameworks/crewai.js"
@@ -63,12 +73,12 @@
63
73
  "access": "public"
64
74
  },
65
75
  "engines": {
66
- "node": ">=20 <21",
76
+ "node": ">=20.19 <25",
67
77
  "pnpm": "9.15.x"
68
78
  },
69
79
  "dependencies": {
70
- "@absol-labs/sdk": "^0.9.0",
71
- "@absol-labs/shared": "^0.12.1",
80
+ "@absol-labs/sdk": "^0.9.2",
81
+ "@absol-labs/shared": "^0.13.1",
72
82
  "@modelcontextprotocol/sdk": "^1.29.0",
73
83
  "@napi-rs/keyring": "1.3.0",
74
84
  "@privy-io/node": "0.33.0",
@@ -121,6 +131,7 @@
121
131
  "scripts": {
122
132
  "build": "tsc -p tsconfig.json",
123
133
  "build:e2e": "tsc -p tsconfig.e2e.build.json",
134
+ "cli": "tsx src/cli/bin.ts",
124
135
  "mcp:stdio": "tsx src/mcp/stdio.ts",
125
136
  "mcp:http": "tsx src/mcp/http-server.ts",
126
137
  "gateway": "tsx src/gateway/server-entry.ts",
@@ -129,7 +140,8 @@
129
140
  "l2:proof": "node scripts/l2-proof.mjs",
130
141
  "test": "vitest run",
131
142
  "test:package": "node scripts/test-packed-package.mjs",
132
- "typecheck": "tsc -p tsconfig.json --noEmit",
143
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.examples.json --noEmit",
144
+ "typecheck:examples": "tsc -p tsconfig.examples.json --noEmit",
133
145
  "typecheck:e2e": "tsc -p tsconfig.e2e.json --noEmit",
134
146
  "lint": "tsc -p tsconfig.json --noEmit",
135
147
  "format": "prettier --write ."
@@ -0,0 +1,102 @@
1
+ import { formatUnits, parseUnits } from "viem";
2
+
3
+ import { usageError } from "./errors.js";
4
+
5
+ /** Circle USDC — 6 decimals on every deployment Metrik settles against. */
6
+ export const USDC_DECIMALS = 6;
7
+
8
+ /**
9
+ * How `--budget` / `--rate` are interpreted.
10
+ *
11
+ * - `"usdc"` (default): decimal USDC, e.g. `--budget 0.27`.
12
+ * - `"atomic"`: raw 6-decimal base units, e.g. `--budget 270000`.
13
+ *
14
+ * Both are offered because the two audiences count differently: a human reads
15
+ * USDC, while the escrow, the oracle and every `METRIK_E2E_*` fixture are in
16
+ * atomic units. Guessing from the magnitude of the number would be a silent
17
+ * 1,000,000x mistake on a fund-moving command, so the unit is explicit.
18
+ */
19
+ export type AmountUnits = "usdc" | "atomic";
20
+
21
+ export function parseAmountUnits(raw: string | undefined): AmountUnits {
22
+ if (raw === undefined || raw === "usdc") return "usdc";
23
+ if (raw === "atomic") return "atomic";
24
+ throw usageError(`--units must be "usdc" or "atomic" (got "${raw}")`);
25
+ }
26
+
27
+ /** Parses a `--budget`/`--rate` value into atomic USDC units. */
28
+ export function parseUsdcAmount(
29
+ raw: string,
30
+ units: AmountUnits,
31
+ flagName: string,
32
+ ): bigint {
33
+ if (units === "atomic") {
34
+ if (!/^\d+$/.test(raw)) {
35
+ throw usageError(
36
+ `--${flagName} must be a non-negative integer of atomic USDC units (got "${raw}")`,
37
+ );
38
+ }
39
+ return BigInt(raw);
40
+ }
41
+
42
+ if (!/^\d+(\.\d+)?$/.test(raw)) {
43
+ throw usageError(
44
+ `--${flagName} must be a decimal USDC amount (got "${raw}")`,
45
+ "pass --units atomic to supply raw 6-decimal base units instead.",
46
+ );
47
+ }
48
+ const fraction = raw.split(".")[1];
49
+ if (fraction !== undefined && fraction.length > USDC_DECIMALS) {
50
+ throw usageError(
51
+ `--${flagName} has more precision than USDC supports (${USDC_DECIMALS} decimals): ${raw}`,
52
+ );
53
+ }
54
+ return parseUnits(raw, USDC_DECIMALS);
55
+ }
56
+
57
+ /** `270000n` -> `"0.27 USDC (270000 atomic)"`. */
58
+ export function formatUsdc(atomic: bigint): string {
59
+ return `${formatUnits(atomic, USDC_DECIMALS)} USDC (${atomic.toString()} atomic)`;
60
+ }
61
+
62
+ /**
63
+ * Parses `--duration`: bare seconds, or a `s`/`m`/`h`/`d` suffix.
64
+ */
65
+ export function parseDurationSeconds(raw: string): number {
66
+ const match = /^(\d+)([smhd]?)$/.exec(raw);
67
+ if (match === null) {
68
+ throw usageError(
69
+ `--duration must be seconds, optionally suffixed s/m/h/d (got "${raw}")`,
70
+ );
71
+ }
72
+ const value = Number(match[1]);
73
+ const multiplier = { "": 1, s: 1, m: 60, h: 3_600, d: 86_400 }[
74
+ match[2] ?? ""
75
+ ];
76
+ if (multiplier === undefined) {
77
+ throw usageError(`--duration has an unsupported unit: ${raw}`);
78
+ }
79
+ const seconds = value * multiplier;
80
+ if (!Number.isSafeInteger(seconds) || seconds <= 0) {
81
+ throw usageError(`--duration must be a positive number of seconds: ${raw}`);
82
+ }
83
+ return seconds;
84
+ }
85
+
86
+ /** Seconds -> a compact human duration (`3600` -> `1h`). */
87
+ export function formatDuration(seconds: number): string {
88
+ if (seconds <= 0) return "0s";
89
+ const parts: string[] = [];
90
+ let remaining = seconds;
91
+ for (const [unit, size] of [
92
+ ["d", 86_400],
93
+ ["h", 3_600],
94
+ ["m", 60],
95
+ ["s", 1],
96
+ ] as const) {
97
+ const count = Math.floor(remaining / size);
98
+ if (count > 0) parts.push(`${count}${unit}`);
99
+ remaining -= count * size;
100
+ }
101
+ return parts.join(" ");
102
+ }
@@ -0,0 +1,150 @@
1
+ import { usageError } from "./errors.js";
2
+
3
+ /**
4
+ * A deliberately small GNU-style argument parser. The CLI ships no argument
5
+ * parsing dependency: the surface is seven commands with a handful of flags, and
6
+ * the packed tarball should stay as light as the library it wraps.
7
+ *
8
+ * Supported forms: `--flag`, `--flag value`, `--flag=value`, `--no-flag`, and
9
+ * positional arguments. `--` ends flag parsing.
10
+ */
11
+ export interface ParsedArgs {
12
+ readonly command: string | null;
13
+ readonly positionals: readonly string[];
14
+ /**
15
+ * Every occurrence of each flag, in order. Repeats are kept rather than
16
+ * overwritten so `--header` can be given more than once; scalar readers take
17
+ * the last occurrence.
18
+ */
19
+ readonly flags: ReadonlyMap<string, readonly (string | boolean)[]>;
20
+ }
21
+
22
+ export function parseArgs(argv: readonly string[]): ParsedArgs {
23
+ const positionals: string[] = [];
24
+ const flags = new Map<string, (string | boolean)[]>();
25
+ const push = (name: string, value: string | boolean): void => {
26
+ const existing = flags.get(name);
27
+ if (existing === undefined) flags.set(name, [value]);
28
+ else existing.push(value);
29
+ };
30
+ let passthrough = false;
31
+
32
+ for (let index = 0; index < argv.length; index++) {
33
+ const token = argv[index];
34
+ if (token === undefined) continue;
35
+
36
+ if (passthrough || !token.startsWith("--")) {
37
+ positionals.push(token);
38
+ continue;
39
+ }
40
+ if (token === "--") {
41
+ passthrough = true;
42
+ continue;
43
+ }
44
+
45
+ const body = token.slice(2);
46
+ const equals = body.indexOf("=");
47
+ if (equals !== -1) {
48
+ const name = body.slice(0, equals);
49
+ assertFlagName(name, token);
50
+ push(name, body.slice(equals + 1));
51
+ continue;
52
+ }
53
+ assertFlagName(body, token);
54
+
55
+ if (body.startsWith("no-")) {
56
+ push(body.slice(3), false);
57
+ continue;
58
+ }
59
+
60
+ const next = argv[index + 1];
61
+ if (next === undefined || next.startsWith("--")) {
62
+ push(body, true);
63
+ continue;
64
+ }
65
+ push(body, next);
66
+ index++;
67
+ }
68
+
69
+ const [command = null, ...rest] = positionals;
70
+ return { command, positionals: rest, flags };
71
+ }
72
+
73
+ function assertFlagName(name: string, token: string): void {
74
+ if (name.length === 0) {
75
+ throw usageError(`not a valid flag: ${token}`);
76
+ }
77
+ }
78
+
79
+ /** Reads a flag that must carry a string value. */
80
+ export function stringFlag(args: ParsedArgs, name: string): string | undefined {
81
+ const values = args.flags.get(name);
82
+ if (values === undefined) return undefined;
83
+ const value = values[values.length - 1];
84
+ if (typeof value !== "string") {
85
+ throw usageError(`--${name} requires a value`);
86
+ }
87
+ return value;
88
+ }
89
+
90
+ /** Every occurrence of a repeatable string flag (e.g. `--header`). */
91
+ export function stringFlags(args: ParsedArgs, name: string): string[] {
92
+ const values = args.flags.get(name) ?? [];
93
+ return values.map((value) => {
94
+ if (typeof value !== "string") {
95
+ throw usageError(`--${name} requires a value`);
96
+ }
97
+ return value;
98
+ });
99
+ }
100
+
101
+ export function requiredStringFlag(args: ParsedArgs, name: string): string {
102
+ const value = stringFlag(args, name);
103
+ if (value === undefined) {
104
+ throw usageError(`--${name} is required`);
105
+ }
106
+ return value;
107
+ }
108
+
109
+ export function booleanFlag(args: ParsedArgs, name: string): boolean {
110
+ const values = args.flags.get(name);
111
+ if (values === undefined) return false;
112
+ const value = values[values.length - 1];
113
+ if (value === undefined) return false;
114
+ if (typeof value === "boolean") return value;
115
+ if (value === "true") return true;
116
+ if (value === "false") return false;
117
+ throw usageError(`--${name} takes no value (got "${value}")`);
118
+ }
119
+
120
+ export function integerFlag(
121
+ args: ParsedArgs,
122
+ name: string,
123
+ ): number | undefined {
124
+ const raw = stringFlag(args, name);
125
+ if (raw === undefined) return undefined;
126
+ if (!/^\d+$/.test(raw)) {
127
+ throw usageError(`--${name} must be a non-negative integer (got "${raw}")`);
128
+ }
129
+ return Number(raw);
130
+ }
131
+
132
+ /**
133
+ * Rejects flags the command does not understand. A silently ignored `--budgt`
134
+ * on a fund-moving command is how someone spends more than they meant to.
135
+ */
136
+ export function assertKnownFlags(
137
+ args: ParsedArgs,
138
+ known: readonly string[],
139
+ ): void {
140
+ const allowed = new Set(known);
141
+ const unknown = [...args.flags.keys()].filter((name) => !allowed.has(name));
142
+ if (unknown.length > 0) {
143
+ throw usageError(
144
+ `unknown flag${unknown.length > 1 ? "s" : ""}: ${unknown
145
+ .map((name) => `--${name}`)
146
+ .join(", ")}`,
147
+ `known flags: ${known.map((name) => `--${name}`).join(", ")}`,
148
+ );
149
+ }
150
+ }
@@ -0,0 +1,86 @@
1
+ import { describeError } from "./errors.js";
2
+ import type { CliReadClient } from "./deps.js";
3
+
4
+ /**
5
+ * Who attests, read off the escrow.
6
+ *
7
+ * Metrik's whole claim is that money moves only for delivery that a NEUTRAL
8
+ * verifier checked. On the live Base Sepolia deployment that neutrality is
9
+ * partial: `getOracleSigners()` returns three addresses, and one of them is the
10
+ * operator of the listings a buyer is being offered. A tool that prints
11
+ * "cryptographically verified" and "tier 2" without saying so is letting the
12
+ * buyer assume something the deployment does not provide.
13
+ *
14
+ * This is DISCLOSURE, not enforcement. The CLI does not refuse such a listing
15
+ * and does not score it — it states the fact and leaves the judgement to the
16
+ * buyer. Fixing the deployment is a deployment decision, not a CLI one.
17
+ */
18
+ export interface AttesterSet {
19
+ /** The signer set, lowercased. `null` when the escrow would not answer. */
20
+ readonly signers: readonly string[] | null;
21
+ /** Why the set could not be read, when applicable. */
22
+ readonly unavailable: string | null;
23
+ }
24
+
25
+ /**
26
+ * Read the signer set, failing SOFT.
27
+ *
28
+ * An unreadable set must never render as "no overlap": "we checked and the
29
+ * operator does not attest" and "we could not check" are different facts, and
30
+ * conflating them would turn an outage into a false assurance. `signers: null`
31
+ * is carried through to every caller and printed as UNKNOWN.
32
+ */
33
+ export async function readAttesterSet(
34
+ read: Pick<CliReadClient, "oracleSigners">,
35
+ ): Promise<AttesterSet> {
36
+ try {
37
+ const signers = await read.oracleSigners();
38
+ return {
39
+ signers: signers.map((signer) => signer.toLowerCase()),
40
+ unavailable: null,
41
+ };
42
+ } catch (error) {
43
+ return { signers: null, unavailable: describeError(error) };
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Is this operator also one of the escrow's attesters?
49
+ *
50
+ * `null` means unknown (the set could not be read) and must be rendered as
51
+ * unknown, never as `false`.
52
+ */
53
+ export function operatorAttestsOwnDelivery(
54
+ set: AttesterSet,
55
+ operator: string,
56
+ ): boolean | null {
57
+ if (set.signers === null) return null;
58
+ return set.signers.includes(operator.toLowerCase());
59
+ }
60
+
61
+ /**
62
+ * One factual stderr line, or `null` when there is nothing to disclose.
63
+ *
64
+ * Deliberately not alarming and deliberately not a verdict: it reports a
65
+ * configuration fact and what that fact means, and stops there.
66
+ */
67
+ export function attesterDisclosure(
68
+ set: AttesterSet,
69
+ operator: string,
70
+ ): string | null {
71
+ const overlap = operatorAttestsOwnDelivery(set, operator);
72
+ if (overlap === null) return attesterSetUnavailableWarning(set);
73
+ if (!overlap) return null;
74
+ const total = set.signers?.length ?? 0;
75
+ return `warning: operator ${operator} is also one of the ${total} oracle signer${total === 1 ? "" : "s"} configured on this escrow. The party being paid is among the parties attesting that delivery happened. Verification here is not independent of the seller.`;
76
+ }
77
+
78
+ /**
79
+ * The one warning that belongs to the ESCROW rather than to any one listing:
80
+ * the signer set could not be read at all. Printed once per command, not once
81
+ * per row, and never as "no overlap".
82
+ */
83
+ export function attesterSetUnavailableWarning(set: AttesterSet): string | null {
84
+ if (set.signers !== null) return null;
85
+ return `warning: the escrow's oracle signer set could not be read (${set.unavailable ?? "no reason given"}) — whether the operator being paid is also one of the parties attesting to its own delivery is UNKNOWN, not "no overlap".`;
86
+ }
package/src/cli/bin.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./run.js";
3
+
4
+ const exitCode = await runCli({ argv: process.argv.slice(2) });
5
+ process.exitCode = exitCode;
@@ -0,0 +1,100 @@
1
+ import { formatUsdc } from "../amounts.js";
2
+ import { assertTestnetRpc, parseBytes32 } from "../config.js";
3
+ import { CliError, describeError, usageError } from "../errors.js";
4
+ import { signClaimMandate } from "../mandate.js";
5
+ import { assertCanPayGas } from "../preflight.js";
6
+ import { PRIVATE_KEY_ENV, resolveCliWallet } from "../wallet.js";
7
+ import type { CommandContext, CommandSpec } from "../context.js";
8
+
9
+ /**
10
+ * The seller's side of the loop.
11
+ *
12
+ * `status` has always computed and printed an `operator claimable` row — a
13
+ * redeemable balance the CLI could see and could not act on. `claimWithProof` is
14
+ * operator-gated on-chain, so this command refuses a wallet that is not the
15
+ * stream's operator BEFORE it broadcasts: an address mismatch is knowable with
16
+ * one read, and letting it become a raw `NotOperator()` revert would cost gas to
17
+ * learn a fact that was already available.
18
+ */
19
+ export const claimCommand: CommandSpec = {
20
+ name: "claim",
21
+ summary: "Operator: redeem what this stream has verifiably earned",
22
+ usage: "metrik claim <streamId>",
23
+ flags: [],
24
+ signs: true,
25
+ async run(context: CommandContext): Promise<void> {
26
+ const { args, config, env, out, deps } = context;
27
+ const [streamIdArg] = args.positionals;
28
+ if (streamIdArg === undefined) {
29
+ throw usageError("metrik claim requires a <streamId>");
30
+ }
31
+ const streamId = parseBytes32(streamIdArg, "<streamId>");
32
+
33
+ const { account } = await resolveCliWallet(env, { note: out.note });
34
+ const read = deps.createReadClient(config);
35
+ await assertTestnetRpc(config, () => read.getChainId());
36
+ await assertCanPayGas(read, account.address);
37
+
38
+ const stream = await read.getStreamV2(streamId);
39
+ if (stream.operator.toLowerCase() !== account.address.toLowerCase()) {
40
+ throw new CliError(
41
+ `wallet ${account.address} is not the operator of this stream`,
42
+ {
43
+ exitCode: 3,
44
+ hint: `only ${stream.operator} can claim it — the escrow enforces this, so no other wallet can. Set ${PRIVATE_KEY_ENV} to the operator's key. (The buyer's side of this stream is \`metrik reclaim ${streamId}\`.)`,
45
+ },
46
+ );
47
+ }
48
+
49
+ // Accrual lives in the oracle's checkpoint proof, never in the stream tuple.
50
+ // No proof means no claim is possible yet, and saying that is better than
51
+ // paying gas for a `NothingToClaim()` revert.
52
+ let cumulativeAmount: bigint;
53
+ try {
54
+ cumulativeAmount = (await read.checkpointProof(streamId))
55
+ .cumulativeAmount;
56
+ } catch (error) {
57
+ throw new CliError(
58
+ "no checkpoint covers this stream yet, so there is nothing provable to claim",
59
+ {
60
+ exitCode: 4,
61
+ hint: `the oracle publishes roughly every 15-22s and pays only for intervals that verified; ${describeError(
62
+ error,
63
+ )}`,
64
+ cause: error,
65
+ },
66
+ );
67
+ }
68
+
69
+ const claimable = await read.claimableForV2(streamId, cumulativeAmount);
70
+ if (claimable === 0n) {
71
+ throw new CliError("this stream has nothing claimable right now", {
72
+ exitCode: 4,
73
+ hint: `everything checkpointed so far has already been claimed. See where it stands: metrik status ${streamId}`,
74
+ });
75
+ }
76
+
77
+ const write = deps.createWriteClient(config, account);
78
+ const result = await write.claimStream({
79
+ streamId,
80
+ signedMandate: await signClaimMandate(account, config.chainId),
81
+ spentSoFarUsdc: 0n,
82
+ nowSeconds: deps.nowSeconds(),
83
+ });
84
+
85
+ out.result({
86
+ streamId,
87
+ txHash: result.txHash,
88
+ operator: stream.operator,
89
+ claimed: claimable,
90
+ verifiedCumulative: cumulativeAmount,
91
+ });
92
+ out.print("Claimed.");
93
+ out.table([
94
+ ["streamId", streamId],
95
+ ["tx", result.txHash],
96
+ ["operator", stream.operator],
97
+ ["claimed", `${formatUsdc(claimable)} (net of protocol fee)`],
98
+ ]);
99
+ },
100
+ };
@@ -0,0 +1,53 @@
1
+ import { assertTestnetRpc, parseBytes32 } from "../config.js";
2
+ import { usageError } from "../errors.js";
3
+ import { signRecoveryMandate } from "../mandate.js";
4
+ import { assertCanPayGas } from "../preflight.js";
5
+ import { resolveCliWallet } from "../wallet.js";
6
+ import type { CommandContext, CommandSpec } from "../context.js";
7
+
8
+ export const closeCommand: CommandSpec = {
9
+ name: "close",
10
+ summary: "Stop the clock on a stream so the unspent budget can be reclaimed",
11
+ usage: "metrik close <streamId>",
12
+ flags: [],
13
+ signs: true,
14
+ async run(context: CommandContext): Promise<void> {
15
+ const { args, config, env, out, deps } = context;
16
+ const [streamIdArg] = args.positionals;
17
+ if (streamIdArg === undefined) {
18
+ throw usageError("metrik close requires a <streamId>");
19
+ }
20
+ const streamId = parseBytes32(streamIdArg, "<streamId>");
21
+
22
+ const { account } = await resolveCliWallet(env, { note: out.note });
23
+ const read = deps.createReadClient(config);
24
+ await assertTestnetRpc(config, () => read.getChainId());
25
+ await assertCanPayGas(read, account.address);
26
+
27
+ const signedMandate = await signRecoveryMandate(account, config.chainId);
28
+ const write = deps.createWriteClient(config, account);
29
+ const result = await write.closeStream({
30
+ streamId,
31
+ signedMandate,
32
+ spentSoFarUsdc: 0n,
33
+ nowSeconds: deps.nowSeconds(),
34
+ });
35
+
36
+ out.result({ streamId, txHash: result.txHash, closed: true });
37
+ out.print("Stream closed.");
38
+ out.table([
39
+ ["streamId", streamId],
40
+ ["tx", result.txHash],
41
+ ]);
42
+ out.print("");
43
+ out.print(
44
+ `Nothing pushes money back. Reclaim what was never earned: metrik reclaim ${streamId}`,
45
+ );
46
+ out.print(
47
+ "That reclaim reverts until the oracle publishes a checkpoint covering the stop time, so it waits and retries for you — leave it running.",
48
+ );
49
+ out.print(
50
+ `Next time: metrik reclaim <streamId> --close-first does this close and that reclaim in one command.`,
51
+ );
52
+ },
53
+ };