@easynet-run/node 0.36.9 → 0.39.29

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
@@ -33,9 +33,34 @@ The SDK ships a native Dendrite bridge binary (loaded via `koffi`) that provides
33
33
  npm install @easynet-run/node
34
34
  ```
35
35
 
36
+ ## Platform / Native Bundle Notes
37
+
38
+ - The published npm artifact currently comes from the Linux x86_64 SDK pack, so the embedded native file is host-matched to that pack.
39
+ - If you need other native targets (iOS/Android/macOS/Linux arm64/Windows), use the target SDK pack from GitHub Releases:
40
+ - `sdk-packs-<target>.tar.gz`
41
+ - extract the `node/` directory and install the `*.tgz` artifact from that directory.
42
+ - For local source development:
43
+ - `SDK_VERSION=<version>` (optional; resolver checks this and then fallback `0.1.0` in the same root)
44
+ - `EASYNET_DENDRITE_BRIDGE_LIB` can force an explicit native path.
45
+ - `EASYNET_DENDRITE_BRIDGE_HOME=<pack-root>` can be set to a shared extracted SDK pack path that contains `native/`.
46
+ - set `EASYNET_DENDRITE_BRIDGE_SOURCE=local` if you want preset/CLI helpers to auto-probe local source-tree bridge artifacts.
47
+ - `EASYNET_DENDRITE_BRIDGE_PLATFORM=ios|android|linux|windows|macos` if host/build target mismatch (for example macOS host + iOS pack).
48
+ - For macOS-hosted iOS dev workflows, prefer a pack-native `.a` and explicit `EASYNET_DENDRITE_BRIDGE_LIB` path binding.
49
+
50
+ Target examples:
51
+
52
+ - macOS arm64: `sdk-packs-aarch64-apple-darwin.tar.gz`
53
+ - macOS x86_64: `sdk-packs-x86_64-apple-darwin.tar.gz`
54
+ - iOS arm64 simulator/device: `sdk-packs-aarch64-apple-ios.tar.gz`
55
+ - iOS x86_64 simulator: `sdk-packs-x86_64-apple-ios.tar.gz`
56
+ - Android arm64: `sdk-packs-aarch64-linux-android.tar.gz`
57
+ - Linux arm64: `sdk-packs-aarch64-unknown-linux-gnu.tar.gz`
58
+ - Linux x86_64: `sdk-packs-x86_64-unknown-linux-gnu.tar.gz`
59
+ - Windows x64: `sdk-packs-x86_64-pc-windows-msvc.tar.gz`
60
+
36
61
  ## Development
37
62
 
38
- Runtime `.js` files and package-facing `.d.ts` files under `src/` are generated from the TypeScript sources with `tsc`.
63
+ Runtime `.js` files and package-facing `.d.ts` files under `src/` are generated locally from the TypeScript sources with `tsc`, but they are intentionally gitignored and must not be committed.
39
64
  Edit `src/**/*.ts`, then run:
40
65
 
41
66
  ```bash
@@ -43,7 +68,7 @@ npm run build
43
68
  npm run generated:check
