agenticros 0.1.18 → 0.2.0

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 (164) hide show
  1. package/dist/__tests__/robot-config.test.d.ts +2 -0
  2. package/dist/__tests__/robot-config.test.d.ts.map +1 -0
  3. package/dist/__tests__/robot-config.test.js +374 -0
  4. package/dist/__tests__/robot-config.test.js.map +1 -0
  5. package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
  6. package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
  7. package/dist/__tests__/transport-shorthand.test.js +88 -0
  8. package/dist/__tests__/transport-shorthand.test.js.map +1 -0
  9. package/dist/commands/robots.d.ts +57 -0
  10. package/dist/commands/robots.d.ts.map +1 -0
  11. package/dist/commands/robots.js +678 -0
  12. package/dist/commands/robots.js.map +1 -0
  13. package/dist/commands/skills.d.ts.map +1 -1
  14. package/dist/commands/skills.js +125 -2
  15. package/dist/commands/skills.js.map +1 -1
  16. package/dist/index.js +29 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/menu.js +22 -4
  19. package/dist/menu.js.map +1 -1
  20. package/dist/util/marketplace.d.ts +57 -0
  21. package/dist/util/marketplace.d.ts.map +1 -0
  22. package/dist/util/marketplace.js +144 -0
  23. package/dist/util/marketplace.js.map +1 -0
  24. package/dist/util/mcp-discovery.d.ts +57 -0
  25. package/dist/util/mcp-discovery.d.ts.map +1 -0
  26. package/dist/util/mcp-discovery.js +153 -0
  27. package/dist/util/mcp-discovery.js.map +1 -0
  28. package/dist/util/robot-config.d.ts +167 -0
  29. package/dist/util/robot-config.d.ts.map +1 -0
  30. package/dist/util/robot-config.js +375 -0
  31. package/dist/util/robot-config.js.map +1 -0
  32. package/dist/util/skills.d.ts +6 -6
  33. package/dist/util/skills.d.ts.map +1 -1
  34. package/dist/util/skills.js +14 -9
  35. package/dist/util/skills.js.map +1 -1
  36. package/dist/util/transport-shorthand.d.ts +66 -0
  37. package/dist/util/transport-shorthand.d.ts.map +1 -0
  38. package/dist/util/transport-shorthand.js +102 -0
  39. package/dist/util/transport-shorthand.js.map +1 -0
  40. package/package.json +12 -10
  41. package/runtime/BUNDLE.json +1 -1
  42. package/runtime/README.md +62 -1
  43. package/runtime/package.json +6 -1
  44. package/runtime/packages/agenticros/openclaw.plugin.json +7 -1
  45. package/runtime/packages/agenticros/package.json +2 -1
  46. package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
  47. package/runtime/packages/agenticros/src/config-page.ts +1 -1
  48. package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
  49. package/runtime/packages/agenticros/src/service.ts +120 -2
  50. package/runtime/packages/agenticros/src/skill-loader.ts +109 -30
  51. package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
  52. package/runtime/packages/agenticros/src/tools/index.ts +49 -9
  53. package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
  54. package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
  55. package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
  56. package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
  57. package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
  58. package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
  59. package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
  60. package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
  61. package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
  62. package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
  63. package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
  64. package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
  65. package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
  66. package/runtime/packages/agenticros-claude-code/README.md +52 -9
  67. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts +2 -0
  68. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts.map +1 -0
  69. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js +773 -0
  70. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js.map +1 -0
  71. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts +2 -0
  72. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts.map +1 -0
  73. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js +97 -0
  74. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js.map +1 -0
  75. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts +2 -2
  76. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts.map +1 -1
  77. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js +5 -5
  78. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js.map +1 -1
  79. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts +109 -0
  80. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts.map +1 -0
  81. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js +392 -0
  82. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js.map +1 -0
  83. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts +8 -3
  84. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts.map +1 -1
  85. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js +30 -12
  86. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js.map +1 -1
  87. package/runtime/packages/agenticros-claude-code/dist/index.js +5 -5
  88. package/runtime/packages/agenticros-claude-code/dist/index.js.map +1 -1
  89. package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts +16 -0
  90. package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts.map +1 -0
  91. package/runtime/packages/agenticros-claude-code/dist/mission-registry.js +19 -0
  92. package/runtime/packages/agenticros-claude-code/dist/mission-registry.js.map +1 -0
  93. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts +9 -0
  94. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
  95. package/runtime/packages/agenticros-claude-code/dist/tools.js +700 -54
  96. package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
  97. package/runtime/packages/agenticros-claude-code/dist/transport.d.ts +28 -8
  98. package/runtime/packages/agenticros-claude-code/dist/transport.d.ts.map +1 -1
  99. package/runtime/packages/agenticros-claude-code/dist/transport.js +37 -34
  100. package/runtime/packages/agenticros-claude-code/dist/transport.js.map +1 -1
  101. package/runtime/packages/agenticros-claude-code/package.json +2 -1
  102. package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +965 -0
  103. package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
  104. package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
  105. package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
  106. package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
  107. package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
  108. package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
  109. package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
  110. package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
  111. package/runtime/packages/agenticros-gemini/package.json +1 -0
  112. package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
  113. package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
  114. package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
  115. package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
  116. package/runtime/packages/core/LICENSE +192 -0
  117. package/runtime/packages/core/README.md +88 -0
  118. package/runtime/packages/core/package.json +36 -3
  119. package/runtime/packages/core/src/__tests__/capabilities.test.ts +266 -0
  120. package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
  121. package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
  122. package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
  123. package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
  124. package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
  125. package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
  126. package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
  127. package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
  128. package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
  129. package/runtime/packages/core/src/capabilities.ts +314 -0
  130. package/runtime/packages/core/src/config.ts +221 -39
  131. package/runtime/packages/core/src/discovery.ts +193 -0
  132. package/runtime/packages/core/src/find-robots-for.ts +164 -0
  133. package/runtime/packages/core/src/index.ts +67 -1
  134. package/runtime/packages/core/src/mission-registry.ts +142 -0
  135. package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
  136. package/runtime/packages/core/src/mission.ts +574 -0
  137. package/runtime/packages/core/src/planner/index.ts +564 -0
  138. package/runtime/packages/core/src/robots.ts +282 -0
  139. package/runtime/packages/core/src/topic-utils.ts +47 -9
  140. package/runtime/packages/core/src/transport-pool.ts +171 -0
  141. package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
  142. package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
  143. package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
  144. package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
  145. package/runtime/scripts/configure_agenticros.sh +0 -0
  146. package/runtime/scripts/configure_for_sim.sh +0 -0
  147. package/runtime/scripts/fix-openclaw-control-ui-path.sh +0 -0
  148. package/runtime/scripts/install_cli.sh +0 -0
  149. package/runtime/scripts/install_rosbridge_from_source.sh +0 -0
  150. package/runtime/scripts/onboard_robot.sh +0 -0
  151. package/runtime/scripts/pack-runtime.mjs +0 -0
  152. package/runtime/scripts/refresh-skill-deps.mjs +381 -0
  153. package/runtime/scripts/run_demo_native.sh +0 -0
  154. package/runtime/scripts/run_nemoclaw_host_stack.sh +0 -0
  155. package/runtime/scripts/run_robot_rosbridge.sh +0 -0
  156. package/runtime/scripts/setup_gateway_plugin.sh +32 -5
  157. package/runtime/scripts/setup_robot.sh +0 -0
  158. package/runtime/scripts/setup_workspace.sh +0 -0
  159. package/runtime/scripts/sim/run_sim.sh +0 -0
  160. package/runtime/scripts/smoke_test_nemoclaw.sh +0 -0
  161. package/runtime/scripts/start_demo.sh +0 -0
  162. package/runtime/scripts/sync-skill-tools.mjs +69 -12
  163. package/runtime/scripts/use-openclaw-2026.2.26.sh +0 -0
  164. package/runtime/scripts/use-openclaw-2026.3.11.sh +0 -0
