@easynet-run/node 0.27.14

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 (61) hide show
  1. package/README.md +135 -0
  2. package/native/dendrite-bridge-manifest.json +38 -0
  3. package/native/dendrite-bridge.json +15 -0
  4. package/native/include/axon_dendrite_bridge.h +460 -0
  5. package/native/libaxon_dendrite_bridge.so +0 -0
  6. package/package.json +67 -0
  7. package/runtime/easynet-runtime-rs-0.27.14-x86_64-unknown-linux-gnu.tar.gz +0 -0
  8. package/runtime/runtime-bridge-manifest.json +20 -0
  9. package/runtime/runtime-bridge.json +9 -0
  10. package/src/ability_lifecycle.d.ts +140 -0
  11. package/src/ability_lifecycle.js +525 -0
  12. package/src/capability_request.d.ts +14 -0
  13. package/src/capability_request.js +247 -0
  14. package/src/dendrite_bridge/bridge.d.ts +98 -0
  15. package/src/dendrite_bridge/bridge.js +712 -0
  16. package/src/dendrite_bridge/ffi.d.ts +60 -0
  17. package/src/dendrite_bridge/ffi.js +139 -0
  18. package/src/dendrite_bridge/index.d.ts +3 -0
  19. package/src/dendrite_bridge/index.js +25 -0
  20. package/src/dendrite_bridge/types.d.ts +179 -0
  21. package/src/dendrite_bridge/types.js +23 -0
  22. package/src/dendrite_bridge.d.ts +1 -0
  23. package/src/dendrite_bridge.js +27 -0
  24. package/src/errors.d.ts +83 -0
  25. package/src/errors.js +146 -0
  26. package/src/index.d.ts +55 -0
  27. package/src/index.js +164 -0
  28. package/src/koffi.d.ts +34 -0
  29. package/src/mcp/server.d.ts +29 -0
  30. package/src/mcp/server.js +190 -0
  31. package/src/presets/ability_dispatch/args.d.ts +5 -0
  32. package/src/presets/ability_dispatch/args.js +36 -0
  33. package/src/presets/ability_dispatch/bundle.d.ts +7 -0
  34. package/src/presets/ability_dispatch/bundle.js +102 -0
  35. package/src/presets/ability_dispatch/media.d.ts +6 -0
  36. package/src/presets/ability_dispatch/media.js +48 -0
  37. package/src/presets/ability_dispatch/orchestrator.d.ts +21 -0
  38. package/src/presets/ability_dispatch/orchestrator.js +117 -0
  39. package/src/presets/ability_dispatch/workflow.d.ts +50 -0
  40. package/src/presets/ability_dispatch/workflow.js +333 -0
  41. package/src/presets/ability_dispatch.d.ts +1 -0
  42. package/src/presets/ability_dispatch.js +2 -0
  43. package/src/presets/remote_control/config.d.ts +16 -0
  44. package/src/presets/remote_control/config.js +63 -0
  45. package/src/presets/remote_control/descriptor.d.ts +34 -0
  46. package/src/presets/remote_control/descriptor.js +183 -0
  47. package/src/presets/remote_control/handlers.d.ts +12 -0
  48. package/src/presets/remote_control/handlers.js +279 -0
  49. package/src/presets/remote_control/kit.d.ts +22 -0
  50. package/src/presets/remote_control/kit.js +72 -0
  51. package/src/presets/remote_control/kit.test.js +87 -0
  52. package/src/presets/remote_control/orchestrator.d.ts +28 -0
  53. package/src/presets/remote_control/orchestrator.js +118 -0
  54. package/src/presets/remote_control/specs.d.ts +2 -0
  55. package/src/presets/remote_control/specs.js +152 -0
  56. package/src/presets/remote_control_case.d.ts +7 -0
  57. package/src/presets/remote_control_case.js +3 -0
  58. package/src/receipt.d.ts +46 -0
  59. package/src/receipt.js +98 -0
  60. package/src/tool_adapter.d.ts +90 -0
  61. package/src/tool_adapter.js +169 -0
