@iicp/client 0.7.101 → 0.7.102

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.
Files changed (49) hide show
  1. package/LICENSE +83 -61
  2. package/README.md +2 -2
  3. package/dist/backends/openai_compat.d.ts +3 -0
  4. package/dist/backends/openai_compat.d.ts.map +1 -1
  5. package/dist/backends/openai_compat.js +119 -0
  6. package/dist/backends/openai_compat.js.map +1 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +236 -34
  9. package/dist/cli.js.map +1 -1
  10. package/dist/iicp_tcp.d.ts +38 -0
  11. package/dist/iicp_tcp.d.ts.map +1 -1
  12. package/dist/iicp_tcp.js +164 -0
  13. package/dist/iicp_tcp.js.map +1 -1
  14. package/dist/index.d.ts +4 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +10 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/mcp_negotiation.d.ts.map +1 -1
  19. package/dist/mcp_negotiation.js +13 -0
  20. package/dist/mcp_negotiation.js.map +1 -1
  21. package/dist/native_call_identity.d.ts +12 -0
  22. package/dist/native_call_identity.d.ts.map +1 -0
  23. package/dist/native_call_identity.js +44 -0
  24. package/dist/native_call_identity.js.map +1 -0
  25. package/dist/native_response_sequence.d.ts +32 -0
  26. package/dist/native_response_sequence.d.ts.map +1 -0
  27. package/dist/native_response_sequence.js +59 -0
  28. package/dist/native_response_sequence.js.map +1 -0
  29. package/dist/node.d.ts +5 -1
  30. package/dist/node.d.ts.map +1 -1
  31. package/dist/node.js +59 -7
  32. package/dist/node.js.map +1 -1
  33. package/dist/operator_profile.d.ts +18 -0
  34. package/dist/operator_profile.d.ts.map +1 -0
  35. package/dist/operator_profile.js +22 -0
  36. package/dist/operator_profile.js.map +1 -0
  37. package/dist/relay_session.d.ts +16 -4
  38. package/dist/relay_session.d.ts.map +1 -1
  39. package/dist/relay_session.js +116 -1
  40. package/dist/relay_session.js.map +1 -1
  41. package/dist/runtime_health.d.ts +77 -0
  42. package/dist/runtime_health.d.ts.map +1 -0
  43. package/dist/runtime_health.js +65 -0
  44. package/dist/runtime_health.js.map +1 -0
  45. package/dist/service.d.ts +7 -0
  46. package/dist/service.d.ts.map +1 -1
  47. package/dist/service.js +44 -0
  48. package/dist/service.js.map +1 -1
  49. package/package.json +6 -3
package/dist/cli.js CHANGED
@@ -44,6 +44,7 @@ const node_child_process_1 = require("node:child_process");
44
44
  const readline = require("node:readline/promises");
45
45
  const node_process_1 = require("node:process");
46
46
  const node_js_1 = require("./node.js");
47
+ const runtime_health_js_1 = require("./runtime_health.js");
47
48
  const client_js_1 = require("./client.js");
48
49
  const node_log_js_1 = require("./node_log.js");
49
50
  const cip_policy_js_1 = require("./cip_policy.js");
@@ -53,6 +54,8 @@ const index_js_2 = require("./backends/index.js");
53
54
  const recovery_js_1 = require("./recovery.js");
54
55
  const identity_js_1 = require("./identity.js");
55
56
  const delegation_js_1 = require("./delegation.js");
57
+ const operator_profile_js_1 = require("./operator_profile.js");
58
+ const updater_js_1 = require("./updater.js");
56
59
  const mcp_policy_js_1 = require("./mcp_policy.js");
57
60
  const mcp_negotiation_js_1 = require("./mcp_negotiation.js");
