@rubicon-caliga/cli 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/circle.ts ADDED
@@ -0,0 +1,232 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
5
+
6
+ export type CircleRunner = (command: string, args: string[]) => Promise<string>;
7
+
8
+ export interface CircleErrorInfo {
9
+ code: "network_unavailable" | "otp_expired" | "not_logged_in" | "missing_cli" | "command_failed";
10
+ message: string;
11
+ guidance: string;
12
+ }
13
+
14
+ export interface CircleWalletInfo {
15
+ address: `0x${string}`;
16
+ raw?: unknown;
17
+ }
18
+
19
+ export interface CircleBalanceInfo {
20
+ balanceAtomic: `${bigint}`;
21
+ backingEOA?: `0x${string}`;
22
+ raw?: unknown;
23
+ }
24
+
25
+ export function defaultCircleCommand(): string {
26
+ return process.env.CIRCLE_CLI_COMMAND ?? "circle";
27
+ }
28
+
29
+ export async function runCircleCli(command: string, args: string[]): Promise<string> {
30
+ try {
31
+ const { stdout } = await execFileAsync(command, args, { maxBuffer: 1024 * 1024 });
32
+ return stdout;
33
+ } catch (error) {
34
+ throw classifyCircleError(error);
35
+ }
36
+ }
37
+
38
+ export function classifyCircleError(error: unknown): Error & { circle?: CircleErrorInfo } {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ const output = [
41
+ message,
42
+ isRecord(error) && typeof error.stdout === "string" ? error.stdout : undefined,
43
+ isRecord(error) && typeof error.stderr === "string" ? error.stderr : undefined,
44
+ ]
45
+ .filter(Boolean)
46
+ .join("\n");
47
+ const lower = output.toLowerCase();
48
+ let info: CircleErrorInfo;
49
+ if (lower.includes("fetch failed") || lower.includes("network") || lower.includes("enotfound") || lower.includes("econnrefused")) {
50
+ info = {
51
+ code: "network_unavailable",
52
+ message: "Circle CLI network request failed.",
53
+ guidance: "Retry this command in a network-capable shell or agent context. Restricted sandboxes often block Circle auth and Gateway calls.",
54
+ };
55
+ } else if (lower.includes("otp") && (lower.includes("expired") || lower.includes("invalid") || lower.includes("request"))) {
56
+ info = {
57
+ code: "otp_expired",
58
+ message: "Circle OTP request id is invalid or expired.",
59
+ guidance: "Start a fresh Circle auth OTP flow, then rerun the Rubicon command after login completes.",
60
+ };
61
+ } else if (lower.includes("login") || lower.includes("logged in") || lower.includes("unauthorized") || lower.includes("auth")) {
62
+ info = {
63
+ code: "not_logged_in",
64
+ message: "Circle CLI is not logged in.",
65
+ guidance: "Run Circle CLI login/auth again, then rerun Rubicon. If an OTP flow was interrupted, start a fresh OTP init.",
66
+ };
67
+ } else if (lower.includes("enoent") || lower.includes("not found")) {
68
+ info = {
69
+ code: "missing_cli",
70
+ message: "Circle CLI was not found.",
71
+ guidance: "Install Circle CLI and make sure the `circle` binary is on PATH.",
72
+ };
73
+ } else {
74
+ info = {
75
+ code: "command_failed",
76
+ message: "Circle CLI command failed.",
77
+ guidance: "Inspect the Circle CLI output, confirm login, wallet, selected chain, and network access, then retry.",
78
+ };
79
+ }
80
+ const wrapped = new Error(`${info.message} ${output}`.trim()) as Error & { circle?: CircleErrorInfo };
81
+ wrapped.circle = info;
82
+ return wrapped;
83
+ }
84
+
85
+ export function circleGuidance(error: unknown): CircleErrorInfo | undefined {
86
+ if (isRecord(error) && isRecord(error.circle)) {
87
+ return error.circle as unknown as CircleErrorInfo;
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ export async function circleVersion(input: { command?: string; runner?: CircleRunner } = {}): Promise<string> {
93
+ const command = input.command ?? defaultCircleCommand();
94
+ const runner = input.runner ?? runCircleCli;
95
+ return (await runner(command, ["--version"])).trim();
96
+ }
97
+
98
+ export async function circleAuthStatus(input: { command?: string; runner?: CircleRunner } = {}): Promise<unknown> {
99
+ const command = input.command ?? defaultCircleCommand();
100
+ const runner = input.runner ?? runCircleCli;
101
+ const output = await runner(command, ["auth", "status", "--output", "json"]);
102
+ return parseMaybeJson(output);
103
+ }
104
+
105
+ export async function circleAgentWallet(input: {
106
+ chain: string;
107
+ configuredAddress?: `0x${string}`;
108
+ command?: string;
109
+ runner?: CircleRunner;
110
+ }): Promise<CircleWalletInfo> {
111
+ if (input.configuredAddress) return { address: input.configuredAddress };
112
+ const command = input.command ?? defaultCircleCommand();
113
+ const runner = input.runner ?? runCircleCli;
114
+ const output = await runner(command, ["wallet", "list", "--chain", input.chain, "--type", "agent", "--output", "json"]);
115
+ const raw = parseMaybeJson(output);
116
+ const address = parseWalletAddress(raw);
117
+ return { address, raw };
118
+ }
119
+
120
+ export async function circleGatewayBalance(input: {
121
+ address: `0x${string}`;
122
+ chain: string;
123
+ command?: string;
124
+ runner?: CircleRunner;
125
+ }): Promise<CircleBalanceInfo> {
126
+ const command = input.command ?? defaultCircleCommand();
127
+ const runner = input.runner ?? runCircleCli;
128
+ const output = await runner(command, ["gateway", "balance", "--address", input.address, "--chain", input.chain, "--output", "json"]);
129
+ const raw = parseMaybeJson(output);
130
+ return {
131
+ balanceAtomic: parseBalanceAtomic(raw),
132
+ backingEOA: parseBackingEOA(raw),
133
+ raw,
134
+ };
135
+ }
136
+
137
+ export async function circleGatewayFaucet(input: {
138
+ address: `0x${string}`;
139
+ chain: string;
140
+ command?: string;
141
+ runner?: CircleRunner;
142
+ }): Promise<unknown> {
143
+ const command = input.command ?? defaultCircleCommand();
144
+ const runner = input.runner ?? runCircleCli;
145
+ const output = await runner(command, ["gateway", "faucet", "--address", input.address, "--chain", input.chain, "--output", "json"]);
146
+ return parseMaybeJson(output);
147
+ }
148
+
149
+ function parseWalletAddress(value: unknown): `0x${string}` {
150
+ const wallets = collectRecords(value);
151
+ const addresses = wallets
152
+ .map((wallet) => findString(wallet, ["address", "walletAddress", "blockchainAddress"]))
153
+ .filter((address): address is `0x${string}` => Boolean(address && isAddress(address)));
154
+ const unique = [...new Set(addresses.map((address) => address.toLowerCase()))];
155
+ if (unique.length === 1) return addresses.find((address) => address.toLowerCase() === unique[0])!;
156
+ if (unique.length === 0) throw new Error("Circle CLI did not return an Agent Wallet address.");
157
+ throw new Error("Multiple Circle Agent Wallets found; configure agent-wallet-address.");
158
+ }
159
+
160
+ function parseBalanceAtomic(value: unknown): `${bigint}` {
161
+ const direct =
162
+ findString(value, [
163
+ "data.balanceAtomic",
164
+ "data.availableAtomic",
165
+ "data.usdc.balanceAtomic",
166
+ "balanceAtomic",
167
+ "availableAtomic",
168
+ "usdc.balanceAtomic",
169
+ ]) ?? findNumber(value, ["data.balance", "data.available", "balance", "available"]);
170
+ if (typeof direct === "number") return `${BigInt(Math.trunc(direct * 1_000_000))}`;
171
+ if (direct && /^\d+$/.test(direct)) return direct as `${bigint}`;
172
+ if (direct && /^\d+(\.\d+)?$/.test(direct)) return `${decimalUsdcToAtomic(direct)}`;
173
+ return "0";
174
+ }
175
+
176
+ function parseBackingEOA(value: unknown): `0x${string}` | undefined {
177
+ const address = findString(value, ["data.backingEOA", "backingEOA", "data.backingEoa", "backingEoa"]);
178
+ return address && isAddress(address) ? address : undefined;
179
+ }
180
+
181
+ function parseMaybeJson(output: string): unknown {
182
+ const trimmed = output.trim();
183
+ if (!trimmed) return {};
184
+ try {
185
+ return JSON.parse(trimmed);
186
+ } catch {
187
+ return trimmed;
188
+ }
189
+ }
190
+
191
+ function decimalUsdcToAtomic(value: string): bigint {
192
+ const [whole = "0", fraction = ""] = value.split(".");
193
+ const padded = `${fraction}000000`.slice(0, 6);
194
+ return BigInt(whole) * 1_000_000n + BigInt(padded);
195
+ }
196
+
197
+ function collectRecords(value: unknown): Record<string, unknown>[] {
198
+ if (Array.isArray(value)) return value.filter(isRecord);
199
+ if (!isRecord(value)) return [];
200
+ const records: Record<string, unknown>[] = [value];
201
+ for (const key of ["wallets", "items", "data"]) {
202
+ const nested = value[key];
203
+ if (Array.isArray(nested)) records.push(...nested.filter(isRecord));
204
+ if (isRecord(nested)) records.push(...collectRecords(nested));
205
+ }
206
+ return records;
207
+ }
208
+
209
+ function findString(value: unknown, paths: string[]): string | undefined {
210
+ for (const path of paths) {
211
+ const found = path.split(".").reduce<unknown>((current, part) => (isRecord(current) ? current[part] : undefined), value);
212
+ if (typeof found === "string") return found;
213
+ }
214
+ return undefined;
215
+ }
216
+
217
+ function findNumber(value: unknown, paths: string[]): number | undefined {
218
+ for (const path of paths) {
219
+ const found = path.split(".").reduce<unknown>((current, part) => (isRecord(current) ? current[part] : undefined), value);
220
+ if (typeof found === "number") return found;
221
+ if (typeof found === "string" && /^\d+(\.\d+)?$/.test(found)) return Number(found);
222
+ }
223
+ return undefined;
224
+ }
225
+
226
+ function isAddress(value: string): value is `0x${string}` {
227
+ return /^0x[a-fA-F0-9]{40}$/.test(value);
228
+ }
229
+
230
+ function isRecord(value: unknown): value is Record<string, unknown> {
231
+ return typeof value === "object" && value !== null;
232
+ }
package/src/errors.ts CHANGED
@@ -10,6 +10,21 @@ export class CliError extends Error {
10
10
 
11
11
  export function toCliError(error: unknown): CliError {
12
12
  if (error instanceof CliError) return error;
13
- if (error instanceof Error) return new CliError("COMMAND_FAILED", error.message);
13
+ if (error instanceof Error) {
14
+ const lower = error.message.toLowerCase();
15
+ if (lower.includes("fetch failed") || lower.includes("enotfound") || lower.includes("econnrefused")) {
16
+ return new CliError(
17
+ "CIRCLE_NETWORK_UNAVAILABLE",
18
+ `${error.message} Retry in a network-capable shell or agent context; restricted sandboxes often block Circle auth and Gateway calls.`,
19
+ );
20
+ }
21
+ if (lower.includes("otp") && (lower.includes("expired") || lower.includes("invalid") || lower.includes("request"))) {
22
+ return new CliError(
23
+ "CIRCLE_OTP_EXPIRED",
24
+ `${error.message} Start a fresh Circle auth OTP flow, then rerun the Rubicon command after login completes.`,
25
+ );
26
+ }
27
+ return new CliError("COMMAND_FAILED", error.message);
28
+ }
14
29
  return new CliError("COMMAND_FAILED", String(error));
15
30
  }
package/src/index.ts CHANGED
@@ -20,7 +20,9 @@ import {
20
20
  receiptSummaryJson,
21
21
  } from "./format.js";
22
22
  import { selectPaymentEngine, type PaymentMode } from "./payments.js";
23
+ import { runDoctor, runQuickstartRead } from "./quickstart.js";
23
24
  import { listReceipts, loadReceipt, saveReceipt } from "./receipts.js";
25
+ import packageJson from "../package.json" with { type: "json" };
24
26
 
25
27
  interface Runtime {
26
28
  parsed: ParsedArgs;
@@ -70,11 +72,24 @@ async function main(): Promise<void> {
70
72
  async function dispatch(runtime: Runtime): Promise<void> {
71
73
  const [command, subcommand, ...rest] = runtime.parsed.positionals;
72
74
 
75
+ if (booleanFlag(runtime.parsed.flags, "version") || command === "version") {
76
+ process.stdout.write(`${packageJson.version}\n`);
77
+ return;
78
+ }
79
+
73
80
  if (!command || command === "help" || booleanFlag(runtime.parsed.flags, "help")) {
74
81
  showHelp(runtime.json);
75
82
  return;
76
83
  }
77
84
 
85
+ if (command === "doctor") {
86
+ printJson(await runDoctor(runtime, { cliVersion: packageJson.version }));
87
+ return;
88
+ }
89
+ if (command === "quickstart-read") {
90
+ printJson(await runQuickstartRead(runtime));
91
+ return;
92
+ }
78
93
  if (command === "repository") {
79
94
  await repository(runtime);
80
95
  return;
@@ -596,6 +611,8 @@ function matchesQuery(article: ArticleSummary, query: string): boolean {
596
611
  function showHelp(json: boolean): void {
597
612
  const usage = [
598
613
  "rubicon repository",
614
+ "rubicon doctor --json",
615
+ "rubicon quickstart-read --first --goal \"<goal>\" --max-usdc 0.10 --json",
599
616
  "rubicon search \"<query>\"",
600
617
  "rubicon article show <article-id>",
601
618
  "rubicon article navigation <article-id> --goal \"<goal>\"",
@@ -0,0 +1,264 @@
1
+ import { mkdtempSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import type { ReadReceipt, RubiconClient } from "@rubicon-caliga/agent-sdk";
7
+ import type { ArticleSummary } from "@rubicon-caliga/core";
8
+ import { parseArgs } from "./args.js";
9
+ import { CliError } from "./errors.js";
10
+ import { finalReceiptJson, runDoctor, runQuickstartRead, type CommandRuntime } from "./quickstart.js";
11
+
12
+ test("doctor reports missing gateway config and Circle CLI missing", async () => {
13
+ const runtime = runtimeFor();
14
+ const result = await runDoctor(runtime, {
15
+ cliVersion: "0.1.1",
16
+ fetch: okFetch,
17
+ circleRunner: async () => {
18
+ throw new Error("spawn circle ENOENT");
19
+ },
20
+ });
21
+
22
+ const checks = result.checks as Array<{ name: string; status: string; guidance?: string }>;
23
+ assert.equal(checks.find((check) => check.name === "gatewayConfig")?.status, "warning");
24
+ assert.match(checks.find((check) => check.name === "gatewayConfig")?.guidance ?? "", /hosted default/);
25
+ assert.equal(checks.find((check) => check.name === "circleCli")?.status, "error");
26
+ assert.match(checks.find((check) => check.name === "circleCli")?.guidance ?? "", /Install Circle CLI/);
27
+ });
28
+
29
+ test("doctor explains Circle CLI not logged in and sandbox network failures", async () => {
30
+ const notLoggedIn = await runDoctor(runtimeFor(), {
31
+ fetch: okFetch,
32
+ circleRunner: async (_command, args) => {
33
+ if (args[0] === "--version") return "circle 1.0.0";
34
+ throw new Error("unauthorized: please login");
35
+ },
36
+ });
37
+ const notLoggedChecks = notLoggedIn.checks as Array<{ name: string; guidance?: string }>;
38
+ assert.match(notLoggedChecks.find((check) => check.name === "circleAuth")?.guidance ?? "", /login/);
39
+
40
+ const networkFailure = await runDoctor(runtimeFor(), {
41
+ fetch: (async () => {
42
+ throw new Error("fetch failed");
43
+ }) as unknown as typeof fetch,
44
+ circleRunner: async () => "circle 1.0.0",
45
+ });
46
+ const networkChecks = networkFailure.checks as Array<{ name: string; guidance?: string }>;
47
+ assert.match(networkChecks.find((check) => check.name === "gatewayReachability")?.guidance ?? "", /network-capable/);
48
+ });
49
+
50
+ test("quickstart refuses paid reads without explicit budget", async () => {
51
+ await assert.rejects(
52
+ () => runQuickstartRead(runtimeFor({ argv: ["quickstart-read", "--first", "--goal", "answer"] })),
53
+ (error) => error instanceof CliError && error.code === "MISSING_BUDGET",
54
+ );
55
+ });
56
+
57
+ test("quickstart detects expired OTP request IDs", async () => {
58
+ await assert.rejects(
59
+ () =>
60
+ runQuickstartRead(runtimeFor(), {
61
+ circleRunner: async (_command, args) => {
62
+ if (args[0] === "auth") throw new Error("OTP request id expired");
63
+ return circleOutput(args, "1000000");
64
+ },
65
+ }),
66
+ (error) => error instanceof CliError && error.code === "OTP_EXPIRED" && /fresh Circle auth OTP/.test(error.message),
67
+ );
68
+ });
69
+
70
+ test("quickstart stops when dry-run estimate exceeds budget", async () => {
71
+ await assert.rejects(
72
+ () => runQuickstartRead(runtimeFor({ article: article({ pricePerWordAtomic: "1000000" }) })),
73
+ (error) => error instanceof CliError && error.code === "DRY_RUN_OVER_BUDGET",
74
+ );
75
+ });
76
+
77
+ test("quickstart uses existing Arc Testnet balance without faucet", async () => {
78
+ const calls: string[][] = [];
79
+ const result = await runQuickstartRead(runtimeFor(), {
80
+ circleRunner: async (_command, args) => {
81
+ calls.push(args);
82
+ return circleOutput(args, "1000000");
83
+ },
84
+ });
85
+
86
+ assert.equal((result.receipt as Record<string, unknown>).amountPaidAtomic, "2");
87
+ assert.equal(calls.some((args) => args[0] === "gateway" && args[1] === "faucet"), false);
88
+ });
89
+
90
+ test("quickstart faucet-funds only the testnet path when needed", async () => {
91
+ const calls: string[][] = [];
92
+ let funded = false;
93
+ const result = await runQuickstartRead(runtimeFor(), {
94
+ circleRunner: async (_command, args) => {
95
+ calls.push(args);
96
+ if (args[0] === "gateway" && args[1] === "faucet") {
97
+ funded = true;
98
+ return JSON.stringify({ ok: true });
99
+ }
100
+ return circleOutput(args, funded ? "1000000" : "0");
101
+ },
102
+ });
103
+
104
+ assert.equal(calls.some((args) => args[0] === "gateway" && args[1] === "faucet"), true);
105
+ assert.equal((result.wallet as Record<string, unknown>).balanceAtomic, "1000000");
106
+ });
107
+
108
+ test("quickstart refuses to suggest mainnet funding", async () => {
109
+ await assert.rejects(
110
+ () =>
111
+ runQuickstartRead(runtimeFor({ article: article({ environment: "mainnet", network: "eip155:1", circleChain: "ETH" }) }), {
112
+ circleRunner: async (_command, args) => circleOutput(args, "0"),
113
+ }),
114
+ (error) => error instanceof CliError && error.code === "INSUFFICIENT_FUNDS" && /Refusing to suggest mainnet funding/.test(error.message),
115
+ );
116
+ });
117
+
118
+ test("final receipt schema includes buyer/Circle wallet mismatch explanation", () => {
119
+ const shaped = finalReceiptJson({
120
+ article: article(),
121
+ receipt: receipt(),
122
+ receiptId: "receipt_1",
123
+ goal: "answer",
124
+ approvedBudgetUsdc: "0.01",
125
+ circleWalletAddress: "0x1111111111111111111111111111111111111111",
126
+ });
127
+
128
+ assert.deepEqual(Object.keys(shaped), [
129
+ "articleId",
130
+ "title",
131
+ "author",
132
+ "sessionId",
133
+ "receiptId",
134
+ "goal",
135
+ "approvedBudgetUsdc",
136
+ "amountPaidAtomic",
137
+ "amountPaidUsdc",
138
+ "wordsRead",
139
+ "completed",
140
+ "stopReason",
141
+ "paymentIds",
142
+ "settlementIds",
143
+ "transactionHashes",
144
+ "buyerWalletAddress",
145
+ "circleWalletAddress",
146
+ "walletAddressMismatchExplanation",
147
+ ]);
148
+ assert.match(String(shaped.walletAddressMismatchExplanation), /Gateway backing EOA/);
149
+ });
150
+
151
+ function runtimeFor(input: { argv?: string[]; article?: ArticleSummary } = {}): CommandRuntime {
152
+ process.env.HOME = mkdtempSync(join(tmpdir(), "rubicon-cli-test-"));
153
+ const articleFixture = input.article ?? article();
154
+ const client = {
155
+ async getRepository() {
156
+ return { repository: "articles" as const, articles: [articleFixture] };
157
+ },
158
+ async getNavigation() {
159
+ return {
160
+ article: articleFixture,
161
+ navigation: {
162
+ articleId: articleFixture.articleId,
163
+ sections: articleFixture.sections,
164
+ sellerAgent: {
165
+ recommendedSectionId: "intro",
166
+ alternativeSectionIds: [],
167
+ rationale: "Start here.",
168
+ safeHints: [],
169
+ withheld: [],
170
+ },
171
+ stopConditions: [],
172
+ },
173
+ };
174
+ },
175
+ async run() {
176
+ return receipt();
177
+ },
178
+ } as unknown as RubiconClient;
179
+ return {
180
+ parsed: parseArgs(input.argv ?? ["quickstart-read", "--first", "--goal", "answer", "--max-usdc", "0.01"]),
181
+ config: {},
182
+ gatewayUrl: "https://rubicon.test",
183
+ paymentMode: "circle-cli",
184
+ circleChain: "ARC-TESTNET",
185
+ client,
186
+ };
187
+ }
188
+
189
+ function article(input: {
190
+ pricePerWordAtomic?: `${bigint}`;
191
+ network?: string;
192
+ circleChain?: string;
193
+ environment?: "testnet" | "mainnet" | "unknown";
194
+ } = {}): ArticleSummary {
195
+ return {
196
+ articleId: "article_1",
197
+ creatorId: "creator_1",
198
+ creatorUsername: "creator",
199
+ title: "Rubicon Field Notes",
200
+ author: "Ada",
201
+ state: "live",
202
+ totalWords: 2,
203
+ pricePerWordAtomic: input.pricePerWordAtomic ?? "1",
204
+ maxArticlePriceAtomic: `${BigInt(input.pricePerWordAtomic ?? "1") * 2n}` as `${bigint}`,
205
+ paymentTerms: {
206
+ asset: "USDC",
207
+ network: input.network ?? "eip155:5042002",
208
+ circleChain: input.circleChain ?? "ARC-TESTNET",
209
+ environment: input.environment ?? "testnet",
210
+ fundingMethod: "Circle testnet faucet.",
211
+ payTo: "0x3333333333333333333333333333333333333333",
212
+ pricePerWordAtomic: input.pricePerWordAtomic ?? "1",
213
+ meteringUnit: "word",
214
+ },
215
+ sections: [{ sectionId: "intro", heading: "Intro", level: 1, wordStart: 0, wordCount: 2 }],
216
+ };
217
+ }
218
+
219
+ function receipt(): ReadReceipt {
220
+ return {
221
+ sessionId: "session_1",
222
+ articleId: "article_1",
223
+ conversationId: "conversation_1",
224
+ wordsRead: 2,
225
+ amountPaidAtomic: "2",
226
+ payments: [
227
+ {
228
+ paymentId: "payment_1",
229
+ sessionId: "session_1",
230
+ articleId: "article_1",
231
+ sequence: 0,
232
+ meteringUnit: "word",
233
+ amountAtomic: "2",
234
+ currency: "USDC",
235
+ settlementIds: ["settlement_1"],
236
+ transactionHashes: ["0xtx"],
237
+ buyerWalletAddress: "0x2222222222222222222222222222222222222222",
238
+ settledAt: "2026-06-19T12:00:00.000Z",
239
+ },
240
+ ],
241
+ transactionHashes: ["0xtx"],
242
+ settlementIds: ["settlement_1"],
243
+ buyerWalletAddress: "0x2222222222222222222222222222222222222222",
244
+ sellerPayTo: "0x3333333333333333333333333333333333333333",
245
+ network: "eip155:5042002",
246
+ text: "hello world",
247
+ completed: true,
248
+ stopReason: "article_completed",
249
+ };
250
+ }
251
+
252
+ function circleOutput(args: string[], balanceAtomic: `${bigint}`): string {
253
+ if (args[0] === "--version") return "circle 1.0.0";
254
+ if (args[0] === "auth") return JSON.stringify({ loggedIn: true });
255
+ if (args[0] === "wallet") {
256
+ return JSON.stringify({ data: { wallets: [{ address: "0x1111111111111111111111111111111111111111" }] } });
257
+ }
258
+ if (args[0] === "gateway" && args[1] === "balance") {
259
+ return JSON.stringify({ data: { balanceAtomic, backingEOA: "0x2222222222222222222222222222222222222222" } });
260
+ }
261
+ return JSON.stringify({ ok: true });
262
+ }
263
+
264
+ const okFetch = (async () => new Response("ok", { status: 200 })) as unknown as typeof fetch;