44
69
  ```
45
70
 
46
- Do not hand-edit generated `src/**/*.js` or `src/**/*.d.ts` files.
71
+ `npm pack` and git-based installs run `prepare`/`prepack`, so published tarballs still include the generated runtime files.
47
72
 
48
73
  ## Quick Start
49
74
 
@@ -106,7 +131,7 @@ No public IP required — the local runtime connects outbound to the Hub and rec
106
131
 
107
132
  Full lifecycle management — not just invocation:
108
133
 
109
- - `createAbility()` / `exportAbility()` — define and register abilities with schemas
134
+ - `buildAbilityDescriptor()` / `exportAbility()` — define and register abilities with schemas
110
135
  - `deployToNode()` — install + activate on target nodes
111
136
  - `listAbilities()` / `invokeAbility()` / `uninstallAbility()`
112
137
  - `discoverNodes()` / `executeCommand()` / `disconnectDevice()` / `drainDevice()`
@@ -131,6 +156,7 @@ First-class voice call lifecycle and transport negotiation (19+ FFI bindings):
131
156
 
132
157
  - `startServer()` spawns a local Axon runtime and joins the Hub — all traffic is outbound.
133
158
  - Federated node discovery and cross-network invocation dispatch.
159
+ - Preset status: the dedicated federation preset helper is currently Python-only; Node exposes federation through runtime bootstrap and transport-facing APIs.
134
160
 
135
161
  ### Remote Control & Orchestration
136
162
 
@@ -1,11 +1,11 @@
1
1
  {
2
- "dendrite_bridge_version": "0.36.9",
2
+ "dendrite_bridge_version": "0.39.29",
3
3
  "target": "x86_64-unknown-linux-gnu",
4
4
  "os": "linux",
5
5
  "arch": "x86_64",
6
6
  "library": {
7
7
  "path": "native/libaxon_dendrite_bridge.so",
8
- "sha256": "e802bd6d15684b1b3f4c7fd2fc7159b591aaa93c58de78c369cac5d0315f7688"
8
+ "sha256": "65e28058d2937cc457b28eb52e86c8f0ad376ce00c0e524cd49eac3372fb8ddf"
9
9
  },
10
10
  "header": {
11
11
  "path": "native/include/axon_dendrite_bridge.h",
@@ -35,5 +35,5 @@
35
35
  "axon_dendrite_protocol_coverage_json",
36
36
  "axon_dendrite_string_free"
37
37
  ],
38
- "generated_at_utc": "2026-03-27T17:24:23Z"
38
+ "generated_at_utc": "2026-03-30T16:23:47Z"
39
39
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "language": "node",
3
- "dendrite_bridge_version": "0.36.9",
3
+ "dendrite_bridge_version": "0.39.29",
4
4
  "target": "x86_64-unknown-linux-gnu",
5
5
  "dendrite_manifest_path": "native/dendrite-bridge-manifest.json",
6
6
  "dendrite_library_path": "native/libaxon_dendrite_bridge.so",
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easynet-run/node",
3
- "version": "0.36.9",
3
+ "version": "0.39.29",
4
4
  "description": "EasyNet Axon Node.js SDK (sidecar-first with Dendrite bridge payload).",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://easynet.run",
@@ -15,14 +15,16 @@
15
15
  },
16
16
  "type": "module",
17
17
  "scripts": {
18
- "build": "tsc -p tsconfig.json",
18
+ "build": "node ./scripts/clean-generated.mjs && tsc -p tsconfig.json",
19
19
  "check": "tsc -p tsconfig.json --noEmit",
20
20
  "types:check": "tsc -p tsconfig.types.json --noEmit",
21
21
  "types:build": "tsc -p tsconfig.types.json",
22
22
  "types:verify": "npm run types:build && node ./scripts/verify-dist-types.mjs",
23
23
  "generated:check": "npm run build && node ./scripts/verify-generated.mjs",
24
24
  "types": "npm run check && npm run types:verify",
25
- "verify": "npm run types && npm run generated:check"
25
+ "verify": "npm run types && npm run generated:check",
26
+ "prepare": "npm run build",
27
+ "prepack": "npm run build"
26
28
  },
27
29
  "main": "src/index.js",
28
30
  "types": "src/index.d.ts",
@@ -1,11 +1,11 @@
1
1
  {
2
- "runtime_version": "0.36.9",
2
+ "runtime_version": "0.39.29",
3
3
  "target": "x86_64-unknown-linux-gnu",
4
4
  "os": "linux",
5
5
  "arch": "x86_64",
6
6
  "artifact": {
7
- "path": "runtime/easynet-runtime-rs-0.36.9-x86_64-unknown-linux-gnu.tar.gz",
8
- "sha256": "6d4c56411bae31ed11ec94face402c170d7a44d7f0686bac601440d25a9e98b2",
7
+ "path": "runtime/easynet-runtime-rs-0.39.29-x86_64-unknown-linux-gnu.tar.gz",
8
+ "sha256": "6234d8b5fa7d71e918c3648e8b627b93d767271f6486a003ffe836fb5ea3bfd1",
9
9
  "binary_name": "axon-runtime"
10
10
  },
11
11
  "language_bridge_descriptors": [
@@ -16,5 +16,5 @@
16
16
  "java/runtime-bridge.json",
17
17
  "swift/runtime-bridge.json"
18
18
  ],
19
- "generated_at_utc": "2026-03-27T17:24:23Z"
19
+ "generated_at_utc": "2026-03-30T16:23:47Z"
20
20
  }
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "language": "node",
3
- "runtime_version": "0.36.9",
3
+ "runtime_version": "0.39.29",
4
4
  "target": "x86_64-unknown-linux-gnu",
5
- "runtime_artifact_path": "runtime/easynet-runtime-rs-0.36.9-x86_64-unknown-linux-gnu.tar.gz",
5
+ "runtime_artifact_path": "runtime/easynet-runtime-rs-0.39.29-x86_64-unknown-linux-gnu.tar.gz",
6
6
  "binary_relative_path": "axon-runtime",
7
- "recommended_extract": "tar -xzf runtime/easynet-runtime-rs-0.36.9-x86_64-unknown-linux-gnu.tar.gz -C <runtime_dir>",
7
+ "recommended_extract": "tar -xzf runtime/easynet-runtime-rs-0.39.29-x86_64-unknown-linux-gnu.tar.gz -C <runtime_dir>",
8
8
  "recommended_launch": "AXON_ENFORCE_MTLS=false <runtime_dir>/axon-runtime"
9
9
  }
@@ -11,6 +11,11 @@ export interface AbilityDescriptor {
11
11
  version: string;
12
12
  tags: string[];
13
13
  resourceUri: string;
14
+ instructions: string;
15
+ inputExamples: unknown[];
16
+ prerequisites: string[];
17
+ contextBindings: Record<string, string>;
18
+ category: string;
14
19
  }
15
20
  export type AbilityTarget = "claude" | "codex" | "openclaw" | "agent_skills";
16
21
  export interface AbilityExportResult {
@@ -72,7 +77,7 @@ export declare function startServer(endpoint?: string, options?: {
72
77
  /** Pre-shared join token used for outbound federation authentication. */
73
78
  hubJoinToken?: string;
74
79
  }): Promise<ServerHandle>;
75
- export declare function createAbility(opts: {
80
+ export declare function buildAbilityDescriptor(opts: {
76
81
  name: string;
77
82
  description: string;
78
83
  commandTemplate: string;
@@ -81,6 +86,11 @@ export declare function createAbility(opts: {
81
86
  version?: string;
82
87
  tags?: string[];
83
88
  resourceUri?: string;
89
+ instructions?: string;
90
+ inputExamples?: unknown[];
91
+ prerequisites?: string[];
92
+ contextBindings?: Record<string, string>;
93
+ category?: string;
84
94
  }): AbilityDescriptor;
85
95
  export declare function toToolSpec(descriptor: AbilityDescriptor): ToolSpec;
86
96
  export declare function exportAbility(descriptor: AbilityDescriptor, target?: AbilityTarget, axonEndpoint?: string): AbilityExportResult;
@@ -2,7 +2,22 @@
2
2
  // =========================
3
3
  //
4
4
  // File: sdk/node/src/ability_lifecycle.ts
5
- // Description: Ability lifecycle API: create, deploy, invoke, and export as Agent Skills.
5
+ // Description: Node SDK ability lifecycle API for descriptor creation, deployment, invocation, and skill export workflows.
6
+ //
7
+ // Protocol Responsibility:
8
+ // - Defines typed ability descriptors, export targets, and deployment helpers exposed to SDK users.
9
+ // - Keeps cross-language ability naming, resource-uri, and tool-spec semantics aligned.
10
+ //
11
+ // Implementation Approach:
12
+ // - Separates pure descriptor/export helpers from bridge-backed deployment and invocation flows.
13
+ // - Uses explicit validation and stable defaults so generated ability artifacts stay deterministic.
14
+ //
15
+ // Usage Contract:
16
+ // - Callers must provide valid command templates, schema objects, and runtime context for deployment helpers.
17
+ // - Returned descriptors and exported skill artifacts should be treated as stable SDK-level contracts.
18
+ //
19
+ // Architectural Position:
20
+ // - High-level SDK facade above preset/orchestrator layers and below application examples.
6
21
  //
7
22
  // Author: Silan.Hu
8
23
  // Email: silan.hu@u.nus.edu
@@ -14,7 +29,7 @@ import { existsSync, mkdirSync, openSync } from "node:fs";
14
29
  import { hostname as localHostname } from "node:os";
15
30
  import { join, dirname } from "node:path";
16
31
  import { env } from "node:process";
17
- import { buildPythonSubprocessTemplate } from "./presets/remote_control/descriptor.js";
32
+ import { buildDescriptor as buildRemotePackageDescriptor, buildPythonSubprocessTemplate, serializeDescriptor as serializeRemotePackageDescriptor, } from "./presets/remote_control/descriptor.js";
18
33
  import { AxonConfigError, AxonBridgeError, AxonInvocationError, AxonPartialSuccessError } from "./errors.js";
19
34
  import { DEFAULT_SIGNATURE } from "./presets/remote_control/config.js";
20
35
  import { beginPhase, buildDeployTrace } from "./receipt.js";
@@ -39,7 +54,10 @@ function normalizeAbilityName(raw) {
39
54
  .replace(/[^a-z0-9_\-]/g, "-")
40
55
  .replace(/-{2,}/g, "-")
41
56
  .replace(/^-+|-+$/g, "");
42
- return result || "ability";
57
+ if (!result) {
58
+ throw new AxonConfigError(`identifier contains no valid characters: ${JSON.stringify(raw)}`);
59
+ }
60
+ return result;
43
61
  }
44
62
  function firstNonEmpty(...values) {
45
63
  for (const value of values) {
@@ -215,9 +233,9 @@ export async function startServer(endpoint, options = {}) {
215
233
  };
216
234
  }
217
235
  // ---------------------------------------------------------------------------
218
- // create
236
+ // build
219
237
  // ---------------------------------------------------------------------------
220
- export function createAbility(opts) {
238
+ export function buildAbilityDescriptor(opts) {
221
239
  if (!opts.name?.trim())
222
240
  throw new AxonConfigError("ability name cannot be empty");
223
241
  if (!opts.commandTemplate?.trim())
@@ -233,6 +251,11 @@ export function createAbility(opts) {
233
251
  version: opts.version ?? "1.0.0",
234
252
  tags: opts.tags ?? [],
235
253
  resourceUri: opts.resourceUri ?? `easynet:///r/org/${token}`,