58
61
  /**
@@ -170,6 +173,7 @@ function printHelp() {
170
173
  ` list List node configs saved under ~/.iicp/nodes/\n` +
171
174
  ` serve Register and serve a node\n` +
172
175
  ` doctor Check local health, directory presence, and recovery action\n` +
176
+ ` healthcheck Check local runtime liveness/readiness snapshot\n` +
173
177
  ` query <prompt> Discover mesh nodes and submit a chat task\n` +
174
178
  ` credits Show your operator wallet plus this node's credit ledger\n` +
175
179
  ` proxy Run the local OpenAI/Ollama/Anthropic-compat gateway (loopback; no registration)\n` +
@@ -693,18 +697,6 @@ async function runServe(opts) {
693
697
  allowCoordinator: true,
694
698
  });
695
699
  }
696
- // 2-C: co-host the compat proxy on loopback alongside the node, supervised so a
697
- // proxy failure logs but never drops the network-facing node. Forced to 127.0.0.1.
698
- if (opts.withProxy) {
699
- const pport = envInt("IICP_PROXY_PORT", 9483);
700
- const pclient = new client_js_1.IicpClient({
701
- directory_url: opts.directoryUrl,
702
- region: opts.region,
703
- });
704
- const pserver = (0, index_js_1.createProxyServer)(pclient);
705
- pserver.on("error", (e) => process.stderr.write(`co-hosted proxy error (node continues): ${String(e)}\n`));
706
- pserver.listen(pport, "127.0.0.1", () => process.stdout.write(`co-hosted proxy → http://127.0.0.1:${pport} (OpenAI/Ollama/Anthropic compat)\n`));
707
- }
708
700
  // Phase 2 (#529/#55) — capture any cached node_token so the node can prove
709
701
  // ownership on re-registration (IICP-E050 token path).
710
702
  let savedNodeToken;
@@ -717,6 +709,31 @@ async function runServe(opts) {
717
709
  savedNodeToken = saved.node_token ?? undefined;
718
710
  opts = applySavedNode(opts, saved);
719
711
  }
712
+ const _op = (0, identity_js_1.loadOperator)();
713
+ const managedDecision = (0, operator_profile_js_1.evaluateManagedOperator)({
714
+ mode: (process.env.IICP_OPERATOR_PROFILE ?? "convenience").trim().toLowerCase(),
715
+ authentication_configured: !!_op && (0, identity_js_1.operatorIsKeyBacked)(_op),
716
+ identity_storage_protected: !!_op && (0, identity_js_1.operatorIsKeyBacked)(_op) && (0, identity_js_1.operatorIsEncrypted)(_op),
717
+ auto_update_requested: (0, updater_js_1.autoUpdateEnabled)(),
718
+ update_authenticated: envBool("IICP_MANAGED_UPDATE_AUTHENTICATED"),
719
+ rollback_verified: envBool("IICP_MANAGED_ROLLBACK_VERIFIED"),
720
+ upnp_requested: opts.autoDetectNat && !envBool("IICP_SKIP_UPNP"),
721
+ tunnel_requested: opts.tunnel !== false,
722
+ upnp_approved: envBool("IICP_MANAGED_UPNP_APPROVED"),
723
+ tunnel_approved: envBool("IICP_MANAGED_TUNNEL_APPROVED"),
724
+ });
725
+ if (!managedDecision.accepted) {
726
+ process.stderr.write(`ERROR: managed operator startup rejected: ${managedDecision.reason}\n`);
727
+ return 2;
728
+ }
729
+ // Start the optional co-hosted listener only after the operator profile passes.
730
+ if (opts.withProxy) {
731
+ const pport = envInt("IICP_PROXY_PORT", 9483);
732
+ const pclient = new client_js_1.IicpClient({ directory_url: opts.directoryUrl, region: opts.region });
733
+ const pserver = (0, index_js_1.createProxyServer)(pclient);
734
+ pserver.on("error", (e) => process.stderr.write(`co-hosted proxy error (node continues): ${String(e)}\n`));
735
+ pserver.listen(pport, "127.0.0.1", () => process.stdout.write(`co-hosted proxy → http://127.0.0.1:${pport} (OpenAI/Ollama/Anthropic compat)\n`));
736
+ }
720
737
  // #410/#414 — built-in backend-url fallback applied LAST (after flag/env/saved-config),
721
738
  // so a bare `serve --model x` works without --backend-url. An `anthropic` backend
722
739
  // defaults to the Anthropic API, not localhost Ollama. Mirrors Python cli.py ~704.
@@ -984,7 +1001,6 @@ async function runServe(opts) {
984
1001
  // #463/#464 — bind the operator identity: issue a delegation FROM the (key-backed) operator
985
1002
  // identity for this node and advertise the public display_name. The directory verifies the
986
1003
  // delegation (operator_pub == operator_id) and records the operator. Never sends the secret/contact.
987
- const _op = (0, identity_js_1.loadOperator)();
988
1004
  let _opDelegation;
989
1005
  let _opDisplayName;
990
1006
  let _opCreatedAt;
@@ -1959,6 +1975,50 @@ async function runDoctor(argv) {
1959
1975
  process.stdout.write(" Note restart is automatic only when supervised services set IICP_SUPERVISED=1\n");
1960
1976
  return 0;
1961
1977
  }
1978
+ function runHealthcheck(argv) {
1979
+ const { values } = safeParseArgs({ args: argv, options: { node: { type: "string" }, json: { type: "boolean" }, ready: { type: "boolean" }, help: { type: "boolean", short: "h" } }, allowPositionals: false });
1980
+ if (values.help) {
1981
+ process.stdout.write("usage: iicp-node healthcheck --node NAME [--json] [--ready]\n");
1982
+ return 0;
1983
+ }
1984
+ const node = values.node ?? process.env.IICP_NODE_NAME;
1985
+ if (!node) {
1986
+ process.stderr.write("ERROR: healthcheck requires --node NAME\n");
1987
+ return 2;
1988
+ }
1989
+ let file;
1990
+ try {
1991
+ file = (0, runtime_health_js_1.runtimeHealthPath)(node);
1992
+ }
1993
+ catch (error) {
1994
+ process.stderr.write(`ERROR: ${error instanceof Error ? error.message : String(error)}\n`);
1995
+ return 2;
1996
+ }
1997
+ let snapshot;
1998
+ try {
1999
+ snapshot = JSON.parse(fs.readFileSync(file, "utf8"));
2000
+ }
2001
+ catch (error) {
2002
+ process.stderr.write(`INDETERMINATE: runtime-health snapshot unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
2003
+ return 2;
2004
+ }
2005
+ const progress = snapshot.progress;
2006
+ const staleAfter = Number(progress?.runtime?.stale_after_ms ?? 0);
2007
+ const age = Date.now() - fs.statSync(file).mtimeMs;
2008
+ if (age < 0 || !Number.isFinite(staleAfter) || staleAfter <= 0) {
2009
+ process.stderr.write("INDETERMINATE: snapshot freshness could not be established\n");
2010
+ return 2;
2011
+ }
2012
+ if (values.json)
2013
+ process.stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
2014
+ else
2015
+ process.stdout.write(`IICP node health — ${node}\n liveness ${String(snapshot.liveness ?? "indeterminate")}\n readiness ${String(snapshot.readiness ?? "not_ready")}\n reasons ${Array.isArray(snapshot.reason_codes) ? snapshot.reason_codes.join(", ") : ""}\n`);
2016
+ if (age > staleAfter)
2017
+ return 1;
2018
+ if (values.ready)
2019
+ return snapshot.readiness === "ready" ? 0 : 1;
2020
+ return snapshot.liveness === "live" ? 0 : snapshot.liveness === "not_live" ? 1 : 2;
2021
+ }
1962
2022
  /**
1963
2023
  * Resolve a passphrase: $IICP_OPERATOR_PASSPHRASE if set (headless/CI), else an interactive
1964
2024
  * readline prompt (this command is operator-run, so a prompt is fine here — only `serve` must
@@ -2687,22 +2747,126 @@ async function runMcpGateway(argv) {
2687
2747
  }
2688
2748
  }
2689
2749
  let mcpRpcId = 0;
2690
- async function callMcp(toolName, args) {
2750
+ let legacySessionId;
2751
+ let legacyInitialization;
2752
+ class McpLegacySessionExpired extends Error {
2753
+ }
2754
+ async function mcpResponseJson(response) {
2755
+ const raw = await response.text();
2756
+ if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
2757
+ const events = raw.split(/\r?\n/).filter((line) => line.startsWith("data: ")).map((line) => line.slice(6));
2758
+ if (!events.length)
2759
+ throw new Error("MCP server returned an empty event stream");
2760
+ return JSON.parse(events.at(-1) ?? "");
2761
+ }
2762
+ return JSON.parse(raw);
2763
+ }
2764
+ async function initializeLegacySession() {
2765
+ if (legacyInitialization)
2766
+ return legacyInitialization;
2767
+ legacyInitialization = (async () => {
2768
+ const id = ++mcpRpcId;
2769
+ const headers = {
2770
+ "Content-Type": "application/json",
2771
+ "MCP-Protocol-Version": mcp_negotiation_js_1.LEGACY_MCP_REVISION,
2772
+ "Accept": "application/json, text/event-stream",
2773
+ };
2774
+ const init = await fetch(`${mcpUrl}/mcp`, {
2775
+ method: "POST",
2776
+ headers,
2777
+ body: JSON.stringify({
2778
+ jsonrpc: "2.0", id, method: "initialize",
2779
+ params: { protocolVersion: mcp_negotiation_js_1.LEGACY_MCP_REVISION, capabilities: {}, clientInfo: { name: "iicp-mcp-gateway", version: "0.7" } },
2780
+ }),
2781
+ signal: AbortSignal.timeout(30_000),
2782
+ });
2783
+ if (!init.ok)
2784
+ throw new Error(`MCP legacy initialization failed: ${init.status}`);
2785
+ const initData = await mcpResponseJson(init);
2786
+ if (initData["error"])
2787
+ throw new Error("MCP server rejected legacy initialization");
2788
+ const sessionId = init.headers.get("mcp-session-id");
2789
+ if (!sessionId)
2790
+ throw new Error("MCP server did not return a legacy session identifier");
2791
+ const initialized = await fetch(`${mcpUrl}/mcp`, {
2792
+ method: "POST",
2793
+ headers: { ...headers, "Mcp-Session-Id": sessionId },
2794
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }),
2795
+ signal: AbortSignal.timeout(30_000),
2796
+ });
2797
+ if (!initialized.ok)
2798
+ throw new Error(`MCP legacy initialized notification failed: ${initialized.status}`);
2799
+ legacySessionId = sessionId;
2800
+ })();
2801
+ try {
2802
+ await legacyInitialization;
2803
+ }
2804
+ finally {
2805
+ legacyInitialization = undefined;
2806
+ }
2807
+ }
2808
+ async function legacyToolCall(rpc, replaySafe) {
2809
+ if (!legacySessionId)
2810
+ await initializeLegacySession();
2811
+ const send = async () => {
2812
+ if (!legacySessionId)
2813
+ throw new Error("legacy MCP session identifier is unavailable");
2814
+ const response = await fetch(`${mcpUrl}/mcp`, {
2815
+ method: "POST",
2816
+ headers: {
2817
+ "Content-Type": "application/json",
2818
+ "MCP-Protocol-Version": mcp_negotiation_js_1.LEGACY_MCP_REVISION,
2819
+ "Mcp-Session-Id": legacySessionId,
2820
+ "Accept": "application/json, text/event-stream",
2821
+ },
2822
+ body: JSON.stringify(rpc),
2823
+ signal: AbortSignal.timeout(30_000),
2824
+ });
2825
+ if (response.status === 401 || response.status === 404) {
2826
+ legacySessionId = undefined;
2827
+ throw new McpLegacySessionExpired("MCP legacy session expired");
2828
+ }
2829
+ if (!response.ok)
2830
+ throw new Error(`MCP server unreachable: ${response.status}`);
2831
+ return mcpResponseJson(response);
2832
+ };
2833
+ try {
2834
+ return await send();
2835
+ }
2836
+ catch (error) {
2837
+ if (!(error instanceof McpLegacySessionExpired))
2838
+ throw error;
2839
+ if (!replaySafe)
2840
+ throw new McpLegacySessionExpired("MCP legacy session expired; caller retry required");
2841
+ await initializeLegacySession();
2842
+ try {
2843
+ return await send();
2844
+ }
2845
+ catch (retryError) {
2846
+ if (retryError instanceof McpLegacySessionExpired)
2847
+ throw new Error("MCP legacy session expired after one reinitialization");
2848
+ throw retryError;
2849
+ }
2850
+ }
2851
+ }
2852
+ async function callMcp(toolName, args, replaySafe = false) {
2691
2853
  mcpRpcId += 1;
2692
2854
  const params = { name: toolName, arguments: args };
2693
2855
  const modern = mcpRevision === mcp_negotiation_js_1.MODERN_MCP_REVISION
2694
2856
  ? (0, mcp_negotiation_js_1.buildModernMcpRequest)({ requestId: mcpRpcId, method: "tools/call", name: toolName, params, extensions: mcpExtensions })
2695
2857
  : null;
2696
2858
  const rpc = modern?.body ?? { jsonrpc: "2.0", id: mcpRpcId, method: "tools/call", params };
2697
- const resp = await fetch(`${mcpUrl}/mcp`, {
2698
- method: "POST",
2699
- headers: { "Content-Type": "application/json", ...(modern?.headers ?? {}) },
2700
- body: JSON.stringify(rpc),
2701
- signal: AbortSignal.timeout(30_000),
2702
- });
2703
- if (!resp.ok)
2704
- throw new Error(`MCP server unreachable: ${resp.status}`);
2705
- const data = await resp.json();
2859
+ const data = modern
2860
+ ? await (async () => {
2861
+ const resp = await fetch(`${mcpUrl}/mcp`, {
2862
+ method: "POST", headers: { "Content-Type": "application/json", ...modern.headers },
2863
+ body: JSON.stringify(rpc), signal: AbortSignal.timeout(30_000),
2864
+ });
2865
+ if (!resp.ok)
2866
+ throw new Error(`MCP server unreachable: ${resp.status}`);
2867
+ return resp.json();
2868
+ })()
2869
+ : await legacyToolCall(rpc, replaySafe);
2706
2870
  if (modern)
2707
2871
  (0, mcp_negotiation_js_1.validateModernMcpResponse)(data, mcpServerName);
2708
2872
  if (data["error"])
@@ -2780,15 +2944,15 @@ async function runMcpGateway(argv) {
2780
2944
  }
2781
2945
  const taskId = body["task_id"] ?? (0, node_crypto_1.randomUUID)();
2782
2946
  try {
2783
- const result = await callMcp(toolName, args);
2947
+ const result = await callMcp(toolName, args, payload["mcp_replay_safe"] === true);
2784
2948
  res.writeHead(200, { "Content-Type": "application/json" });
2785
2949
  res.end(JSON.stringify({ task_id: taskId, status: "completed", result, policy_receipt: toolPolicy.receipt(toolName, "allowed", argumentCount) }));
2786
2950
  }
2787
2951
  catch (err) {
2788
2952
  const msg = err.message ?? "error";
2789
- const code = msg.includes("unreachable") ? 502 : 422;
2953
+ const code = err instanceof McpLegacySessionExpired ? 409 : msg.includes("unreachable") ? 502 : 422;
2790
2954
  res.writeHead(code, { "Content-Type": "application/json" });
2791
- res.end(JSON.stringify({ error: msg }));
2955
+ res.end(JSON.stringify(err instanceof McpLegacySessionExpired ? { error: "mcp_session_expired_retry_required", retryable: true } : { error: msg }));
2792
2956
  }
2793
2957
  return;
2794
2958
  }
@@ -2846,7 +3010,8 @@ async function runService(argv) {
2846
3010
  ` --node NAME Saved node name to serve (required)\n` +
2847
3011
  ` --name NAME Override service label/unit name\n` +
2848
3012
  ` --platform KIND auto | launchd | systemd (default auto)\n` +
2849
- ` --dry-run For install: print the generated unit without writing files\n`);
3013
+ ` --no-start Install and enable without starting the service\n` +
3014
+ ` --dry-run Print unit/actions without changing files or service state\n`);
2850
3015
  return subcmd ? 0 : 2;
2851
3016
  }
2852
3017
  if (!["install", "status", "restart", "uninstall"].includes(subcmd)) {
@@ -2859,6 +3024,7 @@ async function runService(argv) {
2859
3024
  name: { type: "string" },
2860
3025
  platform: { type: "string" },
2861
3026
  "dry-run": { type: "boolean" },
3027
+ "no-start": { type: "boolean" },
2862
3028
  help: { type: "boolean", short: "h" },
2863
3029
  },
2864
3030
  allowPositionals: false,
@@ -2868,17 +3034,37 @@ async function runService(argv) {
2868
3034
  const node = values.node;
2869
3035
  if (!node)
2870
3036
  throw new CliError("service requires --node NAME");
2871
- const { renderServiceUnit } = await import("./service.js");
3037
+ const { managerActions, renderServiceUnit } = await import("./service.js");
2872
3038
  const unit = renderServiceUnit(node, values.name, values.platform ?? "auto");
3039
+ const dryRun = values["dry-run"] === true;
3040
+ const noStart = values["no-start"] === true;
3041
+ const printAction = (command, args) => process.stdout.write(`$ ${[command, ...args].map((part) => JSON.stringify(part)).join(" ")}\n`);
3042
+ const execute = (operation) => {
3043
+ for (const action of managerActions(unit, operation, noStart)) {
3044
+ if (dryRun) {
3045
+ printAction(action.command, action.args);
3046
+ continue;
3047
+ }
3048
+ const result = (0, node_child_process_1.spawnSync)(action.command, action.args, { stdio: "inherit" });
3049
+ if (result.error && !action.tolerateFailure)
3050
+ throw new CliError(`${action.command} failed: ${result.error.message}`);
3051
+ if ((result.status ?? (result.error ? 1 : 0)) !== 0 && !action.tolerateFailure) {
3052
+ throw new CliError(`${action.command} exited with status ${result.status ?? "unknown"}`);
3053
+ }
3054
+ }
3055
+ };
2873
3056
  if (subcmd === "install") {
2874
- if (values["dry-run"]) {
3057
+ if (dryRun) {
2875
3058
  process.stdout.write(`# ${unit.platform} service: ${unit.name}\n# path: ${unit.path}\n${unit.content}`);
2876
3059
  }
2877
3060
  else {
2878
3061
  fs.mkdirSync(path.dirname(unit.path), { recursive: true });
2879
- fs.writeFileSync(unit.path, unit.content);
3062
+ const temporary = `${unit.path}.tmp-${process.pid}`;
3063
+ fs.writeFileSync(temporary, unit.content, { mode: 0o600 });
3064
+ fs.renameSync(temporary, unit.path);
2880
3065
  process.stdout.write(`Installed ${unit.platform} service unit: ${unit.path}\n`);
2881
3066
  }
3067
+ execute("install");
2882
3068
  process.stdout.write(`status: ${unit.statusHint}\n`);
2883
3069
  process.stdout.write(`restart: ${unit.restartHint}\n`);
2884
3070
  process.stdout.write(`logs: ${unit.logHint}\n`);
@@ -2886,11 +3072,25 @@ async function runService(argv) {
2886
3072
  return 0;
2887
3073
  }
2888
3074
  if (subcmd === "status")
2889
- process.stdout.write(`${unit.statusHint}\n`);
3075
+ execute("status");
2890
3076
  if (subcmd === "restart")
2891
- process.stdout.write(`${unit.restartHint}\n`);
2892
- if (subcmd === "uninstall")
2893
- process.stdout.write(`${unit.uninstallHint}\n`);
3077
+ execute("restart");
3078
+ if (subcmd === "uninstall") {
3079
+ execute("uninstall");
3080
+ if (dryRun)
3081
+ process.stdout.write(`$ ${JSON.stringify("rm")} ${JSON.stringify("-f")} ${JSON.stringify(unit.path)}\n`);
3082
+ else
3083
+ fs.rmSync(unit.path, { force: true });
3084
+ if (unit.platform === "systemd") {
3085
+ if (dryRun)
3086
+ printAction("systemctl", ["--user", "daemon-reload"]);
3087
+ else {
3088
+ const result = (0, node_child_process_1.spawnSync)("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
3089
+ if (result.error || result.status !== 0)
3090
+ throw new CliError("systemctl --user daemon-reload failed after uninstall");
3091
+ }
3092
+ }
3093
+ }
2894
3094
  return 0;
2895
3095
  }
2896
3096
  async function dispatch(argv) {
@@ -2905,6 +3105,8 @@ async function dispatch(argv) {
2905
3105
  return runCredits(argv.slice(1));
2906
3106
  if (cmd === "doctor")
2907
3107
  return runDoctor(argv.slice(1));
3108
+ if (cmd === "healthcheck")
3109
+ return runHealthcheck(argv.slice(1));
2908
3110
  if (cmd === "operator")
2909
3111
  return runOperator(argv.slice(1));
2910
3112
  if (cmd === "proxy")