@@ -1,6 +1,11 @@
1
1
  import type { TransportConfig } from "@agenticros/core";
2
- import type { RosTransport } from "@agenticros/core";
3
- import { createTransport, getTransportConfig } from "@agenticros/core";
2
+ import type { RosTransport, ResolvedRobot } from "@agenticros/core";
3
+ import {
4
+ createTransport,
5
+ getTransportConfig,
6
+ getTransportConfigForRobot,
7
+ hasRobotTransportOverride,
8
+ } from "@agenticros/core";
4
9
  import type { OpenClawPluginApi } from "./plugin-api.js";
5
10
  import type { PluginLogger } from "./plugin-api.js";
6
11
  import type { AgenticROSConfig } from "@agenticros/core";
@@ -238,6 +243,112 @@ async function ensureTransportConnected(
238
243
  await next;
239
244
  }
240
245
 
246
+ // ─────────────────────────────────────────────────────────────────────────────
247
+ // Phase 1.d-pool: per-robot transport override pool.
248
+ //
249
+ // The existing `transport` singleton above (with eager-retry / preflight /
250
+ // poll-reconnect) stays the source of truth for every robot WITHOUT a
251
+ // per-robot `transport` override in config. That's the common case and
252
+ // the plugin's hard-won robustness lives in that singleton.
253
+ //
254
+ // For robots that DO declare an override we maintain a smaller, lazier
255
+ // pool here. Override entries don't (yet) get the eager-connect /
256
+ // preflight / polling treatment — they connect on first tool call, fail
257
+ // loudly to the agent on connect timeout, and self-heal on the next
258
+ // acquire. That's intentional: most fleets have one or two override
259
+ // robots and the agent-facing error path is sufficient feedback.
260
+ //
261
+ // Pool key: `robot.id` (validated by `hasRobotTransportOverride`).
262
+ // ─────────────────────────────────────────────────────────────────────────────
263
+
264
+ const overrideTransports = new Map<string, RosTransport>();
265
+ const overrideInFlight = new Map<string, Promise<RosTransport>>();
266
+ const OVERRIDE_CONNECT_TIMEOUT_MS = 15_000;
267
+
268
+ /**
269
+ * Return the right transport for `robot`. Per-robot tools call this
270
+ * INSTEAD of `getTransport()` so a robot with a custom transport gets
271
+ * its own connection. Robots without an override fall through to the
272
+ * service-managed singleton with all its retry / preflight machinery.
273
+ *
274
+ * Throws when the singleton is requested but the service hasn't
275
+ * connected yet — same contract as `getTransport()`.
276
+ *
277
+ * Throws (or surfaces as a tool error after the timeout) when an
278
+ * override entry can't connect — the agent sees a clean failure and
279
+ * can fall back to a different robot id.
280
+ */
281
+ export async function getTransportForRobot(
282
+ config: AgenticROSConfig,
283
+ robot: ResolvedRobot,
284
+ ): Promise<RosTransport> {
285
+ if (!hasRobotTransportOverride(config, robot.id)) {
286
+ // Fast path: no override → reuse the service-managed singleton
287
+ // (with its retries and preflight). Same as the pre-pool behaviour.
288
+ return getTransport();
289
+ }
290
+ return acquireOverrideTransport(config, robot);
291
+ }
292
+
293
+ async function acquireOverrideTransport(
294
+ config: AgenticROSConfig,
295
+ robot: ResolvedRobot,
296
+ ): Promise<RosTransport> {
297
+ const cached = overrideTransports.get(robot.id);
298
+ if (cached && cached.getStatus() === "connected") return cached;
299
+
300
+ // Concurrent first-acquires share one connect promise so two near-
301
+ // simultaneous tool calls don't double-open the same Zenoh socket.
302
+ const flight = overrideInFlight.get(robot.id);
303
+ if (flight) return flight;
304
+
305
+ if (cached) {
306
+ // Stale entry — drop cleanly so a fresh build can proceed.
307
+ overrideTransports.delete(robot.id);
308
+ cached.disconnect().catch(() => {});
309
+ }
310
+
311
+ const connectFlight = (async (): Promise<RosTransport> => {
312
+ const transportCfg = buildTransportConfig(getTransportConfigForRobot(config, robot.id));
313
+ const t = await createTransport(transportCfg);
314
+ await connectWithTimeout(t, OVERRIDE_CONNECT_TIMEOUT_MS);
315
+ overrideTransports.set(robot.id, t);
316
+ return t;
317
+ })();
318
+ overrideInFlight.set(robot.id, connectFlight);
319
+ try {
320
+ return await connectFlight;
321
+ } finally {
322
+ overrideInFlight.delete(robot.id);
323
+ }
324
+ }
325
+
326
+ async function connectWithTimeout(t: RosTransport, timeoutMs: number): Promise<void> {
327
+ const timeout = new Promise<never>((_, reject) => {
328
+ setTimeout(
329
+ () =>
330
+ reject(
331
+ new Error(
332
+ `Per-robot transport connect timed out after ${timeoutMs / 1000}s. ` +
333
+ "Check the robot's transport override in config (router endpoint, mode).",
334
+ ),
335
+ ),
336
+ timeoutMs,
337
+ );
338
+ });
339
+ await Promise.race([t.connect(), timeout]);
340
+ }
341
+
342
+ async function drainOverridePool(): Promise<void> {
343
+ const pending: Promise<unknown>[] = [];
344
+ for (const t of overrideTransports.values()) {
345
+ pending.push(t.disconnect().catch(() => {}));
346
+ }
347
+ overrideTransports.clear();
348
+ overrideInFlight.clear();
349
+ await Promise.all(pending);
350
+ }
351
+
241
352
  const RETRY_INTERVAL_MS = 10000;
