mycomesh-consumer 0.1.53 → 0.2.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/cli.mjs CHANGED
@@ -1,651 +1,139 @@
1
- import { once } from "node:events";
2
- import { readFile } from "node:fs/promises";
3
- import { CONSUMER_RELEASE_VERSION } from "./release.mjs";
4
-
5
- const CLI_VERSION = CONSUMER_RELEASE_VERSION;
6
- // The npm client is local-first. A public Gateway is an explicit override;
7
- // the default remains the loopback Consumer edge so a blocked domain cannot
8
- // strand the Codex client.
9
- const DEFAULT_BASE_URL = "http://127.0.0.1:8110/v1";
10
- const DEFAULT_TIMEOUT_SECONDS = 300;
11
- const MAX_TIMEOUT_SECONDS = 3600;
12
- const MAX_INPUT_BYTES = 16 * 1024 * 1024;
13
- const MAX_ERROR_BYTES = 64 * 1024;
14
- const MAX_JSON_RESPONSE_BYTES = 32 * 1024 * 1024;
15
- const COMMANDS = new Set(["health", "models", "responses", "chat"]);
16
-
17
- const HELP = `Usage: mycomesh <command> [options] [input]
18
-
19
- Consumer commands:
20
- health Check gateway health
21
- models List available models
22
- responses [input] Create a Responses API request
23
- chat [message] Create a Chat Completions API request
24
-
25
- Connection options:
26
- --base-url <url> Consumer root or /v1 URL (default: local loopback)
27
- (env: MYCOMESH_BASE_URL)
28
- --api-key <key> Bearer API key (env: MYCOMESH_API_KEY)
29
- --timeout <seconds> Request deadline, up to 3600 seconds
30
- (env: MYCOMESH_TIMEOUT_SECONDS; default: 300)
31
- --allow-insecure-http Allow cleartext HTTP to a non-loopback test gateway
32
-
33
- Request body options:
34
- --json <json|@file|-> JSON object, JSON file, or JSON from stdin
35
- --model <model> Set the request model
36
- --stream Request an SSE response
37
- --no-stream Disable streaming from a JSON base body
38
-
39
- Responses options:
40
- --input <text> Set the Responses API input
41
- --max-output-tokens <n> Set max_output_tokens
42
-
43
- Chat options:
44
- --message <text> Set the user message
45
- --system <text> Prepend a system message
46
- --max-tokens <n> Set max_tokens
47
- --max-completion-tokens <n>
48
- Set max_completion_tokens
49
-
50
- Other options:
51
- -h, --help Show this help
52
- -v, --version Show the CLI version
53
-
54
- When --json is omitted, piped stdin is read as a JSON request object. Explicit
55
- request options override fields loaded from JSON.`;
56
-
57
- class CliError extends Error {
58
- constructor(message, exitCode = 1) {
59
- super(message);
60
- this.name = "CliError";
61
- this.exitCode = exitCode;
62
- }
63
- }
64
-
65
- class HttpError extends CliError {
66
- constructor(status, statusText, body) {
67
- const label = [status, statusText].filter(Boolean).join(" ");
68
- super(`HTTP ${label}${body ? `\n${body}` : ""}`);
69
- this.name = "HttpError";
70
- }
71
- }
72
-
73
- export async function main(argv, dependencies = {}) {
74
- const env = dependencies.env ?? process.env;
75
- const stdin = dependencies.stdin ?? process.stdin;
76
- const stdout = dependencies.stdout ?? process.stdout;
77
- const stderr = dependencies.stderr ?? process.stderr;
78
- const fetchImpl = dependencies.fetch ?? globalThis.fetch;
79
- const maxJsonResponseBytes =
80
- dependencies.maxJsonResponseBytes ?? MAX_JSON_RESPONSE_BYTES;
81
- let apiKey = env.MYCOMESH_API_KEY || "";
82
- let requestTimeout;
83
-
84
- try {
85
- const parsed = parseArguments(argv, env);
86
- apiKey = parsed.apiKey;
87
-
88
- if (parsed.help) {
89
- stdout.write(`${HELP}\n`);
90
- return 0;
91
- }
92
- if (parsed.version) {
93
- stdout.write(`${CLI_VERSION}\n`);
94
- return 0;
95
- }
96
- if (typeof fetchImpl !== "function") {
97
- throw new CliError("Node.js 20 or newer is required");
98
- }
99
-
100
- const request = await buildRequest(parsed, stdin);
101
- requestTimeout = createRequestTimeout(parsed.timeoutSeconds);
102
- const response = await sendRequest(
103
- request,
104
- fetchImpl,
105
- requestTimeout.signal,
106
- parsed.timeoutSeconds,
107
- );
108
- if (!response.ok) {
109
- const errorBody = await readLimitedText(response.body, MAX_ERROR_BYTES);
110
- throw new HttpError(response.status, response.statusText, errorBody.trim());
111
- }
112
-
113
- await writeResponse(response, stdout, maxJsonResponseBytes);
1
+ // mycomesh-consumer (V11): init, setup, balance, request, serve.
2
+ import { randomBytes } from "node:crypto";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { parseArgs } from "node:util";
8
+ import { addressOf, encodeCall, settlementKey } from "./eip712.mjs";
9
+ import { availableBalance, rpcCall, sendTransaction } from "./chain.mjs";
10
+ import { Consumer, httpJson, loadNetwork, serveConsumer } from "./consumer.mjs";
11
+ import { buildEvidence, evidenceHash, loadRequest, openDisputeCall, reportId } from "./disputes.mjs";
12
+
13
+ const DEFAULT_NETWORK = join(dirname(fileURLToPath(import.meta.url)), "../networks/mycomesh-v11-sepolia.json");
14
+
15
+ const USAGE = `Usage: mycomesh-consumer <command> [options]
16
+
17
+ init create the local payment key (never leaves this machine)
18
+ address print the payment key address
19
+ setup --owner-key-file F --deposit UNITS [--max-per-request UNITS] [--faucet UNITS]
20
+ deposit into the settlement contract and authorize the payment key
21
+ (--faucet first mints testnet tUSDC to the owner)
22
+ balance [--owner ADDR] show the custodied deposit
23
+ request "prompt" send one request and print the verified answer
24
+ serve [--port 8110] run the local OpenAI-compatible endpoint (default)
25
+ dispute <settlement-key|last> --owner-key-file F [--reason CODE] [--statement TEXT]
26
+ reveal a recorded request and response to a Provider-AI jury
27
+ (within 24 hours; the reporter bond is returned if fraud is confirmed)
28
+
29
+ Common: --network FILE (default: the bundled MycoMesh V11 Sepolia manifest), --data-dir DIR, --model ID, --max-fee UNITS, --provider SIGNER`;
30
+
31
+ function dataDir(value) {
32
+ const dir = value || process.env.MYCOMESH_CONSUMER_DATA_DIR || join(homedir(), ".mycomesh", "v11");
33
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
34
+ return dir;
35
+ }
36
+
37
+ function readKey(path) {
38
+ const value = readFileSync(path, "utf8").trim();
39
+ if (!/^(0x)?[0-9a-fA-F]{64}$/.test(value)) throw new Error(`${path} does not hold a private key`);
40
+ return value.startsWith("0x") ? value : `0x${value}`;
41
+ }
42
+
43
+ function paymentKey(dir, create = false) {
44
+ const path = join(dir, "payment-key");
45
+ if (!existsSync(path)) {
46
+ if (!create) throw new Error("no payment key; run `mycomesh-consumer init` first");
47
+ writeFileSync(path, `0x${randomBytes(32).toString("hex")}\n`, { mode: 0o600 });
48
+ chmodSync(path, 0o600);
49
+ }
50
+ return readKey(path);
51
+ }
52
+
53
+ export async function main(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
54
+ const { values, positionals } = parseArgs({
55
+ args: argv, allowPositionals: true,
56
+ options: {
57
+ network: { type: "string" }, "data-dir": { type: "string" }, "owner-key-file": { type: "string" },
58
+ deposit: { type: "string" }, "max-per-request": { type: "string" }, owner: { type: "string" },
59
+ model: { type: "string", default: "gpt-5.5" }, "max-fee": { type: "string", default: "1000000" },
60
+ port: { type: "string", default: "8110" }, host: { type: "string", default: "127.0.0.1" },
61
+ "max-output-tokens": { type: "string", default: "4096" }, help: { type: "boolean" },
62
+ reason: { type: "string", default: "unrelated_response" }, faucet: { type: "string" }, provider: { type: "string" }, statement: { type: "string", default: "" },
63
+ },
64
+ });
65
+ const [command = "serve", ...rest] = positionals;
66
+ if (values.help || command === "help") { stdout.write(`${USAGE}\n`); return 0; }
67
+ const dir = dataDir(values["data-dir"]);
68
+ if (command === "init" || command === "address") {
69
+ stdout.write(`${addressOf(paymentKey(dir, command === "init"))}\n`);
114
70
  return 0;
115
- } catch (error) {
116
- const exitCode = error instanceof CliError ? error.exitCode : 1;
117
- const message = requestTimeout?.signal.aborted
118
- ? `request timed out after ${requestTimeout.timeoutSeconds} seconds`
119
- : error instanceof Error
120
- ? error.message
121
- : String(error);
122
- stderr.write(`mycomesh: ${redact(message, apiKey)}\n`);
123
- return exitCode;
124
- } finally {
125
- requestTimeout?.cancel();
126
71
  }
127
- }
128
-
129
- export function parseArguments(argv, env = process.env) {
130
- const parsed = {
131
- command: undefined,
132
- baseUrl: env.MYCOMESH_BASE_URL || DEFAULT_BASE_URL,
133
- apiKey: env.MYCOMESH_API_KEY || "",
134
- timeoutSeconds: requestTimeoutSeconds(
135
- env.MYCOMESH_TIMEOUT_SECONDS || String(DEFAULT_TIMEOUT_SECONDS),
136
- "MYCOMESH_TIMEOUT_SECONDS",
137
- ),
138
- allowInsecureHttp: false,
139
- sessionId: env.MYCOMESH_SESSION_ID || "",
140
- sessionIdExplicit: false,
141
- jsonSource: undefined,
142
- model: undefined,
143
- input: undefined,
144
- message: undefined,
145
- system: undefined,
146
- stream: undefined,
147
- maxOutputTokens: undefined,
148
- maxTokens: undefined,
149
- maxCompletionTokens: undefined,
150
- positionals: [],
151
- help: false,
152
- version: false,
153
- };
154
-
155
- for (let index = 0; index < argv.length; index += 1) {
156
- const token = argv[index];
157
- if (token === "--") {
158
- parsed.positionals.push(...argv.slice(index + 1));
159
- break;
160
- }
161
- if (token === "-h" || token === "--help") {
162
- parsed.help = true;
163
- continue;
164
- }
165
- if (token === "-v" || token === "--version") {
166
- parsed.version = true;
167
- continue;
168
- }
169
- if (token === "--stream") {
170
- parsed.stream = true;
171
- continue;
172
- }
173
- if (token === "--no-stream") {
174
- parsed.stream = false;
175
- continue;
176
- }
177
- if (token === "--allow-insecure-http") {
178
- parsed.allowInsecureHttp = true;
179
- continue;
180
- }
181
- if (token.startsWith("--")) {
182
- const separator = token.indexOf("=");
183
- const name = separator === -1 ? token : token.slice(0, separator);
184
- let value = separator === -1 ? undefined : token.slice(separator + 1);
185
- if (value === undefined) {
186
- index += 1;
187
- value = argv[index];
188
- }
189
- if (value === undefined) {
190
- throw new CliError(`${name} requires a value`, 2);
191
- }
192
- switch (name) {
193
- case "--base-url":
194
- parsed.baseUrl = value;
195
- break;
196
- case "--api-key":
197
- parsed.apiKey = value;
198
- break;
199
- case "--timeout":
200
- parsed.timeoutSeconds = requestTimeoutSeconds(value, name);
201
- break;
202
- case "--session-id":
203
- parsed.sessionId = value;
204
- parsed.sessionIdExplicit = true;
205
- break;
206
- case "--json":
207
- parsed.jsonSource = value;
208
- break;
209
- case "--model":
210
- parsed.model = value;
211
- break;
212
- case "--input":
213
- parsed.input = value;
214
- break;
215
- case "--message":
216
- parsed.message = value;
217
- break;
218
- case "--system":
219
- parsed.system = value;
220
- break;
221
- case "--max-output-tokens":
222
- parsed.maxOutputTokens = positiveInteger(value, name);
223
- break;
224
- case "--max-tokens":
225
- parsed.maxTokens = positiveInteger(value, name);
226
- break;
227
- case "--max-completion-tokens":
228
- parsed.maxCompletionTokens = positiveInteger(value, name);
229
- break;
230
- default:
231
- throw new CliError(`unknown option: ${name}`, 2);
232
- }
233
- continue;
234
- }
235
- if (token.startsWith("-") && token !== "-") {
236
- throw new CliError(`unknown option: ${token}`, 2);
237
- }
238
- if (parsed.command === undefined) {
239
- parsed.command = token;
240
- } else {
241
- parsed.positionals.push(token);
242
- }
243
- }
244
-
245
- if (!parsed.command && !parsed.help && !parsed.version) {
246
- throw new CliError("a command is required; run mycomesh --help", 2);
247
- }
248
- if (parsed.command && !COMMANDS.has(parsed.command)) {
249
- throw new CliError(`unknown command: ${parsed.command}`, 2);
250
- }
251
- if (
252
- parsed.sessionId &&
253
- (parsed.command === "responses" || parsed.command === "chat")
254
- ) {
255
- parsed.sessionId = sessionId(parsed.sessionId, "--session-id");
256
- }
257
- validateCommandOptions(parsed);
258
- return parsed;
259
- }
260
-
261
- async function buildRequest(parsed, stdin) {
262
- const url = endpointUrl(
263
- parsed.baseUrl,
264
- parsed.command,
265
- parsed.allowInsecureHttp,
266
- );
267
- const headers = {
268
- accept: "application/json",
269
- "user-agent": `mycomesh-cli/${CLI_VERSION}`,
270
- };
271
- if (parsed.apiKey) {
272
- headers.authorization = `Bearer ${parsed.apiKey}`;
273
- }
274
-
275
- if (parsed.command === "health" || parsed.command === "models") {
276
- return { url, method: "GET", headers };
277
- }
278
-
279
- const body = await requestBody(parsed, stdin);
280
- headers["content-type"] = "application/json";
281
- if (body.stream === true) {
282
- headers.accept = "text/event-stream, application/json";
283
- }
284
- return {
285
- url,
286
- method: "POST",
287
- headers,
288
- body: JSON.stringify(body),
289
- };
290
- }
291
-
292
- async function requestBody(parsed, stdin) {
293
- let body = {};
294
- if (parsed.jsonSource !== undefined) {
295
- body = await parseJsonSource(parsed.jsonSource, stdin);
296
- } else if (stdin && stdin.isTTY !== true) {
297
- const piped = await readLimitedText(stdin, MAX_INPUT_BYTES);
298
- if (piped.trim()) {
299
- body = parseJsonObject(piped, "stdin");
300
- }
301
- }
302
-
303
- if (parsed.model !== undefined) {
304
- body.model = parsed.model;
305
- }
306
- if (parsed.stream !== undefined) {
307
- body.stream = parsed.stream;
308
- }
309
- if (parsed.sessionId) {
310
- body.mycomesh_session = { session_id: parsed.sessionId };
311
- }
312
-
313
- const positionalInput = parsed.positionals.length
314
- ? parsed.positionals.join(" ")
315
- : undefined;
316
- if (parsed.command === "responses") {
317
- const input = parsed.input ?? positionalInput;
318
- if (input !== undefined) {
319
- body.input = input;
320
- }
321
- if (parsed.maxOutputTokens !== undefined) {
322
- body.max_output_tokens = parsed.maxOutputTokens;
323
- }
324
- if (body.input === undefined) {
325
- throw new CliError(
326
- "responses requires input in JSON, --input, or a positional argument",
327
- 2,
328
- );
329
- }
330
- } else {
331
- const message = parsed.message ?? positionalInput;
332
- if (message !== undefined) {
333
- const messages = [];
334
- if (parsed.system !== undefined) {
335
- messages.push({ role: "system", content: parsed.system });
336
- }
337
- messages.push({ role: "user", content: message });
338
- body.messages = messages;
339
- } else if (parsed.system !== undefined) {
340
- const messages = Array.isArray(body.messages) ? body.messages : [];
341
- body.messages = [
342
- { role: "system", content: parsed.system },
343
- ...messages,
344
- ];
345
- }
346
- if (parsed.maxTokens !== undefined) {
347
- body.max_tokens = parsed.maxTokens;
348
- }
349
- if (parsed.maxCompletionTokens !== undefined) {
350
- body.max_completion_tokens = parsed.maxCompletionTokens;
351
- }
352
- if (!Array.isArray(body.messages) || body.messages.length === 0) {
353
- throw new CliError(
354
- "chat requires messages in JSON, --message, or a positional argument",
355
- 2,
356
- );
357
- }
358
- }
359
- return body;
360
- }
361
-
362
- async function parseJsonSource(source, stdin) {
363
- if (source === "-") {
364
- const text = await readLimitedText(stdin, MAX_INPUT_BYTES);
365
- return parseJsonObject(text, "stdin");
366
- }
367
- if (source.startsWith("@")) {
368
- const path = source.slice(1);
369
- if (!path) {
370
- throw new CliError("--json @file requires a file path", 2);
371
- }
372
- let contents;
373
- try {
374
- contents = await readFile(path);
375
- } catch (error) {
376
- const detail = error instanceof Error ? error.message : String(error);
377
- throw new CliError(`could not read JSON file: ${detail}`, 2);
378
- }
379
- if (contents.byteLength > MAX_INPUT_BYTES) {
380
- throw new CliError(`JSON input exceeds ${MAX_INPUT_BYTES} bytes`, 2);
381
- }
382
- return parseJsonObject(contents.toString("utf8"), path);
383
- }
384
- if (Buffer.byteLength(source, "utf8") > MAX_INPUT_BYTES) {
385
- throw new CliError(`JSON input exceeds ${MAX_INPUT_BYTES} bytes`, 2);
386
- }
387
- return parseJsonObject(source, "--json");
388
- }
389
-
390
- function parseJsonObject(text, source) {
391
- let value;
392
- try {
393
- value = JSON.parse(text);
394
- } catch (error) {
395
- const detail = error instanceof Error ? error.message : String(error);
396
- throw new CliError(`invalid JSON from ${source}: ${detail}`, 2);
397
- }
398
- if (value === null || Array.isArray(value) || typeof value !== "object") {
399
- throw new CliError(`JSON from ${source} must be an object`, 2);
400
- }
401
- return { ...value };
402
- }
403
-
404
- function endpointUrl(baseUrl, command, allowInsecureHttp = false) {
405
- let url;
406
- try {
407
- url = new URL(baseUrl);
408
- } catch {
409
- throw new CliError("base URL must be an absolute HTTP or HTTPS URL", 2);
410
- }
411
- if (url.protocol !== "http:" && url.protocol !== "https:") {
412
- throw new CliError("base URL must use HTTP or HTTPS", 2);
413
- }
414
- if (url.username || url.password) {
415
- throw new CliError("base URL must not contain credentials", 2);
416
- }
417
- if (
418
- url.protocol === "http:" &&
419
- !allowInsecureHttp &&
420
- !isLoopbackHostname(url.hostname)
421
- ) {
422
- throw new CliError(
423
- "remote base URLs must use HTTPS; use --allow-insecure-http only for testing",
424
- 2,
425
- );
426
- }
427
- if (url.search || url.hash) {
428
- throw new CliError("base URL must not contain a query string or fragment", 2);
429
- }
430
-
431
- let path = url.pathname.replace(/\/+$/, "");
432
- if (command === "health") {
433
- path = path.replace(/\/v1$/, "");
434
- url.pathname = `${path}/health`;
435
- } else {
436
- if (!path.endsWith("/v1")) {
437
- path = `${path}/v1`;
438
- }
439
- const resource = command === "chat" ? "chat/completions" : command;
440
- url.pathname = `${path}/${resource}`;
441
- }
442
- return url;
443
- }
444
-
445
- function isLoopbackHostname(hostname) {
446
- const normalized = hostname.toLowerCase().replace(/\.$/, "");
447
- if (normalized === "localhost" || normalized === "[::1]") {
448
- return true;
449
- }
450
- if (!/^127(?:\.[0-9]{1,3}){3}$/.test(normalized)) {
451
- return false;
452
- }
453
- return normalized.split(".").every((part) => Number(part) <= 255);
454
- }
455
-
456
- async function sendRequest(request, fetchImpl, signal, timeoutSeconds) {
457
- try {
458
- return await fetchImpl(request.url, {
459
- method: request.method,
460
- headers: request.headers,
461
- body: request.body,
462
- redirect: "error",
463
- signal,
464
- });
465
- } catch (error) {
466
- if (signal.aborted) {
467
- throw new CliError(`request timed out after ${timeoutSeconds} seconds`);
468
- }
469
- const detail = error instanceof Error ? error.message : String(error);
470
- throw new CliError(`request failed: ${detail}`);
72
+ const network = loadNetwork(values.network || process.env.MYCOMESH_NETWORK || DEFAULT_NETWORK);
73
+ const keyPrivate = paymentKey(dir);
74
+ if (command === "setup") {
75
+ const owner = readKey(values["owner-key-file"]);
76
+ const deposit = BigInt(values.deposit);
77
+ const limit = BigInt(values["max-per-request"] || values["max-fee"]);
78
+ if (values.faucet) {
79
+ await sendTransaction(network.rpc_urls, owner, { to: network.stablecoin,
80
+ data: encodeCall("mint(address,uint256)", [["address", addressOf(owner)], ["uint", BigInt(values.faucet)]]) });
81
+ }
82
+ await sendTransaction(network.rpc_urls, owner, { to: network.stablecoin,
83
+ data: encodeCall("approve(address,uint256)", [["address", network.settlement], ["uint", deposit]]) });
84
+ await sendTransaction(network.rpc_urls, owner, { to: network.settlement, data: encodeCall("deposit(uint256)", [["uint", deposit]]) });
85
+ await sendTransaction(network.rpc_urls, owner, { to: network.settlement,
86
+ data: encodeCall("registerKey(address,uint256,uint64)", [["address", addressOf(keyPrivate)], ["uint", limit], ["uint", 0]]) });
87
+ stdout.write(`deposited ${deposit} and authorized ${addressOf(keyPrivate)} up to ${limit} per request\n`);
88
+ return 0;
471
89
  }
472
- }
473
-
474
- async function writeResponse(response, output, maxJsonResponseBytes) {
475
- const contentType = (response.headers.get("content-type") || "")
476
- .split(";", 1)[0]
477
- .trim()
478
- .toLowerCase();
479
-
480
- if (contentType === "text/event-stream") {
481
- await pipeBody(response.body, output);
482
- return;
90
+ if (command === "balance") {
91
+ const owner = values.owner || (values["owner-key-file"] ? addressOf(readKey(values["owner-key-file"])) : null);
92
+ if (!owner) throw new Error("--owner or --owner-key-file is required");
93
+ stdout.write(`${await availableBalance(network, owner)}\n`);
94
+ return 0;
483
95
  }
484
- if (contentType === "application/json" || contentType.endsWith("+json")) {
485
- const text = await readLimitedText(response.body, maxJsonResponseBytes, {
486
- rejectOnLimit: true,
487
- label: "JSON response",
96
+ if (command === "dispute") {
97
+ const owner = readKey(values["owner-key-file"]);
98
+ const record = loadRequest(dir, rest[0] || "last");
99
+ const evidence = buildEvidence(record, { reasonCode: values.reason, statement: values.statement || rest.slice(1).join(" ") });
100
+ const digest = evidenceHash(evidence);
101
+ // The reporter bond comes from the owner's wallet and is returned when fraud is confirmed.
102
+ const params = await rpcCall(network.rpc_urls, "eth_call", [{ to: network.settlement, data: encodeCall("params()", []) }, "latest"]);
103
+ const bond = BigInt(`0x${params.slice(2 + 64 * 3, 2 + 64 * 4)}`);
104
+ if (bond > 0n) {
105
+ await sendTransaction(network.rpc_urls, owner, { to: network.stablecoin,
106
+ data: encodeCall("approve(address,uint256)", [["address", network.settlement], ["uint", bond]]) });
107
+ }
108
+ await sendTransaction(network.rpc_urls, owner, { to: network.settlement, data: openDisputeCall(record.settlement_key, digest) });
109
+ const accepted = [];
110
+ for (const relay of network.relays) {
111
+ try {
112
+ const reply = await httpJson(`${relay.url}/v11/evidence`, { method: "POST", body: evidence, ca: network.tls_ca, timeoutMs: 30_000 });
113
+ if (reply.status === 200) accepted.push(relay.url);
114
+ } catch {}
115
+ }
116
+ stdout.write(`${JSON.stringify({ settlement_key: record.settlement_key, evidence_hash: digest,
117
+ report_id: reportId(record.settlement_key, addressOf(owner), digest), relays_accepted: accepted })}\n`);
118
+ return accepted.length ? 0 : 2;
119
+ }
120
+ const consumer = new Consumer({ network, keyPrivate, maxFee: Number(values["max-fee"]), journalDir: dir });
121
+ if (command === "request") {
122
+ const { response, receipt } = await consumer.request({
123
+ endpoint: "responses", model: values.model, content: rest.join(" "), maxOutputTokens: Number(values["max-output-tokens"]),
124
+ provider: values.provider,
488
125
  });
489
- if (!text.trim()) {
490
- return;
491
- }
492
- let value;
493
- try {
494
- value = JSON.parse(text);
495
- } catch {
496
- throw new CliError("server returned malformed JSON");
497
- }
498
- output.write(`${JSON.stringify(value, null, 2)}\n`);
499
- return;
500
- }
501
- await pipeBody(response.body, output);
502
- }
503
-
504
- async function pipeBody(body, output) {
505
- if (!body) {
506
- return;
507
- }
508
- for await (const chunk of body) {
509
- if (!output.write(chunk)) {
510
- await once(output, "drain");
511
- }
512
- }
513
- }
514
-
515
- async function readLimitedText(
516
- stream,
517
- limit,
518
- { rejectOnLimit = false, label = "response" } = {},
519
- ) {
520
- if (!stream) {
521
- return "";
522
- }
523
- const chunks = [];
524
- let size = 0;
525
- let truncated = false;
526
- for await (const rawChunk of stream) {
527
- const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
528
- const remaining = limit - size;
529
- if (chunk.byteLength > remaining) {
530
- if (remaining > 0) {
531
- chunks.push(chunk.subarray(0, remaining));
532
- }
533
- truncated = true;
534
- break;
535
- }
536
- chunks.push(chunk);
537
- size += chunk.byteLength;
538
- }
539
- const text = Buffer.concat(chunks).toString("utf8");
540
- if (truncated && rejectOnLimit) {
541
- throw new CliError(`${label} exceeds ${limit} bytes`);
542
- }
543
- return truncated ? `${text}\n[response truncated]` : text;
544
- }
545
-
546
- function createRequestTimeout(timeoutSeconds) {
547
- const controller = new AbortController();
548
- const timer = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
549
- timer.unref?.();
550
- return {
551
- signal: controller.signal,
552
- timeoutSeconds,
553
- cancel() {
554
- clearTimeout(timer);
555
- },
556
- };
557
- }
558
-
559
- function requestTimeoutSeconds(value, option) {
560
- const timeout = positiveInteger(String(value), option);
561
- if (timeout > MAX_TIMEOUT_SECONDS) {
562
- throw new CliError(
563
- `${option} must not exceed ${MAX_TIMEOUT_SECONDS} seconds`,
564
- 2,
565
- );
566
- }
567
- return timeout;
568
- }
569
-
570
- function positiveInteger(value, option) {
571
- if (!/^[1-9][0-9]*$/.test(value)) {
572
- throw new CliError(`${option} must be a positive integer`, 2);
573
- }
574
- const number = Number(value);
575
- if (!Number.isSafeInteger(number)) {
576
- throw new CliError(`${option} is too large`, 2);
577
- }
578
- return number;
579
- }
580
-
581
- function sessionId(value, option) {
582
- if (!/^0x[0-9a-fA-F]{64}$/.test(value)) {
583
- throw new CliError(`${option} must be a 32-byte 0x-prefixed hex value`, 2);
584
- }
585
- return value.toLowerCase();
586
- }
587
-
588
- function validateCommandOptions(parsed) {
589
- if (parsed.help || parsed.version || !parsed.command) {
590
- return;
591
- }
592
- const hasBodyOptions =
593
- parsed.jsonSource !== undefined ||
594
- parsed.model !== undefined ||
595
- parsed.stream !== undefined ||
596
- parsed.input !== undefined ||
597
- parsed.message !== undefined ||
598
- parsed.system !== undefined ||
599
- parsed.maxOutputTokens !== undefined ||
600
- parsed.maxTokens !== undefined ||
601
- parsed.maxCompletionTokens !== undefined ||
602
- parsed.positionals.length > 0;
603
- if ((parsed.command === "health" || parsed.command === "models") && hasBodyOptions) {
604
- throw new CliError(`${parsed.command} does not accept request body options`, 2);
605
- }
606
- if (
607
- (parsed.command === "health" || parsed.command === "models") &&
608
- parsed.sessionIdExplicit
609
- ) {
610
- throw new CliError(`${parsed.command} does not accept --session-id`, 2);
611
- }
612
- if (
613
- parsed.command === "responses" &&
614
- (parsed.message !== undefined ||
615
- parsed.system !== undefined ||
616
- parsed.maxTokens !== undefined ||
617
- parsed.maxCompletionTokens !== undefined)
618
- ) {
619
- throw new CliError("chat options cannot be used with responses", 2);
620
- }
621
- if (
622
- parsed.command === "chat" &&
623
- (parsed.input !== undefined || parsed.maxOutputTokens !== undefined)
624
- ) {
625
- throw new CliError("responses options cannot be used with chat", 2);
626
- }
627
- if (parsed.input !== undefined && parsed.positionals.length > 0) {
628
- throw new CliError("use either --input or positional input, not both", 2);
629
- }
630
- if (parsed.message !== undefined && parsed.positionals.length > 0) {
631
- throw new CliError("use either --message or a positional message, not both", 2);
632
- }
633
- }
634
-
635
- function redact(value, secret) {
636
- let text = String(value);
637
- if (!secret) {
638
- return text;
126
+ const output = response.output;
127
+ stdout.write(`${JSON.stringify({ output_text: output.output_text ?? output, fee: receipt.receipt.actual_fee,
128
+ provider: receipt.authorization.provider_signer, request_id: receipt.authorization.request_id,
129
+ settlement_key: settlementKey(receipt.authorization.key, receipt.authorization.request_id) })}\n`);
130
+ return 0;
639
131
  }
640
- const variants = new Set([
641
- secret,
642
- encodeURIComponent(secret),
643
- JSON.stringify(secret).slice(1, -1),
644
- ]);
645
- for (const variant of variants) {
646
- if (variant) {
647
- text = text.split(variant).join("[REDACTED]");
648
- }
132
+ if (command === "serve") {
133
+ const server = await serveConsumer(consumer, { host: values.host, port: Number(values.port), apiKey: process.env.MYCOMESH_CONSUMER_API_KEY });
134
+ const { port } = server.address();
135
+ stdout.write(`MycoMesh V11 Consumer on http://${values.host}:${port}/v1 (payment key ${consumer.key})\n`);
136
+ return new Promise(() => {});
649
137
  }
650
- return text;
138
+ throw new Error(`unknown command ${command}\n${USAGE}`);
651
139
  }