@iicp/client 0.7.75 → 0.7.77
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 +69 -4
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +245 -13
- 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 +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
[](https://iicp.network/spec)
|
|
6
6
|
[](https://www.npmjs.com/package/@iicp/client)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Use the open AI mesh from your TypeScript or JavaScript app. Install the
|
|
9
|
+
client, send an intent, and get a routed response from an IICP node.
|
|
10
|
+
|
|
11
|
+
You do **not** need to run a node to try the client path. Consume first,
|
|
12
|
+
provide later.
|
|
9
13
|
|
|
10
14
|
Works in **Node.js ≥ 18**, Deno, Bun, and modern browsers with the native Fetch API.
|
|
11
15
|
|
|
@@ -23,7 +27,68 @@ npm install @iicp/client@latest
|
|
|
23
27
|
# pnpm add @iicp/client@latest
|
|
24
28
|
```
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
## One-line test
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g @iicp/client@latest
|
|
34
|
+
iicp-node query "Hello, mesh."
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
What good looks like:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
iicp-node --help # shows query, serve, proxy, mcp-gateway, credits, ...
|
|
41
|
+
which iicp-node # points to your Node/npm environment
|
|
42
|
+
iicp-node --version # prints iicp-node 0.7.77 or newer
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The query command contacts the public directory, discovers a matching live node,
|
|
46
|
+
routes your prompt, and prints the response. No account, API key, or local node
|
|
47
|
+
is required for this consumer path.
|
|
48
|
+
|
|
49
|
+
## Use from TypeScript
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { IicpClient } from "@iicp/client";
|
|
53
|
+
|
|
54
|
+
const reply = await new IicpClient().chat([
|
|
55
|
+
{ role: "user", content: "Hello, mesh." },
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
console.log(reply.choices[0].message.content);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Do I need to run a node?
|
|
62
|
+
|
|
63
|
+
No. Running a node is only needed when you want to provide compute or tools to
|
|
64
|
+
the mesh. Start as a client; run a node later when you want to contribute.
|
|
65
|
+
|
|
66
|
+
## Migrate from existing AI tools
|
|
67
|
+
|
|
68
|
+
Direct call:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// Before: call one vendor endpoint directly.
|
|
72
|
+
// After: ask IICP to discover and route by capability.
|
|
73
|
+
const reply = await new IicpClient().chat([
|
|
74
|
+
{ role: "user", content: "Summarize this document." },
|
|
75
|
+
]);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Existing OpenAI-compatible tools:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install -g @iicp/client@latest
|
|
82
|
+
iicp-node proxy
|
|
83
|
+
export OPENAI_BASE_URL=http://127.0.0.1:9483/v1
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then point LangChain, Cursor, liteLLM or another OpenAI-compatible tool at that
|
|
87
|
+
base URL. Full guide: <https://iicp.network/docs/proxy>
|
|
88
|
+
|
|
89
|
+
## Provider upgrade note
|
|
90
|
+
|
|
91
|
+
> **Upgrade note (0.7.77)** — upgrade provider nodes so Quick Tunnel endpoints
|
|
27
92
|
> recover safely after sleep, idle, Cloudflare edge drops, and local DNS
|
|
28
93
|
> propagation lag on freshly-created `trycloudflare.com` URLs. Tunnel
|
|
29
94
|
> twilight/recovery still heartbeats as unavailable and only re-registers once
|
|
@@ -47,7 +112,7 @@ If a node is older than 0.7.67, perform one manual upgrade/restart first,
|
|
|
47
112
|
especially for Dockerized Python or TypeScript providers: early updater wiring
|
|
48
113
|
did not reliably cover every normal `serve` path. For Docker, use a Compose
|
|
49
114
|
`restart: unless-stopped` policy (or `docker run --restart unless-stopped`) so
|
|
50
|
-
0.7.
|
|
115
|
+
0.7.77 can intentionally exit from a confirmed tunnel-dead state and let Docker
|
|
51
116
|
bring it back cleanly.
|
|
52
117
|
|
|
53
118
|
> **Upgrade note (0.5.3)** — if you operate a node and use the native IICP
|
|
@@ -71,7 +136,7 @@ Consumer and provider can run in the same process. For production provider nodes
|
|
|
71
136
|
|
|
72
137
|
---
|
|
73
138
|
|
|
74
|
-
##
|
|
139
|
+
## Library quickstart
|
|
75
140
|
|
|
76
141
|
```typescript
|
|
77
142
|
import { IicpClient } from "@iicp/client";
|
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":";AAiDA,OAAO,EAeL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAGvB,KAAK,aAAa,GAAG,cAAc,cAAc,CAAC,CAAC;AAEnD,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC3C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,GAAE,yBAA8B,GAAG,MAAM,IAAI,CA+C3F;AAED,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,CAAC,EAAE,MAAM,CAAC;IAChB,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,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iGAAiG;IACjG,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAoBD,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;AAE/D,wBAAgB,uBAAuB,IAAI,gBAAgB,CAM1D;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,EAAE,UAAU,EAAE,OAAO,GAAG,kBAAkB,CAGpG;AAwZD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,OAAO,EACxB,aAAa,EAAE,OAAO,GACrB,KAAK,CAAC,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC,CAGtC;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAWlE;AAED,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,EAEhB,OAAO,CAAC,EAAE,GAAG,GACZ,MAAM,GAAG,IAAI,CAUf;AAiFD,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,SAAS,CAsB9E;AAo0BD;;;;;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;AAmoBD,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBlF"}
|
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,7 @@ const cip_policy_js_1 = require("./cip_policy.js");
|
|
|
46
46
|
const index_js_1 = require("./proxy/index.js");
|
|
47
47
|
const instance_lock_js_1 = require("./instance_lock.js");
|
|
48
48
|
const index_js_2 = require("./backends/index.js");
|
|
49
|
+
const recovery_js_1 = require("./recovery.js");
|
|
49
50
|
const identity_js_1 = require("./identity.js");
|
|
50
51
|
const delegation_js_1 = require("./delegation.js");
|
|
51
52
|
/**
|
|
@@ -138,11 +139,13 @@ function printHelp() {
|
|
|
138
139
|
` init Interactive wizard — set up operator + first node config\n` +
|
|
139
140
|
` list List node configs saved under ~/.iicp/nodes/\n` +
|
|
140
141
|
` serve Register and serve a node\n` +
|
|
142
|
+
` doctor Check local health, directory presence, and recovery action\n` +
|
|
141
143
|
` query <prompt> Discover mesh nodes and submit a chat task\n` +
|
|
142
|
-
` credits Show this node's
|
|
144
|
+
` credits Show your operator wallet plus this node's credit ledger\n` +
|
|
143
145
|
` proxy Run the local OpenAI/Ollama/Anthropic-compat gateway (loopback; no registration)\n` +
|
|
144
146
|
` mcp-gateway Bridge a local MCP server as an IICP provider node (registers + serves)\n` +
|
|
145
147
|
` service Generate/install OS supervisor units for unattended node serving\n` +
|
|
148
|
+
` update Check whether a newer @iicp/client release is available\n` +
|
|
146
149
|
` operator rename <name> Change your public display_name (signed by your operator key)\n` +
|
|
147
150
|
` operator encrypt Password-encrypt the operator secret at rest ($IICP_OPERATOR_PASSPHRASE)\n` +
|
|
148
151
|
` operator decrypt Remove at-rest encryption of the operator secret\n\n` +
|
|
@@ -187,6 +190,58 @@ function printHelp() {
|
|
|
187
190
|
` --max-tokens N Limit response length\n` +
|
|
188
191
|
` --timeout-ms N Request timeout (default 60000)\n`);
|
|
189
192
|
}
|
|
193
|
+
function argsHaveHelp(argv) {
|
|
194
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
195
|
+
}
|
|
196
|
+
function printListHelp() {
|
|
197
|
+
process.stdout.write(`usage: iicp-node list\n\n` +
|
|
198
|
+
`List saved node configs under ~/.iicp/nodes/ without contacting the directory.\n\n` +
|
|
199
|
+
`Options:\n` +
|
|
200
|
+
` -h, --help Show this help and exit without listing nodes\n`);
|
|
201
|
+
}
|
|
202
|
+
function printUpdateHelp() {
|
|
203
|
+
process.stdout.write(`usage: iicp-node update\n\n` +
|
|
204
|
+
`Check whether a newer @iicp/client release is available. Read-only: this command never installs or restarts anything.\n\n` +
|
|
205
|
+
`Exit codes:\n` +
|
|
206
|
+
` 0 current, unreachable registry, or help\n` +
|
|
207
|
+
` 10 newer release available\n\n` +
|
|
208
|
+
`Options:\n` +
|
|
209
|
+
` -h, --help Show this help and exit without contacting npm\n`);
|
|
210
|
+
}
|
|
211
|
+
function printServeHelp() {
|
|
212
|
+
process.stdout.write(`usage: iicp-node serve [options]\n\n` +
|
|
213
|
+
`Register and serve an IICP provider node backed by an OpenAI-compatible backend.\n` +
|
|
214
|
+
`Use \`iicp-node init\` first for the lowest-friction saved-node path, then run:\n` +
|
|
215
|
+
` iicp-node serve --node <NAME>\n\n` +
|
|
216
|
+
`Required (flag/env/saved node):\n` +
|
|
217
|
+
` --model NAME IICP_BACKEND_MODEL — model name (e.g. qwen2.5:0.5b)\n` +
|
|
218
|
+
` --node NAME load ~/.iicp/nodes/<NAME>.json from \`iicp-node init\`\n\n` +
|
|
219
|
+
`Core options:\n` +
|
|
220
|
+
` --backend-url URL IICP_BACKEND_URL — Ollama / vLLM / LM Studio (default http://localhost:11434; anthropic → https://api.anthropic.com)\n` +
|
|
221
|
+
` --backend-type TYPE IICP_BACKEND_TYPE — openai_compat | vllm | llamacpp | anthropic\n` +
|
|
222
|
+
` --backend-api-key KEY IICP_BACKEND_API_KEY — Bearer key for auth'd backends\n` +
|
|
223
|
+
` --public-endpoint URL IICP_PUBLIC_ENDPOINT — externally reachable URL of this node\n` +
|
|
224
|
+
` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
|
|
225
|
+
` --region REGION IICP_REGION (e.g. eu-central)\n` +
|
|
226
|
+
` --intent URN IICP_INTENT (default urn:iicp:intent:llm:chat:v1)\n` +
|
|
227
|
+
` --max-concurrent N IICP_MAX_CONCURRENT (default 4)\n` +
|
|
228
|
+
` --node-id ID IICP_NODE_ID (auto-generated if absent)\n` +
|
|
229
|
+
` --port N IICP_PORT (default 9484)\n` +
|
|
230
|
+
` --host HOST IICP_HOST (default :: — dual-stack IPv4+IPv6)\n` +
|
|
231
|
+
` --skip-registration IICP_SKIP_REGISTRATION — register-free dev mode\n` +
|
|
232
|
+
` --force IICP_FORCE — take over the single-instance lock for this node_id\n\n` +
|
|
233
|
+
`Reachability and resilience:\n` +
|
|
234
|
+
` --auto-detect-nat IICP_AUTO_DETECT_NAT — run NAT detection at startup (default on)\n` +
|
|
235
|
+
` --no-auto-detect-nat disable NAT detection at startup\n` +
|
|
236
|
+
` --external-ip-probe-url U IICP_EXTERNAL_IP_PROBE_URL — fallback IPv4 probe\n` +
|
|
237
|
+
` --tunnel / --no-tunnel IICP_TUNNEL — auto Cloudflare Quick Tunnel fallback when direct reachability fails\n` +
|
|
238
|
+
` --relay-worker-endpoint H IICP_RELAY_WORKER_ENDPOINT — <host>:<port> of a relay node\n` +
|
|
239
|
+
` --relay-capable IICP_RELAY_CAPABLE — advertise as relay server\n` +
|
|
240
|
+
` --relay-accept-port N IICP_RELAY_ACCEPT_PORT — relay accept TCP port (default 9485)\n` +
|
|
241
|
+
` --log-dir DIR IICP_LOG_DIR — directory for persistent log files\n` +
|
|
242
|
+
` --with-proxy IICP_WITH_PROXY — also run the loopback compat proxy (127.0.0.1:9483)\n` +
|
|
243
|
+
` -h, --help Show this focused serve help\n`);
|
|
244
|
+
}
|
|
190
245
|
/**
|
|
191
246
|
* Thrown by safeParseArgs / port parsing to signal a clean, user-facing CLI error.
|
|
192
247
|
* main() catches it and prints a one-line `ERROR:` (exit 2) — never a raw stack trace.
|
|
@@ -427,7 +482,18 @@ async function runInit() {
|
|
|
427
482
|
rl.close();
|
|
428
483
|
}
|
|
429
484
|
}
|
|
430
|
-
function runList() {
|
|
485
|
+
function runList(argv = []) {
|
|
486
|
+
const { values } = safeParseArgs({
|
|
487
|
+
args: argv,
|
|
488
|
+
options: {
|
|
489
|
+
help: { type: "boolean", short: "h" },
|
|
490
|
+
},
|
|
491
|
+
allowPositionals: false,
|
|
492
|
+
});
|
|
493
|
+
if (values.help) {
|
|
494
|
+
printListHelp();
|
|
495
|
+
return 0;
|
|
496
|
+
}
|
|
431
497
|
const nodes = (0, identity_js_1.listNodes)();
|
|
432
498
|
if (nodes.length === 0) {
|
|
433
499
|
process.stdout.write(`No saved node configs. Run \`iicp-node init\` first.\n`);
|
|
@@ -1406,14 +1472,14 @@ async function verifyCreditAwards(directoryUrl, nodeId) {
|
|
|
1406
1472
|
return { sum, verified, failed };
|
|
1407
1473
|
}
|
|
1408
1474
|
/**
|
|
1409
|
-
* `iicp-node credits` (#456) —
|
|
1475
|
+
* `iicp-node credits` (#456) — operator wallet + node ledger from the directory's
|
|
1410
1476
|
* reconcile-checked GET /v1/credits/summary. Figures come authenticated from the
|
|
1411
1477
|
* directory (not the local config), so editing the saved file cannot inflate them;
|
|
1412
1478
|
* `reconciles` flags a ledger that does not add up.
|
|
1413
1479
|
*/
|
|
1414
1480
|
function printCreditsHelp() {
|
|
1415
1481
|
process.stdout.write(`usage: iicp-node credits [options]\n\n` +
|
|
1416
|
-
`Show this node's
|
|
1482
|
+
`Show your operator wallet plus this node's credit ledger (authenticated from the directory).\n\n` +
|
|
1417
1483
|
`options:\n` +
|
|
1418
1484
|
` --node NAME Load token + node_id from ~/.iicp/nodes/<NAME>.json\n` +
|
|
1419
1485
|
` --node-id ID Node id (if not using --node)\n` +
|
|
@@ -1478,11 +1544,12 @@ async function runCredits(argv) {
|
|
|
1478
1544
|
// One node failing must not hide the others — show every node,
|
|
1479
1545
|
// then exit non-zero if any failed (2026-06-11).
|
|
1480
1546
|
let failed = 0;
|
|
1547
|
+
const walletState = { shown: false };
|
|
1481
1548
|
for (let i = 0; i < withToken.length; i++) {
|
|
1482
1549
|
if (i > 0)
|
|
1483
1550
|
process.stdout.write("\n");
|
|
1484
1551
|
const n = withToken[i];
|
|
1485
|
-
const rc = await fetchAndDisplayCredits(n.directory_url ?? dir, n.node_id, n.node_token, n.name, Boolean(values["json"]), Boolean(values["verify"]));
|
|
1552
|
+
const rc = await fetchAndDisplayCredits(n.directory_url ?? dir, n.node_id, n.node_token, n.name, Boolean(values["json"]), Boolean(values["verify"]), walletState);
|
|
1486
1553
|
if (rc !== 0) {
|
|
1487
1554
|
process.stderr.write(`ERROR: credits fetch failed for node '${n.name}' — continuing with remaining nodes\n`);
|
|
1488
1555
|
failed++;
|
|
@@ -1528,7 +1595,7 @@ async function runCredits(argv) {
|
|
|
1528
1595
|
return fetchAndDisplayCredits(directoryUrl, nodeId, token, label, Boolean(values["json"]), Boolean(values["verify"]));
|
|
1529
1596
|
}
|
|
1530
1597
|
/** Shared fetch+display logic for one node's credits summary. */
|
|
1531
|
-
async function fetchAndDisplayCredits(directoryUrl, nodeId, token, label, asJson, verify) {
|
|
1598
|
+
async function fetchAndDisplayCredits(directoryUrl, nodeId, token, label, asJson, verify, walletState) {
|
|
1532
1599
|
const url = `${directoryUrl.replace(/\/+$/, "")}/v1/credits/summary?node_id=${encodeURIComponent(nodeId)}`;
|
|
1533
1600
|
// Transient failures (network error, 5xx, undecodable body) get ONE retry
|
|
1534
1601
|
// after a short pause — shared-hosting blips and deploy windows otherwise
|
|
@@ -1586,7 +1653,32 @@ async function fetchAndDisplayCredits(directoryUrl, nodeId, token, label, asJson
|
|
|
1586
1653
|
const tpc = Number(body["tokens_per_credit"] ?? 1000);
|
|
1587
1654
|
const pad = (n) => n.toFixed(3).padStart(12);
|
|
1588
1655
|
const check = reconciles ? "✓ reconciles" : "✗ DOES NOT RECONCILE";
|
|
1589
|
-
|
|
1656
|
+
const wallet = body["operator_wallet"];
|
|
1657
|
+
if (wallet && typeof wallet === "object") {
|
|
1658
|
+
const showWallet = !walletState?.shown;
|
|
1659
|
+
const fingerprint = typeof wallet["operator_fingerprint"] === "string" ? ` · operator ${wallet["operator_fingerprint"]}` : "";
|
|
1660
|
+
const walletBalance = Number(wallet["total_balance"] ?? balance);
|
|
1661
|
+
const walletNodes = Number(wallet["node_count"] ?? 0);
|
|
1662
|
+
if (showWallet) {
|
|
1663
|
+
process.stdout.write(`IICP operator wallet${fingerprint}\n`);
|
|
1664
|
+
if (wallet["total_earned"] !== undefined)
|
|
1665
|
+
process.stdout.write(` Total earned ${pad(Number(wallet["total_earned"] ?? 0))}\n`);
|
|
1666
|
+
if (wallet["total_spent"] !== undefined)
|
|
1667
|
+
process.stdout.write(` Total spent ${pad(Number(wallet["total_spent"] ?? 0))}\n`);
|
|
1668
|
+
process.stdout.write(" ─────────────────────────────\n");
|
|
1669
|
+
const wr = wallet["reconciles"];
|
|
1670
|
+
const walletCheck = wr === true ? "✓ reconciles" : wr === false ? "✗ DOES NOT RECONCILE" : "rollup";
|
|
1671
|
+
process.stdout.write(` Wallet balance ${pad(walletBalance)} ${walletCheck} (≈ ${Math.trunc(walletBalance * tpc)} tokens)\n`);
|
|
1672
|
+
process.stdout.write(` ${walletNodes} linked node(s) · per-node audit below\n\n`);
|
|
1673
|
+
if (walletState)
|
|
1674
|
+
walletState.shown = true;
|
|
1675
|
+
}
|
|
1676
|
+
process.stdout.write(`Node ledger — ${label}\n`);
|
|
1677
|
+
}
|
|
1678
|
+
else {
|
|
1679
|
+
process.stdout.write(`IICP credits — ${label}\n`);
|
|
1680
|
+
process.stdout.write(" Node-local ledger bind an operator identity to combine nodes\n");
|
|
1681
|
+
}
|
|
1590
1682
|
process.stdout.write(` Earned (income) ${pad(earned)}\n`);
|
|
1591
1683
|
process.stdout.write(` Spent ${pad(spent)}\n`);
|
|
1592
1684
|
process.stdout.write(" ─────────────────────────────\n");
|
|
@@ -1620,6 +1712,103 @@ async function fetchAndDisplayCredits(directoryUrl, nodeId, token, label, asJson
|
|
|
1620
1712
|
}
|
|
1621
1713
|
return 0;
|
|
1622
1714
|
}
|
|
1715
|
+
function printDoctorHelp() {
|
|
1716
|
+
process.stdout.write(`usage: iicp-node doctor [options]\n\n` +
|
|
1717
|
+
`Check local health, directory presence, and deterministic recovery action.\n\n` +
|
|
1718
|
+
`options:\n` +
|
|
1719
|
+
` --node NAME Load saved node config (~/.iicp/nodes/<NAME>.json)\n` +
|
|
1720
|
+
` --directory-url URL Override the saved IICP directory base URL\n` +
|
|
1721
|
+
` --json Print machine-readable recovery state\n` +
|
|
1722
|
+
` -h, --help Show this help and exit\n`);
|
|
1723
|
+
}
|
|
1724
|
+
function doctorLoopbackHost(host) {
|
|
1725
|
+
const h = (host || "").trim();
|
|
1726
|
+
if (h === "" || h === "::" || h === "0.0.0.0")
|
|
1727
|
+
return "127.0.0.1";
|
|
1728
|
+
return h;
|
|
1729
|
+
}
|
|
1730
|
+
function doctorUrl(host, port) {
|
|
1731
|
+
const h = doctorLoopbackHost(host);
|
|
1732
|
+
return h.includes(":") && !h.startsWith("[")
|
|
1733
|
+
? `http://[${h}]:${port}/iicp/health`
|
|
1734
|
+
: `http://${h}:${port}/iicp/health`;
|
|
1735
|
+
}
|
|
1736
|
+
async function runDoctor(argv) {
|
|
1737
|
+
const { values } = safeParseArgs({
|
|
1738
|
+
args: argv,
|
|
1739
|
+
options: {
|
|
1740
|
+
node: { type: "string" },
|
|
1741
|
+
"directory-url": { type: "string" },
|
|
1742
|
+
json: { type: "boolean" },
|
|
1743
|
+
help: { type: "boolean", short: "h" },
|
|
1744
|
+
},
|
|
1745
|
+
allowPositionals: false,
|
|
1746
|
+
});
|
|
1747
|
+
if (values.help) {
|
|
1748
|
+
printDoctorHelp();
|
|
1749
|
+
return 0;
|
|
1750
|
+
}
|
|
1751
|
+
const nodeName = values.node ?? process.env.IICP_NODE_NAME ?? "default";
|
|
1752
|
+
const saved = (0, identity_js_1.loadNode)(nodeName);
|
|
1753
|
+
if (!saved) {
|
|
1754
|
+
process.stderr.write(`ERROR: no saved config at ~/.iicp/nodes/${nodeName}.json — run \`iicp-node init\` first.\n`);
|
|
1755
|
+
return 1;
|
|
1756
|
+
}
|
|
1757
|
+
const directoryUrl = values["directory-url"] ?? saved.directory_url ?? process.env.IICP_DIRECTORY_URL ?? "https://iicp.network/api";
|
|
1758
|
+
const localHealthUrl = doctorUrl(saved.host ?? "0.0.0.0", saved.port ?? 8020);
|
|
1759
|
+
let localHealthOk = false;
|
|
1760
|
+
let health = null;
|
|
1761
|
+
let healthError = null;
|
|
1762
|
+
try {
|
|
1763
|
+
const resp = await fetch(localHealthUrl, { signal: AbortSignal.timeout(2_000) });
|
|
1764
|
+
if (!resp.ok)
|
|
1765
|
+
throw new Error(`HTTP ${resp.status}`);
|
|
1766
|
+
health = (await resp.json());
|
|
1767
|
+
localHealthOk = true;
|
|
1768
|
+
}
|
|
1769
|
+
catch (err) {
|
|
1770
|
+
healthError = err instanceof Error ? err.message : String(err);
|
|
1771
|
+
}
|
|
1772
|
+
const presence = await (0, recovery_js_1.registryNodePresence)(directoryUrl, saved.node_id, 5_000);
|
|
1773
|
+
const stability = (health?.["backend_stability"] ?? {});
|
|
1774
|
+
const backendAttention = stability["backend_state"] === "draining";
|
|
1775
|
+
const failures = !localHealthOk || presence === "absent" ? 1 : 0;
|
|
1776
|
+
const { state, action } = (0, recovery_js_1.classifyRecovery)({
|
|
1777
|
+
localHealthOk,
|
|
1778
|
+
publicAvailable: localHealthOk,
|
|
1779
|
+
directoryPresence: presence,
|
|
1780
|
+
consecutiveFailures: failures,
|
|
1781
|
+
graceChecks: (0, recovery_js_1.envGraceChecks)(),
|
|
1782
|
+
backendAttention,
|
|
1783
|
+
});
|
|
1784
|
+
const prefix = (0, recovery_js_1.nodeRegistryPrefix)(saved.node_id);
|
|
1785
|
+
if (values.json) {
|
|
1786
|
+
process.stdout.write(JSON.stringify({
|
|
1787
|
+
node: saved.name,
|
|
1788
|
+
node_id: saved.node_id,
|
|
1789
|
+
prefix,
|
|
1790
|
+
directory_url: directoryUrl,
|
|
1791
|
+
local_health_url: localHealthUrl,
|
|
1792
|
+
local_health_ok: localHealthOk,
|
|
1793
|
+
local_health_error: healthError,
|
|
1794
|
+
directory_presence: presence,
|
|
1795
|
+
recovery_state: state,
|
|
1796
|
+
recommended_action: action,
|
|
1797
|
+
health,
|
|
1798
|
+
}, null, 2) + "\n");
|
|
1799
|
+
return 0;
|
|
1800
|
+
}
|
|
1801
|
+
process.stdout.write(`IICP node doctor — ${saved.name}\n`);
|
|
1802
|
+
process.stdout.write(` Local health ${localHealthOk ? "ok" : "failed"} (${localHealthUrl})\n`);
|
|
1803
|
+
if (healthError)
|
|
1804
|
+
process.stdout.write(` Local health detail ${healthError}\n`);
|
|
1805
|
+
process.stdout.write(` Directory prefix ${prefix}\n`);
|
|
1806
|
+
process.stdout.write(` Directory presence ${presence}\n`);
|
|
1807
|
+
process.stdout.write(` Recovery state ${state}\n`);
|
|
1808
|
+
process.stdout.write(` Recommended action ${action}\n`);
|
|
1809
|
+
process.stdout.write(" Note restart is automatic only when supervised services set IICP_SUPERVISED=1\n");
|
|
1810
|
+
return 0;
|
|
1811
|
+
}
|
|
1623
1812
|
/**
|
|
1624
1813
|
* Resolve a passphrase: $IICP_OPERATOR_PASSPHRASE if set (headless/CI), else an interactive
|
|
1625
1814
|
* readline prompt (this command is operator-run, so a prompt is fine here — only `serve` must
|
|
@@ -1712,16 +1901,46 @@ function printOperatorHelp() {
|
|
|
1712
1901
|
` --directory-url URL IICP directory base URL (defaults to env / iicp.network)\n` +
|
|
1713
1902
|
` -h, --help Show this help and exit\n`);
|
|
1714
1903
|
}
|
|
1904
|
+
function printOperatorEncryptHelp() {
|
|
1905
|
+
process.stdout.write(`usage: iicp-node operator encrypt\n\n` +
|
|
1906
|
+
`Password-encrypt the operator secret at rest. The command prompts for a passphrase\n` +
|
|
1907
|
+
`unless $IICP_OPERATOR_PASSPHRASE is set for headless use.\n\n` +
|
|
1908
|
+
`Options:\n` +
|
|
1909
|
+
` -h, --help Show this help and exit without prompting\n`);
|
|
1910
|
+
}
|
|
1911
|
+
function printOperatorDecryptHelp() {
|
|
1912
|
+
process.stdout.write(`usage: iicp-node operator decrypt\n\n` +
|
|
1913
|
+
`Remove at-rest encryption and store the operator secret in plaintext again. The\n` +
|
|
1914
|
+
`command prompts for the passphrase unless $IICP_OPERATOR_PASSPHRASE is set.\n\n` +
|
|
1915
|
+
`Options:\n` +
|
|
1916
|
+
` -h, --help Show this help and exit without prompting\n`);
|
|
1917
|
+
}
|
|
1715
1918
|
async function runOperator(argv) {
|
|
1716
1919
|
const sub = argv[0];
|
|
1717
1920
|
if (sub === undefined || sub === "--help" || sub === "-h") {
|
|
1718
1921
|
printOperatorHelp();
|
|
1719
1922
|
return sub === undefined ? 2 : 0;
|
|
1720
1923
|
}
|
|
1721
|
-
if (sub === "encrypt")
|
|
1924
|
+
if (sub === "encrypt") {
|
|
1925
|
+
const rest = argv.slice(1);
|
|
1926
|
+
if (argsHaveHelp(rest)) {
|
|
1927
|
+
printOperatorEncryptHelp();
|
|
1928
|
+
return 0;
|
|
1929
|
+
}
|
|
1930
|
+
if (rest.length > 0)
|
|
1931
|
+
throw new CliError(`unknown operator encrypt option '${rest[0]}'`);
|
|
1722
1932
|
return runOperatorEncrypt();
|
|
1723
|
-
|
|
1933
|
+
}
|
|
1934
|
+
if (sub === "decrypt") {
|
|
1935
|
+
const rest = argv.slice(1);
|
|
1936
|
+
if (argsHaveHelp(rest)) {
|
|
1937
|
+
printOperatorDecryptHelp();
|
|
1938
|
+
return 0;
|
|
1939
|
+
}
|
|
1940
|
+
if (rest.length > 0)
|
|
1941
|
+
throw new CliError(`unknown operator decrypt option '${rest[0]}'`);
|
|
1724
1942
|
return runOperatorDecrypt();
|
|
1943
|
+
}
|
|
1725
1944
|
if (sub !== "rename") {
|
|
1726
1945
|
process.stderr.write(`unknown operator subcommand: ${sub}\n`);
|
|
1727
1946
|
printOperatorHelp();
|
|
@@ -2047,7 +2266,18 @@ async function runMcpGateway(argv) {
|
|
|
2047
2266
|
/** Command dispatch — separated so main() can wrap parse failures as clean CliError output. */
|
|
2048
2267
|
// #521 P1 — read-only version check. Exit 10 when a newer release exists
|
|
2049
2268
|
// (so cron/scripts can act), 0 when current/unreachable. Never installs.
|
|
2050
|
-
async function runUpdate() {
|
|
2269
|
+
async function runUpdate(argv = []) {
|
|
2270
|
+
const { values } = safeParseArgs({
|
|
2271
|
+
args: argv,
|
|
2272
|
+
options: {
|
|
2273
|
+
help: { type: "boolean", short: "h" },
|
|
2274
|
+
},
|
|
2275
|
+
allowPositionals: false,
|
|
2276
|
+
});
|
|
2277
|
+
if (values.help) {
|
|
2278
|
+
printUpdateHelp();
|
|
2279
|
+
return 0;
|
|
2280
|
+
}
|
|
2051
2281
|
const { checkUpdate, latestNpmVersion } = await import("./updater.js");
|
|
2052
2282
|
const latest = await latestNpmVersion();
|
|
2053
2283
|
const v = checkUpdate(SDK_VERSION, latest);
|
|
@@ -2124,11 +2354,13 @@ async function dispatch(argv) {
|
|
|
2124
2354
|
if (cmd === "init")
|
|
2125
2355
|
return runInit();
|
|
2126
2356
|
if (cmd === "list")
|
|
2127
|
-
return runList();
|
|
2357
|
+
return runList(argv.slice(1));
|
|
2128
2358
|
if (cmd === "query")
|
|
2129
2359
|
return runQuery(argv.slice(1));
|
|
2130
2360
|
if (cmd === "credits")
|
|
2131
2361
|
return runCredits(argv.slice(1));
|
|
2362
|
+
if (cmd === "doctor")
|
|
2363
|
+
return runDoctor(argv.slice(1));
|
|
2132
2364
|
if (cmd === "operator")
|
|
2133
2365
|
return runOperator(argv.slice(1));
|
|
2134
2366
|
if (cmd === "proxy")
|
|
@@ -2136,7 +2368,7 @@ async function dispatch(argv) {
|
|
|
2136
2368
|
if (cmd === "mcp-gateway")
|
|
2137
2369
|
return runMcpGateway(argv.slice(1));
|
|
2138
2370
|
if (cmd === "update")
|
|
2139
|
-
return runUpdate();
|
|
2371
|
+
return runUpdate(argv.slice(1));
|
|
2140
2372
|
if (cmd === "service")
|
|
2141
2373
|
return runService(argv.slice(1));
|
|
2142
2374
|
if (cmd !== "serve") {
|
|
@@ -2179,7 +2411,7 @@ async function dispatch(argv) {
|
|
|
2179
2411
|
allowPositionals: false,
|
|
2180
2412
|
});
|
|
2181
2413
|
if (values.help) {
|
|
2182
|
-
|
|
2414
|
+
printServeHelp();
|
|
2183
2415
|
return 0;
|
|
2184
2416
|
}
|
|
2185
2417
|
const opts = {
|