From a2c9fda65ccdd944f87f3bfed9c93b77113c9f9b Mon Sep 17 00:00:00 2001 From: CollinsKRO Date: Tue, 30 Jun 2026 10:10:13 +0000 Subject: [PATCH 1/2] . --- .prettierrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierrc.json b/.prettierrc.json index 8a0f27e..93520a5 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,4 +3,5 @@ "singleQuote": false, "trailingComma": "none", "printWidth": 100 + } From 355be59aca152c37c9c0915897aca1065f7868a9 Mon Sep 17 00:00:00 2001 From: codaholic Date: Thu, 2 Jul 2026 08:01:07 +0000 Subject: [PATCH 2/2] feat(cli): add --receipt/--json flag for structured JSON receipt output --- apps/agent-client/src/cli.test.ts | 86 +++++++++++++++++++++++++++++++ apps/agent-client/src/cli.ts | 54 ++++++++++++++++--- 2 files changed, 132 insertions(+), 8 deletions(-) diff --git a/apps/agent-client/src/cli.test.ts b/apps/agent-client/src/cli.test.ts index 4125076..0ce3dfd 100644 --- a/apps/agent-client/src/cli.test.ts +++ b/apps/agent-client/src/cli.test.ts @@ -41,4 +41,90 @@ describe("CLI Validation", () => { expect(error.stdout).toContain("Usage:"); } }); + + it("exits with error when query is missing with --receipt flag", async () => { + try { + await execAsync(`${tsx} "${cliPath}" search --receipt`); + expect.fail("Should have failed"); + } catch (error: any) { + expect(error.code).toBe(1); + expect(error.stderr).toContain("Missing query for search mode."); + } + }); + + it("exits with error when query is missing with --json flag", async () => { + try { + await execAsync(`${tsx} "${cliPath}" news --json`); + expect.fail("Should have failed"); + } catch (error: any) { + expect(error.code).toBe(1); + expect(error.stderr).toContain("Missing query for news mode."); + } + }); +}); + +describe("redactInput", () => { + it("returns short inputs unchanged", async () => { + const { redactInput } = await import("./cli.js"); + expect(redactInput("short query")).toBe("short query"); + expect(redactInput("a".repeat(50))).toBe("a".repeat(50)); + }); + + it("truncates long inputs with ellipsis", async () => { + const { redactInput } = await import("./cli.js"); + const long = "a".repeat(100); + expect(redactInput(long)).toBe("a".repeat(47) + "..."); + }); +}); + +describe("buildReceipt", () => { + it("builds correct receipt structure", async () => { + const { buildReceipt } = await import("./cli.js"); + const receipt = buildReceipt({ + mode: "search", + provider: "search.basic", + term: "test query", + price: 0.01, + traceId: "trace_abc123", + }); + + expect(receipt).toEqual({ + command: "search", + provider: "search.basic", + input: "test query", + price: 0.01, + traceId: "trace_abc123", + }); + }); + + it("handles missing price and traceId", async () => { + const { buildReceipt } = await import("./cli.js"); + const receipt = buildReceipt({ + mode: "scrape", + provider: "scrape.page", + term: "https://example.com", + }); + + expect(receipt).toEqual({ + command: "scrape", + provider: "scrape.page", + input: "https://example.com", + price: null, + traceId: null, + }); + }); + + it("redacts long inputs in receipt", async () => { + const { buildReceipt } = await import("./cli.js"); + const long = "a".repeat(100); + const receipt = buildReceipt({ + mode: "search", + provider: "search.basic", + term: long, + price: 0.05, + traceId: "trace_xyz", + }); + + expect(receipt.input).toBe("a".repeat(47) + "..."); + }); }); diff --git a/apps/agent-client/src/cli.ts b/apps/agent-client/src/cli.ts index 5273236..4386dd9 100644 --- a/apps/agent-client/src/cli.ts +++ b/apps/agent-client/src/cli.ts @@ -7,6 +7,9 @@ function usage() { console.log(' npm run cli -- search "latest soroban updates" --provider search.basic'); console.log(' npm run cli -- news "stablecoin micropayments" --provider news.fast'); console.log(' npm run cli -- scrape "https://example.com" --provider scrape.page'); + console.log("Options:"); + console.log(" --provider Provider ID (default: search.basic / news.fast / scrape.page)"); + console.log(" --receipt Output structured JSON receipt only"); } function readArg(flag: string, args: string[]) { @@ -17,10 +20,36 @@ function readArg(flag: string, args: string[]) { return args[index + 1]; } +function hasFlag(flag: string, args: string[]) { + return args.includes(flag); +} + +export function redactInput(input: string): string { + if (input.length <= 50) return input; + return input.slice(0, 47) + "..."; +} + +export function buildReceipt(input: { + mode: QueryMode; + provider: string; + term: string; + price?: number; + traceId?: string; +}) { + return { + command: input.mode, + provider: input.provider, + input: redactInput(input.term), + price: input.price ?? null, + traceId: input.traceId ?? null, + }; +} + async function main() { const args = process.argv.slice(2); const modeArg = args[0]; const term = args[1]; + const receiptMode = hasFlag("--receipt", args) || hasFlag("--json", args); if (!modeArg || !["search", "news", "scrape"].includes(modeArg)) { usage(); @@ -50,16 +79,22 @@ async function main() { url: mode === "scrape" ? term : undefined }); + const payload = result.body as Record; + const price = payload?.result?.priceUsd ?? payload?.body?.result?.priceUsd; + const trace = payload?.result?.traceId ?? payload?.body?.result?.traceId; + + if (receiptMode) { + const receipt = buildReceipt({ mode, provider, term, price, traceId: trace }); + console.log(JSON.stringify(receipt, null, 2)); + return; + } + console.log("\n=== Query402 Paid Request ==="); console.log(`Endpoint: ${result.endpoint}`); console.log(`Provider: ${provider}`); console.log(`Status: ${result.status}`); console.log(`Payment Header: ${result.paymentResponse ?? ""}`); - const payload = result.body as Record; - const price = payload?.result?.priceUsd ?? payload?.body?.result?.priceUsd; - const trace = payload?.result?.traceId ?? payload?.body?.result?.traceId; - if (price) { console.log(`Price Paid (USD): ${price}`); } @@ -82,7 +117,10 @@ async function main() { console.log(JSON.stringify(payload, null, 2)); } -main().catch((error) => { - console.error("CLI request failed:", error instanceof Error ? error.message : error); - process.exit(1); -}); +const runningDirectly = process.argv[1] && new URL(process.argv[1], "file://").href === import.meta.url; +if (runningDirectly) { + main().catch((error) => { + console.error("CLI request failed:", error instanceof Error ? error.message : error); + process.exit(1); + }); +}