agenticros 0.1.17 → 0.1.19
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/dist/__tests__/robot-config.test.d.ts +2 -0
- package/dist/__tests__/robot-config.test.d.ts.map +1 -0
- package/dist/__tests__/robot-config.test.js +374 -0
- package/dist/__tests__/robot-config.test.js.map +1 -0
- package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
- package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
- package/dist/__tests__/transport-shorthand.test.js +88 -0
- package/dist/__tests__/transport-shorthand.test.js.map +1 -0
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +55 -3
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/robots.d.ts +57 -0
- package/dist/commands/robots.d.ts.map +1 -0
- package/dist/commands/robots.js +678 -0
- package/dist/commands/robots.js.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/util/mcp-discovery.d.ts +57 -0
- package/dist/util/mcp-discovery.d.ts.map +1 -0
- package/dist/util/mcp-discovery.js +153 -0
- package/dist/util/mcp-discovery.js.map +1 -0
- package/dist/util/robot-config.d.ts +167 -0
- package/dist/util/robot-config.d.ts.map +1 -0
- package/dist/util/robot-config.js +375 -0
- package/dist/util/robot-config.js.map +1 -0
- package/dist/util/transport-shorthand.d.ts +66 -0
- package/dist/util/transport-shorthand.d.ts.map +1 -0
- package/dist/util/transport-shorthand.js +102 -0
- package/dist/util/transport-shorthand.js.map +1 -0
- package/package.json +2 -1
- package/runtime/BUNDLE.json +1 -1
- package/runtime/README.md +4 -0
- package/runtime/package.json +6 -1
- package/runtime/packages/agenticros/openclaw.plugin.json +6 -0
- package/runtime/packages/agenticros/package.json +2 -1
- package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
- package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
- package/runtime/packages/agenticros/src/service.ts +120 -2
- package/runtime/packages/agenticros/src/skill-loader.ts +9 -1
- package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
- package/runtime/packages/agenticros/src/tools/index.ts +49 -9
- package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
- package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
- package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
- package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
- package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
- package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
- package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
- package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
- package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
- package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
- package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
- package/runtime/packages/agenticros-claude-code/package.json +2 -1
- package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +964 -0
- package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
- package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
- package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
- package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
- package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
- package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
- package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
- package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
- package/runtime/packages/agenticros-gemini/package.json +1 -0
- package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
- package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
- package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
- package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
- package/runtime/packages/core/package.json +1 -1
- package/runtime/packages/core/src/__tests__/capabilities.test.ts +261 -0
- package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
- package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
- package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
- package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
- package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
- package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
- package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
- package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
- package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
- package/runtime/packages/core/src/capabilities.ts +340 -0
- package/runtime/packages/core/src/config.ts +220 -38
- package/runtime/packages/core/src/discovery.ts +193 -0
- package/runtime/packages/core/src/find-robots-for.ts +164 -0
- package/runtime/packages/core/src/index.ts +67 -1
- package/runtime/packages/core/src/mission-registry.ts +142 -0
- package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
- package/runtime/packages/core/src/mission.ts +574 -0
- package/runtime/packages/core/src/planner/index.ts +564 -0
- package/runtime/packages/core/src/robots.ts +282 -0
- package/runtime/packages/core/src/topic-utils.ts +47 -9
- package/runtime/packages/core/src/transport-pool.ts +171 -0
- package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
- package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
- package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
- package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
- package/runtime/scripts/refresh-skill-deps.mjs +381 -0
- package/runtime/scripts/setup_gateway_plugin.sh +32 -5
- package/runtime/scripts/start_demo.sh +26 -3
- package/runtime/scripts/sync-skill-tools.mjs +67 -10
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agenticros robots` — manage the multi-robot fleet config.
|
|
3
|
+
*
|
|
4
|
+
* Subactions:
|
|
5
|
+
* list / show / ls Print configured robots + their online status (default).
|
|
6
|
+
* discover / scan Live-discover robots on the wire, offer to add unknown ones.
|
|
7
|
+
* add [id] Add a robot to config.robots[] (interactive when id omitted).
|
|
8
|
+
* Flags: --name, --namespace, --camera, --default,
|
|
9
|
+
* --transport=<shorthand>, --transport-json=<json>.
|
|
10
|
+
* remove / rm <id> Remove a robot from config.robots[].
|
|
11
|
+
* set-default <id> Mark a robot as the active default.
|
|
12
|
+
* set-transport <id> [shorthand] Apply a per-robot transport override (or use
|
|
13
|
+
* --transport / --transport-json instead of [shorthand]).
|
|
14
|
+
* clear-transport <id> Drop the per-robot transport override so the robot
|
|
15
|
+
* inherits the global transport config.
|
|
16
|
+
*
|
|
17
|
+
* All mutations target `~/.agenticros/config.json`. The legacy single-robot
|
|
18
|
+
* `config.robot` is automatically promoted into `config.robots[]` on the first
|
|
19
|
+
* multi-robot write — see `util/robot-config.ts` for the exact rules.
|
|
20
|
+
*
|
|
21
|
+
* Live discovery is delegated to the `ros2_discover_robots` MCP tool via a
|
|
22
|
+
* subprocess to the agenticros-claude-code server. That keeps this CLI small
|
|
23
|
+
* (no `@agenticros/core` import) AND ensures the user sees the same robots
|
|
24
|
+
* the AI agent sees.
|
|
25
|
+
*/
|
|
26
|
+
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
27
|
+
import { addRobot, clearTransportForRobot, getActiveRobotId, readConfigObject, readRobots, removeRobot, robotConfigPath, setDefaultRobot, setTransportForRobot, writeConfigObject, } from "../util/robot-config.js";
|
|
28
|
+
import { discoverViaMcp, } from "../util/mcp-discovery.js";
|
|
29
|
+
import { colors, dim, err, header, info, isTty, ok, warn } from "../util/logger.js";
|
|
30
|
+
import { parseTransportJson, parseTransportShorthand, } from "../util/transport-shorthand.js";
|
|
31
|
+
/** Known sensor flags. We reject unknown keys with an actionable error. */
|
|
32
|
+
const KNOWN_SENSOR_KEYS = new Set([
|
|
33
|
+
"has_realsense",
|
|
34
|
+
"has_lidar",
|
|
35
|
+
"has_arm",
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Parse a `--sensors=<csv>` value into a RobotSensors partial.
|
|
39
|
+
*
|
|
40
|
+
* Grammar: `key[,...]` where each entry is `keyname` (sets true) or
|
|
41
|
+
* `!keyname` (sets false). Unknown keys throw with the accepted set so
|
|
42
|
+
* the user can self-correct.
|
|
43
|
+
*
|
|
44
|
+
* Examples:
|
|
45
|
+
* "has_realsense" → { has_realsense: true }
|
|
46
|
+
* "has_realsense,has_lidar" → { has_realsense: true, has_lidar: true }
|
|
47
|
+
* "has_realsense,!has_arm" → { has_realsense: true, has_arm: false }
|
|
48
|
+
* "" → {}
|
|
49
|
+
*
|
|
50
|
+
* Returns `undefined` when the input is undefined (so callers can
|
|
51
|
+
* distinguish "user didn't pass the flag" from "user passed empty").
|
|
52
|
+
*/
|
|
53
|
+
function parseSensorsCsv(raw) {
|
|
54
|
+
if (raw === undefined)
|
|
55
|
+
return undefined;
|
|
56
|
+
const trimmed = raw.trim();
|
|
57
|
+
if (!trimmed)
|
|
58
|
+
return {};
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const part of trimmed.split(",")) {
|
|
61
|
+
const entry = part.trim();
|
|
62
|
+
if (!entry)
|
|
63
|
+
continue;
|
|
64
|
+
let value = true;
|
|
65
|
+
let key = entry;
|
|
66
|
+
if (entry.startsWith("!")) {
|
|
67
|
+
value = false;
|
|
68
|
+
key = entry.slice(1).trim();
|
|
69
|
+
}
|
|
70
|
+
if (!KNOWN_SENSOR_KEYS.has(key)) {
|
|
71
|
+
throw new Error(`Unknown sensor tag "${key}". Accepted: ${[...KNOWN_SENSOR_KEYS].join(", ")}. ` +
|
|
72
|
+
'Prefix with "!" to set false (e.g. --sensors=has_realsense,!has_arm).');
|
|
73
|
+
}
|
|
74
|
+
out[key] = value;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse a `--capabilities=<csv>` value. Empty string returns an empty
|
|
80
|
+
* array (which the caller interprets as "clear the field"); undefined
|
|
81
|
+
* returns undefined (meaning the flag wasn't supplied).
|
|
82
|
+
*/
|
|
83
|
+
function parseCapabilitiesCsv(raw) {
|
|
84
|
+
if (raw === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
const trimmed = raw.trim();
|
|
87
|
+
if (!trimmed)
|
|
88
|
+
return [];
|
|
89
|
+
return trimmed
|
|
90
|
+
.split(",")
|
|
91
|
+
.map((s) => s.trim())
|
|
92
|
+
.filter((s) => s.length > 0);
|
|
93
|
+
}
|
|
94
|
+
export async function robotsCommand(opts) {
|
|
95
|
+
const action = (opts.action ?? "list").toLowerCase();
|
|
96
|
+
switch (action) {
|
|
97
|
+
case "list":
|
|
98
|
+
case "ls":
|
|
99
|
+
case "show":
|
|
100
|
+
return listAction();
|
|
101
|
+
case "discover":
|
|
102
|
+
case "scan":
|
|
103
|
+
return discoverAction();
|
|
104
|
+
case "add":
|
|
105
|
+
return addAction(opts);
|
|
106
|
+
case "remove":
|
|
107
|
+
case "rm":
|
|
108
|
+
case "delete":
|
|
109
|
+
return removeAction(opts.arg);
|
|
110
|
+
case "set-default":
|
|
111
|
+
case "default":
|
|
112
|
+
return setDefaultAction(opts.arg);
|
|
113
|
+
case "set-transport":
|
|
114
|
+
case "transport":
|
|
115
|
+
return setTransportAction(opts);
|
|
116
|
+
case "clear-transport":
|
|
117
|
+
return clearTransportAction(opts.arg);
|
|
118
|
+
default:
|
|
119
|
+
err(`Unknown robots action '${opts.action}'.`);
|
|
120
|
+
err("Use: list | discover | add [id] | remove <id> | set-default <id> | set-transport <id> [shorthand] | clear-transport <id>");
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Parse `--transport` / `--transport-json` (mutually exclusive) into the
|
|
126
|
+
* JSON-shape stored under `config.robots[i].transport`. Returns
|
|
127
|
+
* `undefined` when neither flag is set. Exits the process on parse
|
|
128
|
+
* errors with an actionable message — we deliberately don't propagate
|
|
129
|
+
* the throw because option-parse errors should be terminal.
|
|
130
|
+
*/
|
|
131
|
+
function parseOverrideFromOptions(opts) {
|
|
132
|
+
if (opts.transport && opts.transportJson) {
|
|
133
|
+
err("Pass either --transport=<shorthand> OR --transport-json=<json>, not both.");
|
|
134
|
+
process.exit(2);
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
if (opts.transport) {
|
|
138
|
+
return parseTransportShorthand(opts.transport);
|
|
139
|
+
}
|
|
140
|
+
if (opts.transportJson) {
|
|
141
|
+
return parseTransportJson(opts.transportJson);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
err(e instanceof Error ? e.message : String(e));
|
|
146
|
+
process.exit(2);
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
151
|
+
// list
|
|
152
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
153
|
+
async function listAction() {
|
|
154
|
+
header("AgenticROS robots");
|
|
155
|
+
info(`Config: ${robotConfigPath()}`);
|
|
156
|
+
const obj = readConfigObject();
|
|
157
|
+
const { robots, from } = readRobots(obj);
|
|
158
|
+
const activeId = getActiveRobotId(obj);
|
|
159
|
+
if (robots.length === 0) {
|
|
160
|
+
dim("No robots configured yet. Run `agenticros robots discover` to find robots on the wire.");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Best-effort discovery so we can mark each entry online/offline.
|
|
164
|
+
// It's fine if this fails — the list still renders without the badge.
|
|
165
|
+
let discovery;
|
|
166
|
+
try {
|
|
167
|
+
discovery = await discoverViaMcp();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* render list-only — badge falls back to "?" */
|
|
171
|
+
}
|
|
172
|
+
process.stdout.write(`\n${colors.bold("Configured robots:")} ${dimSource(from)}\n`);
|
|
173
|
+
for (const r of robots) {
|
|
174
|
+
const isActive = r.id === activeId;
|
|
175
|
+
const badge = renderOnlineBadge(r, discovery);
|
|
176
|
+
const star = isActive ? colors.bold(colors.green(" (active)")) : "";
|
|
177
|
+
process.stdout.write(` ${badge} ${colors.bold(r.id)}${star} ${colors.dim(r.name ?? "Robot")}\n`);
|
|
178
|
+
process.stdout.write(` namespace: ${colors.dim(r.namespace || "(empty)")} ` +
|
|
179
|
+
`camera: ${colors.dim(r.cameraTopic || "(default)")}\n`);
|
|
180
|
+
// Phase 1.e: show kind + sensors so a user can confirm at a glance
|
|
181
|
+
// that the fleet metadata behind ros2_find_robots_for is correct.
|
|
182
|
+
// Suppress the row when kind is "amr" AND sensors are unset — that's
|
|
183
|
+
// the implicit default and doesn't need to add visual noise.
|
|
184
|
+
const showKind = r.kind && r.kind !== "amr";
|
|
185
|
+
const sensorsSummary = r.sensors ? describeSensors(r.sensors) : "";
|
|
186
|
+
if (showKind || sensorsSummary) {
|
|
187
|
+
const kindPart = showKind ? `kind: ${colors.cyan(r.kind)}` : "";
|
|
188
|
+
const sensorsPart = sensorsSummary
|
|
189
|
+
? `sensors: ${colors.cyan(sensorsSummary)}`
|
|
190
|
+
: "";
|
|
191
|
+
process.stdout.write(` ${[kindPart, sensorsPart].filter((s) => s).join(" ")}\n`);
|
|
192
|
+
}
|
|
193
|
+
if (r.capabilities && r.capabilities.length > 0) {
|
|
194
|
+
process.stdout.write(` capabilities: ${colors.cyan(r.capabilities.join(", "))}\n`);
|
|
195
|
+
}
|
|
196
|
+
if (r.transport) {
|
|
197
|
+
process.stdout.write(` transport: ${colors.cyan(describeOverride(r.transport))}\n`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (discovery && discovery.unknown_detected.length > 0) {
|
|
201
|
+
process.stdout.write(`\n${colors.bold(colors.yellow("Detected on the wire but NOT in config:"))}\n`);
|
|
202
|
+
for (const d of discovery.unknown_detected) {
|
|
203
|
+
process.stdout.write(` ${colors.yellow("○")} ${colors.bold(d.id)} ${colors.dim(d.cmdVelTopic)} ${colors.dim(`(${d.topicCount} topic(s))`)}\n`);
|
|
204
|
+
}
|
|
205
|
+
dim(`Promote any of them with 'agenticros robots add <id>' or 'agenticros robots discover'.`);
|
|
206
|
+
}
|
|
207
|
+
process.stdout.write("\n");
|
|
208
|
+
}
|
|
209
|
+
function dimSource(from) {
|
|
210
|
+
if (from === "explicit")
|
|
211
|
+
return colors.dim("(explicit robots[] in config)");
|
|
212
|
+
if (from === "legacy")
|
|
213
|
+
return colors.dim("(synthesised from legacy config.robot — promoted on next add)");
|
|
214
|
+
return colors.dim("(none)");
|
|
215
|
+
}
|
|
216
|
+
function renderOnlineBadge(r, discovery) {
|
|
217
|
+
if (!discovery)
|
|
218
|
+
return colors.dim("●");
|
|
219
|
+
const onlineIds = new Set(discovery.configured_online.map((x) => x.id));
|
|
220
|
+
if (onlineIds.has(r.id))
|
|
221
|
+
return colors.green("●");
|
|
222
|
+
return colors.dim("○");
|
|
223
|
+
}
|
|
224
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
225
|
+
// discover
|
|
226
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
227
|
+
async function discoverAction() {
|
|
228
|
+
header("Discover AgenticROS robots");
|
|
229
|
+
info("Scanning the live ROS2 topic graph via the MCP server …");
|
|
230
|
+
let discovery;
|
|
231
|
+
try {
|
|
232
|
+
discovery = await discoverViaMcp();
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
err(`Discovery failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
236
|
+
err("Check that the ROS transport is reachable (zenohd / rosbridge / etc) and that " +
|
|
237
|
+
"`packages/agenticros-claude-code/dist/index.js` is built. See `agenticros doctor`.");
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
info(`Scanned ${discovery.total_topics} topic(s).`);
|
|
241
|
+
process.stdout.write(`\n${colors.bold("On the wire right now:")}\n`);
|
|
242
|
+
if (discovery.detected.length === 0) {
|
|
243
|
+
dim(" No /<ns>/cmd_vel topics detected. Is the robot's bridge/router up?");
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
for (const d of discovery.detected) {
|
|
247
|
+
const tag = d.configuredRobotId !== null
|
|
248
|
+
? colors.green(`● configured as ${d.configuredRobotId}`)
|
|
249
|
+
: colors.yellow("○ unknown (not in config)");
|
|
250
|
+
process.stdout.write(` ${tag} ${colors.bold(d.id)} ${colors.dim(d.cmdVelTopic)} ${colors.dim(`(${d.topicCount} topic(s))`)}\n`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (discovery.configured_offline.length > 0) {
|
|
254
|
+
process.stdout.write(`\n${colors.bold(colors.yellow("Configured but silent:"))}\n`);
|
|
255
|
+
for (const r of discovery.configured_offline) {
|
|
256
|
+
process.stdout.write(` ${colors.yellow("○")} ${colors.bold(r.id)}\n`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (discovery.unknown_detected.length === 0) {
|
|
260
|
+
process.stdout.write("\n");
|
|
261
|
+
ok("No new robots to add — every detected namespace is already in config.");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!isTty) {
|
|
265
|
+
process.stdout.write("\n");
|
|
266
|
+
info(`Found ${discovery.unknown_detected.length} unknown robot(s).`);
|
|
267
|
+
info("Run interactively to register them, or use `agenticros robots add <id>`.");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
process.stdout.write("\n");
|
|
271
|
+
const picks = await checkbox({
|
|
272
|
+
message: `Add ${discovery.unknown_detected.length} new robot(s) to config?`,
|
|
273
|
+
choices: discovery.unknown_detected.map((d) => ({
|
|
274
|
+
name: `${d.id} (${d.topicCount} topic(s), cmd_vel: ${d.cmdVelTopic})`,
|
|
275
|
+
value: d.id,
|
|
276
|
+
checked: true,
|
|
277
|
+
})),
|
|
278
|
+
});
|
|
279
|
+
if (picks.length === 0) {
|
|
280
|
+
info("Nothing selected. Run `agenticros robots add <id>` later to add one by id.");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const obj = readConfigObject();
|
|
284
|
+
let writes = 0;
|
|
285
|
+
for (const id of picks) {
|
|
286
|
+
const detected = discovery.unknown_detected.find((d) => d.id === id);
|
|
287
|
+
const result = addRobot({
|
|
288
|
+
id: detected.id,
|
|
289
|
+
name: detected.id,
|
|
290
|
+
namespace: detected.id,
|
|
291
|
+
cameraTopic: "",
|
|
292
|
+
}, { obj });
|
|
293
|
+
if (result.added)
|
|
294
|
+
writes += 1;
|
|
295
|
+
if (result.promotedLegacy && writes === 1) {
|
|
296
|
+
info(`Promoted legacy config.robot into config.robots[0] (kept as default). The new entries are appended.`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (writes > 0) {
|
|
300
|
+
writeConfigObject(obj);
|
|
301
|
+
ok(`Added ${writes} robot(s) to ${robotConfigPath()}.`);
|
|
302
|
+
warn("Restart any running MCP servers (Claude Code, Claude desktop, OpenClaw) so they re-read the config.");
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
info("Nothing new to write (all picks were already in config).");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
309
|
+
// add
|
|
310
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
311
|
+
async function addAction(opts) {
|
|
312
|
+
let id = opts.arg?.trim();
|
|
313
|
+
// Resolve --transport / --transport-json up-front so we fail fast on
|
|
314
|
+
// a malformed value before we touch the live MCP server or prompt
|
|
315
|
+
// the user. parseOverrideFromOptions exits the process on parse
|
|
316
|
+
// failures, so undefined here means "no override flag was passed".
|
|
317
|
+
const overrideFromFlag = parseOverrideFromOptions(opts);
|
|
318
|
+
// Phase 1.e — parse --sensors and --capabilities up-front (same
|
|
319
|
+
// fail-fast story as transport). Both are best-effort: we accept
|
|
320
|
+
// unknown sensor keys with a friendly error rather than crashing the
|
|
321
|
+
// process via Zod, since the user is more likely to typo here than
|
|
322
|
+
// in the transport shorthand.
|
|
323
|
+
let sensorsFromFlag;
|
|
324
|
+
try {
|
|
325
|
+
sensorsFromFlag = parseSensorsCsv(opts.sensors);
|
|
326
|
+
}
|
|
327
|
+
catch (e) {
|
|
328
|
+
err(e instanceof Error ? e.message : String(e));
|
|
329
|
+
process.exit(2);
|
|
330
|
+
}
|
|
331
|
+
const capabilitiesFromFlag = parseCapabilitiesCsv(opts.capabilities);
|
|
332
|
+
// True when the user gave us any --flag, meaning "do not prompt".
|
|
333
|
+
// We treat non-interactive mode as the *implicit* default once any
|
|
334
|
+
// flag is set — running `robots add alpha --transport=zenoh` should
|
|
335
|
+
// not interrupt the user with extra prompts for name/namespace.
|
|
336
|
+
const usingFlags = overrideFromFlag !== undefined ||
|
|
337
|
+
opts.name !== undefined ||
|
|
338
|
+
opts.namespace !== undefined ||
|
|
339
|
+
opts.camera !== undefined ||
|
|
340
|
+
opts.default === true ||
|
|
341
|
+
opts.kind !== undefined ||
|
|
342
|
+
sensorsFromFlag !== undefined ||
|
|
343
|
+
capabilitiesFromFlag !== undefined;
|
|
344
|
+
// Try to fetch live discovery once so the prompts can pre-fill defaults
|
|
345
|
+
// and so an interactive `add` (no id) lets the user pick from the wire.
|
|
346
|
+
// Skipped in flag-driven runs (we have everything we need from argv).
|
|
347
|
+
let discovery;
|
|
348
|
+
if (!usingFlags) {
|
|
349
|
+
try {
|
|
350
|
+
discovery = await discoverViaMcp();
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
/* offline-add is supported — just no auto-fill */
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!id) {
|
|
357
|
+
if (!isTty) {
|
|
358
|
+
err("Usage: agenticros robots add <id> [--name=...] [--namespace=...] [--transport=<shorthand>]");
|
|
359
|
+
process.exit(2);
|
|
360
|
+
}
|
|
361
|
+
id = await pickIdInteractively(discovery);
|
|
362
|
+
if (!id) {
|
|
363
|
+
info("Cancelled.");
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Look up any existing entry so flag-driven updates can merge with
|
|
368
|
+
// prior values instead of clobbering them. (addRobot itself doesn't
|
|
369
|
+
// merge name/namespace/cameraTopic — see util/robot-config.ts.)
|
|
370
|
+
const obj = readConfigObject();
|
|
371
|
+
const existing = readRobots(obj).robots.find((r) => r.id === id);
|
|
372
|
+
// Default fallbacks. Precedence: explicit --flag > prior persisted value > discovery hint > id.
|
|
373
|
+
const detected = discovery?.unknown_detected.find((d) => d.id === id);
|
|
374
|
+
let name = opts.name ?? existing?.name ?? id;
|
|
375
|
+
let namespace = opts.namespace ?? existing?.namespace ?? detected?.id ?? id;
|
|
376
|
+
let cameraTopic = opts.camera ?? existing?.cameraTopic ?? "";
|
|
377
|
+
// Interactive prompts only when the user didn't pass any flags AND
|
|
378
|
+
// we have a TTY. This preserves the original wizard UX while still
|
|
379
|
+
// allowing one-shot CLI usage.
|
|
380
|
+
if (isTty && !usingFlags) {
|
|
381
|
+
name = (await input({
|
|
382
|
+
message: "Display name:",
|
|
383
|
+
default: name,
|
|
384
|
+
validate: (v) => v.trim().length > 0 || "Required",
|
|
385
|
+
})).trim();
|
|
386
|
+
namespace = (await input({ message: "ROS2 namespace:", default: namespace })).trim();
|
|
387
|
+
cameraTopic = (await input({
|
|
388
|
+
message: "Default camera topic (optional, blank ⇒ /<namespace>/camera/...):",
|
|
389
|
+
default: cameraTopic,
|
|
390
|
+
})).trim();
|
|
391
|
+
}
|
|
392
|
+
// Mark default? --default flag forces it; otherwise prompt in TTY-and-no-flags mode.
|
|
393
|
+
let setDefault = opts.default === true;
|
|
394
|
+
if (!setDefault && isTty && !usingFlags) {
|
|
395
|
+
const { robots } = readRobots(obj);
|
|
396
|
+
if (robots.length >= 1 && robots[0].id !== id) {
|
|
397
|
+
setDefault = await confirm({
|
|
398
|
+
message: `Make "${id}" the default robot?`,
|
|
399
|
+
default: false,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Phase 1.e merges: when --sensors=key1,key2 was passed, merge the
|
|
404
|
+
// delta over the existing sensors block so the user can flip ONE
|
|
405
|
+
// flag without re-declaring the rest. (Vs --transport which is a
|
|
406
|
+
// whole-object replacement — the contracts are different because
|
|
407
|
+
// sensors are a small named set.)
|
|
408
|
+
let mergedSensors;
|
|
409
|
+
if (sensorsFromFlag !== undefined) {
|
|
410
|
+
mergedSensors = { ...(existing?.sensors ?? {}), ...sensorsFromFlag };
|
|
411
|
+
}
|
|
412
|
+
// Capability allowlist: treat the flag as a full replacement (empty
|
|
413
|
+
// string already became []). Same semantics as --transport.
|
|
414
|
+
const capabilitiesToApply = capabilitiesFromFlag;
|
|
415
|
+
const result = addRobot({
|
|
416
|
+
id,
|
|
417
|
+
name,
|
|
418
|
+
namespace,
|
|
419
|
+
cameraTopic,
|
|
420
|
+
transport: overrideFromFlag,
|
|
421
|
+
kind: opts.kind,
|
|
422
|
+
sensors: mergedSensors,
|
|
423
|
+
capabilities: capabilitiesToApply,
|
|
424
|
+
}, { obj, setDefault });
|
|
425
|
+
writeConfigObject(obj);
|
|
426
|
+
if (result.promotedLegacy) {
|
|
427
|
+
info("Promoted legacy config.robot into config.robots[0] (kept as default).");
|
|
428
|
+
}
|
|
429
|
+
if (result.added) {
|
|
430
|
+
ok(`Added robot "${id}" to ${robotConfigPath()}.`);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
ok(`Updated existing robot "${id}" in ${robotConfigPath()}.`);
|
|
434
|
+
}
|
|
435
|
+
if (overrideFromFlag) {
|
|
436
|
+
ok(`Per-robot transport override applied: ${describeOverride(overrideFromFlag)}.`);
|
|
437
|
+
}
|
|
438
|
+
if (opts.kind)
|
|
439
|
+
ok(`Robot kind set to "${opts.kind}".`);
|
|
440
|
+
if (mergedSensors) {
|
|
441
|
+
const formatted = describeSensors(mergedSensors);
|
|
442
|
+
if (formatted)
|
|
443
|
+
ok(`Sensors: ${formatted}.`);
|
|
444
|
+
}
|
|
445
|
+
if (capabilitiesToApply !== undefined) {
|
|
446
|
+
if (capabilitiesToApply.length === 0) {
|
|
447
|
+
ok("Per-robot capability allowlist cleared (robot now inherits the gateway-wide registry).");
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
ok(`Per-robot capability allowlist: ${capabilitiesToApply.join(", ")}.`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (setDefault)
|
|
454
|
+
ok(`"${id}" is now the default robot.`);
|
|
455
|
+
warn("Restart any running MCP servers so they re-read the config.");
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Render a sensors object as a stable, comma-separated human summary
|
|
459
|
+
* for log messages. Omits keys that are unset (undefined) so we don't
|
|
460
|
+
* lie about defaults.
|
|
461
|
+
*/
|
|
462
|
+
function describeSensors(s) {
|
|
463
|
+
const parts = [];
|
|
464
|
+
for (const k of ["has_realsense", "has_lidar", "has_arm"]) {
|
|
465
|
+
const v = s[k];
|
|
466
|
+
if (v === undefined)
|
|
467
|
+
continue;
|
|
468
|
+
parts.push(v ? k : `!${k}`);
|
|
469
|
+
}
|
|
470
|
+
return parts.join(", ");
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Render a one-line human summary of a transport override for log
|
|
474
|
+
* messages. Deliberately tolerant — the JSON has already passed our
|
|
475
|
+
* shorthand parser (or made it through `--transport-json`), and the
|
|
476
|
+
* core Zod schema will field-validate at load time. We only need this
|
|
477
|
+
* good enough to confirm to the user "yes, we wrote that thing you
|
|
478
|
+
* just typed".
|
|
479
|
+
*/
|
|
480
|
+
function describeOverride(override) {
|
|
481
|
+
const mode = String(override["mode"] ?? "?");
|
|
482
|
+
switch (mode) {
|
|
483
|
+
case "zenoh": {
|
|
484
|
+
const ep = override["zenoh"] && typeof override["zenoh"] === "object"
|
|
485
|
+
? override["zenoh"]["routerEndpoint"]
|
|
486
|
+
: undefined;
|
|
487
|
+
return ep ? `zenoh @ ${String(ep)}` : "zenoh (inherits global router endpoint)";
|
|
488
|
+
}
|
|
489
|
+
case "rosbridge": {
|
|
490
|
+
const url = override["rosbridge"] && typeof override["rosbridge"] === "object"
|
|
491
|
+
? override["rosbridge"]["url"]
|
|
492
|
+
: undefined;
|
|
493
|
+
return url ? `rosbridge @ ${String(url)}` : "rosbridge (inherits global url)";
|
|
494
|
+
}
|
|
495
|
+
case "local": {
|
|
496
|
+
const dom = override["local"] && typeof override["local"] === "object"
|
|
497
|
+
? override["local"]["domainId"]
|
|
498
|
+
: undefined;
|
|
499
|
+
return dom !== undefined
|
|
500
|
+
? `local DDS (domainId=${String(dom)})`
|
|
501
|
+
: "local DDS (inherits global domainId)";
|
|
502
|
+
}
|
|
503
|
+
case "webrtc": {
|
|
504
|
+
const sig = override["webrtc"] && typeof override["webrtc"] === "object"
|
|
505
|
+
? override["webrtc"]["signalingUrl"]
|
|
506
|
+
: undefined;
|
|
507
|
+
return sig ? `webrtc @ ${String(sig)}` : "webrtc (inherits global signaling url)";
|
|
508
|
+
}
|
|
509
|
+
default:
|
|
510
|
+
return `mode=${mode}`;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function pickIdInteractively(discovery) {
|
|
514
|
+
const unknown = discovery?.unknown_detected ?? [];
|
|
515
|
+
if (unknown.length === 0) {
|
|
516
|
+
return (await input({
|
|
517
|
+
message: "Robot id (free-form, e.g. robot-alpha):",
|
|
518
|
+
validate: (v) => v.trim().length > 0 || "Required",
|
|
519
|
+
})).trim();
|
|
520
|
+
}
|
|
521
|
+
const choice = await select({
|
|
522
|
+
message: "Pick a robot to add:",
|
|
523
|
+
choices: [
|
|
524
|
+
...unknown.map((d) => ({
|
|
525
|
+
name: `${d.id} (cmd_vel: ${d.cmdVelTopic}, ${d.topicCount} topic(s))`,
|
|
526
|
+
value: d.id,
|
|
527
|
+
})),
|
|
528
|
+
{ name: "Enter id manually…", value: "__manual__" },
|
|
529
|
+
{ name: "Cancel", value: "__cancel__" },
|
|
530
|
+
],
|
|
531
|
+
});
|
|
532
|
+
if (choice === "__cancel__")
|
|
533
|
+
return undefined;
|
|
534
|
+
if (choice === "__manual__") {
|
|
535
|
+
return (await input({
|
|
536
|
+
message: "Robot id:",
|
|
537
|
+
validate: (v) => v.trim().length > 0 || "Required",
|
|
538
|
+
})).trim();
|
|
539
|
+
}
|
|
540
|
+
return choice;
|
|
541
|
+
}
|
|
542
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
543
|
+
// remove
|
|
544
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
545
|
+
async function removeAction(arg) {
|
|
546
|
+
const id = arg?.trim();
|
|
547
|
+
if (!id) {
|
|
548
|
+
err("Usage: agenticros robots remove <id>");
|
|
549
|
+
process.exit(2);
|
|
550
|
+
}
|
|
551
|
+
const obj = readConfigObject();
|
|
552
|
+
const result = removeRobot(id, obj);
|
|
553
|
+
if (!result.removed) {
|
|
554
|
+
warn(`No robot with id "${id}" found in ${robotConfigPath()}.`);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
writeConfigObject(obj);
|
|
558
|
+
ok(`Removed robot "${id}".`);
|
|
559
|
+
if (result.robots.length === 0) {
|
|
560
|
+
info("robots[] is now empty — the gateway will fall back to legacy config.robot (if set). " +
|
|
561
|
+
"Run `agenticros robots discover` to re-populate.");
|
|
562
|
+
}
|
|
563
|
+
warn("Restart any running MCP servers so they re-read the config.");
|
|
564
|
+
}
|
|
565
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
566
|
+
// set-default
|
|
567
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
568
|
+
async function setDefaultAction(arg) {
|
|
569
|
+
const id = arg?.trim();
|
|
570
|
+
if (!id) {
|
|
571
|
+
err("Usage: agenticros robots set-default <id>");
|
|
572
|
+
process.exit(2);
|
|
573
|
+
}
|
|
574
|
+
const obj = readConfigObject();
|
|
575
|
+
try {
|
|
576
|
+
const { promotedLegacy } = setDefaultRobot(id, obj);
|
|
577
|
+
writeConfigObject(obj);
|
|
578
|
+
if (promotedLegacy) {
|
|
579
|
+
info("Promoted legacy config.robot into config.robots[0] first.");
|
|
580
|
+
}
|
|
581
|
+
ok(`"${id}" is now the default robot.`);
|
|
582
|
+
warn("Restart any running MCP servers so they re-read the config.");
|
|
583
|
+
}
|
|
584
|
+
catch (e) {
|
|
585
|
+
err(e instanceof Error ? e.message : String(e));
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
590
|
+
// set-transport / clear-transport
|
|
591
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
592
|
+
/**
|
|
593
|
+
* `agenticros robots set-transport <id> [shorthand]`
|
|
594
|
+
*
|
|
595
|
+
* Two ways to supply the override:
|
|
596
|
+
* - Positional `[shorthand]` (parsed via parseTransportShorthand).
|
|
597
|
+
* - `--transport=<shorthand>` / `--transport-json=<json>` flags.
|
|
598
|
+
* They're mutually exclusive — picking one is enough.
|
|
599
|
+
*
|
|
600
|
+
* Why a dedicated subcommand when `add` already takes the same flags?
|
|
601
|
+
* Two reasons. First, it surfaces the transport-override knob in
|
|
602
|
+
* --help so users discover the feature without reading code. Second,
|
|
603
|
+
* it makes the intent unambiguous in scripts: `add` can't tell whether
|
|
604
|
+
* a new entry is wanted or an update; `set-transport` is always-update
|
|
605
|
+
* and throws if the id is unknown (no silent typo-promotion).
|
|
606
|
+
*/
|
|
607
|
+
async function setTransportAction(opts) {
|
|
608
|
+
const id = opts.arg?.trim();
|
|
609
|
+
if (!id) {
|
|
610
|
+
err("Usage: agenticros robots set-transport <id> <shorthand>\n" +
|
|
611
|
+
" or: agenticros robots set-transport <id> --transport=<shorthand>\n" +
|
|
612
|
+
" or: agenticros robots set-transport <id> --transport-json=<json>");
|
|
613
|
+
process.exit(2);
|
|
614
|
+
}
|
|
615
|
+
// Resolve the override. Positional argument is a convenience for the
|
|
616
|
+
// common case `set-transport alpha zenoh:ws://farm:10000`; flags are
|
|
617
|
+
// preferred when scripting or when the user has TTY tab completion.
|
|
618
|
+
// We don't accept "positional + flag" simultaneously — too ambiguous.
|
|
619
|
+
// We *do* re-use parseOverrideFromOptions so flag-driven runs share
|
|
620
|
+
// the same error path as `add`.
|
|
621
|
+
let override;
|
|
622
|
+
if (opts.transport || opts.transportJson) {
|
|
623
|
+
override = parseOverrideFromOptions(opts);
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
err("Missing override. Supply either:\n" +
|
|
627
|
+
" agenticros robots set-transport <id> --transport=<shorthand>\n" +
|
|
628
|
+
" agenticros robots set-transport <id> --transport-json=<json>");
|
|
629
|
+
process.exit(2);
|
|
630
|
+
}
|
|
631
|
+
if (!override)
|
|
632
|
+
return;
|
|
633
|
+
const obj = readConfigObject();
|
|
634
|
+
try {
|
|
635
|
+
const { promotedLegacy } = setTransportForRobot(id, override, obj);
|
|
636
|
+
writeConfigObject(obj);
|
|
637
|
+
if (promotedLegacy)
|
|
638
|
+
info("Promoted legacy config.robot into config.robots[0] first.");
|
|
639
|
+
ok(`Applied per-robot transport override to "${id}": ${describeOverride(override)}.`);
|
|
640
|
+
warn("Restart any running MCP servers so they re-read the config.");
|
|
641
|
+
}
|
|
642
|
+
catch (e) {
|
|
643
|
+
err(e instanceof Error ? e.message : String(e));
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* `agenticros robots clear-transport <id>` — drop the per-robot
|
|
649
|
+
* transport override, letting the robot inherit the global transport
|
|
650
|
+
* settings again. Idempotent: prints a friendly "(nothing to clear)"
|
|
651
|
+
* message when the robot didn't have an override to begin with.
|
|
652
|
+
*/
|
|
653
|
+
async function clearTransportAction(arg) {
|
|
654
|
+
const id = arg?.trim();
|
|
655
|
+
if (!id) {
|
|
656
|
+
err("Usage: agenticros robots clear-transport <id>");
|
|
657
|
+
process.exit(2);
|
|
658
|
+
}
|
|
659
|
+
const obj = readConfigObject();
|
|
660
|
+
try {
|
|
661
|
+
const { cleared, promotedLegacy } = clearTransportForRobot(id, obj);
|
|
662
|
+
writeConfigObject(obj);
|
|
663
|
+
if (promotedLegacy)
|
|
664
|
+
info("Promoted legacy config.robot into config.robots[0] first.");
|
|
665
|
+
if (cleared) {
|
|
666
|
+
ok(`Cleared per-robot transport override on "${id}".`);
|
|
667
|
+
warn("Restart any running MCP servers so they re-read the config.");
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
info(`"${id}" had no transport override (nothing to clear).`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
catch (e) {
|
|
674
|
+
err(e instanceof Error ? e.message : String(e));
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
//# sourceMappingURL=robots.js.map
|