@iicp/client 0.7.11 → 0.7.32

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
@@ -31,6 +31,19 @@ npm install iicp-client
31
31
 
32
32
  ---
33
33
 
34
+ ## Architecture — consumer or provider?
35
+
36
+ This SDK covers **both** sides of the IICP protocol:
37
+
38
+ | Role | What you do | Class |
39
+ |------|-------------|-------|
40
+ | **Consumer** | Send AI tasks to the mesh; discover and submit | `IicpClient` |
41
+ | **Provider** | Run a node, register with the directory, serve tasks | `IicpNode` |
42
+
43
+ Consumer and provider can run in the same process. For production provider nodes backed by Ollama/vLLM, see [iicp.network/docs/node-setup](https://iicp.network/docs/node-setup).
44
+
45
+ ---
46
+
34
47
  ## Quickstart
35
48
 
36
49
  ```typescript
@@ -16,6 +16,8 @@ export interface CooperativeInferencePolicyOptions {
16
16
  /** Bounded to [1, 60000]ms. */
17
17
  maxWorkerTimeoutMs?: number;
18
18
  maxConcurrentRemote?: number;
19
+ /** #403 — allow tool-execution-domain intents (default false). */
20
+ allowToolExecution?: boolean;
19
21
  }
20
22
  /** Safe-by-default CIP policy with the S.12 §2.2 capacity gate built-in. */
21
23
  export declare class CooperativeInferencePolicy {
@@ -25,8 +27,15 @@ export declare class CooperativeInferencePolicy {
25
27
  readonly maxReplicas: number;
26
28
  readonly maxWorkerTimeoutMs: number;
27
29
  readonly maxConcurrentRemote: number;
30
+ readonly allowToolExecution: boolean;
28
31
  private _inFlight;
29
32
  constructor(opts?: CooperativeInferencePolicyOptions);
33
+ /**
34
+ * #403 — per-task admission: reject tool-execution-domain intents unless the
35
+ * operator opted in via allowToolExecution. Mirrors the adapter cip_gate.
36
+ * Intent URN form: urn:iicp:intent:<domain>:... — domain is segment index 3.
37
+ */
38
+ permitsIntent(intent: string): boolean;
30
39
  /** CIP-W01: returns true if this node may act as a CIP coordinator. */
31
40
  checkCoordinator(): boolean;
32
41
  /** CIP-W02: returns true if this node may accept CIP worker tasks. */
@@ -1 +1 @@
1
- {"version":3,"file":"cip_policy.d.ts","sourceRoot":"","sources":["../src/cip_policy.ts"],"names":[],"mappings":"AACA;;;;;;;;;GASG;AAEH,MAAM,WAAW,iCAAiC;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,4EAA4E;AAC5E,qBAAa,0BAA0B;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,OAAO,CAAC,SAAS,CAAK;gBAEV,IAAI,GAAE,iCAAsC;IASxD,uEAAuE;IACvE,gBAAgB,IAAI,OAAO;IAI3B,sEAAsE;IACtE,WAAW,IAAI,OAAO;IAItB;;;;;OAKG;IACH,iBAAiB,IAAI,OAAO;IAM5B,uDAAuD;IACvD,cAAc,IAAI,IAAI;IAItB;;;;OAIG;IACH,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAMjD;AAMD,uFAAuF;AACvF,wBAAgB,YAAY,IAAI,0BAA0B,CAEzD;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAChC,IAAI,GAAE,iCAAsC,GAC3C,0BAA0B,CAG5B"}
1
+ {"version":3,"file":"cip_policy.d.ts","sourceRoot":"","sources":["../src/cip_policy.ts"],"names":[],"mappings":"AACA;;;;;;;;;GASG;AAEH,MAAM,WAAW,iCAAiC;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,4EAA4E;AAC5E,qBAAa,0BAA0B;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC,OAAO,CAAC,SAAS,CAAK;gBAEV,IAAI,GAAE,iCAAsC;IAWxD;;;;OAIG;IACH,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKtC,uEAAuE;IACvE,gBAAgB,IAAI,OAAO;IAI3B,sEAAsE;IACtE,WAAW,IAAI,OAAO;IAItB;;;;;OAKG;IACH,iBAAiB,IAAI,OAAO;IAM5B,uDAAuD;IACvD,cAAc,IAAI,IAAI;IAItB;;;;OAIG;IACH,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAOjD;AAMD,uFAAuF;AACvF,wBAAgB,YAAY,IAAI,0BAA0B,CAEzD;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAChC,IAAI,GAAE,iCAAsC,GAC3C,0BAA0B,CAG5B"}
@@ -22,6 +22,7 @@ class CooperativeInferencePolicy {
22
22
  maxReplicas;
23
23
  maxWorkerTimeoutMs;
24
24
  maxConcurrentRemote;
25
+ allowToolExecution;
25
26
  _inFlight = 0;
26
27
  constructor(opts = {}) {
27
28
  this.enabled = opts.enabled ?? false;
@@ -30,6 +31,17 @@ class CooperativeInferencePolicy {
30
31
  this.maxReplicas = Math.max(1, opts.maxReplicas ?? 3);
31
32
  this.maxWorkerTimeoutMs = Math.max(1, Math.min(60_000, opts.maxWorkerTimeoutMs ?? 30_000));
32
33
  this.maxConcurrentRemote = Math.max(1, opts.maxConcurrentRemote ?? 2);
34
+ // #403 — per-task admission: tool-execution intents rejected unless opted in.
35
+ this.allowToolExecution = opts.allowToolExecution ?? false;
36
+ }
37
+ /**
38
+ * #403 — per-task admission: reject tool-execution-domain intents unless the
39
+ * operator opted in via allowToolExecution. Mirrors the adapter cip_gate.
40
+ * Intent URN form: urn:iicp:intent:<domain>:... — domain is segment index 3.
41
+ */
42
+ permitsIntent(intent) {
43
+ const domain = intent.split(":")[3] ?? "";
44
+ return this.allowToolExecution || domain !== "tool";
33
45
  }
34
46
  /** CIP-W01: returns true if this node may act as a CIP coordinator. */
35
47
  checkCoordinator() {
@@ -66,6 +78,7 @@ class CooperativeInferencePolicy {
66
78
  return {};
67
79
  return {
68
80
  allow_remote_inference: this.allowWorker,
81
+ allow_tool_execution: this.allowToolExecution,
69
82
  };
70
83
  }
71
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cip_policy.js","sourceRoot":"","sources":["../src/cip_policy.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC;;;;;;;;;GASG;;;AA4EH,oCAEC;AAGD,gDAKC;AA1ED,4EAA4E;AAC5E,MAAa,0BAA0B;IAC5B,OAAO,CAAU;IACjB,gBAAgB,CAAU;IAC1B,WAAW,CAAU;IACrB,WAAW,CAAS;IACpB,kBAAkB,CAAS;IAC3B,mBAAmB,CAAS;IAC7B,SAAS,GAAG,CAAC,CAAC;IAEtB,YAAY,OAA0C,EAAE;QACtD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC;QACrC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,KAAK,CAAC;QACvD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC;QAC7C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,kBAAkB,IAAI,MAAM,CAAC,CAAC,CAAC;QAC3F,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,mBAAmB,IAAI,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,uEAAuE;IACvE,gBAAgB;QACd,OAAO,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAC/C,CAAC;IAED,sEAAsE;IACtE,WAAW;QACT,OAAO,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,WAAW,CAAC;IAC1C,CAAC;IAED;;;;;OAKG;IACH,iBAAiB;QACf,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,mBAAmB;YAAE,OAAO,KAAK,CAAC;QAC7D,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,uDAAuD;IACvD,cAAc;QACZ,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC;YAAE,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,qBAAqB;QACnB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QAC7B,OAAO;YACL,sBAAsB,EAAE,IAAI,CAAC,WAAW;SACzC,CAAC;IACJ,CAAC;CACF;AAxDD,gEAwDC;AAED,4EAA4E;AAE5E,IAAI,OAAO,GAA+B,IAAI,0BAA0B,EAAE,CAAC;AAE3E,uFAAuF;AACvF,SAAgB,YAAY;IAC1B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,gFAAgF;AAChF,SAAgB,kBAAkB,CAChC,OAA0C,EAAE;IAE5C,OAAO,GAAG,IAAI,0BAA0B,CAAC,IAAI,CAAC,CAAC;IAC/C,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"cip_policy.js","sourceRoot":"","sources":["../src/cip_policy.ts"],"names":[],"mappings":";AAAA,sCAAsC;AACtC;;;;;;;;;GASG;;;AA4FH,oCAEC;AAGD,gDAKC;AAxFD,4EAA4E;AAC5E,MAAa,0BAA0B;IAC5B,OAAO,CAAU;IACjB,gBAAgB,CAAU;IAC1B,WAAW,CAAU;IACrB,WAAW,CAAS;IACpB,kBAAkB,CAAS;IAC3B,mBAAmB,CAAS;IAC5B,kBAAkB,CAAU;IAC7B,SAAS,GAAG,CAAC,CAAC;IAEtB,YAAY,OAA0C,EAAE;QACtD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC;QACrC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,KAAK,CAAC;QACvD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC;QAC7C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC;QACtD,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,kBAAkB,IAAI,MAAM,CAAC,CAAC,CAAC;QAC3F,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,mBAAmB,IAAI,CAAC,CAAC,CAAC;QACtE,8EAA8E;QAC9E,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,kBAAkB,IAAI,KAAK,CAAC;IAC7D,CAAC;IAED;;;;OAIG;IACH,aAAa,CAAC,MAAc;QAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1C,OAAO,IAAI,CAAC,kBAAkB,IAAI,MAAM,KAAK,MAAM,CAAC;IACtD,CAAC;IAED,uEAAuE;IACvE,gBAAgB;QACd,OAAO,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAC/C,CAAC;IAED,sEAAsE;IACtE,WAAW;QACT,OAAO,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,WAAW,CAAC;IAC1C,CAAC;IAED;;;;;OAKG;IACH,iBAAiB;QACf,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,mBAAmB;YAAE,OAAO,KAAK,CAAC;QAC7D,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,uDAAuD;IACvD,cAAc;QACZ,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC;YAAE,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED;;;;OAIG;IACH,qBAAqB;QACnB,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QAC7B,OAAO;YACL,sBAAsB,EAAE,IAAI,CAAC,WAAW;YACxC,oBAAoB,EAAE,IAAI,CAAC,kBAAkB;SAC9C,CAAC;IACJ,CAAC;CACF;AAtED,gEAsEC;AAED,4EAA4E;AAE5E,IAAI,OAAO,GAA+B,IAAI,0BAA0B,EAAE,CAAC;AAE3E,uFAAuF;AACvF,SAAgB,YAAY;IAC1B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,gFAAgF;AAChF,SAAgB,kBAAkB,CAChC,OAA0C,EAAE;IAE5C,OAAO,GAAG,IAAI,0BAA0B,CAAC,IAAI,CAAC,CAAC;IAC/C,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,27 @@
1
1
  #!/usr/bin/env node
2
+ import { type NodeIdentity } from "./identity.js";
3
+ export interface ServeOpts {
4
+ backendUrl: string;
5
+ backendType: string;
6
+ /** #5 — Bearer key for an auth-requiring OpenAI-compat backend (LM Studio, hosted). Empty = none. */
7
+ backendApiKey: string;
8
+ model: string;
9
+ publicEndpoint: string;
10
+ directoryUrl: string;
11
+ region: string;
12
+ intent: string;
13
+ maxConcurrent: number;
14
+ nodeId: string;
15
+ port: number;
16
+ host: string;
17
+ skipRegistration: boolean;
18
+ force: boolean;
19
+ autoDetectNat: boolean;
20
+ externalIpProbeUrl: string;
21
+ relayWorkerEndpoint: string;
22
+ node: string;
23
+ logDir?: string;
24
+ }
25
+ export declare function applySavedNode(opts: ServeOpts, saved: NodeIdentity): ServeOpts;
2
26
  export declare function main(argv?: string[]): Promise<number>;
3
27
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA+pBA,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAkFlF"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAiCA,OAAO,EASL,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAEvB,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;AA8UD,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,SAAS,CAiB9E;AAwaD,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CA8FlF"}
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.applySavedNode = applySavedNode;
4
5
  exports.main = main;
5
6
  // SPDX-License-Identifier: Apache-2.0
6
7
  /**
@@ -21,12 +22,17 @@ exports.main = main;
21
22
  */
22
23
  const node_util_1 = require("node:util");
23
24
  const node_crypto_1 = require("node:crypto");
25
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
26
+ const SDK_VERSION = require("../package.json").version;
24
27
  const net = require("node:net");
25
28
  const node_child_process_1 = require("node:child_process");
26
29
  const readline = require("node:readline/promises");
27
30
  const node_process_1 = require("node:process");
28
31
  const node_js_1 = require("./node.js");
32
+ const client_js_1 = require("./client.js");
33
+ const node_log_js_1 = require("./node_log.js");
29
34
  const cip_policy_js_1 = require("./cip_policy.js");
35
+ const instance_lock_js_1 = require("./instance_lock.js");
30
36
  const index_js_1 = require("./backends/index.js");
31
37
  const identity_js_1 = require("./identity.js");
32
38
  function envOr(name, fallback) {
@@ -51,7 +57,8 @@ function printHelp() {
51
57
  `Commands:\n` +
52
58
  ` init Interactive wizard — set up operator + first node config\n` +
53
59
  ` list List node configs saved under ~/.iicp/nodes/\n` +
54
- ` serve Register and serve a node\n\n` +
60
+ ` serve Register and serve a node\n` +
61
+ ` query <prompt> Discover mesh nodes and submit a chat task\n\n` +
55
62
  `Run an IICP provider node backed by an OpenAI-compatible server.\n\n` +
56
63
  `serve required (flag or env):\n` +
57
64
  ` --model NAME IICP_BACKEND_MODEL — model name (e.g. qwen2.5:0.5b)\n` +
@@ -59,6 +66,7 @@ function printHelp() {
59
66
  `serve optional:\n` +
60
67
  ` --backend-url URL IICP_BACKEND_URL — Ollama / vLLM / LM Studio (default http://localhost:11434)\n` +
61
68
  ` --backend-type TYPE IICP_BACKEND_TYPE — openai_compat | vllm | llamacpp (default openai_compat)\n` +
69
+ ` --backend-api-key KEY IICP_BACKEND_API_KEY — Bearer key for an auth'd backend (LM Studio, hosted)\n` +
62
70
  ` --public-endpoint URL IICP_PUBLIC_ENDPOINT — externally reachable URL of this node\n` +
63
71
  ` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
64
72
  ` --region REGION IICP_REGION (default eu-central)\n` +
@@ -66,10 +74,16 @@ function printHelp() {
66
74
  ` --max-concurrent N IICP_MAX_CONCURRENT (default 4)\n` +
67
75
  ` --node-id ID IICP_NODE_ID (auto-generated if absent)\n` +
68
76
  ` --port N IICP_PORT (default 9484)\n` +
69
- ` --host HOST IICP_HOST (default 0.0.0.0)\n` +
77
+ ` --host HOST IICP_HOST (default :: — dual-stack IPv4+IPv6)\n` +
70
78
  ` --skip-registration IICP_SKIP_REGISTRATION — register-free dev mode\n` +
71
79
  ` --auto-detect-nat IICP_AUTO_DETECT_NAT — run NAT detection at startup\n` +
72
- ` --external-ip-probe-url U IICP_EXTERNAL_IP_PROBE_URL — fallback IPv4 probe\n`);
80
+ ` --external-ip-probe-url U IICP_EXTERNAL_IP_PROBE_URL — fallback IPv4 probe\n\n` +
81
+ `query optional:\n` +
82
+ ` --directory-url URL IICP_DIRECTORY_URL (default https://iicp.network/api)\n` +
83
+ ` --intent URN IICP_INTENT (default urn:iicp:intent:llm:chat:v1)\n` +
84
+ ` --model NAME Pin to a specific model on the remote node\n` +
85
+ ` --max-tokens N Limit response length\n` +
86
+ ` --timeout-ms N Request timeout (default 60000)\n`);
73
87
  }
74
88
  async function checkDependencies(backendUrl) {
75
89
  const out = [];
@@ -101,7 +115,7 @@ async function checkDependencies(backendUrl) {
101
115
  out.push({ name: mod, severity: "ok", message: purpose, installable: false, npmExtra: "" });
102
116
  }
103
117
  catch {
104
- out.push({ name: mod, severity: "missing", message: `${purpose} (not installed)`, installable: true, npmExtra: npmName });
118
+ out.push({ name: mod, severity: "optional", message: `${purpose} (optional — not installed)`, installable: true, npmExtra: npmName });
105
119
  }
106
120
  }
107
121
  // 3) IPv6 routing surface (advisory)
@@ -124,14 +138,14 @@ async function checkDependencies(backendUrl) {
124
138
  return out;
125
139
  }
126
140
  function printDepStatus(issues) {
127
- const glyph = { ok: " ✓", warn: " !", missing: " ✗" };
141
+ const glyph = { ok: " ✓", optional: " ○", warn: " !", missing: " ✗" };
128
142
  for (const i of issues) {
129
143
  process.stdout.write(`${glyph[i.severity] ?? " ?"} ${i.name.padEnd(18)} ${i.message}\n`);
130
144
  }
131
145
  }
132
146
  function installMissing(issues) {
133
147
  const extras = Array.from(new Set(issues
134
- .filter((i) => i.severity === "missing" && i.installable && i.npmExtra)
148
+ .filter((i) => (i.severity === "optional" || i.severity === "missing") && i.installable && i.npmExtra)
135
149
  .map((i) => i.npmExtra))).sort();
136
150
  if (extras.length === 0)
137
151
  return;
@@ -212,14 +226,14 @@ async function runInit() {
212
226
  process.stdout.write(`Checking dependencies …\n`);
213
227
  const issues = await checkDependencies(backend);
214
228
  printDepStatus(issues);
215
- const missingCount = issues.filter((i) => i.severity === "missing" && i.installable).length;
216
- if (missingCount > 0) {
217
- const yn = (await ask(rl, `\nInstall ${missingCount} missing optional package(s)? [Y/n]`, "y")).toLowerCase();
229
+ const optionalCount = issues.filter((i) => (i.severity === "optional" || i.severity === "missing") && i.installable).length;
230
+ if (optionalCount > 0) {
231
+ const yn = (await ask(rl, `\nEnable ${optionalCount} optional package(s)? (your node runs without them) [Y/n]`, "y")).toLowerCase();
218
232
  if (yn === "" || yn === "y" || yn === "yes") {
219
233
  installMissing(issues);
220
234
  }
221
235
  else {
222
- process.stdout.write(` ! skipping — install later with: npm install <pkg>\n`);
236
+ process.stdout.write(` skipping — enable later with: npm install <pkg>\n`);
223
237
  }
224
238
  }
225
239
  process.stdout.write(`\nDocumentation:\n`);
@@ -384,6 +398,20 @@ async function runServe(opts) {
384
398
  return 2;
385
399
  }
386
400
  const nodeId = (opts.nodeId || (0, node_crypto_1.randomUUID)()).slice(0, 36);
401
+ const logDir = opts.logDir;
402
+ // #405 — single-instance lock: refuse a second LIVE process for this node_id
403
+ // (the token-rotation war). Distinct node_ids are unaffected. Fails open.
404
+ let instanceLock;
405
+ try {
406
+ instanceLock = instance_lock_js_1.InstanceLock.acquire(nodeId, opts.force);
407
+ }
408
+ catch (exc) {
409
+ if (exc instanceof instance_lock_js_1.NodeAlreadyServingError) {
410
+ console.error(`[iicp-node] ${exc.message}`);
411
+ process.exit(2);
412
+ }
413
+ throw exc;
414
+ }
387
415
  // Resolve the actual listen port before NAT detection: start at the
388
416
  // requested port (default 9484, the official IICP port) and auto-increment
389
417
  // to the next free port. Keeps one port per node (multiple models share it)
@@ -513,44 +541,105 @@ async function runServe(opts) {
513
541
  const handler = (0, index_js_1.getBackendHandler)(opts.backendType, {
514
542
  baseUrl: _baseUrl,
515
543
  model: opts.model,
544
+ // #5 — Bearer key for auth'd backends (LM Studio, hosted). Empty/undefined = no header.
545
+ apiKey: opts.backendApiKey || undefined,
516
546
  });
517
547
  // GAP-6: probe backend for all available models so the registration advertises
518
548
  // the full list — not just the single configured model. Best-effort; fall back
519
549
  // to the single configured model on any error.
520
550
  try {
521
- const tagsUrl = opts.backendUrl.replace(/\/$/, "") + "/api/tags";
522
- const tagsResp = await fetch(tagsUrl, { signal: AbortSignal.timeout(3000) });
523
- if (tagsResp.ok) {
524
- const tagsData = await tagsResp.json();
525
- const extra = (tagsData.models ?? [])
526
- .map((m) => m.name)
527
- .filter((m) => m !== opts.model);
528
- if (extra.length > 0) {
529
- node["_cfg"].capabilities = extra;
530
- // eslint-disable-next-line no-console
531
- console.log(`[iicp-node] GAP-6: advertising ${extra.length} additional model(s): ${extra.slice(0, 6).join(", ")}`);
551
+ // #409 strip a trailing /v1 to a root so probe URLs are well-formed for
552
+ // both Ollama (`http://host:11434`) and LM Studio/OpenAI-compat
553
+ // (`http://host:1234/v1`); attach the Bearer key (LM Studio /v1/models 401s
554
+ // without it). Without this, /v1 backends got `…/v1/v1/models` (404) and no
555
+ // models were discovered, so multi-intent never fired.
556
+ const base = opts.backendUrl.replace(/\/$/, "");
557
+ const root = base.endsWith("/v1") ? base.slice(0, -3) : base;
558
+ const headers = opts.backendApiKey
559
+ ? { Authorization: `Bearer ${opts.backendApiKey}` }
560
+ : {};
561
+ const allModels = await (async () => {
562
+ // Ollama /api/tags ({models:[{name}]})
563
+ try {
564
+ const r = await fetch(`${root}/api/tags`, { headers, signal: AbortSignal.timeout(3000) });
565
+ if (r.ok) {
566
+ const d = await r.json();
567
+ const names = (d.models ?? []).map((m) => m.name);
568
+ if (names.length > 0)
569
+ return names;
570
+ }
571
+ }
572
+ catch { /* try OpenAI next */ }
573
+ // OpenAI-compat /v1/models ({data:[{id}]})
574
+ try {
575
+ const r = await fetch(`${root}/v1/models`, { headers, signal: AbortSignal.timeout(3000) });
576
+ if (r.ok) {
577
+ const d = await r.json();
578
+ return (d.data ?? []).map((m) => m.id).filter(Boolean);
579
+ }
532
580
  }
581
+ catch { /* best-effort */ }
582
+ return [];
583
+ })();
584
+ const extra = allModels.filter((m) => m !== opts.model);
585
+ if (extra.length > 0) {
586
+ node["_cfg"].capabilities = extra;
587
+ // eslint-disable-next-line no-console
588
+ console.log(`[iicp-node] GAP-6: advertising ${extra.length} additional model(s): ${extra.slice(0, 6).join(", ")}`);
533
589
  }
534
590
  }
535
591
  catch {
536
592
  // best-effort; no-op on error
537
593
  }
594
+ // NAT-4 guard: if endpoint is non-routable and no relay configured, skip
595
+ // registration to avoid a confusing 422 from the directory's RoutableEndpoint check.
596
+ const epLocal = publicEndpoint.startsWith("http://localhost") ||
597
+ publicEndpoint.startsWith("http://127.") ||
598
+ publicEndpoint.startsWith("http://0.0.0.0") ||
599
+ publicEndpoint.startsWith("http://192.168.") ||
600
+ publicEndpoint.startsWith("http://10.");
601
+ if (epLocal && !opts.relayWorkerEndpoint && !opts.skipRegistration) {
602
+ // eslint-disable-next-line no-console
603
+ console.warn("[iicp-node] no routable endpoint detected and no relay configured — " +
604
+ "skipping directory registration. Node will accept direct connections " +
605
+ "but will not appear in discover results. " +
606
+ "Set IICP_PUBLIC_ENDPOINT=<url> or IICP_RELAY_WORKER_ENDPOINT=<host>:<port> to register.");
607
+ opts.skipRegistration = true;
608
+ }
609
+ // #404 — register with bounded backoff retry. On persistent failure, pass an
610
+ // empty token (NOT undefined) so the heartbeat loop still starts and re-registers
611
+ // on the first 401 (#399 path) once the directory is reachable — the self-healing
612
+ // watchdog. undefined is reserved for --skip-registration (no heartbeat by design).
538
613
  let token;
539
614
  if (!opts.skipRegistration) {
540
- try {
541
- token = await node.register();
542
- // eslint-disable-next-line no-console
543
- console.log(`[iicp-node] registered as ${nodeId} (token=${(token ?? "").slice(0, 8)}…)`);
544
- }
545
- catch (exc) {
546
- const msg = exc instanceof Error ? exc.message : String(exc);
547
- // eslint-disable-next-line no-console
548
- console.warn(`[iicp-node] registration failed: ${msg} — continuing without heartbeat`);
615
+ for (let attempt = 1; attempt <= 3; attempt++) {
616
+ try {
617
+ token = await node.register();
618
+ // eslint-disable-next-line no-console
619
+ console.log(`[iicp-node] registered as ${nodeId} (token=${(token ?? "").slice(0, 8)}…)`);
620
+ (0, node_log_js_1.writeNodeEvent)(nodeId, "register_ok", `endpoint=${opts.publicEndpoint || `http://localhost:${opts.port}`}`, logDir);
621
+ break;
622
+ }
623
+ catch (exc) {
624
+ const msg = exc instanceof Error ? exc.message : String(exc);
625
+ if (attempt >= 3) {
626
+ // eslint-disable-next-line no-console
627
+ console.warn(`[iicp-node] registration failed after ${attempt} attempts: ${msg} — starting heartbeat loop anyway; it will re-register on the first 401`);
628
+ (0, node_log_js_1.writeNodeEvent)(nodeId, "register_fail", `error=${msg} attempts=${attempt}`, logDir);
629
+ token = ""; // empty (not undefined) → heartbeat loop starts and self-heals
630
+ break;
631
+ }
632
+ const backoff = 2 ** attempt;
633
+ // eslint-disable-next-line no-console
634
+ console.warn(`[iicp-node] registration attempt ${attempt} failed: ${msg} — retrying in ${backoff}s`);
635
+ await new Promise((r) => setTimeout(r, backoff * 1000));
636
+ }
549
637
  }
550
638
  }
551
639
  // eslint-disable-next-line no-console
552
640
  console.log(`[iicp-node] serving ${opts.intent} on ${opts.host}:${opts.port} — ` +
553
641
  `backend ${opts.backendUrl} (model=${opts.model}, max_concurrent=${opts.maxConcurrent})`);
642
+ (0, node_log_js_1.writeNodeEvent)(nodeId, "serve_start", `port=${opts.port} model=${opts.model} intent=${opts.intent}`, logDir);
554
643
  // serve() returns a stop() handle but never resolves on its own; we wait for
555
644
  // SIGINT/SIGTERM to terminate.
556
645
  const stop = node.serve(handler, { host: opts.host, port: opts.port, nodeToken: token });
@@ -568,13 +657,17 @@ async function runServe(opts) {
568
657
  try {
569
658
  if (token) {
570
659
  await node.deregister(token);
660
+ (0, node_log_js_1.writeNodeEvent)(nodeId, "deregister_ok", "", logDir);
571
661
  }
572
662
  }
573
663
  catch (exc) {
664
+ const deregMsg = exc instanceof Error ? exc.message : String(exc);
574
665
  // eslint-disable-next-line no-console
575
- console.warn(`[iicp-node] deregister failed: ${exc instanceof Error ? exc.message : exc}`);
666
+ console.warn(`[iicp-node] deregister failed: ${deregMsg}`);
667
+ (0, node_log_js_1.writeNodeEvent)(nodeId, "deregister_fail", `error=${deregMsg}`, logDir);
576
668
  }
577
669
  stop();
670
+ instanceLock.release(); // #405 — free the pidfile on shutdown
578
671
  resolve();
579
672
  };
580
673
  process.once("SIGINT", () => void shutdown("SIGINT"));
@@ -584,16 +677,84 @@ async function runServe(opts) {
584
677
  void node_crypto_1.randomUUID;
585
678
  return 0;
586
679
  }
680
+ async function runQuery(argv) {
681
+ const { values, positionals } = (0, node_util_1.parseArgs)({
682
+ args: argv,
683
+ options: {
684
+ "directory-url": { type: "string" },
685
+ intent: { type: "string" },
686
+ model: { type: "string" },
687
+ "max-tokens": { type: "string" },
688
+ "timeout-ms": { type: "string" },
689
+ },
690
+ allowPositionals: true,
691
+ strict: false,
692
+ });
693
+ const prompt = positionals.join(" ").trim();
694
+ if (!prompt) {
695
+ process.stderr.write("Usage: iicp-node query <prompt> [flags]\n");
696
+ return 1;
697
+ }
698
+ const directoryUrl = values["directory-url"] ??
699
+ process.env["IICP_DIRECTORY_URL"] ??
700
+ "https://iicp.network/api";
701
+ const intent = values["intent"] ??
702
+ process.env["IICP_INTENT"] ??
703
+ "urn:iicp:intent:llm:chat:v1";
704
+ const timeoutMs = parseInt(values["timeout-ms"] ?? "60000", 10);
705
+ const payload = {
706
+ messages: [{ role: "user", content: prompt }],
707
+ };
708
+ if (values["model"])
709
+ payload["model"] = values["model"];
710
+ if (values["max-tokens"])
711
+ payload["max_tokens"] = parseInt(values["max-tokens"], 10);
712
+ const client = new client_js_1.IicpClient({ directory_url: directoryUrl, timeout_ms: timeoutMs, tls_verify: true });
713
+ process.stderr.write(`[iicp-node] Discovering nodes for ${intent}...\n`);
714
+ try {
715
+ const resp = await client.submit({
716
+ task_id: (0, node_crypto_1.randomUUID)(),
717
+ intent,
718
+ payload,
719
+ });
720
+ if (resp.status === "completed" && resp.result) {
721
+ const res = resp.result;
722
+ const content = typeof res["content"] === "string"
723
+ ? res["content"]
724
+ : JSON.stringify(resp.result, null, 2);
725
+ process.stdout.write(content + "\n");
726
+ if (resp.metrics?.node_id) {
727
+ process.stderr.write(`[iicp-node] routed to node ${resp.metrics.node_id.slice(0, 8)}\n`);
728
+ }
729
+ if (resp.metrics?.latency_ms != null) {
730
+ process.stderr.write(`[iicp-node] latency ${resp.metrics.latency_ms.toFixed(0)}ms\n`);
731
+ }
732
+ return 0;
733
+ }
734
+ process.stderr.write(`[iicp-node] task status: ${resp.status}\n`);
735
+ return 1;
736
+ }
737
+ catch (e) {
738
+ process.stderr.write(`ERROR: ${e}\n`);
739
+ return 1;
740
+ }
741
+ }
587
742
  async function main(argv = process.argv.slice(2)) {
588
743
  if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
589
744
  printHelp();
590
745
  return argv.length === 0 ? 2 : 0;
591
746
  }
747
+ if (argv[0] === "--version" || argv[0] === "-V") {
748
+ process.stdout.write(`iicp-node ${SDK_VERSION}\n`);
749
+ return 0;
750
+ }
592
751
  const cmd = argv[0];
593
752
  if (cmd === "init")
594
753
  return runInit();
595
754
  if (cmd === "list")
596
755
  return runList();
756
+ if (cmd === "query")
757
+ return runQuery(argv.slice(1));
597
758
  if (cmd !== "serve") {
598
759
  process.stderr.write(`unknown command: ${cmd}\n`);
599
760
  printHelp();
@@ -605,6 +766,7 @@ async function main(argv = process.argv.slice(2)) {
605
766
  node: { type: "string" },
606
767
  "backend-url": { type: "string" },
607
768
  "backend-type": { type: "string" },
769
+ "backend-api-key": { type: "string" },
608
770
  model: { type: "string" },
609
771
  "public-endpoint": { type: "string" },
610
772
  "directory-url": { type: "string" },
@@ -615,9 +777,11 @@ async function main(argv = process.argv.slice(2)) {
615
777
  port: { type: "string" },
616
778
  host: { type: "string" },
617
779
  "skip-registration": { type: "boolean" },
780
+ force: { type: "boolean" },
618
781
  "auto-detect-nat": { type: "boolean" },
619
782
  "external-ip-probe-url": { type: "string" },
620
783
  "relay-worker-endpoint": { type: "string" },
784
+ "log-dir": { type: "string" },
621
785
  help: { type: "boolean", short: "h" },
622
786
  },
623
787
  allowPositionals: false,
@@ -631,6 +795,7 @@ async function main(argv = process.argv.slice(2)) {
631
795
  backendUrl: values["backend-url"] ?? envOr("IICP_BACKEND_URL") ?? "",
632
796
  backendType: values["backend-type"] ??
633
797
  envOr("IICP_BACKEND_TYPE", "openai_compat"),
798
+ backendApiKey: values["backend-api-key"] ?? envOr("IICP_BACKEND_API_KEY") ?? "",
634
799
  model: values.model ?? envOr("IICP_BACKEND_MODEL") ?? "",
635
800
  publicEndpoint: values["public-endpoint"] ?? envOr("IICP_PUBLIC_ENDPOINT") ?? "",
636
801
  directoryUrl: values["directory-url"] ??
@@ -644,8 +809,9 @@ async function main(argv = process.argv.slice(2)) {
644
809
  port: values.port !== undefined
645
810
  ? parseInt(values.port, 10)
646
811
  : envInt("IICP_PORT", 9484),
647
- host: values.host ?? envOr("IICP_HOST", "0.0.0.0"),
812
+ host: values.host ?? envOr("IICP_HOST", "::"),
648
813
  skipRegistration: Boolean(values["skip-registration"]) || envBool("IICP_SKIP_REGISTRATION"),
814
+ force: Boolean(values["force"]) || envBool("IICP_FORCE"),
649
815
  // Default ON — matches Python CLI behaviour; operator must set IICP_AUTO_DETECT_NAT=false to opt out.
650
816
  autoDetectNat: values["auto-detect-nat"] !== undefined
651
817
  ? Boolean(values["auto-detect-nat"])
@@ -655,6 +821,7 @@ async function main(argv = process.argv.slice(2)) {
655
821
  ?? envOr("IICP_EXTERNAL_IP_PROBE_URL")
656
822
  ?? "https://api.ipify.org",
657
823
  relayWorkerEndpoint: values["relay-worker-endpoint"] ?? envOr("IICP_RELAY_WORKER_ENDPOINT") ?? "",
824
+ logDir: values["log-dir"] ?? envOr("IICP_LOG_DIR"),
658
825
  };
659
826
  return runServe(opts);
660
827
  }