@iicp/client 0.7.76 → 0.7.78
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 +14 -13
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +243 -9
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +2 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +44 -1
- package/dist/node.js.map +1 -1
- package/dist/recovery.d.ts +24 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +71 -0
- package/dist/recovery.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ What good looks like:
|
|
|
39
39
|
```bash
|
|
40
40
|
iicp-node --help # shows query, serve, proxy, mcp-gateway, credits, ...
|
|
41
41
|
which iicp-node # points to your Node/npm environment
|
|
42
|
-
iicp-node --version # prints iicp-node 0.7.
|
|
42
|
+
iicp-node --version # prints iicp-node 0.7.78 or newer
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
The query command contacts the public directory, discovers a matching live node,
|
|
@@ -88,17 +88,18 @@ base URL. Full guide: <https://iicp.network/docs/proxy>
|
|
|
88
88
|
|
|
89
89
|
## Provider upgrade note
|
|
90
90
|
|
|
91
|
-
> **Upgrade note (0.7.
|
|
92
|
-
>
|
|
93
|
-
>
|
|
94
|
-
>
|
|
95
|
-
>
|
|
96
|
-
>
|
|
97
|
-
>
|
|
98
|
-
>
|
|
99
|
-
>
|
|
100
|
-
>
|
|
101
|
-
> `
|
|
91
|
+
> **Upgrade note (0.7.78)** — upgrade relay-capable and provider nodes so relay
|
|
92
|
+
> services do not disappear during temporary public-tunnel recovery. Relay-capable
|
|
93
|
+
> nodes now keep their role as relay infrastructure: if their own Quick Tunnel is
|
|
94
|
+
> cooling down or unavailable, they do **not** fall back through another relay or
|
|
95
|
+
> accidentally self-elect as a relay worker. Supervised services fail visibly so
|
|
96
|
+
> launchd/systemd/Docker can retry the public route, while ordinary provider nodes
|
|
97
|
+
> can still use relay fallback as the last-resort path.
|
|
98
|
+
>
|
|
99
|
+
> This keeps the 0.7.77 tunnel hardening intact: Quick Tunnel endpoints still
|
|
100
|
+
> recover safely after sleep, idle, Cloudflare edge drops, local DNS propagation
|
|
101
|
+
> lag, and `429` / `1015` cooldowns. Persistent relays should use a named tunnel
|
|
102
|
+
> or `IICP_PUBLIC_ENDPOINT`.
|
|
102
103
|
|
|
103
104
|
### Keeping provider nodes current
|
|
104
105
|
|
|
@@ -112,7 +113,7 @@ If a node is older than 0.7.67, perform one manual upgrade/restart first,
|
|
|
112
113
|
especially for Dockerized Python or TypeScript providers: early updater wiring
|
|
113
114
|
did not reliably cover every normal `serve` path. For Docker, use a Compose
|
|
114
115
|
`restart: unless-stopped` policy (or `docker run --restart unless-stopped`) so
|
|
115
|
-
0.7.
|
|
116
|
+
0.7.78 can intentionally exit from a confirmed tunnel-dead state and let Docker
|
|
116
117
|
bring it back cleanly.
|
|
117
118
|
|
|
118
119
|
> **Upgrade note (0.5.3)** — if you operate a node and use the native IICP
|
package/dist/cli.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export type TunnelDeadPolicy = "auto" | "retry" | "exit" | "log-only";
|
|
|
49
49
|
export type TunnelDeadBehavior = "exit" | "retry" | "log-only";
|
|
50
50
|
export declare function tunnelDeadPolicyFromEnv(): TunnelDeadPolicy;
|
|
51
51
|
export declare function tunnelDeadBehavior(policy: TunnelDeadPolicy, supervised: boolean): TunnelDeadBehavior;
|
|
52
|
+
export declare function relayWorkerFallbackAllowed(relayCapable: boolean | undefined): boolean;
|
|
52
53
|
/**
|
|
53
54
|
* Pure reachability escalation planner (tunnel-FIRST, relay = last resort; maintainer
|
|
54
55
|
* 2026-06-13). Returns the ordered attempt sequence for a tier-≥3 node with no explicitly
|
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":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ exports.TUNNEL_DEAD_EXIT_CODE = void 0;
|
|
|
5
5
|
exports.startProviderAutoUpdate = startProviderAutoUpdate;
|
|
6
6
|
exports.tunnelDeadPolicyFromEnv = tunnelDeadPolicyFromEnv;
|
|
7
7
|
exports.tunnelDeadBehavior = tunnelDeadBehavior;
|
|
8
|
+
exports.relayWorkerFallbackAllowed = relayWorkerFallbackAllowed;
|
|
8
9
|
exports.planReachability = planReachability;
|
|
9
10
|
exports.endpointIsLocalOrPrivate = endpointIsLocalOrPrivate;
|
|
10
11
|
exports.directTunnelFallbackReason = directTunnelFallbackReason;
|
|
@@ -46,6 +47,7 @@ const cip_policy_js_1 = require("./cip_policy.js");
|
|
|
46
47
|
const index_js_1 = require("./proxy/index.js");
|
|
47
48
|
const instance_lock_js_1 = require("./instance_lock.js");
|
|
48
49
|
const index_js_2 = require("./backends/index.js");
|
|
50
|
+
const recovery_js_1 = require("./recovery.js");
|
|
49
51
|
const identity_js_1 = require("./identity.js");
|
|
50
52
|
const delegation_js_1 = require("./delegation.js");
|
|
51
53
|
/**
|
|
@@ -132,17 +134,34 @@ function tunnelDeadBehavior(policy, supervised) {
|
|
|
132
134
|
return supervised ? "exit" : "retry";
|
|
133
135
|
return policy;
|
|
134
136
|
}
|
|
137
|
+
function relayWorkerFallbackAllowed(relayCapable) {
|
|
138
|
+
// A relay-capable node is relay infrastructure. If its own public route is
|
|
139
|
+
// unavailable, supervised services should restart and retry the public route
|
|
140
|
+
// instead of turning into a relay worker and removing relay capacity.
|
|
141
|
+
return relayCapable !== true;
|
|
142
|
+
}
|
|
143
|
+
async function exitIfSupervisedPublicFallbackUnrecovered() {
|
|
144
|
+
if (tunnelDeadBehavior(tunnelDeadPolicyFromEnv(), envBool("IICP_SUPERVISED")) !== "exit")
|
|
145
|
+
return;
|
|
146
|
+
// eslint-disable-next-line no-console
|
|
147
|
+
console.error(`[iicp-node] public fallback is still unavailable after tunnel/relay attempts. ` +
|
|
148
|
+
`Exiting with code ${exports.TUNNEL_DEAD_EXIT_CODE} so the supervisor can retry instead of ` +
|
|
149
|
+
`advertising an unverified direct route.`);
|
|
150
|
+
process.exit(exports.TUNNEL_DEAD_EXIT_CODE);
|
|
151
|
+
}
|
|
135
152
|
function printHelp() {
|
|
136
153
|
process.stdout.write(`usage: iicp-node <command> [options]\n\n` +
|
|
137
154
|
`Commands:\n` +
|
|
138
155
|
` init Interactive wizard — set up operator + first node config\n` +
|
|
139
156
|
` list List node configs saved under ~/.iicp/nodes/\n` +
|
|
140
157
|
` serve Register and serve a node\n` +
|
|
158
|
+
` doctor Check local health, directory presence, and recovery action\n` +
|
|
141
159
|
` query <prompt> Discover mesh nodes and submit a chat task\n` +
|
|
142
160
|
` credits Show your operator wallet plus this node's credit ledger\n` +
|
|
143
161
|
` proxy Run the local OpenAI/Ollama/Anthropic-compat gateway (loopback; no registration)\n` +
|
|
144
162
|
` mcp-gateway Bridge a local MCP server as an IICP provider node (registers + serves)\n` +
|
|
145
163
|
` service Generate/install OS supervisor units for unattended node serving\n` +
|
|
164
|
+
` update Check whether a newer @iicp/client release is available\n` +
|
|
146
165
|
` operator rename <name> Change your public display_name (signed by your operator key)\n` +
|
|
147
166
|
` operator encrypt Password-encrypt the operator secret at rest ($IICP_OPERATOR_PASSPHRASE)\n` +
|
|
148
167
|
` operator decrypt Remove at-rest encryption of the operator secret\n\n` +
|
|
@@ -187,6 +206,58 @@ function printHelp() {
|
|
|
187
206
|
` --max-tokens N Limit response length\n` +
|
|
188
207
|
` --timeout-ms N Request timeout (default 60000)\n`);
|
|
189
208
|
}
|
|
209
|
+
function argsHaveHelp(argv) {
|
|
210
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
211
|
+
}
|
|
212
|
+
function printListHelp() {
|
|
213
|
+
process.stdout.write(`usage: iicp-node list\n\n` +
|
|
214
|
+
`List saved node configs under ~/.iicp/nodes/ without contacting the directory.\n\n` +
|
|
215
|
+
`Options:\n` +
|
|
216
|
+
` -h, --help Show this help and exit without listing nodes\n`);
|
|
217
|
+
}
|
|
218
|
+
function printUpdateHelp() {
|
|
219
|
+
process.stdout.write(`usage: iicp-node update\n\n` +
|
|
220
|
+
`Check whether a newer @iicp/client release is available. Read-only: this command never installs or restarts anything.\n\n` +
|
|
221
|
+
`Exit codes:\n` +
|
|
222
|
+
` 0 current, unreachable registry, or help\n` +
|
|
223
|
+
` 10 newer release available\n\n` +
|
|
224
|
+
`Options:\n` +
|
|
225
|
+
` -h, --help Show this help and exit without contacting npm\n`);
|
|
226
|
+
}
|
|
227
|
+
function printServeHelp() {
|
|
228
|
+
process.stdout.write(`usage: iicp-node serve [options]\n\n` +
|
|
229
|
+
`Register and serve an IICP provider node backed by an OpenAI-compatible backend.\n` +
|
|
230
|
+
`Use \`iicp-node init\` first for the lowest-friction saved-node path, then run:\n` +
|
|
231
|
+
` iicp-node serve --node <NAME>\n\n` +
|
|
232
|
+
`Required (flag/env/saved node):\n` +
|
|
233
|
+
` --model NAME IICP_BACKEND_MODEL — model name (e.g. qwen2.5:0.5b)\n` +
|
|
234
|
+
` --node NAME load ~/.iicp/nodes/<NAME>.json from \`iicp-node init\`\n\n` +
|
|
235
|
+
`Core options:\n` +
|
|
236
|
+
` --backend-url URL IICP_BACKEND_URL — Ollama / vLLM / LM Studio (default http://localhost:11434; anthropic → https://api.anthropic.com)\n` +
|
|
237
|
+
` --backend-type TYPE IICP_BACKEND_TYPE — openai_compat | vllm | llamacpp | anthropic\n` +
|
|
238
|
+
` --backend-api-key KEY IICP_BACKEND_API_KEY — Bearer key for auth'd backends\n` +
|
|
239
|
+
` --public-endpoint URL IICP_PUBLIC_ENDPOINT — externally reachable URL of this node\n` +
|
|
240
|
+
` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
|
|
241
|
+
` --region REGION IICP_REGION (e.g. eu-central)\n` +
|
|
242
|
+
` --intent URN IICP_INTENT (default urn:iicp:intent:llm:chat:v1)\n` +
|
|
243
|
+
` --max-concurrent N IICP_MAX_CONCURRENT (default 4)\n` +
|
|
244
|
+
` --node-id ID IICP_NODE_ID (auto-generated if absent)\n` +
|
|
245
|
+
` --port N IICP_PORT (default 9484)\n` +
|
|
246
|
+
` --host HOST IICP_HOST (default :: — dual-stack IPv4+IPv6)\n` +
|
|
247
|
+
` --skip-registration IICP_SKIP_REGISTRATION — register-free dev mode\n` +
|
|
248
|
+
` --force IICP_FORCE — take over the single-instance lock for this node_id\n\n` +
|
|
249
|
+
`Reachability and resilience:\n` +
|
|
250
|
+
` --auto-detect-nat IICP_AUTO_DETECT_NAT — run NAT detection at startup (default on)\n` +
|
|
251
|
+
` --no-auto-detect-nat disable NAT detection at startup\n` +
|
|
252
|
+
` --external-ip-probe-url U IICP_EXTERNAL_IP_PROBE_URL — fallback IPv4 probe\n` +
|
|
253
|
+
` --tunnel / --no-tunnel IICP_TUNNEL — auto Cloudflare Quick Tunnel fallback when direct reachability fails\n` +
|
|
254
|
+
` --relay-worker-endpoint H IICP_RELAY_WORKER_ENDPOINT — <host>:<port> of a relay node\n` +
|
|
255
|
+
` --relay-capable IICP_RELAY_CAPABLE — advertise as relay server\n` +
|
|
256
|
+
` --relay-accept-port N IICP_RELAY_ACCEPT_PORT — relay accept TCP port (default 9485)\n` +
|
|
257
|
+
` --log-dir DIR IICP_LOG_DIR — directory for persistent log files\n` +
|
|
258
|
+
` --with-proxy IICP_WITH_PROXY — also run the loopback compat proxy (127.0.0.1:9483)\n` +
|
|
259
|
+
` -h, --help Show this focused serve help\n`);
|
|
260
|
+
}
|
|
190
261
|
/**
|
|
191
262
|
* Thrown by safeParseArgs / port parsing to signal a clean, user-facing CLI error.
|
|
192
263
|
* main() catches it and prints a one-line `ERROR:` (exit 2) — never a raw stack trace.
|
|
@@ -427,7 +498,18 @@ async function runInit() {
|
|
|
427
498
|
rl.close();
|
|
428
499
|
}
|
|
429
500
|
}
|
|
430
|
-
function runList() {
|
|
501
|
+
function runList(argv = []) {
|
|
502
|
+
const { values } = safeParseArgs({
|
|
503
|
+
args: argv,
|
|
504
|
+
options: {
|
|
505
|
+
help: { type: "boolean", short: "h" },
|
|
506
|
+
},
|
|
507
|
+
allowPositionals: false,
|
|
508
|
+
});
|
|
509
|
+
if (values.help) {
|
|
510
|
+
printListHelp();
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
431
513
|
const nodes = (0, identity_js_1.listNodes)();
|
|
432
514
|
if (nodes.length === 0) {
|
|
433
515
|
process.stdout.write(`No saved node configs. Run \`iicp-node init\` first.\n`);
|
|
@@ -491,7 +573,7 @@ async function _autoElectRelay(directoryUrl, intent, nodeId) {
|
|
|
491
573
|
if (!resp.ok)
|
|
492
574
|
return null;
|
|
493
575
|
const data = await resp.json();
|
|
494
|
-
const candidates = (data.nodes ?? []).filter((n) => n.relay_capable && n.endpoint);
|
|
576
|
+
const candidates = (data.nodes ?? []).filter((n) => n.relay_capable && n.endpoint && n.node_id !== nodeId);
|
|
495
577
|
if (!candidates.length)
|
|
496
578
|
return null;
|
|
497
579
|
const { createHash } = await import("node:crypto");
|
|
@@ -757,6 +839,12 @@ async function runServe(opts) {
|
|
|
757
839
|
}
|
|
758
840
|
}
|
|
759
841
|
else if (step === "relay") {
|
|
842
|
+
if (!relayWorkerFallbackAllowed(opts.relayCapable)) {
|
|
843
|
+
// eslint-disable-next-line no-console
|
|
844
|
+
console.warn(`[iicp-node] NAT tier=${natProfile.tier}: no tunnel for relay-capable node — ` +
|
|
845
|
+
"not falling back through another relay; supervisor will retry the public route.");
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
760
848
|
// eslint-disable-next-line no-console
|
|
761
849
|
console.log(`[iicp-node] NAT tier=${natProfile.tier}: no tunnel — auto-electing a relay from directory (last resort)…`);
|
|
762
850
|
const elected = await _autoElectRelay(opts.directoryUrl ?? "https://iicp.network/api", opts.intent, nodeId);
|
|
@@ -814,7 +902,7 @@ async function runServe(opts) {
|
|
|
814
902
|
if (tunnelHandle) {
|
|
815
903
|
publicEndpoint = tunnelHandle.url;
|
|
816
904
|
}
|
|
817
|
-
else {
|
|
905
|
+
else if (relayWorkerFallbackAllowed(opts.relayCapable)) {
|
|
818
906
|
// eslint-disable-next-line no-console
|
|
819
907
|
console.log("[iicp-node] no tunnel available — auto-electing a relay from directory (last resort)…");
|
|
820
908
|
const elected = await _autoElectRelay(opts.directoryUrl ?? "https://iicp.network/api", opts.intent, nodeId);
|
|
@@ -825,6 +913,12 @@ async function runServe(opts) {
|
|
|
825
913
|
console.log(`[iicp-node] auto-elected relay (last resort): ${relayHost}:${relayPort}`);
|
|
826
914
|
}
|
|
827
915
|
}
|
|
916
|
+
else {
|
|
917
|
+
// eslint-disable-next-line no-console
|
|
918
|
+
console.warn("[iicp-node] no tunnel available for relay-capable node — not falling back through " +
|
|
919
|
+
"another relay; supervisor will retry the public route.");
|
|
920
|
+
await exitIfSupervisedPublicFallbackUnrecovered();
|
|
921
|
+
}
|
|
828
922
|
}
|
|
829
923
|
}
|
|
830
924
|
const backendFlavor = await detectBackendFlavor(opts.backendUrl, opts.backendApiKey, opts.backendType);
|
|
@@ -1646,6 +1740,103 @@ async function fetchAndDisplayCredits(directoryUrl, nodeId, token, label, asJson
|
|
|
1646
1740
|
}
|
|
1647
1741
|
return 0;
|
|
1648
1742
|
}
|
|
1743
|
+
function printDoctorHelp() {
|
|
1744
|
+
process.stdout.write(`usage: iicp-node doctor [options]\n\n` +
|
|
1745
|
+
`Check local health, directory presence, and deterministic recovery action.\n\n` +
|
|
1746
|
+
`options:\n` +
|
|
1747
|
+
` --node NAME Load saved node config (~/.iicp/nodes/<NAME>.json)\n` +
|
|
1748
|
+
` --directory-url URL Override the saved IICP directory base URL\n` +
|
|
1749
|
+
` --json Print machine-readable recovery state\n` +
|
|
1750
|
+
` -h, --help Show this help and exit\n`);
|
|
1751
|
+
}
|
|
1752
|
+
function doctorLoopbackHost(host) {
|
|
1753
|
+
const h = (host || "").trim();
|
|
1754
|
+
if (h === "" || h === "::" || h === "0.0.0.0")
|
|
1755
|
+
return "127.0.0.1";
|
|
1756
|
+
return h;
|
|
1757
|
+
}
|
|
1758
|
+
function doctorUrl(host, port) {
|
|
1759
|
+
const h = doctorLoopbackHost(host);
|
|
1760
|
+
return h.includes(":") && !h.startsWith("[")
|
|
1761
|
+
? `http://[${h}]:${port}/iicp/health`
|
|
1762
|
+
: `http://${h}:${port}/iicp/health`;
|
|
1763
|
+
}
|
|
1764
|
+
async function runDoctor(argv) {
|
|
1765
|
+
const { values } = safeParseArgs({
|
|
1766
|
+
args: argv,
|
|
1767
|
+
options: {
|
|
1768
|
+
node: { type: "string" },
|
|
1769
|
+
"directory-url": { type: "string" },
|
|
1770
|
+
json: { type: "boolean" },
|
|
1771
|
+
help: { type: "boolean", short: "h" },
|
|
1772
|
+
},
|
|
1773
|
+
allowPositionals: false,
|
|
1774
|
+
});
|
|
1775
|
+
if (values.help) {
|
|
1776
|
+
printDoctorHelp();
|
|
1777
|
+
return 0;
|
|
1778
|
+
}
|
|
1779
|
+
const nodeName = values.node ?? process.env.IICP_NODE_NAME ?? "default";
|
|
1780
|
+
const saved = (0, identity_js_1.loadNode)(nodeName);
|
|
1781
|
+
if (!saved) {
|
|
1782
|
+
process.stderr.write(`ERROR: no saved config at ~/.iicp/nodes/${nodeName}.json — run \`iicp-node init\` first.\n`);
|
|
1783
|
+
return 1;
|
|
1784
|
+
}
|
|
1785
|
+
const directoryUrl = values["directory-url"] ?? saved.directory_url ?? process.env.IICP_DIRECTORY_URL ?? "https://iicp.network/api";
|
|
1786
|
+
const localHealthUrl = doctorUrl(saved.host ?? "0.0.0.0", saved.port ?? 8020);
|
|
1787
|
+
let localHealthOk = false;
|
|
1788
|
+
let health = null;
|
|
1789
|
+
let healthError = null;
|
|
1790
|
+
try {
|
|
1791
|
+
const resp = await fetch(localHealthUrl, { signal: AbortSignal.timeout(2_000) });
|
|
1792
|
+
if (!resp.ok)
|
|
1793
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
1794
|
+
health = (await resp.json());
|
|
1795
|
+
localHealthOk = true;
|
|
1796
|
+
}
|
|
1797
|
+
catch (err) {
|
|
1798
|
+
healthError = err instanceof Error ? err.message : String(err);
|
|
1799
|
+
}
|
|
1800
|
+
const presence = await (0, recovery_js_1.registryNodePresence)(directoryUrl, saved.node_id, 5_000);
|
|
1801
|
+
const stability = (health?.["backend_stability"] ?? {});
|
|
1802
|
+
const backendAttention = stability["backend_state"] === "draining";
|
|
1803
|
+
const failures = !localHealthOk || presence === "absent" ? 1 : 0;
|
|
1804
|
+
const { state, action } = (0, recovery_js_1.classifyRecovery)({
|
|
1805
|
+
localHealthOk,
|
|
1806
|
+
publicAvailable: localHealthOk,
|
|
1807
|
+
directoryPresence: presence,
|
|
1808
|
+
consecutiveFailures: failures,
|
|
1809
|
+
graceChecks: (0, recovery_js_1.envGraceChecks)(),
|
|
1810
|
+
backendAttention,
|
|
1811
|
+
});
|
|
1812
|
+
const prefix = (0, recovery_js_1.nodeRegistryPrefix)(saved.node_id);
|
|
1813
|
+
if (values.json) {
|
|
1814
|
+
process.stdout.write(JSON.stringify({
|
|
1815
|
+
node: saved.name,
|
|
1816
|
+
node_id: saved.node_id,
|
|
1817
|
+
prefix,
|
|
1818
|
+
directory_url: directoryUrl,
|
|
1819
|
+
local_health_url: localHealthUrl,
|
|
1820
|
+
local_health_ok: localHealthOk,
|
|
1821
|
+
local_health_error: healthError,
|
|
1822
|
+
directory_presence: presence,
|
|
1823
|
+
recovery_state: state,
|
|
1824
|
+
recommended_action: action,
|
|
1825
|
+
health,
|
|
1826
|
+
}, null, 2) + "\n");
|
|
1827
|
+
return 0;
|
|
1828
|
+
}
|
|
1829
|
+
process.stdout.write(`IICP node doctor — ${saved.name}\n`);
|
|
1830
|
+
process.stdout.write(` Local health ${localHealthOk ? "ok" : "failed"} (${localHealthUrl})\n`);
|
|
1831
|
+
if (healthError)
|
|
1832
|
+
process.stdout.write(` Local health detail ${healthError}\n`);
|
|
1833
|
+
process.stdout.write(` Directory prefix ${prefix}\n`);
|
|
1834
|
+
process.stdout.write(` Directory presence ${presence}\n`);
|
|
1835
|
+
process.stdout.write(` Recovery state ${state}\n`);
|
|
1836
|
+
process.stdout.write(` Recommended action ${action}\n`);
|
|
1837
|
+
process.stdout.write(" Note restart is automatic only when supervised services set IICP_SUPERVISED=1\n");
|
|
1838
|
+
return 0;
|
|
1839
|
+
}
|
|
1649
1840
|
/**
|
|
1650
1841
|
* Resolve a passphrase: $IICP_OPERATOR_PASSPHRASE if set (headless/CI), else an interactive
|
|
1651
1842
|
* readline prompt (this command is operator-run, so a prompt is fine here — only `serve` must
|
|
@@ -1738,16 +1929,46 @@ function printOperatorHelp() {
|
|
|
1738
1929
|
` --directory-url URL IICP directory base URL (defaults to env / iicp.network)\n` +
|
|
1739
1930
|
` -h, --help Show this help and exit\n`);
|
|
1740
1931
|
}
|
|
1932
|
+
function printOperatorEncryptHelp() {
|
|
1933
|
+
process.stdout.write(`usage: iicp-node operator encrypt\n\n` +
|
|
1934
|
+
`Password-encrypt the operator secret at rest. The command prompts for a passphrase\n` +
|
|
1935
|
+
`unless $IICP_OPERATOR_PASSPHRASE is set for headless use.\n\n` +
|
|
1936
|
+
`Options:\n` +
|
|
1937
|
+
` -h, --help Show this help and exit without prompting\n`);
|
|
1938
|
+
}
|
|
1939
|
+
function printOperatorDecryptHelp() {
|
|
1940
|
+
process.stdout.write(`usage: iicp-node operator decrypt\n\n` +
|
|
1941
|
+
`Remove at-rest encryption and store the operator secret in plaintext again. The\n` +
|
|
1942
|
+
`command prompts for the passphrase unless $IICP_OPERATOR_PASSPHRASE is set.\n\n` +
|
|
1943
|
+
`Options:\n` +
|
|
1944
|
+
` -h, --help Show this help and exit without prompting\n`);
|
|
1945
|
+
}
|
|
1741
1946
|
async function runOperator(argv) {
|
|
1742
1947
|
const sub = argv[0];
|
|
1743
1948
|
if (sub === undefined || sub === "--help" || sub === "-h") {
|
|
1744
1949
|
printOperatorHelp();
|
|
1745
1950
|
return sub === undefined ? 2 : 0;
|
|
1746
1951
|
}
|
|
1747
|
-
if (sub === "encrypt")
|
|
1952
|
+
if (sub === "encrypt") {
|
|
1953
|
+
const rest = argv.slice(1);
|
|
1954
|
+
if (argsHaveHelp(rest)) {
|
|
1955
|
+
printOperatorEncryptHelp();
|
|
1956
|
+
return 0;
|
|
1957
|
+
}
|
|
1958
|
+
if (rest.length > 0)
|
|
1959
|
+
throw new CliError(`unknown operator encrypt option '${rest[0]}'`);
|
|
1748
1960
|
return runOperatorEncrypt();
|
|
1749
|
-
|
|
1961
|
+
}
|
|
1962
|
+
if (sub === "decrypt") {
|
|
1963
|
+
const rest = argv.slice(1);
|
|
1964
|
+
if (argsHaveHelp(rest)) {
|
|
1965
|
+
printOperatorDecryptHelp();
|
|
1966
|
+
return 0;
|
|
1967
|
+
}
|
|
1968
|
+
if (rest.length > 0)
|
|
1969
|
+
throw new CliError(`unknown operator decrypt option '${rest[0]}'`);
|
|
1750
1970
|
return runOperatorDecrypt();
|
|
1971
|
+
}
|
|
1751
1972
|
if (sub !== "rename") {
|
|
1752
1973
|
process.stderr.write(`unknown operator subcommand: ${sub}\n`);
|
|
1753
1974
|
printOperatorHelp();
|
|
@@ -2073,7 +2294,18 @@ async function runMcpGateway(argv) {
|
|
|
2073
2294
|
/** Command dispatch — separated so main() can wrap parse failures as clean CliError output. */
|
|
2074
2295
|
// #521 P1 — read-only version check. Exit 10 when a newer release exists
|
|
2075
2296
|
// (so cron/scripts can act), 0 when current/unreachable. Never installs.
|
|
2076
|
-
async function runUpdate() {
|
|
2297
|
+
async function runUpdate(argv = []) {
|
|
2298
|
+
const { values } = safeParseArgs({
|
|
2299
|
+
args: argv,
|
|
2300
|
+
options: {
|
|
2301
|
+
help: { type: "boolean", short: "h" },
|
|
2302
|
+
},
|
|
2303
|
+
allowPositionals: false,
|
|
2304
|
+
});
|
|
2305
|
+
if (values.help) {
|
|
2306
|
+
printUpdateHelp();
|
|
2307
|
+
return 0;
|
|
2308
|
+
}
|
|
2077
2309
|
const { checkUpdate, latestNpmVersion } = await import("./updater.js");
|
|
2078
2310
|
const latest = await latestNpmVersion();
|
|
2079
2311
|
const v = checkUpdate(SDK_VERSION, latest);
|
|
@@ -2150,11 +2382,13 @@ async function dispatch(argv) {
|
|
|
2150
2382
|
if (cmd === "init")
|
|
2151
2383
|
return runInit();
|
|
2152
2384
|
if (cmd === "list")
|
|
2153
|
-
return runList();
|
|
2385
|
+
return runList(argv.slice(1));
|
|
2154
2386
|
if (cmd === "query")
|
|
2155
2387
|
return runQuery(argv.slice(1));
|
|
2156
2388
|
if (cmd === "credits")
|
|
2157
2389
|
return runCredits(argv.slice(1));
|
|
2390
|
+
if (cmd === "doctor")
|
|
2391
|
+
return runDoctor(argv.slice(1));
|
|
2158
2392
|
if (cmd === "operator")
|
|
2159
2393
|
return runOperator(argv.slice(1));
|
|
2160
2394
|
if (cmd === "proxy")
|
|
@@ -2162,7 +2396,7 @@ async function dispatch(argv) {
|
|
|
2162
2396
|
if (cmd === "mcp-gateway")
|
|
2163
2397
|
return runMcpGateway(argv.slice(1));
|
|
2164
2398
|
if (cmd === "update")
|
|
2165
|
-
return runUpdate();
|
|
2399
|
+
return runUpdate(argv.slice(1));
|
|
2166
2400
|
if (cmd === "service")
|
|
2167
2401
|
return runService(argv.slice(1));
|
|
2168
2402
|
if (cmd !== "serve") {
|
|
@@ -2205,7 +2439,7 @@ async function dispatch(argv) {
|
|
|
2205
2439
|
allowPositionals: false,
|
|
2206
2440
|
});
|
|
2207
2441
|
if (values.help) {
|
|
2208
|
-
|
|
2442
|
+
printServeHelp();
|
|
2209
2443
|
return 0;
|
|
2210
2444
|
}
|
|
2211
2445
|
const opts = {
|