@@ -0,0 +1,525 @@
1
+ // sdk/node/src/ability_lifecycle.ts — Ability lifecycle API: create, deploy, and export as Agent Skills.
2
+ //
3
+ // Copyright (c) 2026-2027 easynet. All rights reserved.
4
+ import { spawn } from "node:child_process";
5
+ import { webcrypto } from "node:crypto";
6
+ import { createServer, createConnection } from "node:net";
7
+ import { existsSync, mkdirSync, openSync } from "node:fs";
8
+ import { hostname as localHostname } from "node:os";
9
+ import { join, dirname } from "node:path";
10
+ import { env } from "node:process";
11
+ import { buildPythonSubprocessTemplate } from "./presets/remote_control/descriptor.js";
12
+ import { AxonConfigError, AxonBridgeError, AxonInvocationError, AxonPartialSuccessError } from "./errors.js";
13
+ import { DEFAULT_SIGNATURE } from "./presets/remote_control/config.js";
14
+ import { beginPhase, buildDeployTrace } from "./receipt.js";
15
+ /** Placeholder signature for ephemeral/temporary skill deployments.
16
+ * Canonical constant lives in presets/remote_control/config.ts.
17
+ */
18
+ const EPHEMERAL_SIGNATURE = DEFAULT_SIGNATURE;
19
+ /** Default Axon runtime port. */
20
+ const DEFAULT_AXON_PORT = 50051;
21
+ /** Generate a random hex string for unique tool name suffixes. */
22
+ function randomHex(bytes) {
23
+ const arr = new Uint8Array(bytes);
24
+ webcrypto.getRandomValues(arr);
25
+ return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // Helpers
29
+ // ---------------------------------------------------------------------------
30
+ function normalizeAbilityName(raw) {
31
+ const result = raw
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9_\-]/g, "-")
34
+ .replace(/-{2,}/g, "-")
35
+ .replace(/^-+|-+$/g, "");
36
+ return result || "ability";
37
+ }
38
+ function firstNonEmpty(...values) {
39
+ for (const value of values) {
40
+ const trimmed = value?.trim();
41
+ if (trimmed)
42
+ return trimmed;
43
+ }
44
+ return undefined;
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Server lifecycle — start / connect / stop
48
+ // ---------------------------------------------------------------------------
49
+ const DEFAULT_LOG_DIR = join(env.HOME ?? env.USERPROFILE ?? "/tmp", ".easynet", "logs");
50
+ function cliLog(msg) {
51
+ const ts = new Date().toLocaleTimeString("en-GB", { hour12: false });
52
+ process.stderr.write(`[axon ${ts}] ${msg}\n`);
53
+ }
54
+ function findRuntimeBinary() {
55
+ const envBin = env.AXON_RUNTIME_BIN;
56
+ if (envBin && existsSync(envBin))
57
+ return envBin;
58
+ // Fall back to PATH lookup
59
+ return "axon-runtime";
60
+ }
61
+ function findFreePort() {
62
+ return new Promise((resolve, reject) => {
63
+ const srv = createServer();
64
+ srv.listen(0, "127.0.0.1", () => {
65
+ const addr = srv.address();
66
+ if (addr && typeof addr === "object") {
67
+ const port = addr.port;
68
+ srv.close(() => resolve(port));
69
+ }
70
+ else {
71
+ srv.close(() => reject(new AxonBridgeError("failed to get free port")));
72
+ }
73
+ });
74
+ srv.on("error", reject);
75
+ });
76
+ }
77
+ function waitForPort(host, port, timeoutMs) {
78
+ const deadline = Date.now() + timeoutMs;
79
+ return new Promise((res) => {
80
+ function tryConnect() {
81
+ if (Date.now() > deadline) {
82
+ res(false);
83
+ return;
84
+ }
85
+ const sock = createConnection({ host, port, timeout: 500 }, () => {
86
+ sock.destroy();
87
+ res(true);
88
+ });
89
+ sock.on("error", () => { sock.destroy(); setTimeout(tryConnect, 100); });
90
+ }
91
+ tryConnect();
92
+ });
93
+ }
94
+ function parseEndpoint(endpoint) {
95
+ const raw = endpoint.trim();
96
+ if (raw.startsWith("axon://")) {
97
+ const authority = raw.slice("axon://".length).split("/")[0];
98
+ if (!authority || authority === "localhost")
99
+ return { host: "127.0.0.1", port: DEFAULT_AXON_PORT };
100
+ if (authority.startsWith("localhost:"))
101
+ return { host: "127.0.0.1", port: parseInt(authority.split(":")[1], 10) || DEFAULT_AXON_PORT };
102
+ const idx = authority.lastIndexOf(":");
103
+ if (idx > 0)
104
+ return { host: authority.slice(0, idx), port: parseInt(authority.slice(idx + 1), 10) || DEFAULT_AXON_PORT };
105
+ return { host: authority, port: DEFAULT_AXON_PORT };
106
+ }
107
+ const url = raw.replace(/^https?:\/\//, "").split("/")[0];
108
+ const idx = url.lastIndexOf(":");
109
+ if (idx > 0)
110
+ return { host: url.slice(0, idx), port: parseInt(url.slice(idx + 1), 10) || DEFAULT_AXON_PORT };
111
+ return { host: url, port: DEFAULT_AXON_PORT };
112
+ }
113
+ function normalizeHubEndpoint(endpoint) {
114
+ const trimmed = endpoint?.trim();
115
+ if (!trimmed)
116
+ return undefined;
117
+ if (trimmed.startsWith("axon://")) {
118
+ const { host, port } = parseEndpoint(trimmed);
119
+ return `http://${host}:${port}`;
120
+ }
121
+ return trimmed;
122
+ }
123
+ /**
124
+ * Start or connect to an Axon runtime.
125
+ *
126
+ * @example
127
+ * // Auto-start a local server (zero config):
128
+ * const srv = await startServer();
129
+ *
130
+ * // Connect via axon:// transport URI:
131
+ * const srv = await startServer("axon://localhost");
132
+ * const srv = await startServer("axon://10.0.0.5:50084");
133
+ *
134
+ * // Federation mode — connect the local runtime to a Hub:
135
+ * const srv = await startServer(undefined, { hub: "axon://hub.easynet.run:50084" });
136
+ */
137
+ export async function startServer(endpoint, options = {}) {
138
+ const timeoutMs = options.timeoutMs ?? 10000;
139
+ // Resolve from env if not provided
140
+ if (!endpoint) {
141
+ endpoint = env.EASYNET_AXON_ENDPOINT;
142
+ }
143
+ // --- Connect to existing server ---
144
+ if (endpoint) {
145
+ cliLog(`connecting to ${endpoint}`);
146
+ const { host, port } = parseEndpoint(endpoint);
147
+ const ok = await waitForPort(host, port, timeoutMs);
148
+ if (!ok)
149
+ throw new AxonBridgeError(`cannot reach server at ${endpoint}`);
150
+ cliLog(`connected to ${endpoint}`);
151
+ return { endpoint, process: null, logFile: null, stop() { } };
152
+ }
153
+ // --- Spawn local server ---
154
+ const port = await findFreePort();
155
+ const host = "127.0.0.1";
156
+ const bindAddr = `${host}:${port}`;
157
+ const endpointUrl = `http://${bindAddr}`;
158
+ const binary = findRuntimeBinary();
159
+ const logDir = DEFAULT_LOG_DIR;
160
+ const logFile = options.logFile ?? join(logDir, "axon-runtime.log");
161
+ mkdirSync(dirname(logFile), { recursive: true });
162
+ const logFd = openSync(logFile, "a");
163
+ const childEnv = {
164
+ ...process.env,
165
+ AXON_BIND: bindAddr,
166
+ AXON_ENFORCE_MTLS: (options.insecure ?? true) ? "false" : "true",
167
+ };
168
+ const hubEndpoint = normalizeHubEndpoint(firstNonEmpty(options.hub, env.AXON_HUB));
169
+ if (hubEndpoint) {
170
+ childEnv.AXON_HUB = hubEndpoint;
171
+ childEnv.AXON_FEDERATION_TENANT = firstNonEmpty(options.hubTenant, env.AXON_FEDERATION_TENANT, "default");
172
+ childEnv.AXON_FEDERATION_LABEL = firstNonEmpty(options.hubLabel, env.AXON_FEDERATION_LABEL, localHostname());
173
+ const hubJoinToken = firstNonEmpty(options.hubJoinToken, env.AXON_HUB_JOIN_TOKEN, env.AXON_FEDERATION_JOIN_TOKEN);
174
+ if (hubJoinToken) {
175
+ childEnv.AXON_HUB_JOIN_TOKEN = hubJoinToken;
176
+ }
177
+ cliLog(`federation: will connect to hub at ${hubEndpoint}`);
178
+ }
179
+ cliLog(`starting axon-runtime on ${bindAddr} (log: ${logFile})`);
180
+ const child = spawn(binary, [], {
181
+ env: childEnv,
182
+ stdio: ["ignore", logFd, logFd],
183
+ });
184
+ const ready = await waitForPort(host, port, timeoutMs);
185
+ if (!ready) {
186
+ child.kill();
187
+ throw new AxonBridgeError(`axon-runtime not ready within ${timeoutMs}ms on ${bindAddr} (see ${logFile})`);
188
+ }
189
+ cliLog(`axon-runtime ready → ${endpointUrl}`);
190
+ return {
191
+ endpoint: endpointUrl,
192
+ process: child,
193
+ logFile,
194
+ stop() {
195
+ if (child.exitCode === null) {
196
+ cliLog(`stopping axon-runtime (pid ${child.pid})`);
197
+ child.kill("SIGTERM");
198
+ const timer = setTimeout(() => {
199
+ if (child.exitCode === null)
200
+ child.kill("SIGKILL");
201
+ }, 5000);
202
+ timer.unref();
203
+ child.once("exit", () => {
204
+ clearTimeout(timer);
205
+ cliLog("axon-runtime stopped");
206
+ });
207
+ }
208
+ },
209
+ };
210
+ }
211
+ // ---------------------------------------------------------------------------
212
+ // create
213
+ // ---------------------------------------------------------------------------
214
+ export function createAbility(opts) {
215
+ if (!opts.name?.trim())
216
+ throw new AxonConfigError("ability name cannot be empty");
217
+ if (!opts.commandTemplate?.trim())
218
+ throw new AxonConfigError("command_template cannot be empty");
219
+ const token = normalizeAbilityName(opts.name);
220
+ return {
221
+ name: opts.name,
222
+ description: opts.description,
223
+ commandTemplate: opts.commandTemplate,
224
+ inputSchema: opts.inputSchema ?? { type: "object", properties: {} },
225
+ outputSchema: opts.outputSchema ?? { type: "object", properties: {} },
226
+ version: opts.version ?? "1.0.0",
227
+ tags: opts.tags ?? [],
228
+ resourceUri: opts.resourceUri ?? `easynet:///r/org/${token}`,
229
+ };
230
+ }
231
+ // ---------------------------------------------------------------------------
232
+ // toToolSpec
233
+ // ---------------------------------------------------------------------------
234
+ export function toToolSpec(descriptor) {
235
+ const token = normalizeAbilityName(descriptor.name);
236
+ return {
237
+ name: token,
238
+ description: descriptor.description,
239
+ resourceUri: descriptor.resourceUri,
240
+ parameters: descriptor.inputSchema,
241
+ };
242
+ }
243
+ // ---------------------------------------------------------------------------
244
+ // exportAbility
245
+ // ---------------------------------------------------------------------------
246
+ export function exportAbility(descriptor, target = "agent_skills", axonEndpoint) {
247
+ const token = normalizeAbilityName(descriptor.name);
248
+ const endpoint = axonEndpoint ?? `http://127.0.0.1:${DEFAULT_AXON_PORT}`;
249
+ const invokeScript = generateInvokeScript(descriptor.resourceUri, endpoint);
250
+ const abilityMd = generateAbilityMd(descriptor, target, token);
251
+ return { abilityMd, invokeScript, abilityName: token };
252
+ }
253
+ // ---------------------------------------------------------------------------
254
+ // deployToNode
255
+ // ---------------------------------------------------------------------------
256
+ /**
257
+ * Deploy an AbilityDescriptor to a node via the MCP deploy pipeline.
258
+ *
259
+ * The underlying bridge call executes a three-phase pipeline
260
+ * (Publish → Install → Activate). This function wraps the entire
261
+ * pipeline in a single {@link DeployTrace} receipt so callers get
262
+ * wall-clock timing and error codes even on failure.
263
+ *
264
+ * On failure an {@link AxonInvocationError} is thrown with its `trace`
265
+ * field set to the collected {@link DeployTrace}:
266
+ *
267
+ * ```ts
268
+ * try { await deployToNode(bridge, tenant, nodeId, desc, sig); }
269
+ * catch (e) { if (e instanceof AxonInvocationError) console.log(e.trace); }
270
+ * ```
271
+ */
272
+ export async function deployToNode(bridge, tenant, nodeId, descriptor, signature) {
273
+ const token = normalizeAbilityName(descriptor.name);
274
+ const abilityId = token;
275
+ const pkg = buildDeployPackage({
276
+ ability_name: descriptor.name,
277
+ tool_name: token,
278
+ description: descriptor.description,
279
+ command_template: descriptor.commandTemplate,
280
+ version: descriptor.version,
281
+ input_schema: descriptor.inputSchema,
282
+ output_schema: descriptor.outputSchema,
283
+ tags: descriptor.tags.length > 0 ? descriptor.tags : undefined,
284
+ }, signature);
285
+ const builder = beginPhase("deploy", tenant, nodeId, abilityId);
286
+ try {
287
+ const result = await bridge.deployAbilityPackage(tenant, nodeId, pkg);
288
+ const installId = String(result.install_id ?? "");
289
+ const receipt = builder.finishOk(installId || undefined);
290
+ const trace = buildDeployTrace([receipt]);
291
+ return { result, trace };
292
+ }
293
+ catch (e) {
294
+ const err = e instanceof Error ? e : new Error(String(e));
295
+ const receipt = builder.finishErr(err);
296
+ const trace = buildDeployTrace([receipt]);
297
+ throw new AxonInvocationError(err.message, { install_id: abilityId }, trace);
298
+ }
299
+ }
300
+ // ---------------------------------------------------------------------------
301
+ // listAbilities
302
+ // ---------------------------------------------------------------------------
303
+ export async function listAbilities(bridge, tenant, nodeId) {
304
+ const result = await bridge.listMcpTools(tenant, "", nodeId);
305
+ return (Array.isArray(result) ? result : []);
306
+ }
307
+ // ---------------------------------------------------------------------------
308
+ // invokeAbility
309
+ // ---------------------------------------------------------------------------
310
+ export async function invokeAbility(bridge, tenant, nodeId, toolName, args = {}) {
311
+ return await bridge.callMcpToolWithArgs(tenant, toolName, nodeId, args);
312
+ }
313
+ // ---------------------------------------------------------------------------
314
+ // uninstallAbility
315
+ // ---------------------------------------------------------------------------
316
+ export async function uninstallAbility(bridge, tenant, nodeId, installId, reason) {
317
+ return await bridge.uninstallAbilityWithReason(tenant, nodeId, installId, reason ?? "ability lifecycle: uninstall");
318
+ }
319
+ // ---------------------------------------------------------------------------
320
+ // discoverNodes
321
+ // ---------------------------------------------------------------------------
322
+ export async function discoverNodes(bridge, tenant) {
323
+ const result = await bridge.listNodes(tenant, null);
324
+ return (Array.isArray(result) ? result : []);
325
+ }
326
+ // ---------------------------------------------------------------------------
327
+ // executeCommand
328
+ // ---------------------------------------------------------------------------
329
+ export async function executeCommand(bridge, tenant, nodeId, command) {
330
+ const toolName = `cmd_${Date.now()}_${randomHex(4)}`;
331
+ const wrapped = buildPythonSubprocessTemplate(command);
332
+ const deployResult = await bridge.deployAbilityPackage(tenant, nodeId, {
333
+ ability_name: toolName,
334
+ tool_name: toolName,
335
+ description: `execute: ${command}`,
336
+ command_template: wrapped,
337
+ signature_base64: EPHEMERAL_SIGNATURE,
338
+ tags: ["ephemeral", "auto-cleanup"],
339
+ });
340
+ const result = await bridge.callMcpToolWithArgs(tenant, toolName, nodeId, {});
341
+ let cleanupOk = true;
342
+ let cleanupError;
343
+ const installId = String(deployResult.install_id ?? "");
344
+ if (installId) {
345
+ try {
346
+ await bridge.uninstallAbilityWithReason(tenant, nodeId, installId, "execute_command cleanup");
347
+ }
348
+ catch (e) {
349
+ process.stderr.write(`[axon] execute_command cleanup failed for ${installId}: ${e}\n`);
350
+ cleanupOk = false;
351
+ cleanupError = String(e);
352
+ }
353
+ }
354
+ return { ...result, cleanup: { ok: cleanupOk, error: cleanupError } };
355
+ }
356
+ /**
357
+ * Remove all deployed abilities from a device.
358
+ * Requires `confirm = true` as a safety gate (destructive operation).
359
+ *
360
+ * When `options.dryRun` is `true`, the function lists all abilities that would
361
+ * be removed without performing any uninstalls. The `confirm` parameter is
362
+ * ignored for dry-run calls since no data is modified.
363
+ *
364
+ * @throws {AxonPartialSuccessError} When any uninstall fails (partial or total failure).
365
+ * @throws {AxonConfigError} When `confirm` is not `true` and `dryRun` is not set.
366
+ */
367
+ export async function forgetAll(bridge, tenant, nodeId, confirm = false, options = {}) {
368
+ const dryRun = options.dryRun ?? false;
369
+ if (!confirm && !dryRun) {
370
+ throw new AxonConfigError("forget_all requires confirm = true (destructive operation)");
371
+ }
372
+ const tools = await listAbilities(bridge, tenant, nodeId);
373
+ // Dry-run mode: collect the list of abilities that would be removed
374
+ // without performing any uninstalls.
375
+ if (dryRun) {
376
+ const wouldRemove = [];
377
+ for (const tool of tools) {
378
+ const installId = String(tool.install_id ?? "");
379
+ const toolName = String(tool.tool_name ?? "");
380
+ if (!installId)
381
+ continue;
382
+ wouldRemove.push(toolName);
383
+ }
384
+ return { removed: wouldRemove, removed_count: wouldRemove.length, failed: [], failed_count: 0 };
385
+ }
386
+ const removed = [];
387
+ const failed = [];
388
+ for (const tool of tools) {
389
+ const installId = String(tool.install_id ?? "");
390
+ const toolName = String(tool.tool_name ?? "");
391
+ if (!installId)
392
+ continue;
393
+ try {
394
+ await bridge.uninstallAbilityWithReason(tenant, nodeId, installId, "forget_all");
395
+ removed.push(toolName);
396
+ }
397
+ catch (e) {
398
+ process.stderr.write(`[axon] forget_all: failed to uninstall ${toolName}: ${e}\n`);
399
+ failed.push({ tool_name: toolName, error: String(e) });
400
+ }
401
+ }
402
+ const result = { removed, removed_count: removed.length, failed, failed_count: failed.length };
403
+ if (failed.length > 0) {
404
+ throw new AxonPartialSuccessError(`forget_all: ${removed.length} succeeded, ${failed.length} failed`, removed.length, failed.length, { removed, failed, result });
405
+ }
406
+ return result;
407
+ }
408
+ // ---------------------------------------------------------------------------
409
+ // disconnectDevice
410
+ // ---------------------------------------------------------------------------
411
+ export async function disconnectDevice(bridge, tenant, nodeId, reason) {
412
+ return await bridge.deregisterNode(tenant, nodeId, reason ?? "sdk: disconnect_device");
413
+ }
414
+ // ---------------------------------------------------------------------------
415
+ // drainDevice
416
+ // ---------------------------------------------------------------------------
417
+ export async function drainDevice(bridge, tenant, nodeId, reason) {
418
+ return await bridge.drainNode(tenant, nodeId, reason ?? "sdk: drain_device");
419
+ }
420
+ // ---------------------------------------------------------------------------
421
+ // listRemoteTools
422
+ // ---------------------------------------------------------------------------
423
+ export async function listRemoteTools(bridge, tenant, namePattern = "", nodeId = "") {
424
+ const result = await bridge.listMcpTools(tenant, namePattern, nodeId);
425
+ return (Array.isArray(result) ? result : []);
426
+ }
427
+ // ---------------------------------------------------------------------------
428
+ // buildDeployPackage
429
+ // ---------------------------------------------------------------------------
430
+ export function buildDeployPackage(args, signature) {
431
+ const abilityName = String(args.ability_name ?? "");
432
+ const toolName = String(args.tool_name ?? abilityName);
433
+ return {
434
+ ability_name: abilityName,
435
+ tool_name: toolName,
436
+ description: args.description ?? "",
437
+ command_template: args.command_template ?? "",
438
+ input_schema: args.input_schema ?? { type: "object", properties: {} },
439
+ output_schema: args.output_schema ?? { type: "object", properties: {} },
440
+ version: args.version ?? "1.0.0",
441
+ tags: args.tags ?? [],
442
+ signature_base64: signature,
443
+ };
444
+ }
445
+ // ---------------------------------------------------------------------------
446
+ // deployPackage
447
+ // ---------------------------------------------------------------------------
448
+ export async function deployPackage(bridge, tenant, nodeId, packageDescriptor) {
449
+ return await bridge.deployAbilityPackage(tenant, nodeId, packageDescriptor);
450
+ }
451
+ // ---------------------------------------------------------------------------
452
+ // Internal generators
453
+ // ---------------------------------------------------------------------------
454
+ function generateInvokeScript(resourceUri, endpoint) {
455
+ return `#!/usr/bin/env bash
456
+ set -euo pipefail
457
+ AXON_ENDPOINT="\${AXON_ENDPOINT:-${endpoint}}"
458
+ TENANT="\${AXON_TENANT:-default}"
459
+ RESOURCE_URI="${resourceUri}"
460
+ ARGS="\${1:-{}}"
461
+ curl -sS -X POST "\${AXON_ENDPOINT}/v1/invoke" \\
462
+ -H "Content-Type: application/json" \\
463
+ -d "{\\"tenant_id\\":\\"\${TENANT}\\",\\"resource_uri\\":\\"\${RESOURCE_URI}\\",\\"payload\\":\${ARGS}}"
464
+ `;
465
+ }
466
+ function pushMetadata(lines, version, resourceUri) {
467
+ lines.push("metadata:");
468
+ lines.push(" author: easynet-axon");
469
+ lines.push(` version: "${version}"`);
470
+ lines.push(` axon-resource-uri: "${resourceUri}"`);
471
+ }
472
+ function generateAbilityMd(descriptor, target, token) {
473
+ const lines = [];
474
+ lines.push("---");
475
+ lines.push(`name: ${token}`);
476
+ lines.push(`description: ${descriptor.description}`);
477
+ lines.push("compatibility: Requires network access to Axon runtime");
478
+ pushMetadata(lines, descriptor.version, descriptor.resourceUri);
479
+ if (target === "claude") {
480
+ lines.push("allowed-tools: Bash(*)");
481
+ }
482
+ else if (target === "openclaw") {
483
+ lines.push(" openclaw:");
484
+ lines.push(' emoji: "⚡"');
485
+ lines.push(" requires:");
486
+ lines.push(" network: true");
487
+ lines.push(" command-dispatch: tool");
488
+ }
489
+ lines.push("---");
490
+ lines.push("");
491
+ lines.push(`# ${descriptor.name}`);
492
+ lines.push("");
493
+ lines.push(descriptor.description);
494
+ lines.push("");
495
+ lines.push("## Parameters");
496
+ lines.push("");
497
+ lines.push("| Name | Type | Required | Description |");
498
+ lines.push("|------|------|----------|-------------|");
499
+ const props = (descriptor.inputSchema.properties ?? {});
500
+ const required = (descriptor.inputSchema.required ?? []);
501
+ for (const [name, schema] of Object.entries(props)) {
502
+ const propType = schema.type ?? "string";
503
+ const propDesc = schema.description ?? "";
504
+ const isRequired = required.includes(name) ? "Yes" : "No";
505
+ lines.push(`| ${name} | ${propType} | ${isRequired} | ${propDesc} |`);
506
+ }
507
+ lines.push("");
508
+ lines.push("## Invoke");
509
+ lines.push("");
510
+ lines.push("Run the bundled script with a JSON argument:");
511
+ lines.push("");
512
+ const skillDirVar = target === "claude" ? "CLAUDE_SKILL_DIR"
513
+ : target === "codex" ? "CODEX_SKILL_DIR"
514
+ : "SKILL_DIR";
515
+ lines.push("```bash");
516
+ lines.push(`\${${skillDirVar}}/scripts/invoke.sh '{"param": "value"}'`);
517
+ lines.push("```");
518
+ lines.push("");
519
+ lines.push("## Axon Resource");
520
+ lines.push("");
521
+ lines.push(`- **URI**: \`${descriptor.resourceUri}\``);
522
+ lines.push(`- **Version**: ${descriptor.version}`);
523
+ lines.push("");
524
+ return lines.join("\n");
525
+ }
@@ -0,0 +1,14 @@
1
+ export declare class DendriteError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ import type { DendritePublishCapabilityOptions, DendriteInstallCapabilityOptions, DendriteDeployMcpListDirOptions, DendriteUpdateMcpListDirOptions, DendriteUninstallCapabilityOptions } from "./dendrite_bridge/types.js";
5
+ export declare function validatePublishCapabilityRequest(tenantId: string, packageId: string, capabilityName: string, options: DendritePublishCapabilityOptions): void;
6
+ export declare function buildPublishCapabilityPayload(tenantId: string, packageId: string, capabilityName: string, options: DendritePublishCapabilityOptions): Record<string, unknown>;
7
+ export declare function validateInstallCapabilityRequest(tenantId: string, nodeId: string, packageId: string, options: DendriteInstallCapabilityOptions): void;
8
+ export declare function buildInstallCapabilityPayload(tenantId: string, nodeId: string, packageId: string, options: DendriteInstallCapabilityOptions): Record<string, unknown>;
9
+ export declare function validateDeployMcpListDirRequest(tenantId: string, nodeId: string, options?: DendriteDeployMcpListDirOptions): void;
10
+ export declare function buildDeployMcpListDirPayload(tenantId: string, nodeId: string, options?: DendriteDeployMcpListDirOptions): Record<string, unknown>;
11
+ export declare function validateUpdateMcpListDirRequest(tenantId: string, nodeId: string, options?: DendriteUpdateMcpListDirOptions): void;
12
+ export declare function buildUpdateMcpListDirPayload(tenantId: string, nodeId: string, options?: DendriteUpdateMcpListDirOptions): Record<string, unknown>;
13
+ export declare function validateUninstallCapabilityRequest(tenantId: string, nodeId: string, installId: string): void;
14
+ export declare function buildUninstallCapabilityPayload(tenantId: string, nodeId: string, installId: string, options?: DendriteUninstallCapabilityOptions): Record<string, unknown>;