agenticros 0.1.11 → 0.1.13

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.
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Skill discovery, validation, and config-file mutation primitives.
3
+ *
4
+ * An AgenticROS skill is a Node package whose `package.json` carries
5
+ * `"agenticrosSkill": true` and exports a `registerSkill(api, config, context)`
6
+ * function from its `main` entry. The OpenClaw AgenticROS plugin loads them
7
+ * at gateway start by walking two arrays in its config:
8
+ *
9
+ * - `skillPaths[]` — absolute directories to scan (validate `package.json`)
10
+ * - `skillPackages[]` — npm-resolvable names (e.g. `agenticros-skill-find`)
11
+ *
12
+ * This module is the single place the CLI talks to those arrays. It also
13
+ * encapsulates discovery (where on disk users typically clone skill repos)
14
+ * and the `deriveSkillId` convention shared with `@agenticros/agenticros`'s
15
+ * skill-loader so menu / list output uses the same short ids the plugin
16
+ * surfaces (e.g. `followme`, `find`).
17
+ */
18
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { dirname, isAbsolute, join, resolve } from "node:path";
21
+ import { getCliPaths } from "./paths.js";
22
+ import { ensureStringArray, getAgenticrosPluginConfig, openclawConfigExists, openclawConfigPath, readOpenclawConfig, writeOpenclawConfig, } from "./openclaw-config.js";
23
+ /**
24
+ * Apply the same short-id transform the OpenClaw plugin's skill-loader uses,
25
+ * so menu output and the plugin's `config.skills.<id>` keys line up exactly.
26
+ * `agenticros-skill-followme` → `followme`; scoped packages drop the scope.
27
+ */
28
+ export function deriveSkillId(packageName) {
29
+ const lower = packageName.toLowerCase();
30
+ if (lower.startsWith("agenticros-skill-")) {
31
+ return lower.slice("agenticros-skill-".length);
32
+ }
33
+ return lower.replace(/^@[^/]+\//, "").replace(/[^a-z0-9]/g, "");
34
+ }
35
+ /**
36
+ * Inspect a directory and decide whether it's a valid AgenticROS skill clone.
37
+ * Returns a partial `SkillRef` (no `registeredAs`) or `undefined` when the
38
+ * directory is missing, lacks `package.json`, or doesn't opt in via the
39
+ * `agenticrosSkill: true` flag.
40
+ */
41
+ export function inspectSkillDir(dir) {
42
+ const absDir = isAbsolute(dir) ? dir : resolve(dir);
43
+ if (!existsSync(absDir))
44
+ return undefined;
45
+ let stat;
46
+ try {
47
+ stat = statSync(absDir);
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ if (!stat.isDirectory())
53
+ return undefined;
54
+ const pkgPath = join(absDir, "package.json");
55
+ if (!existsSync(pkgPath))
56
+ return undefined;
57
+ let pkg;
58
+ try {
59
+ pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ if (pkg.agenticrosSkill !== true)
65
+ return undefined;
66
+ const packageName = (pkg.name ?? "unknown").toString();
67
+ const mainRel = pkg.main ?? "index.js";
68
+ const entry = join(absDir, mainRel);
69
+ return {
70
+ id: deriveSkillId(packageName),
71
+ packageName,
72
+ dir: absDir,
73
+ entry,
74
+ built: existsSync(entry),
75
+ };
76
+ }
77
+ /**
78
+ * Return a deduped list of "places we should look for skill clones" — the
79
+ * directories whose children the CLI scans on `agenticros skills discover`.
80
+ *
81
+ * We include both the parent of the active repo / install dir (so siblings
82
+ * of `agenticros/` work, e.g. `../agenticros-skill-find`) and `$HOME` /
83
+ * `$HOME/Projects/` / `$HOME/Code/` so a vanilla clone-into-Projects flow
84
+ * is found without configuration.
85
+ */
86
+ export function discoveryRoots(extra = []) {
87
+ const paths = getCliPaths();
88
+ const home = homedir();
89
+ const roots = [];
90
+ const push = (p) => {
91
+ if (!p)
92
+ return;
93
+ const abs = resolve(p);
94
+ if (!roots.includes(abs))
95
+ roots.push(abs);
96
+ };
97
+ if (paths.repoRoot)
98
+ push(dirname(paths.repoRoot));
99
+ push(dirname(paths.installDir));
100
+ push(home);
101
+ push(join(home, "Projects"));
102
+ push(join(home, "Code"));
103
+ push(join(home, "agenticros-skills"));
104
+ for (const e of extra)
105
+ push(e);
106
+ return roots.filter((r) => {
107
+ try {
108
+ return statSync(r).isDirectory();
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ });
114
+ }
115
+ /**
116
+ * Walk every directory in `roots`, returning a `SkillRef` for each child
117
+ * directory that passes `inspectSkillDir`. Used by both `discover` and
118
+ * `list` (which uses it to render orphaned-but-cloned skills).
119
+ */
120
+ export function scanForSkills(extra = []) {
121
+ const out = [];
122
+ const seenDirs = new Set();
123
+ for (const root of discoveryRoots(extra)) {
124
+ let entries;
125
+ try {
126
+ entries = readdirSync(root);
127
+ }
128
+ catch {
129
+ continue;
130
+ }
131
+ for (const name of entries) {
132
+ const child = join(root, name);
133
+ if (seenDirs.has(child))
134
+ continue;
135
+ seenDirs.add(child);
136
+ const info = inspectSkillDir(child);
137
+ if (info) {
138
+ out.push({ ...info, registeredAs: [] });
139
+ }
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+ /**
145
+ * Pull skill arrays out of the OpenClaw config in a shape that's easy to
146
+ * mutate. The returned `paths` / `packages` arrays are filtered to strings
147
+ * (non-string elements are silently skipped in the view but preserved in
148
+ * the underlying arrays on disk).
149
+ */
150
+ function loadOpenclawSkillsView() {
151
+ const cfg = readOpenclawConfig();
152
+ if (!cfg)
153
+ return undefined;
154
+ const pluginCfg = getAgenticrosPluginConfig(cfg);
155
+ const rawPaths = ensureStringArray(pluginCfg, "skillPaths");
156
+ const rawPkgs = ensureStringArray(pluginCfg, "skillPackages");
157
+ const paths = rawPaths.filter((x) => typeof x === "string");
158
+ const packages = rawPkgs.filter((x) => typeof x === "string");
159
+ return { cfg, pluginCfg, paths, packages };
160
+ }
161
+ /**
162
+ * Build a snapshot of the user's current skill state: what's registered,
163
+ * what's available-but-not-registered, and what registrations are broken.
164
+ * The single source of truth for the `agenticros skills` (list) view.
165
+ */
166
+ export function listSkills() {
167
+ const view = loadOpenclawSkillsView();
168
+ const registered = [];
169
+ const brokenPaths = [];
170
+ const byId = new Map();
171
+ if (view) {
172
+ for (const p of view.paths) {
173
+ const info = inspectSkillDir(p);
174
+ if (info) {
175
+ const existing = byId.get(info.id);
176
+ if (existing) {
177
+ if (!existing.registeredAs.includes("path"))
178
+ existing.registeredAs.push("path");
179
+ if (existing.entry === undefined) {
180
+ existing.entry = info.entry;
181
+ existing.built = info.built;
182
+ }
183
+ }
184
+ else {
185
+ const ref = { ...info, registeredAs: ["path"] };
186
+ byId.set(info.id, ref);
187
+ registered.push(ref);
188
+ }
189
+ }
190
+ else {
191
+ brokenPaths.push(p);
192
+ }
193
+ }
194
+ for (const name of view.packages) {
195
+ const id = deriveSkillId(name);
196
+ const existing = byId.get(id);
197
+ if (existing) {
198
+ if (!existing.registeredAs.includes("package"))
199
+ existing.registeredAs.push("package");
200
+ }
201
+ else {
202
+ const ref = { id, packageName: name, registeredAs: ["package"] };
203
+ byId.set(id, ref);
204
+ registered.push(ref);
205
+ }
206
+ }
207
+ }
208
+ const available = scanForSkills().filter((s) => !byId.has(s.id));
209
+ return { registered, available, brokenPaths };
210
+ }
211
+ /**
212
+ * Add a skill to the OpenClaw plugin config by absolute directory path.
213
+ * Idempotent: re-adding an existing entry is a no-op and returns `ok: true`
214
+ * with an explanatory message. The directory must contain a valid
215
+ * `package.json` with `agenticrosSkill: true`.
216
+ */
217
+ export function addSkillByPath(dir) {
218
+ const info = inspectSkillDir(dir);
219
+ if (!info) {
220
+ return {
221
+ ok: false,
222
+ message: `Not an AgenticROS skill directory: ${dir} (need package.json with "agenticrosSkill": true)`,
223
+ };
224
+ }
225
+ if (!openclawConfigExists()) {
226
+ return {
227
+ ok: false,
228
+ message: `OpenClaw config not found at ${openclawConfigPath()}. ` +
229
+ "Run `agenticros init` first to install the OpenClaw plugin.",
230
+ };
231
+ }
232
+ const cfg = readOpenclawConfig();
233
+ if (!cfg) {
234
+ return {
235
+ ok: false,
236
+ message: `Cannot parse OpenClaw config at ${openclawConfigPath()}.`,
237
+ };
238
+ }
239
+ const pluginCfg = getAgenticrosPluginConfig(cfg);
240
+ const paths = ensureStringArray(pluginCfg, "skillPaths");
241
+ if (paths.includes(info.dir)) {
242
+ return {
243
+ ok: true,
244
+ message: `Skill '${info.id}' already registered at ${info.dir}.`,
245
+ skill: { ...info, registeredAs: ["path"] },
246
+ };
247
+ }
248
+ paths.push(info.dir);
249
+ writeOpenclawConfig(cfg);
250
+ return {
251
+ ok: true,
252
+ message: `Registered skill '${info.id}' at ${info.dir} (${openclawConfigPath()} → skillPaths).`,
253
+ skill: { ...info, registeredAs: ["path"] },
254
+ };
255
+ }
256
+ /**
257
+ * Add a skill by npm package name (must be resolvable on the gateway's
258
+ * Node search path). Mainly intended for skills published to a registry —
259
+ * for local clones, prefer `addSkillByPath` so the absolute directory is
260
+ * recorded.
261
+ */
262
+ export function addSkillByPackage(name) {
263
+ if (!openclawConfigExists()) {
264
+ return {
265
+ ok: false,
266
+ message: `OpenClaw config not found at ${openclawConfigPath()}. ` +
267
+ "Run `agenticros init` first to install the OpenClaw plugin.",
268
+ };
269
+ }
270
+ const cfg = readOpenclawConfig();
271
+ if (!cfg) {
272
+ return { ok: false, message: `Cannot parse OpenClaw config at ${openclawConfigPath()}.` };
273
+ }
274
+ const pluginCfg = getAgenticrosPluginConfig(cfg);
275
+ const packages = ensureStringArray(pluginCfg, "skillPackages");
276
+ if (packages.includes(name)) {
277
+ return {
278
+ ok: true,
279
+ message: `Skill package '${name}' already registered.`,
280
+ skill: { id: deriveSkillId(name), packageName: name, registeredAs: ["package"] },
281
+ };
282
+ }
283
+ packages.push(name);
284
+ writeOpenclawConfig(cfg);
285
+ return {
286
+ ok: true,
287
+ message: `Registered skill package '${name}' (${openclawConfigPath()} → skillPackages).`,
288
+ skill: { id: deriveSkillId(name), packageName: name, registeredAs: ["package"] },
289
+ };
290
+ }
291
+ /**
292
+ * Remove every registration that matches `idOrName` — either a derived skill
293
+ * id (e.g. `find`), a full package name (e.g. `agenticros-skill-find`), or an
294
+ * absolute path that appears in `skillPaths`. Both `skillPaths` (matched by
295
+ * resolving each path back through `inspectSkillDir`) and `skillPackages`
296
+ * are cleaned in a single pass.
297
+ */
298
+ export function removeSkill(idOrName) {
299
+ const cfg = readOpenclawConfig();
300
+ if (!cfg) {
301
+ return {
302
+ ok: false,
303
+ message: `OpenClaw config not found or unparseable at ${openclawConfigPath()}.`,
304
+ removedPaths: [],
305
+ removedPackages: [],
306
+ };
307
+ }
308
+ const pluginCfg = getAgenticrosPluginConfig(cfg);
309
+ const rawPaths = ensureStringArray(pluginCfg, "skillPaths");
310
+ const rawPkgs = ensureStringArray(pluginCfg, "skillPackages");
311
+ const wanted = idOrName.trim();
312
+ const wantedAbs = isAbsolute(wanted) ? resolve(wanted) : undefined;
313
+ const removedPaths = [];
314
+ const keptPaths = rawPaths.filter((entry) => {
315
+ if (typeof entry !== "string")
316
+ return true;
317
+ if (wantedAbs && resolve(entry) === wantedAbs) {
318
+ removedPaths.push(entry);
319
+ return false;
320
+ }
321
+ const info = inspectSkillDir(entry);
322
+ if (info && (info.id === wanted || info.packageName === wanted)) {
323
+ removedPaths.push(entry);
324
+ return false;
325
+ }
326
+ return true;
327
+ });
328
+ const removedPackages = [];
329
+ const keptPkgs = rawPkgs.filter((entry) => {
330
+ if (typeof entry !== "string")
331
+ return true;
332
+ if (entry === wanted || deriveSkillId(entry) === wanted) {
333
+ removedPackages.push(entry);
334
+ return false;
335
+ }
336
+ return true;
337
+ });
338
+ if (removedPaths.length === 0 && removedPackages.length === 0) {
339
+ return {
340
+ ok: false,
341
+ message: `No registered skill matches '${idOrName}'.`,
342
+ removedPaths: [],
343
+ removedPackages: [],
344
+ };
345
+ }
346
+ pluginCfg["skillPaths"] = keptPaths;
347
+ pluginCfg["skillPackages"] = keptPkgs;
348
+ writeOpenclawConfig(cfg);
349
+ const parts = [];
350
+ if (removedPaths.length > 0)
351
+ parts.push(`${removedPaths.length} path(s)`);
352
+ if (removedPackages.length > 0)
353
+ parts.push(`${removedPackages.length} package(s)`);
354
+ return {
355
+ ok: true,
356
+ message: `Removed ${parts.join(" + ")} matching '${idOrName}' from ${openclawConfigPath()}.`,
357
+ removedPaths,
358
+ removedPackages,
359
+ };
360
+ }
361
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/util/skills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE/D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAoB9B;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,WAAmB;IAC/C,MAAM,KAAK,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,KAAK,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAC1C,OAAO,KAAK,CAAC,KAAK,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AAClE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACpD,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAC1C,IAAI,IAAI,CAAC;IACT,IAAI,CAAC;QACH,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC7C,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3C,IAAI,GAAiD,CAAC;IACtD,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,GAAG,CAAC,eAAe,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACnD,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,SAAS,CAAC,CAAC,QAAQ,EAAE,CAAC;IACvD,MAAM,OAAO,GAAK,GAA0B,CAAC,IAA2B,IAAI,UAAU,CAAC;IACvF,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,OAAO;QACL,EAAE,EAAE,aAAa,CAAC,WAAW,CAAC;QAC9B,WAAW;QACX,GAAG,EAAE,MAAM;QACX,KAAK;QACL,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC;KACzB,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,QAAkB,EAAE;IACjD,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;IAC5B,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,CAAC,CAAqB,EAAQ,EAAE;QAC3C,IAAI,CAAC,CAAC;YAAE,OAAO;QACf,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACvB,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5C,CAAC,CAAC;IACF,IAAI,KAAK,CAAC,QAAQ;QAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IAClD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IAChC,IAAI,CAAC,IAAI,CAAC,CAAC;IACX,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;IAC7B,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IACzB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,mBAAmB,CAAC,CAAC,CAAC;IACtC,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACxB,IAAI,CAAC;YACH,OAAO,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,QAAkB,EAAE;IAChD,MAAM,GAAG,GAAe,EAAE,CAAC;IAC3B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,KAAK,MAAM,IAAI,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QACzC,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACH,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC/B,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC;gBAAE,SAAS;YAClC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;YACpC,IAAI,IAAI,EAAE,CAAC;gBACT,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;YAC1C,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AASD;;;;;GAKG;AACH,SAAS,sBAAsB;IAC7B,MAAM,GAAG,GAAG,kBAAkB,EAAE,CAAC;IACjC,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,SAAS,GAAG,yBAAyB,CAAC,GAAG,CAAC,CAAC;IACjD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,iBAAiB,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IAC9D,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC;IAC3E,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;AAC7C,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,IAAI,GAAG,sBAAsB,EAAE,CAAC;IACtC,MAAM,UAAU,GAAe,EAAE,CAAC;IAClC,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAoB,CAAC;IAEzC,IAAI,IAAI,EAAE,CAAC;QACT,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;YAChC,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACnC,IAAI,QAAQ,EAAE,CAAC;oBACb,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC;wBAAE,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;oBAChF,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;wBACjC,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;wBAC5B,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;oBAC9B,CAAC;gBACH,CAAC;qBAAM,CAAC;oBACN,MAAM,GAAG,GAAa,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC1D,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;oBACvB,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACvB,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACjC,MAAM,EAAE,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;YAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC9B,IAAI,QAAQ,EAAE,CAAC;gBACb,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,SAAS,CAAC;oBAAE,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACxF,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,GAAa,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC3E,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;gBAClB,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,SAAS,GAAe,aAAa,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7E,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;AAChD,CAAC;AASD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,IAAI,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,sCAAsC,GAAG,mDAAmD;SACtG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,oBAAoB,EAAE,EAAE,CAAC;QAC5B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EACL,gCAAgC,kBAAkB,EAAE,IAAI;gBACxD,6DAA6D;SAChE,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,kBAAkB,EAAE,CAAC;IACjC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,mCAAmC,kBAAkB,EAAE,GAAG;SACpE,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,yBAAyB,CAAC,GAAG,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,iBAAiB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IACzD,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAI,CAAC,EAAE,CAAC;QAC9B,OAAO;YACL,EAAE,EAAE,IAAI;YACR,OAAO,EAAE,UAAU,IAAI,CAAC,EAAE,2BAA2B,IAAI,CAAC,GAAG,GAAG;YAChE,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,CAAC,MAAM,CAAC,EAAE;SAC3C,CAAC;IACJ,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAI,CAAC,CAAC;IACtB,mBAAmB,CAAC,GAAG,CAAC,CAAC;IACzB,OAAO;QACL,EAAE,EAAE,IAAI;QACR,OAAO,EAAE,qBAAqB,IAAI,CAAC,EAAE,QAAQ,IAAI,CAAC,GAAG,KAAK,kBAAkB,EAAE,iBAAiB;QAC/F,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,YAAY,EAAE,CAAC,MAAM,CAAC,EAAE;KAC3C,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,CAAC,oBAAoB,EAAE,EAAE,CAAC;QAC5B,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EACL,gCAAgC,kBAAkB,EAAE,IAAI;gBACxD,6DAA6D;SAChE,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,kBAAkB,EAAE,CAAC;IACjC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,mCAAmC,kBAAkB,EAAE,GAAG,EAAE,CAAC;IAC5F,CAAC;IACD,MAAM,SAAS,GAAG,yBAAyB,CAAC,GAAG,CAAC,CAAC;IACjD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IAC/D,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,EAAE,EAAE,IAAI;YACR,OAAO,EAAE,kBAAkB,IAAI,uBAAuB;YACtD,KAAK,EAAE,EAAE,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,SAAS,CAAC,EAAE;SACjF,CAAC;IACJ,CAAC;IACD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpB,mBAAmB,CAAC,GAAG,CAAC,CAAC;IACzB,OAAO;QACL,EAAE,EAAE,IAAI;QACR,OAAO,EAAE,6BAA6B,IAAI,MAAM,kBAAkB,EAAE,oBAAoB;QACxF,KAAK,EAAE,EAAE,EAAE,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,SAAS,CAAC,EAAE;KACjF,CAAC;AACJ,CAAC;AASD;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,MAAM,GAAG,GAAG,kBAAkB,EAAE,CAAC;IACjC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,+CAA+C,kBAAkB,EAAE,GAAG;YAC/E,YAAY,EAAE,EAAE;YAChB,eAAe,EAAE,EAAE;SACpB,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,yBAAyB,CAAC,GAAG,CAAC,CAAC;IACjD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC5D,MAAM,OAAO,GAAG,iBAAiB,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IAE9D,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAEnE,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;QAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3C,IAAI,SAAS,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,SAAS,EAAE,CAAC;YAC9C,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,MAAM,IAAI,IAAI,CAAC,WAAW,KAAK,MAAM,CAAC,EAAE,CAAC;YAChE,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,MAAM,eAAe,GAAa,EAAE,CAAC;IACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;QACxC,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3C,IAAI,KAAK,KAAK,MAAM,IAAI,aAAa,CAAC,KAAK,CAAC,KAAK,MAAM,EAAE,CAAC;YACxD,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC5B,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,gCAAgC,QAAQ,IAAI;YACrD,YAAY,EAAE,EAAE;YAChB,eAAe,EAAE,EAAE;SACpB,CAAC;IACJ,CAAC;IAED,SAAS,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC;IACpC,SAAS,CAAC,eAAe,CAAC,GAAG,QAAQ,CAAC;IACtC,mBAAmB,CAAC,GAAG,CAAC,CAAC;IACzB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,MAAM,UAAU,CAAC,CAAC;IAC1E,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC,MAAM,aAAa,CAAC,CAAC;IACnF,OAAO;QACL,EAAE,EAAE,IAAI;QACR,OAAO,EAAE,WAAW,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,QAAQ,UAAU,kBAAkB,EAAE,GAAG;QAC5F,YAAY;QACZ,eAAe;KAChB,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenticros",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "AgenticROS - agentic AI for ROS-powered robots. Single CLI to launch real-robot or simulated demos, manage configuration, and inspect status.",
6
6
  "keywords": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "packedAt": "2026-06-06T14:01:37.009Z",
2
+ "packedAt": "2026-06-06T22:08:53.765Z",
3
3
  "repo": "https://github.com/PlaiPin/agenticros",
4
4
  "note": "This directory is a snapshot of the agenticros monorepo source. `agenticros init` will copy it to ~/agenticros and run pnpm install + colcon build there.",
5
5
  "layout": {
package/runtime/README.md CHANGED
@@ -56,34 +56,95 @@ Gemini CLI → @agenticros/gemini (function calling) → Core → ROS2 robots
56
56
  - `**docker/**` — Docker Compose and Dockerfiles for ROS2 + plugin images.
57
57
  - `**examples/**` — Example projects.
58
58
 
59
- ## Requirements
59
+ ## Install
60
60
 
61
- - Node.js >= 20, pnpm >= 9
62
- - ROS2 (Jazzy or compatible) for building and running the ROS2 packages
63
- - OpenClaw gateway for the OpenClaw plugin
61
+ You only need one command. The `agenticros` CLI handles everything else —
62
+ installing the ROS 2 workspace, building the MCP server, registering the
63
+ OpenClaw plugin, and wiring up your robot config.
64
64
 
65
- ## Quick start
65
+ ```bash
66
+ npx agenticros
67
+ ```
66
68
 
67
- 1. **Install dependencies**
68
- ```bash
69
- pnpm install
70
- ```
71
- 2. **Build ROS2 workspace** (optional, if you need discovery/agent/follow_me nodes)
72
- ```bash
73
- cd ros2_ws
74
- colcon build --packages-select agenticros_msgs agenticros_bringup agenticros_discovery agenticros_agent agenticros_follow_me
75
- source install/setup.bash
76
- ```
77
- 3. **Type-check packages**
78
- ```bash
79
- pnpm typecheck
80
- ```
81
- 4. **Install and test the OpenClaw plugin**
82
- Point the OpenClaw gateway at this repo’s `packages/agenticros` (or at a built package). Configure the plugin under `plugins.entries.agenticros.config` in your OpenClaw config file. Run `./scripts/setup_gateway_plugin.sh` from the repo root to register the plugin and print next steps. **Recommended:** OpenClaw **2026.3.11** or later — plugin routes work at [http://127.0.0.1:18789/plugins/agenticros/](http://127.0.0.1:18789/plugins/agenticros/) (config, teleop). For local dev without token auth, run `**node scripts/setup-openclaw-local.cjs`** then restart the gateway. **If URLs don't load** (e.g. gateway logs "missing or invalid auth" on older versions): run `**./scripts/use-openclaw-2026.2.26.sh`** as fallback. See **docs/openclaw-releases-and-plugin-routes.md**.
69
+ That's it. Run it on any machine with **Node ≥ 20**, no `git clone` required.
70
+ The first run launches the interactive menu:
71
+
72
+ ```text
73
+ ╔──────────────────────────────────────────────────╗
74
+ AgenticROS - agentic AI for ROS-powered robots ║
75
+ ╚──────────────────────────────────────────────────╝
76
+
77
+ ? What would you like to do?
78
+ Launch with real robot
79
+ Launch with simulation
80
+ First-time setup (workspace + OpenClaw plugin + API key)
81
+ Stop everything
82
+ Doctor (health check)
83
+ Configure (API keys, namespace, transport)
84
+ Tail logs
85
+ ```
86
+
87
+ Pick **First-time setup** once (workspace + OpenClaw plugin + API key, all
88
+ idempotent), then choose how you want to run:
89
+
90
+ | You want to … | Pick |
91
+ |---|---|
92
+ | Drive your **real robot** (RealSense + motors + MCP) | **Launch with real robot** |
93
+ | Demo a **simulated 2-wheel AMR** in Gazebo + RViz | **Launch with simulation → AMR** |
94
+ | Demo a **simulated 6-DOF arm** (UR5e-shaped, per-joint position control) | **Launch with simulation → 6-DOF arm** |
83
95
 
84
- **With token auth:** Run `node scripts/agenticros-proxy.cjs 18790` and open [http://127.0.0.1:18790/plugins/agenticros/](http://127.0.0.1:18790/plugins/agenticros/). See **docs/teleop.md**.
96
+ Once a stack is up, point any of the supported agents — OpenClaw, Claude Code,
97
+ Claude Desktop / Dispatch, or Gemini CLI — at the same robot and start talking
98
+ to it. The CLI tracks what it spawned (pidfiles + logs under `/tmp/agenticros-*`),
99
+ so **Stop everything** cleanly tears the demo down.
100
+
101
+ Prefer scripted invocations? Every menu item maps to a direct command:
102
+
103
+ ```bash
104
+ npx agenticros init # one-time workspace + plugin + API key
105
+ agenticros up real # real robot stack
106
+ agenticros up sim-amr # simulated AMR (Gazebo + RViz, headless on Jetson)
107
+ agenticros up sim-arm # simulated 6-DOF arm
108
+ agenticros mode <real|sim> # swap the active config profile (namespace, transport)
109
+ agenticros doctor # coloured health check
110
+ agenticros down # stop everything we started
111
+ ```
112
+
113
+ Full CLI reference: **[packages/agenticros-cli/README.md](packages/agenticros-cli/README.md)**.
114
+
115
+ ### Requirements
116
+
117
+ - **Node.js ≥ 20** (the only hard requirement — `npx agenticros` installs pnpm
118
+ itself if missing)
119
+ - **ROS 2 Humble or Jazzy** if you plan to use the real-robot stack or any
120
+ simulation (the CLI sources `/opt/ros/<distro>/setup.bash` and runs
121
+ `colcon build` for you)
122
+ - **OpenClaw gateway** only if you also want the OpenClaw web UI / chat /
123
+ teleop adapter
124
+
125
+ ### Contributing / building from source
126
+
127
+ Hacking on the packages themselves? Clone and use the local checkout — the CLI
128
+ auto-detects the workspace and uses live sources instead of the bundled snapshot:
129
+
130
+ ```bash
131
+ git clone https://github.com/PlaiPin/agenticros && cd agenticros
132
+ pnpm install && pnpm build
133
+ ./agenticros # repo-local CLI shim, same menu as `npx agenticros`
134
+ ```
85
135
 
86
- See `**docs/`** for robot setup, skills, teleop, and Docker.
136
+ For the OpenClaw plugin specifically, point the gateway at this repo's
137
+ `packages/agenticros` and configure under `plugins.entries.agenticros.config`.
138
+ **Recommended:** OpenClaw **2026.3.11+** — routes work at
139
+ [http://127.0.0.1:18789/plugins/agenticros/](http://127.0.0.1:18789/plugins/agenticros/)
140
+ (config, teleop). For local dev without token auth:
141
+ `node scripts/setup-openclaw-local.cjs` then restart the gateway. Older gateways
142
+ needing token auth: run `node scripts/agenticros-proxy.cjs 18790` and open
143
+ [http://127.0.0.1:18790/plugins/agenticros/](http://127.0.0.1:18790/plugins/agenticros/).
144
+ See **[docs/openclaw-releases-and-plugin-routes.md](docs/openclaw-releases-and-plugin-routes.md)**
145
+ and **[docs/teleop.md](docs/teleop.md)**.
146
+
147
+ See **`docs/`** for robot setup, skills, teleop, simulation internals, and Docker.
87
148
 
88
149
  ## RViz2 and Gazebo (TurtleBot3 + rosbridge)
89
150
 
@@ -23,7 +23,8 @@
23
23
  "memory_status",
24
24
  "follow_robot",
25
25
  "follow_me_see",
26
- "ollama_status"
26
+ "ollama_status",
27
+ "find_object"
27
28
  ]
28
29
  },
29
30
  "configSchema": {
@@ -311,7 +312,10 @@
311
312
  },
312
313
  "backend": {
313
314
  "type": "string",
314
- "enum": ["local", "mem0"],
315
+ "enum": [
316
+ "local",
317
+ "mem0"
318
+ ],
315
319
  "default": "local",
316
320
  "description": "local = JSON-on-disk, no deps; mem0 = semantic search via mem0ai (requires `pnpm add mem0ai`)."
317
321
  },
@@ -360,11 +360,40 @@ export async function handleToolCall(
360
360
  const topics = await transport.listTopics();
361
361
  const MAX = 50;
362
362
  const truncated = topics.length > MAX ? topics.slice(0, MAX) : topics;
363
+
364
+ // Surface task-specific control hints so the agent (and any LLM
365
+ // calling list_topics to figure out what's available) doesn't have to
366
+ // memorise the topic surface for each demo. Cheap to compute and only
367
+ // included when matching topics are actually present.
368
+ const hints: Record<string, string> = {};
369
+ const armJoints = topics
370
+ .filter((t) => /^\/arm\/[a-z0-9_]+\/cmd_pos$/i.test(t.name))
371
+ .map((t) => t.name.replace(/^\/arm\/(.+)\/cmd_pos$/, "$1"));
372
+ if (armJoints.length > 0) {
373
+ hints["arm"] =
374
+ `Detected the AgenticROS sim arm (joints: ${armJoints.join(", ")}). ` +
375
+ "To move a joint, call ros2_publish with topic '/arm/<joint>/cmd_pos', " +
376
+ "type 'std_msgs/msg/Float64', message {data: <radians>}. " +
377
+ "Example: rotate shoulder_pan 90 degrees left -> publish {data: 1.5707} " +
378
+ "to /arm/shoulder_pan/cmd_pos. Listen to /joint_states (sensor_msgs/msg/JointState) " +
379
+ "to read current positions.";
380
+ }
381
+ const baseTwistTopics = topics.filter(
382
+ (t) => /\/cmd_vel$/.test(t.name) && t.type === "geometry_msgs/msg/Twist",
383
+ );
384
+ if (baseTwistTopics.length > 0) {
385
+ hints["base"] =
386
+ "To drive the base, publish geometry_msgs/msg/Twist to a cmd_vel topic. " +
387
+ `Available cmd_vel topics: ${baseTwistTopics.map((t) => t.name).join(", ")}. ` +
388
+ "When in sim mode the unnamespaced /cmd_vel is the one the simulator listens on.";
389
+ }
390
+
363
391
  const text = JSON.stringify({
364
392
  success: true,
365
393
  topics: truncated,
366
394
  total: topics.length,
367
395
  truncated: topics.length > MAX,
396
+ ...(Object.keys(hints).length > 0 ? { hints } : {}),
368
397
  });
369
398
  return { content: [{ type: "text", text }] };
370
399
  }
@@ -91,7 +91,11 @@ export class LocalTransport implements RosTransport {
91
91
  // Give DDS discovery a brief moment to receive announcements from peers
92
92
  // before we report "connected". Without this the very first listTopics()
93
93
  // can race the executor and return empty even though peers exist.
94
- await new Promise<void>((resolve) => setTimeout(resolve, 750));
94
+ // 1500ms was chosen empirically on Jetson + Humble where /arm/*/cmd_pos
95
+ // topics from a sim_arm gz_bridge child take ~1.2s to propagate via
96
+ // multicast (vs ~400ms on x86); the prior 750ms cap caused first
97
+ // list_topics() calls to miss them.
98
+ await new Promise<void>((resolve) => setTimeout(resolve, 1500));
95
99
 
96
100
  this.setStatus("connected");
97
101
  } catch (err) {
@@ -280,24 +284,41 @@ export class LocalTransport implements RosTransport {
280
284
  async listTopics(): Promise<TopicInfo[]> {
281
285
  this.ensureConnected();
282
286
 
283
- // DDS discovery is asynchronous. On a freshly-connected node,
284
- // getTopicNamesAndTypes() trickles in results as peers respond - the
285
- // first call may return 0, the second 2, the third all of them.
286
- // Poll until the count is stable across two consecutive 300ms reads (or
287
- // time-bounded to ~3s) so we don't return a partial view.
287
+ // DDS discovery is asynchronous AND stateful. On a freshly-connected
288
+ // node, getTopicNamesAndTypes() trickles in results as peers respond -
289
+ // the first call may return 0, the second 2, the third all of them.
290
+ //
291
+ // Two issues motivated the current tuning:
292
+ //
293
+ // 1. A short single-stable-hit policy (600ms) was returning truncated
294
+ // views to Claude: e.g. with the AMR sim alone it'd see /cmd_vel
295
+ // etc. on first call, then a second call 2s later would see all
296
+ // 29 topics including /arm/*/cmd_pos. Claude only calls
297
+ // list_topics once and was missing arm joint topics entirely.
298
+ //
299
+ // 2. DDS multicast announcements on Jetson take longer than on x86 -
300
+ // we routinely see /arm/* topics appear ~1.5s after first call.
301
+ //
302
+ // Fix: require 3 consecutive matching polls (2 stable hits at 300ms
303
+ // intervals = ~900ms of stability), with a 6 s deadline. Worst case
304
+ // ~900ms latency on a steady graph, ~5s+ if discovery is still
305
+ // streaming. The min-count guard (>=3) avoids exiting early when only
306
+ // /clock + /tf + /tf_static have come through.
288
307
  const externalFilter = (t: { name: string; types: string[] }) =>
289
308
  !INTERNAL_TOPIC_PREFIXES.some((prefix) => t.name.startsWith(prefix));
290
309
 
291
- const deadline = Date.now() + 3000;
310
+ const deadline = Date.now() + 6000;
311
+ const MIN_STABLE_COUNT = 3;
312
+ const REQUIRED_STABLE_HITS = 2;
292
313
  let raw: Array<{ name: string; types: string[] }> = [];
293
314
  let prevCount = -1;
294
315
  let stableHits = 0;
295
316
  while (Date.now() < deadline) {
296
317
  raw = this.node.getTopicNamesAndTypes();
297
318
  const externalCount = raw.filter(externalFilter).length;
298
- if (externalCount === prevCount && externalCount > 0) {
319
+ if (externalCount === prevCount && externalCount >= MIN_STABLE_COUNT) {
299
320
  stableHits++;
300
- if (stableHits >= 1) break; // one stable confirmation is enough
321
+ if (stableHits >= REQUIRED_STABLE_HITS) break;
301
322
  } else {
302
323
  stableHits = 0;
303
324
  prevCount = externalCount;