242
353
  const DISCONNECTED_POLL_MS = 15000;
243
354
 
@@ -283,6 +394,13 @@ export function registerService(api: OpenClawPluginApi, config: AgenticROSConfig
283
394
  currentMode = null;
284
395
  api.logger.info("ROS2 transport disconnected");
285
396
  }
397
+ // Drain any per-robot override connections too — otherwise they'd
398
+ // leak open Zenoh sockets across plugin reloads.
399
+ const overrideCount = overrideTransports.size;
400
+ if (overrideCount > 0) {
401
+ await drainOverridePool();
402
+ api.logger.info(`Disconnected ${overrideCount} per-robot override transport(s)`);
403
+ }
286
404
  },
287
405
  });
288
406
 
@@ -20,8 +20,8 @@
20
20
 
21
21
  import { createRequire } from "node:module";
22
22
  import { pathToFileURL } from "node:url";
23
- import { readFileSync, readdirSync, existsSync } from "node:fs";
24
- import { join } from "node:path";
23
+ import { readFileSync, existsSync } from "node:fs";
24
+ import { dirname, join } from "node:path";
25
25
  import type { OpenClawPluginApi } from "./plugin-api.js";
26
26
  import type { AgenticROSConfig } from "@agenticros/core";
27
27
  import type { RegisterSkill, SkillContext } from "./skill-api.js";