254
+ instructions: opts.instructions ?? "",
255
+ inputExamples: opts.inputExamples ?? [],
256
+ prerequisites: opts.prerequisites ?? [],
257
+ contextBindings: opts.contextBindings ?? {},
258
+ category: opts.category ?? "",
236
259
  };
237
260
  }
238
261
  // ---------------------------------------------------------------------------
@@ -278,7 +301,7 @@ export function exportAbility(descriptor, target = "agent_skills", axonEndpoint)
278
301
  export async function deployToNode(bridge, tenant, nodeId, descriptor, signature) {
279
302
  const token = descriptor.toolName;
280
303
  const abilityId = token;
281
- const pkg = buildDeployPackage({
304
+ const deployArgs = {
282
305
  ability_name: descriptor.name,
283
306
  tool_name: token,
284
307
  description: descriptor.description,
@@ -286,8 +309,20 @@ export async function deployToNode(bridge, tenant, nodeId, descriptor, signature
286
309
  version: descriptor.version,
287
310
  input_schema: descriptor.inputSchema,
288
311
  output_schema: descriptor.outputSchema,
289
- tags: descriptor.tags.length > 0 ? descriptor.tags : undefined,
290
- }, signature);
312
+ };
313
+ if (descriptor.tags.length > 0)
314
+ deployArgs.tags = descriptor.tags;
315
+ if (descriptor.instructions)
316
+ deployArgs.instructions = descriptor.instructions;
317
+ if (descriptor.inputExamples.length > 0)
318
+ deployArgs.input_examples = descriptor.inputExamples;
319
+ if (descriptor.prerequisites.length > 0)
320
+ deployArgs.prerequisites = descriptor.prerequisites;
321
+ if (Object.keys(descriptor.contextBindings).length > 0)
322
+ deployArgs.context_bindings = descriptor.contextBindings;
323
+ if (descriptor.category)
324
+ deployArgs.category = descriptor.category;
325
+ const pkg = buildDeployPackage(deployArgs, signature);
291
326
  const builder = beginPhase("deploy", tenant, nodeId, abilityId);
292
327
  try {
293
328
  const result = await bridge.deployAbilityPackage(tenant, nodeId, pkg);
@@ -380,14 +415,17 @@ export async function forgetAll(bridge, tenant, nodeId, confirm = false, options
380
415
  // without performing any uninstalls.
381
416
  if (dryRun) {
382
417
  const wouldRemove = [];
418
+ const wouldFail = [];
383
419
  for (const tool of tools) {
384
420
  const installId = String(tool.install_id ?? "");
385
- const toolName = String(tool.tool_name ?? "");
386
- if (!installId)
421
+ const toolName = String(tool.tool_name ?? "unknown");
422
+ if (!installId) {
423
+ wouldFail.push({ tool_name: toolName, error: "missing install_id" });
387
424
  continue;
425
+ }
388
426
  wouldRemove.push(toolName);
389
427
  }
390
- return { removed: wouldRemove, removed_count: wouldRemove.length, failed: [], failed_count: 0 };
428
+ return { removed: wouldRemove, removed_count: wouldRemove.length, failed: wouldFail, failed_count: wouldFail.length };
391
429
  }
392
430
  const removed = [];
393
431
  const failed = [];
@@ -434,19 +472,10 @@ export async function listRemoteTools(bridge, tenant, namePattern = "", nodeId =
434
472
  // buildDeployPackage
435
473
  // ---------------------------------------------------------------------------
436
474
  export function buildDeployPackage(args, signature) {
437
- const abilityName = String(args.ability_name ?? "");
438
- const toolName = String(args.tool_name ?? abilityName);
439
- return {
440
- ability_name: abilityName,
441
- tool_name: toolName,
442
- description: args.description ?? "",
443
- command_template: args.command_template ?? "",
444
- input_schema: args.input_schema ?? { type: "object", properties: {} },
445
- output_schema: args.output_schema ?? { type: "object", properties: {} },
446
- version: args.version ?? "1.0.0",
447
- tags: args.tags ?? [],
448
- signature_base64: signature,
449
- };
475
+ return serializeRemotePackageDescriptor(buildRemotePackageDescriptor({
476
+ ...args,
477
+ signature_base64: args.signature_base64 ?? signature,
478
+ }, signature));
450
479
  }
451
480
  // ---------------------------------------------------------------------------
452
481
  // deployPackage
@@ -457,12 +486,21 @@ export async function deployPackage(bridge, tenant, nodeId, packageDescriptor) {
457
486
  // ---------------------------------------------------------------------------
458
487
  // Internal generators
459
488
  // ---------------------------------------------------------------------------
489
+ const SHELL_UNSAFE_RE = /[`$\\"'\n\r;|&<>(){}!#\x00-\x1f]/;
490
+ function sanitizeShellValue(value, label) {
491
+ if (SHELL_UNSAFE_RE.test(value)) {
492
+ throw new AxonConfigError(`${label} contains disallowed shell characters: ${value}`);
493
+ }
494
+ return value;
495
+ }
460
496
  function generateInvokeScript(resourceUri, endpoint) {
497
+ const safeEP = sanitizeShellValue(endpoint, "endpoint");
498
+ const safeURI = sanitizeShellValue(resourceUri, "resource_uri");
461
499
  return `#!/usr/bin/env bash
462
500
  set -euo pipefail
463
- AXON_ENDPOINT="\${AXON_ENDPOINT:-${endpoint}}"
501
+ AXON_ENDPOINT="\${AXON_ENDPOINT:-${safeEP}}"
464
502
  TENANT="\${AXON_TENANT:-default}"
465
- RESOURCE_URI="${resourceUri}"
503
+ RESOURCE_URI="${safeURI}"
466
504
  ARGS="\${1:-{}}"
467
505
  curl -sS -X POST "\${AXON_ENDPOINT}/v1/invoke" \\
468
506
  -H "Content-Type: application/json" \\
@@ -498,6 +536,20 @@ function generateAbilityMd(descriptor, target, token) {
498
536
  lines.push("");
499
537
  lines.push(descriptor.description);
500
538
  lines.push("");
539
+ // Agent extension: instructions
540
+ if (descriptor.instructions) {
541
+ lines.push(descriptor.instructions);
542
+ lines.push("");
543
+ }
544
+ // Agent extension: prerequisites
545
+ if (descriptor.prerequisites.length > 0) {
546
+ lines.push("## Prerequisites");
547
+ lines.push("");
548
+ for (const p of descriptor.prerequisites) {
549
+ lines.push(`- ${p}`);
550
+ }
551
+ lines.push("");
552
+ }
501
553
  lines.push("## Parameters");
502
554
  lines.push("");
503
555
  lines.push("| Name | Type | Required | Description |");
@@ -510,6 +562,31 @@ function generateAbilityMd(descriptor, target, token) {
510
562
  const isRequired = required.includes(name) ? "Yes" : "No";
511
563
  lines.push(`| ${name} | ${propType} | ${isRequired} | ${propDesc} |`);
512
564
  }
565
+ // Agent extension: examples
566
+ if (descriptor.inputExamples.length > 0) {
567
+ lines.push("");
568
+ lines.push("## Examples");
569
+ lines.push("");
570
+ for (let i = 0; i < descriptor.inputExamples.length; i++) {
571
+ lines.push(`**Example ${i + 1}:**`);
572
+ lines.push("");
573
+ lines.push("```json");
574
+ lines.push(JSON.stringify(descriptor.inputExamples[i]));
575
+ lines.push("```");
576
+ lines.push("");
577
+ }
578
+ }
579
+ // Agent extension: context bindings
580
+ if (Object.keys(descriptor.contextBindings).length > 0) {
581
+ lines.push("## Context Bindings");
582
+ lines.push("");
583
+ lines.push("| Key | Value |");
584
+ lines.push("|-----|-------|");
585
+ for (const [k, v] of Object.entries(descriptor.contextBindings)) {
586
+ lines.push(`| \`${k}\` | ${v} |`);
587
+ }
588
+ lines.push("");
589
+ }
513
590
  lines.push("");
514
591
  lines.push("## Invoke");
515
592
  lines.push("");
@@ -526,6 +603,9 @@ function generateAbilityMd(descriptor, target, token) {
526
603
  lines.push("");
527
604
  lines.push(`- **URI**: \`${descriptor.resourceUri}\``);
528
605
  lines.push(`- **Version**: ${descriptor.version}`);
606
+ if (descriptor.category) {
607
+ lines.push(`- **Category**: ${descriptor.category}`);
608
+ }
529
609
  lines.push("");
530
610
  return lines.join("\n");
531
611
  }
@@ -23,6 +23,7 @@ export declare class DendriteBridge {
23
23
  listNodes(tenantId: string, options?: DendriteListNodesOptions): Record<string, unknown>[];
24
24
  registerNode(tenantId: string, nodeId: string, options?: DendriteRegisterNodeOptions): Record<string, unknown>;
25
25
  deregisterNode(tenantId: string, nodeId: string, reason?: string): Record<string, unknown>;
26
+ drainNode(tenantId: string, nodeId: string, reason?: string): Record<string, unknown>;
26
27
  heartbeat(tenantId: string, nodeId: string): Record<string, unknown>;
27
28
  publishCapability(tenantId: string, packageId: string, capabilityName: string, options: DendritePublishCapabilityOptions): Record<string, unknown>;
28
29
  installCapability(tenantId: string, nodeId: string, packageId: string, options: DendriteInstallCapabilityOptions): Record<string, unknown>;
@@ -2,23 +2,22 @@
2
2
  // =========================
3
3
  //
4
4
  // File: sdk/node/src/dendrite_bridge/bridge.ts
5
- // Description: DendriteBridge class implementation; provides the high-level API surface for interacting with the native dendrite bridge over FFI/JSON.
5
+ // Description: Node DendriteBridge implementation that exposes typed protocol methods over the native Axon bridge.
6
6
  //
7
7
  // Protocol Responsibility:
8
- // - Implements the DendriteBridge class with all RPC, capability, voice, and transport methods.
9
- // - Preserves stable request/response semantics and error mapping.
8
+ // - Implements the high-level DendriteBridge API for Node callers across RPC, capability, interop, and voice surfaces.
9
+ // - Preserves JSON request and error semantics expected by the Node SDK public API.
10
10
  //
11
11
  // Implementation Approach:
12
- // - Delegates FFI calls to the ffi submodule.
13
- // - Delegates validation/payload building to capability_request helpers.
12
+ // - Delegates native symbol loading to the FFI layer while keeping method-level payload shaping near the class boundary.
13
+ // - Uses typed options and explicit method families so transport intent remains readable at call sites.
14
14
  //
15
15
  // Usage Contract:
16
- // - Callers construct a DendriteBridge with endpoint options and invoke methods for each protocol surface.
17
- // - Errors are surfaced as DendriteError instances.
16
+ // - Construct bridge instances with valid endpoint or library configuration before invoking protocol methods.
17
+ // - Callers should prefer this layer over direct FFI usage unless they are extending native bindings.
18
18
  //
19
19
  // Architectural Position:
20
- // - Part of the Node SDK dendrite_bridge submodule layer.
21
- // - Should not embed FFI loading or type declarations outside this class's responsibility.
20
+ // - Node SDK bridge facade above FFI bindings and below application or preset code.
22
21
  //
23
22
  // Author: Silan.Hu
24
23
  // Email: silan.hu@u.nus.edu
@@ -202,6 +201,19 @@ export class DendriteBridge {
202
201
  });
203
202
  return callJson(this.lib.axon_dendrite_deregister_node_json, this.handle, req);
204
203
  }
204
+ drainNode(tenantId, nodeId, reason = "") {
205
+ if (this.handle <= 0n)
206
+ throw new DendriteError("dendrite bridge already closed");
207
+ if (!this.lib.axon_dendrite_drain_node_json) {
208
+ throw new DendriteError("dendrite bridge symbol not available: axon_dendrite_drain_node_json");
209
+ }
210
+ const req = JSON.stringify({
211
+ tenant_id: tenantId,
212
+ node_id: nodeId,
213
+ reason,
214
+ });
215
+ return callJson(this.lib.axon_dendrite_drain_node_json, this.handle, req);
216
+ }
205
217
  heartbeat(tenantId, nodeId) {
206
218
  if (this.handle <= 0n)
207
219
  throw new DendriteError("dendrite bridge already closed");
@@ -21,6 +21,7 @@ export type DendriteBridgeLib = {
21
21
  axon_dendrite_list_nodes_json: DendriteJsonFn<[bigint, string]>;
22
22
  axon_dendrite_register_node_json: DendriteJsonFn<[bigint, string]>;
23
23
  axon_dendrite_deregister_node_json?: DendriteJsonFn<[bigint, string]>;
24
+ axon_dendrite_drain_node_json?: DendriteJsonFn<[bigint, string]>;
24
25
  axon_dendrite_heartbeat_json: DendriteJsonFn<[bigint, string]>;
25
26
  axon_dendrite_publish_capability_json: DendriteJsonFn<[bigint, string]>;
26
27
  axon_dendrite_install_capability_json: DendriteJsonFn<[bigint, string]>;