@rubicon-caliga/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export const HOSTED_GATEWAY_URL = "https://rubicon-caligagateway-production.up.railway.app";
6
+
7
+ export interface RubiconCliConfig {
8
+ gatewayUrl?: string;
9
+ apiKey?: string;
10
+ paymentMode?: "static" | "circle-cli";
11
+ circleChain?: string;
12
+ agentWalletAddress?: `0x${string}`;
13
+ }
14
+
15
+ export function configPath(): string {
16
+ return join(homedir(), ".rubicon", "config.json");
17
+ }
18
+
19
+ export async function readConfig(): Promise<RubiconCliConfig> {
20
+ try {
21
+ const raw = await readFile(configPath(), "utf8");
22
+ return JSON.parse(raw) as RubiconCliConfig;
23
+ } catch (error) {
24
+ if (isNotFound(error)) return {};
25
+ throw error;
26
+ }
27
+ }
28
+
29
+ export async function writeConfig(config: RubiconCliConfig): Promise<void> {
30
+ const path = configPath();
31
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
32
+ await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
33
+ await chmod(path, 0o600);
34
+ }
35
+
36
+ function isNotFound(error: unknown): boolean {
37
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
38
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,15 @@
1
+ export class CliError extends Error {
2
+ constructor(
3
+ public readonly code: string,
4
+ message: string,
5
+ public readonly exitCode = 1,
6
+ ) {
7
+ super(message);
8
+ }
9
+ }
10
+
11
+ export function toCliError(error: unknown): CliError {
12
+ if (error instanceof CliError) return error;
13
+ if (error instanceof Error) return new CliError("COMMAND_FAILED", error.message);
14
+ return new CliError("COMMAND_FAILED", String(error));
15
+ }
package/src/format.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { formatAtomicUsdc } from "@rubicon-caliga/core";
2
+ import type { ArticleSummary, ArticleNavigation, SellerPaymentTerms } from "@rubicon-caliga/core";
3
+ import type { ReadReceipt } from "@rubicon-caliga/agent-sdk";
4
+
5
+ export function printJson(value: unknown): void {
6
+ process.stdout.write(`${JSON.stringify(value)}\n`);
7
+ }
8
+
9
+ export function printJsonEvent(type: string, value: unknown): void {
10
+ printJson({ type, ...asRecord(value) });
11
+ }
12
+
13
+ export function articleJson(article: ArticleSummary): Record<string, unknown> {
14
+ return {
15
+ articleId: article.articleId,
16
+ title: article.title,
17
+ author: article.author,
18
+ creatorId: article.creatorId,
19
+ creatorUsername: article.creatorUsername,
20
+ state: article.state,
21
+ totalWords: article.totalWords,
22
+ pricePerWordAtomic: article.pricePerWordAtomic,
23
+ maxArticlePriceAtomic: article.maxArticlePriceAtomic,
24
+ paymentTerms: article.paymentTerms,
25
+ sections: article.sections,
26
+ };
27
+ }
28
+
29
+ export function humanArticle(article: ArticleSummary): string {
30
+ const lines = [
31
+ `Article: ${article.title}`,
32
+ `ID: ${article.articleId}`,
33
+ `Author: ${article.author}`,
34
+ `Creator: ${article.creatorUsername}`,
35
+ `Words: ${article.totalWords.toLocaleString("en-US")}`,
36
+ `Price/word: ${formatAtomic(article.pricePerWordAtomic)} USDC`,
37
+ `Max article price: ${formatAtomic(article.maxArticlePriceAtomic)} USDC`,
38
+ ];
39
+ if (article.paymentTerms) {
40
+ lines.push("", ...humanPaymentTerms(article.paymentTerms));
41
+ }
42
+ if (article.sections.length > 0) {
43
+ lines.push("", "Sections:");
44
+ for (const section of article.sections) {
45
+ lines.push(`- ${section.sectionId}: ${section.heading} (${section.wordCount.toLocaleString("en-US")} words)`);
46
+ }
47
+ }
48
+ return lines.join("\n");
49
+ }
50
+
51
+ export function humanPaymentTerms(terms: SellerPaymentTerms): string[] {
52
+ return [
53
+ "Payment terms:",
54
+ `- Asset: ${terms.asset}`,
55
+ `- Network: ${terms.networkLabel ?? terms.network}`,
56
+ `- Pay to: ${terms.payTo}`,
57
+ `- Price/word: ${formatAtomic(terms.pricePerWordAtomic)} USDC`,
58
+ ];
59
+ }
60
+
61
+ export function humanNavigation(navigation: ArticleNavigation): string {
62
+ const seller = navigation.sellerAgent;
63
+ const lines = [
64
+ `Recommended section: ${seller.recommendedSectionId}`,
65
+ `Alternatives: ${seller.alternativeSectionIds.length ? seller.alternativeSectionIds.join(", ") : "none"}`,
66
+ `Rationale: ${seller.rationale}`,
67
+ ];
68
+ if (seller.safeHints.length > 0) {
69
+ lines.push("", "Safe hints:", ...seller.safeHints.map((hint) => `- ${hint}`));
70
+ }
71
+ if (seller.withheld.length > 0) {
72
+ lines.push("", "Withheld:", ...seller.withheld.map((notice) => `- ${notice}`));
73
+ }
74
+ if (navigation.sections.length > 0) {
75
+ lines.push("", "Sections:");
76
+ for (const section of navigation.sections) {
77
+ lines.push(`- ${section.sectionId}: ${section.heading} (${section.wordCount.toLocaleString("en-US")} words)`);
78
+ }
79
+ }
80
+ return lines.join("\n");
81
+ }
82
+
83
+ export function humanReceipt(receipt: ReadReceipt): string {
84
+ const lines = [
85
+ "Receipt:",
86
+ `- Session: ${receipt.sessionId}`,
87
+ `- Article: ${receipt.articleId}`,
88
+ `- Words read: ${receipt.wordsRead.toLocaleString("en-US")}`,
89
+ `- Amount paid: ${formatAtomic(receipt.amountPaidAtomic)} USDC`,
90
+ `- Stop reason: ${receipt.stopReason}`,
91
+ ];
92
+ if (receipt.settlementIds.length > 0) {
93
+ lines.push(`- Settlement IDs: ${receipt.settlementIds.join(", ")}`);
94
+ }
95
+ if (receipt.transactionHashes.length > 0) {
96
+ lines.push(`- Transaction hashes: ${receipt.transactionHashes.join(", ")}`);
97
+ }
98
+ if (receipt.buyerWalletAddress) {
99
+ lines.push(`- Buyer wallet: ${receipt.buyerWalletAddress}`);
100
+ }
101
+ if (receipt.sellerPayTo) {
102
+ lines.push(`- Seller pay to: ${receipt.sellerPayTo}`);
103
+ }
104
+ if (receipt.network) {
105
+ lines.push(`- Network: ${receipt.network}`);
106
+ }
107
+ return lines.join("\n");
108
+ }
109
+
110
+ export function formatAtomic(value: string | bigint): string {
111
+ return formatAtomicUsdc(typeof value === "bigint" ? value : BigInt(value));
112
+ }
113
+
114
+ function asRecord(value: unknown): Record<string, unknown> {
115
+ return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : { value };
116
+ }
package/src/index.ts ADDED
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { RubiconClient } from "@rubicon-caliga/agent-sdk";
5
+ import { parseUsdcToAtomic, type ArticleSummary } from "@rubicon-caliga/core";
6
+ import { parseArgs, booleanFlag, stringFlag, type ParsedArgs } from "./args.js";
7
+ import { configPath, HOSTED_GATEWAY_URL, readConfig, writeConfig, type RubiconCliConfig } from "./config.js";
8
+ import { CliError, toCliError } from "./errors.js";
9
+ import {
10
+ articleJson,
11
+ formatAtomic,
12
+ humanArticle,
13
+ humanNavigation,
14
+ humanReceipt,
15
+ printJson,
16
+ printJsonEvent,
17
+ } from "./format.js";
18
+ import { selectPaymentEngine, type PaymentMode } from "./payments.js";
19
+ import { listReceipts, loadReceipt, saveReceipt } from "./receipts.js";
20
+
21
+ interface Runtime {
22
+ parsed: ParsedArgs;
23
+ json: boolean;
24
+ config: RubiconCliConfig;
25
+ gatewayUrl: string;
26
+ apiKey?: string;
27
+ paymentMode: PaymentMode;
28
+ client: RubiconClient;
29
+ }
30
+
31
+ async function main(): Promise<void> {
32
+ const parsed = parseArgs(process.argv.slice(2));
33
+ const json = booleanFlag(parsed.flags, "json");
34
+
35
+ try {
36
+ const config = await readConfig();
37
+ const gatewayUrl =
38
+ stringFlag(parsed.flags, "gateway-url") ??
39
+ process.env.RUBICON_GATEWAY_URL ??
40
+ config.gatewayUrl ??
41
+ HOSTED_GATEWAY_URL;
42
+ const apiKey = stringFlag(parsed.flags, "api-key") ?? process.env.RUBICON_AGENT_API_KEY ?? config.apiKey;
43
+ const payment = selectPaymentEngine({
44
+ requestedMode: stringFlag(parsed.flags, "payment-mode"),
45
+ gatewayUrl,
46
+ config,
47
+ });
48
+ const client = new RubiconClient({
49
+ baseUrl: gatewayUrl,
50
+ authorization: apiKey ? `Bearer ${apiKey}` : undefined,
51
+ paymentEngine: payment.engine,
52
+ });
53
+ await dispatch({ parsed, json, config, gatewayUrl, apiKey, paymentMode: payment.mode, client });
54
+ } catch (error) {
55
+ const cliError = toCliError(error);
56
+ if (json) {
57
+ printJson({ success: false, error: { code: cliError.code, message: cliError.message } });
58
+ } else {
59
+ process.stderr.write(`Error: ${cliError.message}\n`);
60
+ }
61
+ process.exitCode = cliError.exitCode;
62
+ }
63
+ }
64
+
65
+ async function dispatch(runtime: Runtime): Promise<void> {
66
+ const [command, subcommand, ...rest] = runtime.parsed.positionals;
67
+
68
+ if (!command || command === "help" || booleanFlag(runtime.parsed.flags, "help")) {
69
+ showHelp(runtime.json);
70
+ return;
71
+ }
72
+
73
+ if (command === "repository") {
74
+ await repository(runtime);
75
+ return;
76
+ }
77
+ if (command === "search") {
78
+ await search(runtime, subcommand);
79
+ return;
80
+ }
81
+ if (command === "article" && subcommand === "show") {
82
+ await articleShow(runtime, rest[0]);
83
+ return;
84
+ }
85
+ if (command === "article" && subcommand === "navigation") {
86
+ await articleNavigation(runtime, rest[0]);
87
+ return;
88
+ }
89
+ if (command === "read") {
90
+ await readArticle(runtime, subcommand);
91
+ return;
92
+ }
93
+ if (command === "receipts" && subcommand === "list") {
94
+ await receiptsList(runtime);
95
+ return;
96
+ }
97
+ if (command === "receipts" && subcommand === "show") {
98
+ await receiptsShow(runtime, rest[0]);
99
+ return;
100
+ }
101
+ if (command === "config" && subcommand === "show") {
102
+ await configShow(runtime);
103
+ return;
104
+ }
105
+ if (command === "config" && subcommand === "set") {
106
+ await configSet(runtime, rest[0], rest[1]);
107
+ return;
108
+ }
109
+
110
+ throw new CliError("UNKNOWN_COMMAND", `Unknown command: ${runtime.parsed.positionals.join(" ")}`);
111
+ }
112
+
113
+ async function repository(runtime: Runtime): Promise<void> {
114
+ const response = await runtime.client.getRepository();
115
+ if (runtime.json) {
116
+ printJson({ success: true, repository: response.repository, articles: response.articles.map(articleJson) });
117
+ return;
118
+ }
119
+ if (response.articles.length === 0) {
120
+ process.stdout.write("No public articles found.\n");
121
+ return;
122
+ }
123
+ process.stdout.write(
124
+ response.articles
125
+ .map((article) => `${article.articleId} | ${article.title} | ${article.author} | ${article.totalWords} words`)
126
+ .join("\n") + "\n",
127
+ );
128
+ }
129
+
130
+ async function search(runtime: Runtime, query: string | undefined): Promise<void> {
131
+ if (!query) throw new CliError("MISSING_QUERY", "rubicon search requires a query.");
132
+ const response = await runtime.client.getRepository();
133
+ const matches = response.articles.filter((article) => matchesQuery(article, query));
134
+ if (runtime.json) {
135
+ printJson({ success: true, query, articles: matches.map(articleJson) });
136
+ return;
137
+ }
138
+ if (matches.length === 0) {
139
+ process.stdout.write("No matches.\n");
140
+ return;
141
+ }
142
+ process.stdout.write(matches.map((article) => `${article.articleId} | ${article.title} | ${article.author}`).join("\n") + "\n");
143
+ }
144
+
145
+ async function articleShow(runtime: Runtime, articleId: string | undefined): Promise<void> {
146
+ const article = await findArticle(runtime, articleId);
147
+ if (runtime.json) {
148
+ printJson({ success: true, article: articleJson(article) });
149
+ return;
150
+ }
151
+ process.stdout.write(`${humanArticle(article)}\n`);
152
+ }
153
+
154
+ async function articleNavigation(runtime: Runtime, articleId: string | undefined): Promise<void> {
155
+ if (!articleId) throw new CliError("MISSING_ARTICLE_ID", "rubicon article navigation requires an article id.");
156
+ const goal = stringFlag(runtime.parsed.flags, "goal");
157
+ if (!goal) throw new CliError("MISSING_GOAL", "rubicon article navigation requires --goal.");
158
+ const response = await runtime.client.getNavigation(articleId, goal);
159
+ if (runtime.json) {
160
+ printJson({ success: true, article: articleJson(response.article), navigation: response.navigation });
161
+ return;
162
+ }
163
+ process.stdout.write(`${humanArticle(response.article)}\n\n${humanNavigation(response.navigation)}\n`);
164
+ }
165
+
166
+ async function readArticle(runtime: Runtime, articleId: string | undefined): Promise<void> {
167
+ if (!articleId) throw new CliError("MISSING_ARTICLE_ID", "rubicon read requires an article id.");
168
+ const maxSpendAtomic = parseBudget(runtime.parsed);
169
+ const goal = stringFlag(runtime.parsed.flags, "goal");
170
+ const maxWordsFlag = stringFlag(runtime.parsed.flags, "max-words");
171
+ const maxWords = maxWordsFlag === undefined ? undefined : Number(maxWordsFlag);
172
+ if (maxWords !== undefined && (!Number.isInteger(maxWords) || maxWords < 1)) {
173
+ throw new CliError("INVALID_MAX_WORDS", "--max-words must be a positive integer.");
174
+ }
175
+
176
+ if (booleanFlag(runtime.parsed.flags, "dry-run")) {
177
+ await dryRun(runtime, articleId, maxSpendAtomic, goal, maxWords);
178
+ return;
179
+ }
180
+
181
+ let finalReceipt = undefined;
182
+ const stream = runtime.client.read({
183
+ articleId,
184
+ goal,
185
+ maxSpendAtomic,
186
+ maxWords,
187
+ });
188
+
189
+ for await (const event of stream) {
190
+ if (runtime.json) {
191
+ printJsonEvent("event", { event });
192
+ if (event.type === "article.completed") {
193
+ finalReceipt = event.receipt;
194
+ }
195
+ continue;
196
+ }
197
+
198
+ switch (event.type) {
199
+ case "seller.message":
200
+ process.stdout.write(`Seller: ${event.content}\n\n`);
201
+ break;
202
+ case "session.started":
203
+ process.stdout.write(`Session: ${event.session.sessionId}\n\n`);
204
+ break;
205
+ case "article.word":
206
+ process.stdout.write(`${event.word} `);
207
+ break;
208
+ case "article.error":
209
+ process.stderr.write(`\nError: ${event.message}\n`);
210
+ break;
211
+ case "article.completed":
212
+ finalReceipt = event.receipt;
213
+ process.stdout.write(`\n\n${humanReceipt(event.receipt)}\n`);
214
+ break;
215
+ case "article.usage":
216
+ break;
217
+ }
218
+ }
219
+
220
+ if (finalReceipt) {
221
+ const stored = await saveReceipt(finalReceipt);
222
+ if (runtime.json) {
223
+ printJson({ type: "receipt.saved", success: true, receiptId: stored.receiptId, savedAt: stored.savedAt, receipt: stored.receipt });
224
+ }
225
+ }
226
+ }
227
+
228
+ async function dryRun(
229
+ runtime: Runtime,
230
+ articleId: string,
231
+ maxSpendAtomic: `${bigint}`,
232
+ goal: string | undefined,
233
+ maxWords: number | undefined,
234
+ ): Promise<void> {
235
+ const article = await findArticle(runtime, articleId);
236
+ if (runtime.json) {
237
+ printJson({
238
+ success: true,
239
+ dryRun: true,
240
+ gatewayUrl: runtime.gatewayUrl,
241
+ paymentMode: runtime.paymentMode,
242
+ budget: {
243
+ maxSpendAtomic,
244
+ maxSpendUsdc: formatAtomic(maxSpendAtomic),
245
+ maxWords,
246
+ },
247
+ goal,
248
+ article: articleJson(article),
249
+ });
250
+ return;
251
+ }
252
+
253
+ process.stdout.write(
254
+ [
255
+ "Dry run: no paid read started.",
256
+ `Gateway: ${runtime.gatewayUrl}`,
257
+ `Payment mode: ${runtime.paymentMode}`,
258
+ `Budget: ${formatAtomic(maxSpendAtomic)} USDC (${maxSpendAtomic} atomic)`,
259
+ maxWords ? `Max words: ${maxWords}` : undefined,
260
+ goal ? `Goal: ${goal}` : undefined,
261
+ "",
262
+ humanArticle(article),
263
+ "",
264
+ ]
265
+ .filter((line): line is string => line !== undefined)
266
+ .join("\n"),
267
+ );
268
+ }
269
+
270
+ async function receiptsList(runtime: Runtime): Promise<void> {
271
+ const receipts = await listReceipts();
272
+ if (runtime.json) {
273
+ printJson({ success: true, receipts });
274
+ return;
275
+ }
276
+ if (receipts.length === 0) {
277
+ process.stdout.write("No local receipts found.\n");
278
+ return;
279
+ }
280
+ process.stdout.write(
281
+ receipts
282
+ .map(
283
+ (stored) =>
284
+ `${stored.receiptId} | ${stored.savedAt} | ${stored.receipt.articleId} | ${formatAtomic(stored.receipt.amountPaidAtomic)} USDC`,
285
+ )
286
+ .join("\n") + "\n",
287
+ );
288
+ }
289
+
290
+ async function receiptsShow(runtime: Runtime, receiptId: string | undefined): Promise<void> {
291
+ if (!receiptId) throw new CliError("MISSING_RECEIPT_ID", "rubicon receipts show requires a receipt id.");
292
+ const stored = await loadReceipt(receiptId);
293
+ if (runtime.json) {
294
+ printJson({ success: true, ...stored });
295
+ return;
296
+ }
297
+ process.stdout.write(`Receipt ID: ${stored.receiptId}\nSaved: ${stored.savedAt}\n${humanReceipt(stored.receipt)}\n`);
298
+ }
299
+
300
+ async function configShow(runtime: Runtime): Promise<void> {
301
+ const shown = {
302
+ configPath: configPath(),
303
+ gatewayUrl: runtime.config.gatewayUrl,
304
+ apiKey: runtime.config.apiKey ? "set" : undefined,
305
+ paymentMode: runtime.config.paymentMode,
306
+ circleChain: runtime.config.circleChain,
307
+ agentWalletAddress: runtime.config.agentWalletAddress,
308
+ effective: {
309
+ gatewayUrl: runtime.gatewayUrl,
310
+ apiKey: runtime.apiKey ? "set" : undefined,
311
+ paymentMode: runtime.paymentMode,
312
+ },
313
+ };
314
+ if (runtime.json) {
315
+ printJson({ success: true, config: shown });
316
+ return;
317
+ }
318
+ process.stdout.write(`${JSON.stringify(shown, null, 2)}\n`);
319
+ }
320
+
321
+ async function configSet(runtime: Runtime, key: string | undefined, value: string | undefined): Promise<void> {
322
+ if (!key || !value) throw new CliError("MISSING_CONFIG_VALUE", "rubicon config set requires a key and value.");
323
+ const next = { ...runtime.config };
324
+ switch (key) {
325
+ case "gateway-url":
326
+ next.gatewayUrl = value;
327
+ break;
328
+ case "api-key":
329
+ next.apiKey = value;
330
+ break;
331
+ case "payment-mode":
332
+ if (value !== "static" && value !== "circle-cli") {
333
+ throw new CliError("INVALID_PAYMENT_MODE", "payment-mode must be static or circle-cli.");
334
+ }
335
+ next.paymentMode = value;
336
+ break;
337
+ case "circle-chain":
338
+ next.circleChain = value;
339
+ break;
340
+ case "agent-wallet-address":
341
+ if (!value.startsWith("0x")) throw new CliError("INVALID_ADDRESS", "agent-wallet-address must start with 0x.");
342
+ next.agentWalletAddress = value as `0x${string}`;
343
+ break;
344
+ default:
345
+ throw new CliError("UNKNOWN_CONFIG_KEY", `Unknown config key: ${key}`);
346
+ }
347
+ await mkdir(dirname(configPath()), { recursive: true, mode: 0o700 });
348
+ await writeConfig(next);
349
+ if (runtime.json) {
350
+ printJson({ success: true, configPath: configPath(), key });
351
+ return;
352
+ }
353
+ process.stdout.write(`Updated ${key} in ${configPath()}\n`);
354
+ }
355
+
356
+ async function findArticle(runtime: Runtime, articleId: string | undefined): Promise<ArticleSummary> {
357
+ if (!articleId) throw new CliError("MISSING_ARTICLE_ID", "Article id is required.");
358
+ const repository = await runtime.client.getRepository();
359
+ const article = repository.articles.find((candidate) => candidate.articleId === articleId);
360
+ if (!article) throw new CliError("ARTICLE_NOT_FOUND", `Article not found: ${articleId}`);
361
+ return article;
362
+ }
363
+
364
+ function parseBudget(parsed: ParsedArgs): `${bigint}` {
365
+ const maxUsdc = stringFlag(parsed.flags, "max-usdc");
366
+ const maxAtomic = stringFlag(parsed.flags, "max-atomic");
367
+ if (!maxUsdc && !maxAtomic) {
368
+ throw new CliError("MISSING_BUDGET", "rubicon read requires --max-usdc or --max-atomic.");
369
+ }
370
+ if (maxUsdc && maxAtomic) {
371
+ throw new CliError("MULTIPLE_BUDGETS", "Use either --max-usdc or --max-atomic, not both.");
372
+ }
373
+ if (maxAtomic) {
374
+ if (!/^\d+$/.test(maxAtomic)) throw new CliError("INVALID_BUDGET", "--max-atomic must be an integer.");
375
+ return maxAtomic as `${bigint}`;
376
+ }
377
+ try {
378
+ return `${parseUsdcToAtomic(maxUsdc!)}` as `${bigint}`;
379
+ } catch {
380
+ throw new CliError("INVALID_BUDGET", "--max-usdc must be a decimal USDC amount.");
381
+ }
382
+ }
383
+
384
+ function matchesQuery(article: ArticleSummary, query: string): boolean {
385
+ const haystack = [
386
+ article.articleId,
387
+ article.title,
388
+ article.author,
389
+ article.creatorUsername,
390
+ ...article.sections.map((section) => section.heading),
391
+ ...article.sections.map((section) => section.sectionId),
392
+ ]
393
+ .join(" ")
394
+ .toLowerCase();
395
+ return query
396
+ .toLowerCase()
397
+ .split(/\s+/)
398
+ .filter(Boolean)
399
+ .every((term) => haystack.includes(term));
400
+ }
401
+
402
+ function showHelp(json: boolean): void {
403
+ const usage = [
404
+ "rubicon repository",
405
+ "rubicon search \"<query>\"",
406
+ "rubicon article show <article-id>",
407
+ "rubicon article navigation <article-id> --goal \"<goal>\"",
408
+ "rubicon read <article-id> --max-usdc 0.10 [--goal \"...\"] [--max-words 50] [--dry-run]",
409
+ "rubicon receipts list",
410
+ "rubicon receipts show <receipt-id>",
411
+ "rubicon config show",
412
+ "rubicon config set gateway-url <url>",
413
+ "rubicon config set api-key <key>",
414
+ ];
415
+ if (json) {
416
+ printJson({ success: true, usage });
417
+ return;
418
+ }
419
+ process.stdout.write(`${usage.join("\n")}\n`);
420
+ }
421
+
422
+ await main();
@@ -0,0 +1,66 @@
1
+ import {
2
+ CircleCliGatewayPaymentEngine,
3
+ StaticPaymentEngine,
4
+ type AgentPaymentEngine,
5
+ } from "@rubicon-caliga/agent-sdk";
6
+ import { HOSTED_GATEWAY_URL, type RubiconCliConfig } from "./config.js";
7
+ import { CliError } from "./errors.js";
8
+
9
+ export type PaymentMode = "static" | "circle-cli";
10
+
11
+ export interface PaymentSelection {
12
+ mode: PaymentMode;
13
+ engine: AgentPaymentEngine;
14
+ }
15
+
16
+ export function selectPaymentEngine(input: {
17
+ requestedMode?: string;
18
+ gatewayUrl: string;
19
+ config: RubiconCliConfig;
20
+ }): PaymentSelection {
21
+ const mode = normalizeMode(input.requestedMode ?? process.env.RUBICON_PAYMENT_MODE ?? input.config.paymentMode);
22
+ const selectedMode = mode ?? defaultPaymentMode(input.gatewayUrl);
23
+
24
+ if (selectedMode === "static") {
25
+ return { mode: selectedMode, engine: new StaticPaymentEngine() };
26
+ }
27
+
28
+ return {
29
+ mode: selectedMode,
30
+ engine: new CircleCliGatewayPaymentEngine({
31
+ agentWalletAddress: envAddress("CIRCLE_AGENT_WALLET_ADDRESS") ?? input.config.agentWalletAddress,
32
+ chain: process.env.CIRCLE_CLI_CHAIN ?? input.config.circleChain ?? "ARC-TESTNET",
33
+ }),
34
+ };
35
+ }
36
+
37
+ function normalizeMode(value: string | undefined): PaymentMode | undefined {
38
+ if (!value) {
39
+ if (process.env.CIRCLE_CLI_PAYMENT === "1") return "circle-cli";
40
+ if (process.env.CIRCLE_AGENT_WALLET_ADDRESS) return "circle-cli";
41
+ return undefined;
42
+ }
43
+ if (value === "static" || value === "circle-cli") return value;
44
+ throw new CliError("INVALID_PAYMENT_MODE", "Payment mode must be static or circle-cli.");
45
+ }
46
+
47
+ function defaultPaymentMode(gatewayUrl: string): PaymentMode {
48
+ if (isLocalGateway(gatewayUrl)) return "static";
49
+ if (process.env.CIRCLE_CLI_PAYMENT === "1" || process.env.CIRCLE_AGENT_WALLET_ADDRESS) return "circle-cli";
50
+ if (gatewayUrl === HOSTED_GATEWAY_URL) return "circle-cli";
51
+ return "circle-cli";
52
+ }
53
+
54
+ function isLocalGateway(gatewayUrl: string): boolean {
55
+ try {
56
+ const url = new URL(gatewayUrl);
57
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ function envAddress(name: string): `0x${string}` | undefined {
64
+ const value = process.env[name];
65
+ return value?.startsWith("0x") ? (value as `0x${string}`) : undefined;
66
+ }