@@ -41,12 +41,40 @@ export function getLoadedSkillIds(): string[] {
41
41
  return [...loadedSkillIds];
42
42
  }
43
43
 
44
- function deriveSkillId(packageName: string): string {
45
- const lower = packageName.toLowerCase();
46
- if (lower.startsWith("agenticros-skill-")) {
47
- return lower.slice("agenticros-skill-".length);
44
+ /**
45
+ * Read the `agenticros` block from a skill's package.json. The block is now
46
+ * the single source of truth for the skill id, display name, and capability
47
+ * manifest. The legacy `agenticrosSkill: true | { capabilities }` form is
48
+ * NOT supported — every skill must declare a fully-formed `agenticros` block.
49
+ */
50
+ interface AgenticROSBlock {
51
+ id: string;
52
+ displayName?: string;
53
+ description?: string;
54
+ categories?: string[];
55
+ screenshots?: string[];
56
+ demoVideoUrl?: string;
57
+ capabilities?: unknown[];
58
+ }
59
+
60
+ interface SkillManifest {
61
+ name?: string;
62
+ main?: string;
63
+ agenticros?: AgenticROSBlock;
64
+ }
65
+
66
+ function readSkillManifest(pkgJsonPath: string): SkillManifest | null {
67
+ try {
68
+ return JSON.parse(readFileSync(pkgJsonPath, "utf-8")) as SkillManifest;
69
+ } catch {
70
+ return null;
48
71
  }
49
- return lower.replace(/^@[^/]+\//, "").replace(/[^a-z0-9]/g, "");
72
+ }
73
+
74
+ function isValidBlock(block: unknown): block is AgenticROSBlock {
75
+ if (!block || typeof block !== "object") return false;
76
+ const b = block as { id?: unknown };
77
+ return typeof b.id === "string" && /^[a-z0-9][a-z0-9-]*$/.test(b.id);
50
78
  }
51
79
 
52
80
  /**
@@ -56,7 +84,7 @@ function deriveSkillId(packageName: string): string {
56
84
  * — including any `api.registerTool(...)` the skill makes — completes before
57
85
  * we return, and therefore before the OpenClaw host snapshots `captured.tools`.
58
86
  *
59
- * Returns the derived skill id on success, or `null` (with a logged warning /
87
+ * Returns the declared skill id on success, or `null` (with a logged warning /
60
88
  * error) on any failure mode: missing export, top-level await (which surfaces
61
89
  * as `ERR_REQUIRE_ASYNC_MODULE`), or thrown initialization error.
62
90
  */
@@ -66,8 +94,8 @@ function loadSkillModuleSync(
66
94
  context: SkillContext,
67
95
  entryPath: string,
68
96
  packageName: string,
97
+ skillId: string,
69
98
  ): string | null {
70
- const skillId = deriveSkillId(packageName);
71
99
  let mod: unknown;
72
100
  try {
73
101
  mod = require(entryPath);
@@ -110,36 +138,80 @@ function loadSkillModuleSync(
110
138
  }
111
139
  }
112
140
 
141
+ interface ResolvedSkill {
142
+ entry: string;
143
+ packageName: string;
144
+ skillId: string;
145
+ }
146
+
113
147
  /**
114
- * Resolve a package name to its main entry path (Node resolution).
148
+ * Resolve an npm package name to a `ResolvedSkill` (entry + name + skill id).
149
+ * Reads the package's package.json to extract `pkg.agenticros.id`; refuses
150
+ * to register the skill if the block is missing or malformed.
115
151
  */
116
- function resolvePackageEntry(packageName: string): string | null {
152
+ function resolveSkillByPackage(
153
+ api: OpenClawPluginApi,
154
+ packageName: string,
155
+ ): ResolvedSkill | null {
156
+ let entry: string;
117
157
  try {
118
- const pkgPath = require.resolve(packageName, { paths: [process.cwd()] });
119
- return pkgPath;
158
+ entry = require.resolve(packageName, { paths: [process.cwd()] });
120
159
  } catch {
160
+ api.logger.warn(`Skill package not found: ${packageName}`);
161
+ return null;
162
+ }
163
+ // Walk up from the resolved entry to find the package.json.
164
+ const pkgJsonPath = findPackageJsonForEntry(entry);
165
+ if (!pkgJsonPath) {
166
+ api.logger.warn(`Skill ${packageName}: cannot locate package.json next to ${entry}`);
167
+ return null;
168
+ }
169
+ const manifest = readSkillManifest(pkgJsonPath);
170
+ if (!manifest || !isValidBlock(manifest.agenticros)) {
171
+ api.logger.warn(
172
+ `Skill ${packageName}: missing or invalid \`agenticros\` block in package.json. ` +
173
+ "Declare an `agenticros.id` (kebab-case) to register as a skill.",
174
+ );
121
175
  return null;
122
176
  }
177
+ return {
178
+ entry,
179
+ packageName: manifest.name ?? packageName,
180
+ skillId: manifest.agenticros!.id,
181
+ };
182
+ }
183
+
184
+ /** Walk up from `entry` (a file inside the package) to find its package.json. */
185
+ function findPackageJsonForEntry(entry: string): string | null {
186
+ let dir = dirname(entry);
187
+ for (let i = 0; i < 6; i++) {
188
+ const candidate = join(dir, "package.json");
189
+ if (existsSync(candidate)) return candidate;
190
+ const parent = dirname(dir);
191
+ if (parent === dir) return null;
192
+ dir = parent;
193
+ }
194
+ return null;
123
195
  }
124
196
 
125
197
  /**
126
- * Scan a directory for package.json with "agenticrosSkill": true and return entry path.
198
+ * Scan a directory for a package.json with a valid `agenticros` block and
199
+ * return its entry path + declared id.
127
200
  */
128
- function findSkillInPath(dirPath: string): { entry: string; packageName: string } | null {
201
+ function findSkillInPath(dirPath: string): ResolvedSkill | null {
129
202
  if (!existsSync(dirPath)) return null;
130
203
  const pkgPath = join(dirPath, "package.json");
131
204
  if (!existsSync(pkgPath)) return null;
132
- let pkg: { agenticrosSkill?: boolean; main?: string; name?: string };
133
- try {
134
- pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
135
- } catch {
136
- return null;
137
- }
138
- if (!pkg.agenticrosSkill) return null;
205
+ const pkg = readSkillManifest(pkgPath);
206
+ if (!pkg || !isValidBlock(pkg.agenticros)) return null;
139
207
  const main = pkg.main ?? "index.js";
140
208
  const entry = join(dirPath, main);
141
209
  if (!existsSync(entry)) return null;
142
- return { entry, packageName: pkg.name ?? "unknown" };
210
+ return {
211
+ entry,
212
+ packageName: pkg.name ?? "unknown",
213
+ skillId: pkg.agenticros!.id,
214
+ };
143
215
  }
144
216
 
145
217
  /**
@@ -166,19 +238,25 @@ export function loadSkills(
166
238
  loadedSkillIds.length = 0;
167
239
 
168
240
  for (const pkgName of packages) {
169
- const entryPath = resolvePackageEntry(pkgName);
170
- if (!entryPath) {
171
- api.logger.warn(`Skill package not found: ${pkgName}`);
172
- continue;
173
- }
174
- const skillId = loadSkillModuleSync(api, config, context, entryPath, pkgName);
241
+ const resolved = resolveSkillByPackage(api, pkgName);
242
+ if (!resolved) continue;
243
+ const skillId = loadSkillModuleSync(
244
+ api,
245
+ config,
246
+ context,
247
+ resolved.entry,
248
+ resolved.packageName,
249
+ resolved.skillId,
250
+ );
175
251
  if (skillId) loadedSkillIds.push(skillId);
176
252
  }
177
253
 
178
254
  for (const dir of paths) {
179
255
  const resolved = findSkillInPath(dir);
180
256
  if (!resolved) {
181
- api.logger.warn(`No agenticros skill in path: ${dir}`);
257
+ api.logger.warn(
258
+ `No agenticros skill in path: ${dir} (missing \`agenticros\` block in package.json).`,
259
+ );
182
260
  continue;
183
261
  }
184
262
  const skillId = loadSkillModuleSync(
@@ -187,6 +265,7 @@ export function loadSkills(
187
265
  context,
188
266
  resolved.entry,
189
267
  resolved.packageName,
268
+ resolved.skillId,
190
269
  );
191
270
  if (skillId && !loadedSkillIds.includes(skillId)) loadedSkillIds.push(skillId);
192
271
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Phase 1.d helper — shared by every OpenClaw ROS2 tool that accepts an
3
+ * optional `robot_id` parameter. Keeping it in one place means the
4
+ * schema definition and resolution error format stay consistent across
5
+ * tools and match the claude-code adapter.
6
+ *
7
+ * Usage in a tool's parameters:
8
+ *
9
+ * parameters: Type.Object({
10
+ * topic: Type.String({ description: "..." }),
11
+ * ...ROBOT_ID_SCHEMA,
12
+ * })
13
+ *
14
+ * Usage in execute():
15
+ *
16
+ * const resolved = resolveRobotForTool(config, params);
17
+ * if ("error" in resolved) return resolved.error;
18
+ * const ns = resolved.robot.namespace;
19
+ */
20
+
21
+ import { Type } from "@sinclair/typebox";
22
+ import { resolveRobotFromArgs, type ResolvedRobot } from "@agenticros/core";
23
+ import type { AgenticROSConfig } from "@agenticros/core";
24
+
25
+ /**
26
+ * TypeBox schema fragment — spread into a tool's parameters object to
27
+ * advertise an optional `robot_id` argument.
28
+ */
29
+ export const ROBOT_ID_SCHEMA = {
30
+ robot_id: Type.Optional(
31
+ Type.String({
32
+ description:
33
+ "Optional robot id (from ros2_list_robots) to scope this call. When omitted, the active robot is used.",
34
+ }),
35
+ ),
36
+ };
37
+
38
+ /**
39
+ * Try to resolve the target robot from the tool's params. On success
40
+ * returns `{ robot }`. On unknown-id failure returns `{ error }` —
41
+ * already shaped as an OpenClaw tool response (content + details) so
42
+ * the caller can `return resolved.error` directly. The error text comes
43
+ * from `@agenticros/core/resolveRobot` and already lists known ids +
44
+ * recommends ros2_list_robots, so the agent can self-correct.
45
+ */
46
+ export function resolveRobotForTool(
47
+ config: AgenticROSConfig,
48
+ params: Record<string, unknown>,
49
+ ):
50
+ | { robot: ResolvedRobot }
51
+ | {
52
+ error: {
53
+ content: Array<{ type: "text"; text: string }>;
54
+ details: { success: false; error: string };
55
+ };
56
+ } {
57
+ try {
58
+ return { robot: resolveRobotFromArgs(config, params) };
59
+ } catch (err) {
60
+ const msg = err instanceof Error ? err.message : String(err);
61
+ return {
62
+ error: {
63
+ content: [{ type: "text", text: msg }],
64
+ details: { success: false, error: msg },
65
+ },
66
+ };
67
+ }
68
+ }
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "../plugin-api.js";
1
+ import type { AgentTool, OpenClawPluginApi } from "../plugin-api.js";
2
2
  import type { AgenticROSConfig } from "@agenticros/core";
3
3
  import { registerPublishTool } from "./ros2-publish.js";
4
4
  import { registerSubscribeTool } from "./ros2-subscribe.js";
@@ -8,6 +8,39 @@ import { registerParamTools } from "./ros2-param.js";
8
8
  import { registerIntrospectTool } from "./ros2-introspect.js";
9
9
  import { registerCameraTool } from "./ros2-camera.js";
10
10
  import { registerDepthDistanceTool } from "./ros2-depth-distance.js";
11
+ import { registerCapabilitiesTool } from "./ros2-capabilities.js";
12
+ import { registerRobotsTool } from "./ros2-robots.js";
13
+ import { registerDiscoverRobotsTool } from "./ros2-discover.js";
14
+ import { registerFindRobotsForTool } from "./ros2-find-robots-for.js";
15
+ import { registerMissionTool, type ToolRegistry } from "./ros2-mission.js";
16
+ import { registerMissionCancelTool } from "./mission-cancel.js";
17
+
18
+ /**
19
+ * Wrap the OpenClaw API so every registerTool() call is also recorded in
20
+ * a local tool registry. The mission runner uses the registry to
21
+ * dispatch sub-tool calls by name (e.g. capability "drive_base" routes
22
+ * to the registered "ros2_publish" tool's execute()).
23
+ *
24
+ * We keep this internal — skills that register their own tools via
25
+ * `api.registerTool` won't appear in the registry, which is fine for
26
+ * v1: today the mission runner only supports the eight intrinsic
27
+ * capability bindings declared in ros2-mission.ts. Phase 1.d will
28
+ * extend this to capture skill-declared tools as well.
29
+ */
30
+ function wrapApiWithToolCapture(api: OpenClawPluginApi): {
31
+ wrappedApi: OpenClawPluginApi;
32
+ registry: ToolRegistry;
33
+ } {
34
+ const registry: ToolRegistry = new Map();
35
+ const wrappedApi: OpenClawPluginApi = {
36
+ ...api,
37
+ registerTool: (tool: AgentTool, opts) => {
38
+ registry.set(tool.name, tool);
39
+ api.registerTool(tool, opts);
40
+ },
41
+ };
42
+ return { wrappedApi, registry };
43
+ }
11
44
 
12
45
  /**
13
46
  * Register core ROS2 tools with the OpenClaw AI agent.
@@ -15,12 +48,19 @@ import { registerDepthDistanceTool } from "./ros2-depth-distance.js";
15
48
  * Memory tools register asynchronously from index.ts after initMemory resolves.
16
49
  */
17
50
  export function registerTools(api: OpenClawPluginApi, config: AgenticROSConfig): void {
18
- registerPublishTool(api, config);
19
- registerSubscribeTool(api, config);
20
- registerServiceTool(api, config);
21
- registerActionTool(api, config);
22
- registerParamTools(api, config);
23
- registerIntrospectTool(api);
24
- registerCameraTool(api, config);
25
- registerDepthDistanceTool(api, config);
51
+ const { wrappedApi, registry } = wrapApiWithToolCapture(api);
52
+ registerPublishTool(wrappedApi, config);
53
+ registerSubscribeTool(wrappedApi, config);
54
+ registerServiceTool(wrappedApi, config);
55
+ registerActionTool(wrappedApi, config);
56
+ registerParamTools(wrappedApi, config);
57
+ registerIntrospectTool(wrappedApi);
58
+ registerCameraTool(wrappedApi, config);
59
+ registerDepthDistanceTool(wrappedApi, config);
60
+ registerCapabilitiesTool(wrappedApi, config);
61
+ registerRobotsTool(wrappedApi, config);
62
+ registerDiscoverRobotsTool(wrappedApi, config);
63
+ registerFindRobotsForTool(wrappedApi, config);
64
+ registerMissionTool(wrappedApi, config, registry);
65
+ registerMissionCancelTool(wrappedApi);
26
66
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Tool: mission_cancel — Phase 1.f of the AgenticROS strategy.
3
+ *
4
+ * Cancel a mission that's currently running in this OpenClaw plugin
5
+ * process. Pass the `mission_id` returned by `run_mission`. The mission
6
+ * runner stops at the next step boundary (the in-flight step finishes
7
+ * naturally — per-tool preemption is out of scope for Phase 1.f),
8
+ * marks remaining steps as "cancelled", and returns a result with
9
+ * `status: "cancelled"`.
10
+ *
11
+ * Idempotent / safe-by-default:
12
+ * - Unknown mission_id → `found: false`, never an error
13
+ * - Calling twice on the same id → second call returns
14
+ * `already_cancelled: true` and is otherwise a no-op
15
+ * - No transport required — this only mutates an in-process
16
+ * `MissionRegistry`
17
+ *
18
+ * Mirrored across all three adapters (Claude Code, OpenClaw, Gemini)
19
+ * with identical request/response shapes — see
20
+ * `packages/agenticros-claude-code/src/tools.ts` for the canonical one.
21
+ */
22
+
23
+ import { Type } from "@sinclair/typebox";
24
+ import type { OpenClawPluginApi } from "../plugin-api.js";
25
+ import { getMissionRegistry } from "../mission-registry.js";
26
+
27
+ export function registerMissionCancelTool(api: OpenClawPluginApi): void {
28
+ api.registerTool({
29
+ name: "mission_cancel",
30
+ label: "Cancel Mission",
31
+ description:
32
+ "Cancel a mission that's currently running in this OpenClaw plugin process. " +
33
+ "Pass the mission_id returned by run_mission. The mission runner stops at the next " +
34
+ "step boundary (the in-flight step finishes naturally), marks remaining steps as " +
35
+ "'cancelled', and returns. If the mission has already finished (or the id is unknown), " +
36
+ "this is a no-op that returns found=false. Optional 'reason' is recorded in the " +
37
+ "cancelled step results for traceability.",
38
+ parameters: Type.Object({
39
+ mission_id: Type.String({
40
+ description: "The mission_id echoed by run_mission. Required.",
41
+ }),
42
+ reason: Type.Optional(
43
+ Type.String({
44
+ description: "Optional free-text reason — surfaced in the cancelled mission result.",
45
+ }),
46
+ ),
47
+ }),
48
+
49
+ async execute(_toolCallId, params) {
50
+ const missionId = typeof params["mission_id"] === "string" ? (params["mission_id"] as string).trim() : "";
51
+ if (!missionId) {
52
+ const text = "mission_cancel requires 'mission_id' (a non-empty string returned by run_mission).";
53
+ return { content: [{ type: "text", text }], details: { success: false, error: text } };
54
+ }
55
+ const reason = typeof params["reason"] === "string" ? (params["reason"] as string) : undefined;
56
+ const outcome = getMissionRegistry().cancel(missionId, reason);
57
+ const details = {
58
+ success: true,
59
+ mission_id: missionId,
60
+ found: outcome.found,
61
+ already_cancelled: outcome.alreadyCancelled,
62
+ reason: reason ?? null,
63
+ };
64
+ return {
65
+ content: [{ type: "text", text: JSON.stringify(details) }],
66
+ details,
67
+ };
68
+ },
69
+ });
70
+ }
@@ -2,7 +2,8 @@ import { Type } from "@sinclair/typebox";
2
2
  import type { OpenClawPluginApi } from "../plugin-api.js";
3
3
  import type { AgenticROSConfig } from "@agenticros/core";
4
4
  import { toNamespacedTopic } from "@agenticros/core";
5
- import { getTransport } from "../service.js";
5
+ import { getTransportForRobot } from "../service.js";
6
+ import { ROBOT_ID_SCHEMA, resolveRobotForTool } from "./_robot-helpers.js";
6
7
 
7
8
  /**
8
9
  * Register the ros2_action_goal tool with the AI agent.
@@ -14,22 +15,28 @@ export function registerActionTool(api: OpenClawPluginApi, config: AgenticROSCon
14
15
  label: "ROS2 Action Goal",
15
16
  description:
16
17
  "Send a goal to a ROS2 action server and stream feedback. " +
17
- "Use this for long-running operations like navigation or arm movements.",
18
+ "Use this for long-running operations like navigation or arm movements. " +
19
+ "Pass robot_id (from ros2_list_robots) to target a specific robot.",
18
20
  parameters: Type.Object({
19
21
  action: Type.String({ description: "The ROS2 action server name (e.g., '/navigate_to_pose')" }),
20
22
  actionType: Type.String({ description: "The ROS2 action type (e.g., 'nav2_msgs/action/NavigateToPose')" }),
21
23
  goal: Type.Record(Type.String(), Type.Unknown(), {
22
24
  description: "The action goal parameters",
23
25
  }),
26
+ ...ROBOT_ID_SCHEMA,
24
27
  }),
25
28
 
26
29
  async execute(_toolCallId, params) {
30
+ const resolved = resolveRobotForTool(config, params);
31
+ if ("error" in resolved) return resolved.error;
32
+ const { robot } = resolved;
33
+
27
34
  const rawAction = params["action"] as string;
28
- const action = toNamespacedTopic(config, rawAction);
35
+ const action = toNamespacedTopic(robot.namespace, rawAction);
29
36
  const actionType = params["actionType"] as string;
30
37
  const goal = params["goal"] as Record<string, unknown>;
31
38
 
32
- const transport = getTransport();
39
+ const transport = await getTransportForRobot(config, robot);
33
40
  const actionResult = await transport.sendActionGoal({
34
41
  action,
35
42
  actionType,
@@ -9,11 +9,12 @@ import {
9
9
  mimeTypeForSnapshotBase64,
10
10
  rosNumericField,
11
11
  } from "@agenticros/ros-camera";
12
- import { getTransport } from "../service.js";
12
+ import { getTransportForRobot } from "../service.js";
13
13
  import { normalizePluginToolImageBase64 } from "../plugin-image-base64.js";
14
14
  import { trimJpegToLastEoi } from "../image-binary-trim.js";
15
15
  import { storeCameraSnapshot } from "../camera-snapshot-cache.js";
16
16
  import { describeImageBestEffort } from "../describer.js";
17
+ import { ROBOT_ID_SCHEMA, resolveRobotForTool } from "./_robot-helpers.js";
17
18
 
18
19
  /** Known camera topic patterns for common setups (e.g. RealSense). */
19
20
  export const REALSENSE_CAMERA_TOPICS = {
@@ -64,20 +65,25 @@ export function registerCameraTool(api: OpenClawPluginApi, config: AgenticROSCon
64
65
  ),
65
66
  ),
66
67
  timeout: Type.Optional(Type.Number({ description: "Timeout in milliseconds (default: 10000)" })),
68
+ ...ROBOT_ID_SCHEMA,
67
69
  }),
68
70
 
69
71
  async execute(_toolCallId, params) {
72
+ const resolved = resolveRobotForTool(config, params);
73
+ if ("error" in resolved) return resolved.error;
74
+ const { robot } = resolved;
75
+
70
76
  const defaultTopic =
71
- (config.robot?.cameraTopic ?? "").trim() || "/camera/camera/color/image_raw/compressed";
77
+ (robot.cameraTopic ?? "").trim() || "/camera/camera/color/image_raw/compressed";
72
78
  const rawTopic = (params["topic"] as string | undefined) ?? defaultTopic;
73
- const topic = resolveCameraSubscribeTopic(config, rawTopic);
79
+ const topic = resolveCameraSubscribeTopic(robot.namespace, rawTopic);
74
80
  const rawMsgType = params["message_type"] as string | undefined;
75
81
  const messageType: "CompressedImage" | "Image" =
76
82
  rawMsgType === "Image" ? "Image" : "CompressedImage";
77
83
  const timeout = (params["timeout"] as number | undefined) ?? 10000;
78
84
 
79
85
  try {
80
- const transport = getTransport();
86
+ const transport = await getTransportForRobot(config, robot);
81
87
  const typeSel = messageType === "Image" ? ROS_MSG_IMAGE : ROS_MSG_COMPRESSED_IMAGE;
82
88
 
83
89
  const result = await new Promise<{