@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 +19 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +230 -24
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6 -0
- package/dist/client.js.map +1 -1
- package/dist/proxy/cip.d.ts +64 -0
- package/dist/proxy/cip.d.ts.map +1 -0
- package/dist/proxy/cip.js +156 -0
- package/dist/proxy/cip.js.map +1 -0
- package/dist/proxy/index.d.ts +47 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +266 -0
- package/dist/proxy/index.js.map +1 -0
- package/package.json +1 -1
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 #
|
|
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
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
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
|
|
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
|
-
` --
|
|
86
|
-
` --
|
|
87
|
+
` --force IICP_FORCE — take over the single-instance lock for this node_id\n` +
|
|
88
|
+
` --auto-detect-nat IICP_AUTO_DETECT_NAT — run 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 (!
|
|
448
|
-
process.stderr.write(`ERROR: --backend-type must be one of ${JSON.stringify(
|
|
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,
|
|
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 } = (
|
|
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
|
-
|
|
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 } = (
|
|
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
|
-
|
|
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
|
|
1371
|
+
process.stderr.write(`unknown operator subcommand: ${sub}\n`);
|
|
1372
|
+
printOperatorHelp();
|
|
1242
1373
|
return 2;
|
|
1243
1374
|
}
|
|
1244
|
-
const { values, positionals } = (
|
|
1375
|
+
const { values, positionals } = safeParseArgs({
|
|
1245
1376
|
args: argv.slice(1),
|
|
1246
|
-
options: {
|
|
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 } = (
|
|
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
|
-
?
|
|
1375
|
-
:
|
|
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
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
//
|
|
1398
|
-
|
|
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
|
}
|