@iicp/client 0.7.38 → 0.7.40

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/README.md CHANGED
@@ -73,6 +73,24 @@ const result = await client.submit({
73
73
 
74
74
  ---
75
75
 
76
+ ## Use as a local API proxy (OpenAI / Ollama / Anthropic compat)
77
+
78
+ Run a local gateway that speaks the OpenAI, Ollama, and Anthropic HTTP APIs and routes
79
+ every request across the IICP mesh — point any tool you already use at it, no code changes.
80
+
81
+ ```bash
82
+ npm i -g @iicp/client
83
+ iicp-node proxy # → http://127.0.0.1:9483
84
+
85
+ export OPENAI_BASE_URL=http://127.0.0.1:9483/v1 # OpenAI SDK / LangChain / Cursor / liteLLM
86
+ export OLLAMA_HOST=http://127.0.0.1:9483 # Open WebUI / Continue.dev / aider / Jan
87
+ ```
88
+
89
+ Loopback-only consumer (never registers with the directory), built on Node's `http` (no
90
+ extra runtime dependency). Override the port with `--port` / `IICP_PROXY_PORT`; co-host
91
+ next to a node with `iicp-node serve --with-proxy`. Every response carries
92
+ `Server: iicp-proxy`. Full guide: <https://iicp.network/docs/proxy>
93
+
76
94
  ## Configuration
77
95
 
78
96
  ```typescript
@@ -352,7 +370,7 @@ Conformance tier: `iicp:sdk:v1` (spec S.14) · [Request a badge](https://iicp.ne
352
370
  ```bash
353
371
  npm install # install deps
354
372
  npm run typecheck # tsc strict
355
- npm test # 224 unit tests
373
+ npm test # run the unit suite
356
374
  npm run build # emit to dist/
357
375
  ```
358
376
 
package/dist/cli.d.ts CHANGED
@@ -21,6 +21,7 @@ export interface ServeOpts {
21
21
  relayWorkerEndpoint: string;
22
22
  node: string;
23
23
  logDir?: string;
24
+ withProxy?: boolean;
24
25
  }
25
26
  export declare function applySavedNode(opts: ServeOpts, saved: NodeIdentity): ServeOpts;
26
27
  /**
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAuCA,OAAO,EAcL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,qGAAqG;IACrG,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,OAAO,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAsXD,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,SAAS,CAqB9E;AA6lBD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA+D5D;AA6RD,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAgGlF"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAwCA,OAAO,EAcL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAGvB,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,qGAAqG;IACrG,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,OAAO,CAAC;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAkaD,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,SAAS,CAqB9E;AAuoBD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA+D5D;AAmZD,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBlF"}
package/dist/cli.js CHANGED
@@ -33,8 +33,9 @@ const node_js_1 = require("./node.js");
33
33
  const client_js_1 = require("./client.js");
34
34
  const node_log_js_1 = require("./node_log.js");
35
35
  const cip_policy_js_1 = require("./cip_policy.js");
36
+ const index_js_1 = require("./proxy/index.js");
36
37
  const instance_lock_js_1 = require("./instance_lock.js");
37
- const index_js_1 = require("./backends/index.js");
38
+ const index_js_2 = require("./backends/index.js");
38
39
  const identity_js_1 = require("./identity.js");
39
40
  const delegation_js_1 = require("./delegation.js");
40
41
  function envOr(name, fallback) {
@@ -62,6 +63,7 @@ function printHelp() {
62
63
  ` serve Register and serve a node\n` +
63
64
  ` query <prompt> Discover mesh nodes and submit a chat task\n` +
64
65
  ` credits Show this node's earned / spent / balance credits\n` +
66
+ ` proxy Run the local OpenAI/Ollama/Anthropic-compat gateway (loopback; no registration)\n` +
65
67
  ` operator rename <name> Change your public display_name (signed by your operator key)\n` +
66
68
  ` operator encrypt Password-encrypt the operator secret at rest ($IICP_OPERATOR_PASSPHRASE)\n` +
67
69
  ` operator decrypt Remove at-rest encryption of the operator secret\n\n` +
@@ -82,8 +84,13 @@ function printHelp() {
82
84
  ` --port N IICP_PORT (default 9484)\n` +
83
85
  ` --host HOST IICP_HOST (default :: — dual-stack IPv4+IPv6)\n` +
84
86
  ` --skip-registration IICP_SKIP_REGISTRATION — register-free dev mode\n` +
85
- ` --auto-detect-nat IICP_AUTO_DETECT_NATrun NAT detection at startup\n` +
86
- ` --external-ip-probe-url U IICP_EXTERNAL_IP_PROBE_URL fallback IPv4 probe\n\n` +
87
+ ` --force IICP_FORCEtake over the single-instance lock for this node_id\n` +
88
+ ` --auto-detect-nat IICP_AUTO_DETECT_NATrun NAT detection at startup (default on)\n` +
89
+ ` --no-auto-detect-nat disable NAT detection at startup\n` +
90
+ ` --external-ip-probe-url U IICP_EXTERNAL_IP_PROBE_URL — fallback IPv4 probe\n` +
91
+ ` --relay-worker-endpoint H IICP_RELAY_WORKER_ENDPOINT — <host>:<port> of a relay node (R2 last-resort)\n` +
92
+ ` --log-dir DIR IICP_LOG_DIR — directory for persistent log files (<node_id>.log + events.jsonl)\n` +
93
+ ` --with-proxy IICP_WITH_PROXY — also run the loopback compat proxy (127.0.0.1:9483) in-process\n\n` +
87
94
  `query optional:\n` +
88
95
  ` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
89
96
  ` --intent URN IICP_INTENT (default urn:iicp:intent:llm:chat:v1)\n` +
@@ -91,6 +98,42 @@ function printHelp() {
91
98
  ` --max-tokens N Limit response length\n` +
92
99
  ` --timeout-ms N Request timeout (default 60000)\n`);
93
100
  }
101
+ /**
102
+ * Thrown by safeParseArgs / port parsing to signal a clean, user-facing CLI error.
103
+ * main() catches it and prints a one-line `ERROR:` (exit 2) — never a raw stack trace.
104
+ */
105
+ class CliError extends Error {
106
+ }
107
+ /**
108
+ * Wrap node:util parseArgs so unknown options / bad values surface as a friendly
109
+ * `ERROR: unknown option '--x'` (exit 2) instead of a raw ERR_PARSE_ARGS_* stack trace.
110
+ */
111
+ function safeParseArgs(config) {
112
+ try {
113
+ return (0, node_util_1.parseArgs)(config);
114
+ }
115
+ catch (exc) {
116
+ const e = exc;
117
+ if (e?.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
118
+ const m = /Unknown option '([^']*)'/.exec(e.message ?? "");
119
+ throw new CliError(`unknown option '${m?.[1] ?? "?"}'`);
120
+ }
121
+ if (e?.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE" || e?.code === "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL") {
122
+ throw new CliError(e.message ?? "invalid argument");
123
+ }
124
+ throw new CliError(e?.message ?? String(exc));
125
+ }
126
+ }
127
+ /** Parse a port flag/env into a number, raising a friendly CliError on a non-numeric value. */
128
+ function parsePort(raw, fallback) {
129
+ if (raw === undefined)
130
+ return fallback;
131
+ const n = parseInt(raw, 10);
132
+ if (!Number.isInteger(n) || n < 0 || n > 65535) {
133
+ throw new CliError("--port must be a number between 0 and 65535");
134
+ }
135
+ return n;
136
+ }
94
137
  async function checkDependencies(backendUrl) {
95
138
  const out = [];
96
139
  // 1) Backend reachability
@@ -413,6 +456,18 @@ async function runServe(opts) {
413
456
  allowCoordinator: true,
414
457
  });
415
458
  }
459
+ // 2-C: co-host the compat proxy on loopback alongside the node, supervised so a
460
+ // proxy failure logs but never drops the network-facing node. Forced to 127.0.0.1.
461
+ if (opts.withProxy) {
462
+ const pport = envInt("IICP_PROXY_PORT", 9483);
463
+ const pclient = new client_js_1.IicpClient({
464
+ directory_url: opts.directoryUrl,
465
+ region: opts.region,
466
+ });
467
+ const pserver = (0, index_js_1.createProxyServer)(pclient);
468
+ pserver.on("error", (e) => process.stderr.write(`co-hosted proxy error (node continues): ${String(e)}\n`));
469
+ pserver.listen(pport, "127.0.0.1", () => process.stdout.write(`co-hosted proxy → http://127.0.0.1:${pport} (OpenAI/Ollama/Anthropic compat)\n`));
470
+ }
416
471
  if (opts.node) {
417
472
  const saved = (0, identity_js_1.loadNode)(opts.node);
418
473
  if (!saved) {
@@ -421,6 +476,13 @@ async function runServe(opts) {
421
476
  }
422
477
  opts = applySavedNode(opts, saved);
423
478
  }
479
+ // #410/#414 — built-in backend-url fallback applied LAST (after flag/env/saved-config),
480
+ // so a bare `serve --model x` works without --backend-url. An `anthropic` backend
481
+ // defaults to the Anthropic API, not localhost Ollama. Mirrors Python cli.py ~704.
482
+ if (!opts.backendUrl) {
483
+ opts.backendUrl =
484
+ opts.backendType === "anthropic" ? "https://api.anthropic.com" : "http://localhost:11434";
485
+ }
424
486
  // Onboarding: if no --model given, auto-select the first model the backend advertises
425
487
  // (Ollama /api/tags) so a bare `iicp-node serve` just works (parity with Rust/Python).
426
488
  if (!opts.model && opts.backendUrl) {
@@ -444,8 +506,8 @@ async function runServe(opts) {
444
506
  process.stderr.write("ERROR: --model is required (--backend-url defaults to http://localhost:11434). Set IICP_BACKEND_MODEL, or use --node NAME.\n");
445
507
  return 2;
446
508
  }
447
- if (!index_js_1.BACKEND_TYPES.includes(opts.backendType)) {
448
- process.stderr.write(`ERROR: --backend-type must be one of ${JSON.stringify(index_js_1.BACKEND_TYPES)}.\n`);
509
+ if (!index_js_2.BACKEND_TYPES.includes(opts.backendType)) {
510
+ process.stderr.write(`ERROR: --backend-type must be one of ${JSON.stringify(index_js_2.BACKEND_TYPES)}.\n`);
449
511
  return 2;
450
512
  }
451
513
  const nodeId = (opts.nodeId || (0, node_crypto_1.randomUUID)()).slice(0, 36);
@@ -610,7 +672,7 @@ async function runServe(opts) {
610
672
  const t = opts.backendUrl.replace(/\/$/, "");
611
673
  return t.endsWith("/v1") ? t : `${t}/v1`;
612
674
  })();
613
- const handler = (0, index_js_1.getBackendHandler)(opts.backendType, {
675
+ const handler = (0, index_js_2.getBackendHandler)(opts.backendType, {
614
676
  baseUrl: _baseUrl,
615
677
  model: opts.model,
616
678
  // #5 — Bearer key for auth'd backends (LM Studio, hosted). Empty/undefined = no header.
@@ -775,8 +837,19 @@ async function runServe(opts) {
775
837
  void node_crypto_1.randomUUID;
776
838
  return 0;
777
839
  }
840
+ function printQueryHelp() {
841
+ process.stdout.write(`usage: iicp-node query <prompt> [options]\n\n` +
842
+ `Discover mesh nodes and submit a chat task.\n\n` +
843
+ `options:\n` +
844
+ ` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
845
+ ` --intent URN IICP_INTENT (default urn:iicp:intent:llm:chat:v1)\n` +
846
+ ` --model NAME Pin to a specific model on the remote node\n` +
847
+ ` --max-tokens N Limit response length\n` +
848
+ ` --timeout-ms N Request timeout in milliseconds (default 60000)\n` +
849
+ ` -h, --help Show this help and exit\n`);
850
+ }
778
851
  async function runQuery(argv) {
779
- const { values, positionals } = (0, node_util_1.parseArgs)({
852
+ const { values, positionals } = safeParseArgs({
780
853
  args: argv,
781
854
  options: {
782
855
  "directory-url": { type: "string" },
@@ -784,13 +857,17 @@ async function runQuery(argv) {
784
857
  model: { type: "string" },
785
858
  "max-tokens": { type: "string" },
786
859
  "timeout-ms": { type: "string" },
860
+ help: { type: "boolean", short: "h" },
787
861
  },
788
862
  allowPositionals: true,
789
- strict: false,
790
863
  });
864
+ if (values.help) {
865
+ printQueryHelp();
866
+ return 0;
867
+ }
791
868
  const prompt = positionals.join(" ").trim();
792
869
  if (!prompt) {
793
- process.stderr.write("Usage: iicp-node query <prompt> [flags]\n");
870
+ printQueryHelp();
794
871
  return 1;
795
872
  }
796
873
  const directoryUrl = values["directory-url"] ??
@@ -1043,8 +1120,22 @@ async function verifyCreditAwards(directoryUrl, nodeId) {
1043
1120
  * directory (not the local config), so editing the saved file cannot inflate them;
1044
1121
  * `reconciles` flags a ledger that does not add up.
1045
1122
  */
1123
+ function printCreditsHelp() {
1124
+ process.stdout.write(`usage: iicp-node credits [options]\n\n` +
1125
+ `Show this node's earned / spent / balance credits (authenticated from the directory).\n\n` +
1126
+ `options:\n` +
1127
+ ` --node NAME Load token + node_id from ~/.iicp/nodes/<NAME>.json\n` +
1128
+ ` --node-id ID Node id (if not using --node)\n` +
1129
+ ` --token TOKEN Node token (env IICP_NODE_TOKEN)\n` +
1130
+ ` --directory-url URL IICP directory base URL (defaults to saved node / env / iicp.network)\n` +
1131
+ ` --json Print the raw summary JSON\n` +
1132
+ ` --verify Cryptographically audit each award against the signed CREDIT_AWARD log\n` +
1133
+ ` -h, --help Show this help and exit\n\n` +
1134
+ `With no --node / --node-id and exactly one saved node (or a node named 'default'),\n` +
1135
+ `that node is used automatically.\n`);
1136
+ }
1046
1137
  async function runCredits(argv) {
1047
- const { values } = (0, node_util_1.parseArgs)({
1138
+ const { values } = safeParseArgs({
1048
1139
  args: argv,
1049
1140
  options: {
1050
1141
  node: { type: "string" },
@@ -1053,13 +1144,37 @@ async function runCredits(argv) {
1053
1144
  "directory-url": { type: "string" },
1054
1145
  json: { type: "boolean" },
1055
1146
  verify: { type: "boolean" },
1147
+ help: { type: "boolean", short: "h" },
1056
1148
  },
1057
1149
  allowPositionals: false,
1058
1150
  });
1059
- const nodeName = values["node"];
1151
+ if (values.help) {
1152
+ printCreditsHelp();
1153
+ return 0;
1154
+ }
1155
+ let nodeName = values["node"];
1060
1156
  let directoryUrl = values["directory-url"];
1061
1157
  let nodeId = values["node-id"];
1062
1158
  let token = values["token"] ?? process.env["IICP_NODE_TOKEN"];
1159
+ // #8 — no --node / --node-id: if exactly one saved node (or a 'default' node) exists,
1160
+ // use it automatically; otherwise emit a clear error listing the saved node names.
1161
+ if (!nodeName && !nodeId) {
1162
+ const saved = (0, identity_js_1.listNodes)();
1163
+ if (saved.length === 1) {
1164
+ nodeName = saved[0].name;
1165
+ }
1166
+ else if (saved.some((n) => n.name === "default")) {
1167
+ nodeName = "default";
1168
+ }
1169
+ else if (saved.length === 0) {
1170
+ process.stderr.write("ERROR: no saved nodes — run `iicp-node init` / `serve` first, or pass --node-id ID.\n");
1171
+ return 1;
1172
+ }
1173
+ else {
1174
+ process.stderr.write(`ERROR: multiple saved nodes — pass --node NAME (one of: ${saved.map((n) => n.name).join(", ")}) or --node-id ID.\n`);
1175
+ return 1;
1176
+ }
1177
+ }
1063
1178
  if (nodeName) {
1064
1179
  const saved = (0, identity_js_1.loadNode)(nodeName);
1065
1180
  if (!saved) {
@@ -1231,21 +1346,44 @@ async function runOperatorDecrypt() {
1231
1346
  * signed call updates the single operator record, reflected on every node + the leaderboard.
1232
1347
  * Updates the local operator.json on success. Never sends the secret/contact.
1233
1348
  */
1349
+ function printOperatorHelp() {
1350
+ process.stdout.write(`usage: iicp-node operator <subcommand> [options]\n\n` +
1351
+ `Manage your operator identity.\n\n` +
1352
+ `subcommands:\n` +
1353
+ ` rename <name> Change your public display_name (signed by your operator key)\n` +
1354
+ ` encrypt Password-encrypt the operator secret at rest ($IICP_OPERATOR_PASSPHRASE)\n` +
1355
+ ` decrypt Remove at-rest encryption of the operator secret\n\n` +
1356
+ `operator rename options:\n` +
1357
+ ` --directory-url URL IICP directory base URL (defaults to env / iicp.network)\n` +
1358
+ ` -h, --help Show this help and exit\n`);
1359
+ }
1234
1360
  async function runOperator(argv) {
1235
1361
  const sub = argv[0];
1362
+ if (sub === undefined || sub === "--help" || sub === "-h") {
1363
+ printOperatorHelp();
1364
+ return sub === undefined ? 2 : 0;
1365
+ }
1236
1366
  if (sub === "encrypt")
1237
1367
  return runOperatorEncrypt();
1238
1368
  if (sub === "decrypt")
1239
1369
  return runOperatorDecrypt();
1240
1370
  if (sub !== "rename") {
1241
- process.stderr.write(`unknown operator subcommand: ${sub ?? "(none)"}\n`);
1371
+ process.stderr.write(`unknown operator subcommand: ${sub}\n`);
1372
+ printOperatorHelp();
1242
1373
  return 2;
1243
1374
  }
1244
- const { values, positionals } = (0, node_util_1.parseArgs)({
1375
+ const { values, positionals } = safeParseArgs({
1245
1376
  args: argv.slice(1),
1246
- options: { "directory-url": { type: "string" } },
1377
+ options: {
1378
+ "directory-url": { type: "string" },
1379
+ help: { type: "boolean", short: "h" },
1380
+ },
1247
1381
  allowPositionals: true,
1248
1382
  });
1383
+ if (values.help) {
1384
+ printOperatorHelp();
1385
+ return 0;
1386
+ }
1249
1387
  const name = positionals[0];
1250
1388
  // eslint-disable-next-line no-control-regex
1251
1389
  if (!name || name.length > 64 || /[\u0000-\u001f\u007f]/.test(name)) {
@@ -1299,8 +1437,50 @@ async function runOperator(argv) {
1299
1437
  process.stdout.write(`Renamed operator display_name to ${JSON.stringify(op.display_name)}.\n`);
1300
1438
  return 0;
1301
1439
  }
1440
+ // ── proxy (ADR-050) — local compat gateway; consumer, loopback, no registration ──
1441
+ function printProxyHelp() {
1442
+ process.stdout.write(`usage: iicp-node proxy [options]\n\n` +
1443
+ `Run the local OpenAI/Ollama/Anthropic-compat gateway (consumer; loopback;\n` +
1444
+ `does NOT register with the directory).\n\n` +
1445
+ `options:\n` +
1446
+ ` --port N Listen port (env IICP_PROXY_PORT, default 9483)\n` +
1447
+ ` --host HOST Bind host (env IICP_PROXY_HOST, default 127.0.0.1 — loopback)\n` +
1448
+ ` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
1449
+ ` --region REGION Preferred region (env IICP_PROXY_PREFERRED_REGION)\n` +
1450
+ ` --token TOKEN Node token (env IICP_NODE_TOKEN)\n` +
1451
+ ` -h, --help Show this help and exit\n`);
1452
+ }
1453
+ async function runProxyCmd(argv) {
1454
+ const { values } = safeParseArgs({
1455
+ args: argv,
1456
+ allowPositionals: false,
1457
+ options: {
1458
+ port: { type: "string" },
1459
+ host: { type: "string" },
1460
+ "directory-url": { type: "string" },
1461
+ region: { type: "string" },
1462
+ token: { type: "string" },
1463
+ help: { type: "boolean", short: "h" },
1464
+ },
1465
+ });
1466
+ if (values.help) {
1467
+ printProxyHelp();
1468
+ return 0;
1469
+ }
1470
+ const port = values.port !== undefined
1471
+ ? parsePort(values.port, 9483)
1472
+ : parsePort(process.env["IICP_PROXY_PORT"], 9483);
1473
+ const host = values.host ?? envOr("IICP_PROXY_HOST", "127.0.0.1");
1474
+ return (0, index_js_1.runProxy)({
1475
+ host,
1476
+ port,
1477
+ directoryUrl: values["directory-url"] ?? envOr("IICP_DIRECTORY_URL", "https://iicp.network/api"),
1478
+ region: values.region ?? envOr("IICP_PROXY_PREFERRED_REGION"),
1479
+ token: values.token ?? envOr("IICP_NODE_TOKEN"),
1480
+ });
1481
+ }
1302
1482
  async function main(argv = process.argv.slice(2)) {
1303
- if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
1483
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
1304
1484
  printHelp();
1305
1485
  return argv.length === 0 ? 2 : 0;
1306
1486
  }
@@ -1308,6 +1488,19 @@ async function main(argv = process.argv.slice(2)) {
1308
1488
  process.stdout.write(`iicp-node ${SDK_VERSION}\n`);
1309
1489
  return 0;
1310
1490
  }
1491
+ try {
1492
+ return await dispatch(argv);
1493
+ }
1494
+ catch (exc) {
1495
+ if (exc instanceof CliError) {
1496
+ process.stderr.write(`ERROR: ${exc.message}\n`);
1497
+ return 2;
1498
+ }
1499
+ throw exc;
1500
+ }
1501
+ }
1502
+ /** Command dispatch — separated so main() can wrap parse failures as clean CliError output. */
1503
+ async function dispatch(argv) {
1311
1504
  const cmd = argv[0];
1312
1505
  if (cmd === "init")
1313
1506
  return runInit();
@@ -1319,12 +1512,14 @@ async function main(argv = process.argv.slice(2)) {
1319
1512
  return runCredits(argv.slice(1));
1320
1513
  if (cmd === "operator")
1321
1514
  return runOperator(argv.slice(1));
1515
+ if (cmd === "proxy")
1516
+ return runProxyCmd(argv.slice(1));
1322
1517
  if (cmd !== "serve") {
1323
1518
  process.stderr.write(`unknown command: ${cmd}\n`);
1324
1519
  printHelp();
1325
1520
  return 2;
1326
1521
  }
1327
- const { values } = (0, node_util_1.parseArgs)({
1522
+ const { values } = safeParseArgs({
1328
1523
  args: argv.slice(1),
1329
1524
  options: {
1330
1525
  node: { type: "string" },
@@ -1343,9 +1538,13 @@ async function main(argv = process.argv.slice(2)) {
1343
1538
  "skip-registration": { type: "boolean" },
1344
1539
  force: { type: "boolean" },
1345
1540
  "auto-detect-nat": { type: "boolean" },
1541
+ // Parity with Python's BooleanOptionalAction: an explicit off-switch for NAT
1542
+ // detection (`--auto-detect-nat=false` can't work — it's a no-arg boolean).
1543
+ "no-auto-detect-nat": { type: "boolean" },
1346
1544
  "external-ip-probe-url": { type: "string" },
1347
1545
  "relay-worker-endpoint": { type: "string" },
1348
1546
  "log-dir": { type: "string" },
1547
+ "with-proxy": { type: "boolean" },
1349
1548
  help: { type: "boolean", short: "h" },
1350
1549
  },
1351
1550
  allowPositionals: false,
@@ -1371,21 +1570,27 @@ async function main(argv = process.argv.slice(2)) {
1371
1570
  : envInt("IICP_MAX_CONCURRENT", 4),
1372
1571
  nodeId: values["node-id"] ?? envOr("IICP_NODE_ID") ?? "",
1373
1572
  port: values.port !== undefined
1374
- ? parseInt(values.port, 10)
1375
- : envInt("IICP_PORT", 9484),
1573
+ ? parsePort(values.port, 9484)
1574
+ : parsePort(process.env["IICP_PORT"], 9484),
1376
1575
  host: values.host ?? envOr("IICP_HOST", "::"),
1377
1576
  skipRegistration: Boolean(values["skip-registration"]) || envBool("IICP_SKIP_REGISTRATION"),
1378
1577
  force: Boolean(values["force"]) || envBool("IICP_FORCE"),
1379
- // Default ON — matches Python CLI behaviour; operator must set IICP_AUTO_DETECT_NAT=false to opt out.
1380
- autoDetectNat: values["auto-detect-nat"] !== undefined
1381
- ? Boolean(values["auto-detect-nat"])
1382
- : (process.env.IICP_AUTO_DETECT_NAT !== undefined ? envBool("IICP_AUTO_DETECT_NAT") : true),
1578
+ // Default ON — matches Python CLI behaviour. Off-switch precedence: explicit
1579
+ // --no-auto-detect-nat > --auto-detect-nat > IICP_AUTO_DETECT_NAT env > default on.
1580
+ autoDetectNat: values["no-auto-detect-nat"]
1581
+ ? false
1582
+ : values["auto-detect-nat"] !== undefined
1583
+ ? Boolean(values["auto-detect-nat"])
1584
+ : process.env.IICP_AUTO_DETECT_NAT !== undefined
1585
+ ? envBool("IICP_AUTO_DETECT_NAT")
1586
+ : true,
1383
1587
  // Default to api.ipify.org so FRITZ!Box/CGNAT detection works out of the box.
1384
1588
  externalIpProbeUrl: values["external-ip-probe-url"]
1385
1589
  ?? envOr("IICP_EXTERNAL_IP_PROBE_URL")
1386
1590
  ?? "https://api.ipify.org",
1387
1591
  relayWorkerEndpoint: values["relay-worker-endpoint"] ?? envOr("IICP_RELAY_WORKER_ENDPOINT") ?? "",
1388
1592
  logDir: values["log-dir"] ?? envOr("IICP_LOG_DIR"),
1593
+ withProxy: Boolean(values["with-proxy"]) || envBool("IICP_WITH_PROXY"),
1389
1594
  };
1390
1595
  return runServe(opts);
1391
1596
  }
@@ -1394,8 +1599,9 @@ if (require.main === module) {
1394
1599
  main()
1395
1600
  .then((code) => process.exit(code))
1396
1601
  .catch((err) => {
1397
- // eslint-disable-next-line no-console
1398
- console.error(err);
1602
+ // Clean one-line error — never dump a raw Node stack trace at the user.
1603
+ const msg = err instanceof Error ? err.message : String(err);
1604
+ process.stderr.write(`ERROR: ${msg}\n`);
1399
1605
  process.exit(1);
1400
1606
  });
1401
1607
  }