agenticros 0.3.5 → 0.4.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 (38) hide show
  1. package/dist/commands/doctor.d.ts.map +1 -1
  2. package/dist/commands/doctor.js +34 -1
  3. package/dist/commands/doctor.js.map +1 -1
  4. package/dist/commands/skills.d.ts +3 -5
  5. package/dist/commands/skills.d.ts.map +1 -1
  6. package/dist/commands/skills.js +21 -7
  7. package/dist/commands/skills.js.map +1 -1
  8. package/dist/util/skills.d.ts +6 -0
  9. package/dist/util/skills.d.ts.map +1 -1
  10. package/dist/util/skills.js +69 -1
  11. package/dist/util/skills.js.map +1 -1
  12. package/package.json +2 -1
  13. package/runtime/BUNDLE.json +1 -1
  14. package/runtime/packages/agenticros/src/index.ts +8 -1
  15. package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +9 -12
  16. package/runtime/packages/agenticros-claude-code/dist/config.d.ts +5 -3
  17. package/runtime/packages/agenticros-claude-code/dist/config.d.ts.map +1 -1
  18. package/runtime/packages/agenticros-claude-code/dist/config.js +32 -10
  19. package/runtime/packages/agenticros-claude-code/dist/config.js.map +1 -1
  20. package/runtime/packages/agenticros-claude-code/dist/index.js +3 -3
  21. package/runtime/packages/agenticros-claude-code/dist/index.js.map +1 -1
  22. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
  23. package/runtime/packages/agenticros-claude-code/dist/tools.js +8 -8
  24. package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
  25. package/runtime/packages/agenticros-claude-code/src/config.ts +33 -10
  26. package/runtime/packages/agenticros-claude-code/src/index.ts +3 -3
  27. package/runtime/packages/agenticros-claude-code/src/tools.ts +11 -8
  28. package/runtime/packages/agenticros-gemini/src/config.ts +27 -18
  29. package/runtime/packages/agenticros-gemini/src/index.ts +2 -2
  30. package/runtime/packages/agenticros-gemini/src/tools.ts +5 -2
  31. package/runtime/packages/core/package.json +1 -1
  32. package/runtime/packages/core/src/__tests__/skill-refs.test.ts +77 -0
  33. package/runtime/packages/core/src/config.ts +8 -0
  34. package/runtime/packages/core/src/discoverable-capabilities.ts +178 -0
  35. package/runtime/packages/core/src/external-capability.ts +16 -3
  36. package/runtime/packages/core/src/index.ts +30 -0
  37. package/runtime/packages/core/src/skill-refs.ts +330 -0
  38. package/runtime/pnpm-lock.yaml +3 -0
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
4
  import type { AgenticROSConfig } from "@agenticros/core";
5
- import { parseConfig } from "@agenticros/core";
5
+ import { parseConfig, withResolvedSkillRefs, applyCachedSkillRefs } from "@agenticros/core";
6
6
 
7
7
  /** Same as Claude Code MCP: optional `robot.namespace` override from the environment. */
8
8
  function applyMcpEnvOverrides(config: AgenticROSConfig): AgenticROSConfig {
@@ -17,11 +17,6 @@ function applyMcpEnvOverrides(config: AgenticROSConfig): AgenticROSConfig {
17
17
  };
18
18
  }
19
19
 
20
- /**
21
- * Resolve path to AgenticROS config file.
22
- * Prefer AGENTICROS_CONFIG_PATH; else ~/.agenticros/config.json.
23
- * Optional: fallback to OpenClaw config and read plugins.entries.agenticros.config.
24
- */
25
20
  function getConfigPath(): string {
26
21
  const env = process.env.AGENTICROS_CONFIG_PATH;
27
22
  if (env && env.trim().length > 0) {
@@ -30,9 +25,6 @@ function getConfigPath(): string {
30
25
  return path.join(os.homedir(), ".agenticros", "config.json");
31
26
  }
32
27
 
33
- /**
34
- * Try to read config from OpenClaw file (plugins.entries.agenticros.config).
35
- */
36
28
  function tryOpenClawConfig(): Record<string, unknown> | null {
37
29
  const openclawEnv = process.env.OPENCLAW_CONFIG;
38
30
  const openclawPath = openclawEnv && openclawEnv.trim().length > 0
@@ -57,12 +49,7 @@ function tryOpenClawConfig(): Record<string, unknown> | null {
57
49
  return null;
58
50
  }
59
51
 
60
- /**
61
- * Load and parse AgenticROS config.
62
- * 1) AGENTICROS_CONFIG_PATH or ~/.agenticros/config.json (full JSON object = config).
63
- * 2) If that file does not exist, try OpenClaw config and read plugins.entries.agenticros.config.
64
- */
65
- export function loadConfig(): AgenticROSConfig {
52
+ function loadConfigSync(): AgenticROSConfig {
66
53
  const primaryPath = getConfigPath();
67
54
  try {
68
55
  const raw = fs.readFileSync(primaryPath, "utf8");
@@ -72,14 +59,14 @@ export function loadConfig(): AgenticROSConfig {
72
59
  if (process.stderr && typeof process.stderr.write === "function") {
73
60
  process.stderr.write(`[AgenticROS] Config from ${primaryPath}\n`);
74
61
  }
75
- return applyMcpEnvOverrides(cfg);
62
+ return applyMcpEnvOverrides(applyCachedSkillRefs(cfg));
76
63
  }
77
64
  } catch (err) {
78
65
  const nodeErr = err as NodeJS.ErrnoException;
79
66
  if (nodeErr.code === "ENOENT") {
80
67
  const openclawConfig = tryOpenClawConfig();
81
68
  if (openclawConfig) {
82
- return applyMcpEnvOverrides(parseConfig(openclawConfig));
69
+ return applyMcpEnvOverrides(applyCachedSkillRefs(parseConfig(openclawConfig)));
83
70
  }
84
71
  throw new Error(
85
72
  `AgenticROS config not found at ${primaryPath}. Create it or set AGENTICROS_CONFIG_PATH. ` +
@@ -88,5 +75,27 @@ export function loadConfig(): AgenticROSConfig {
88
75
  }
89
76
  throw err;
90
77
  }
91
- return applyMcpEnvOverrides(parseConfig({}));
78
+ return applyMcpEnvOverrides(applyCachedSkillRefs(parseConfig({})));
79
+ }
80
+
81
+ export function loadConfig(): AgenticROSConfig {
82
+ return loadConfigSync();
83
+ }
84
+
85
+ export async function loadConfigAsync(): Promise<AgenticROSConfig> {
86
+ const base = loadConfigSync();
87
+ if (!(base.skillRefs?.length)) return base;
88
+ const { config, errors } = await withResolvedSkillRefs(base, {
89
+ onLog: (msg) => {
90
+ if (process.stderr && typeof process.stderr.write === "function") {
91
+ process.stderr.write(`[AgenticROS] ${msg}\n`);
92
+ }
93
+ },
94
+ });
95
+ for (const e of errors) {
96
+ if (process.stderr && typeof process.stderr.write === "function") {
97
+ process.stderr.write(`[AgenticROS] skillRef ${e.ref}: ${e.error}\n`);
98
+ }
99
+ }
100
+ return applyMcpEnvOverrides(config);
92
101
  }
@@ -10,7 +10,7 @@
10
10
  * Optional: GEMINI_MODEL (default gemini-2.5-flash) if you hit quota on a specific model.
11
11
  */
12
12
 
13
- import { loadConfig } from "./config.js";
13
+ import { loadConfigAsync } from "./config.js";
14
14
  import { connect, disconnect } from "./transport.js";
15
15
  import { chatWithRobot } from "./chat.js";
16
16
  import { renderAgenticROSBanner } from "@agenticros/core";
@@ -32,7 +32,7 @@ async function main(): Promise<void> {
32
32
  process.exit(1);
33
33
  }
34
34
 
35
- const config = loadConfig();
35
+ const config = await loadConfigAsync();
36
36
  await connect(config);
37
37
 
38
38
  try {
@@ -14,6 +14,7 @@ import {
14
14
  toNamespacedTopic,
15
15
  toNamespacedTopicFull,
16
16
  listAllCapabilities,
17
+ listCapabilitiesWithDiscoverable,
17
18
  runMission,
18
19
  listRobots,
19
20
  getActiveRobotId,
@@ -500,15 +501,17 @@ export async function executeTool(
500
501
  if (name === "ros2_list_capabilities") {
501
502
  const resolved = resolveRobotForTool(config, args);
502
503
  if ("error" in resolved) return resolved.error;
503
- const caps: Capability[] = listAllCapabilities(config);
504
+ const caps = await listCapabilitiesWithDiscoverable(config);
504
505
  const intrinsic = caps.filter((c) => c.source?.kind === "builtin").length;
505
- const skill = caps.filter((c) => c.source?.kind === "skill").length;
506
+ const skill = caps.filter((c) => c.installed !== false && c.source?.kind === "skill").length;
507
+ const discoverable = caps.filter((c) => c.discoverable === true).length;
506
508
  return {
507
509
  output: JSON.stringify({
508
510
  success: true,
509
511
  total: caps.length,
510
512
  intrinsic_count: intrinsic,
511
513
  skill_count: skill,
514
+ discoverable_count: discoverable,
512
515
  capabilities: caps,
513
516
  }),
514
517
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticros/core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "AgenticROS core — platform-agnostic ROS2 transport, types, and config (the dep every AgenticROS skill imports).",
6
6
  "keywords": [
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Unit tests for skillRefs parsing and discoverable capability merge.
3
+ */
4
+ import { test } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import {
7
+ parseSkillRef,
8
+ parseConfig,
9
+ listCapabilitiesWithDiscoverable,
10
+ listAllCapabilities,
11
+ } from "../index.js";
12
+
13
+ test("parseSkillRef: owner/skill and @pin", () => {
14
+ assert.deepEqual(parseSkillRef("agenticros/navigate-to"), {
15
+ marketplaceRef: "agenticros/navigate-to",
16
+ owner: "agenticros",
17
+ skill: "navigate-to",
18
+ gitRef: "main",
19
+ });
20
+ assert.equal(parseSkillRef("agenticros/start-slam@v0.1.0")?.gitRef, "v0.1.0");
21
+ assert.equal(parseSkillRef("nope"), null);
22
+ assert.equal(parseSkillRef(""), null);
23
+ });
24
+
25
+ test("parseConfig: skillRefs defaults to []", () => {
26
+ const cfg = parseConfig({});
27
+ assert.deepEqual(cfg.skillRefs, []);
28
+ const cfg2 = parseConfig({ skillRefs: ["agenticros/detect-humans"] });
29
+ assert.deepEqual(cfg2.skillRefs, ["agenticros/detect-humans"]);
30
+ });
31
+
32
+ test("listCapabilitiesWithDiscoverable: soft-fail offline keeps installed", async () => {
33
+ const cfg = parseConfig({});
34
+ const caps = await listCapabilitiesWithDiscoverable(cfg, {
35
+ apiBase: "http://127.0.0.1:9", // nothing listening
36
+ softFail: true,
37
+ });
38
+ const baseline = listAllCapabilities(cfg);
39
+ assert.equal(caps.length, baseline.length);
40
+ assert.ok(caps.every((c) => c.installed !== false || c.discoverable !== true));
41
+ });
42
+
43
+ test("listCapabilitiesWithDiscoverable: merges marketplace caps when fetch works", async () => {
44
+ const originalFetch = globalThis.fetch;
45
+ globalThis.fetch = (async () =>
46
+ new Response(
47
+ JSON.stringify({
48
+ skills: [
49
+ {
50
+ marketplaceRef: "agenticros/detect-humans",
51
+ visibility: "public",
52
+ capabilities: [
53
+ {
54
+ id: "detect_humans",
55
+ verb: "detect",
56
+ description: "Read detections",
57
+ },
58
+ ],
59
+ },
60
+ ],
61
+ }),
62
+ { status: 200, headers: { "Content-Type": "application/json" } },
63
+ )) as typeof fetch;
64
+
65
+ try {
66
+ const cfg = parseConfig({});
67
+ const caps = await listCapabilitiesWithDiscoverable(cfg, { softFail: false });
68
+ const det = caps.find((c) => c.id === "detect_humans");
69
+ assert.ok(det);
70
+ assert.equal(det!.discoverable, true);
71
+ assert.equal(det!.installed, false);
72
+ assert.equal(det!.install_ref, "agenticros/detect-humans");
73
+ assert.ok(caps.some((c) => c.id === "drive_base" && c.installed === true));
74
+ } finally {
75
+ globalThis.fetch = originalFetch;
76
+ }
77
+ });
@@ -418,6 +418,14 @@ export const AgenticROSConfigSchema = z.object({
418
418
 
419
419
  /** Npm (or local) package names to load as skills. Resolved via require.resolve from plugin context. */
420
420
  skillPackages: z.array(z.string()).default([]),
421
+
422
+ /**
423
+ * Marketplace skill refs to auto-fetch into `~/.agenticros/skills-cache/`
424
+ * (e.g. `agenticros/navigate-to` or `owner/skill@main`). Resolved at
425
+ * adapter startup into effective skillPaths. Never auto-upgrades pins.
426
+ * Distinct from `skills` (per-skill runtime config object).
427
+ */
428
+ skillRefs: z.array(z.string()).default([]),
421
429
  });
422
430
 
423
431
  export type AgenticROSConfig = z.infer<typeof AgenticROSConfigSchema>;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Discoverable marketplace capabilities — list verbs from skills.agenticros.com
3
+ * that are not yet installed locally, so agents can propose installs.
4
+ */
5
+
6
+ import type { AgenticROSConfig } from "./config.js";
7
+ import {
8
+ listAllCapabilities,
9
+ type Capability,
10
+ type CapabilityField,
11
+ type CapabilitySource,
12
+ } from "./capabilities.js";
13
+ import { skillsApiBase } from "./skill-refs.js";
14
+
15
+ export interface DiscoverableCapability extends Capability {
16
+ /** Always true for marketplace-only entries. */
17
+ discoverable: true;
18
+ /** False when not present in the local registry. */
19
+ installed: false;
20
+ /** Marketplace install ref (owner/skill). */
21
+ install_ref: string;
22
+ }
23
+
24
+ export interface ListedCapability extends Capability {
25
+ discoverable?: boolean;
26
+ installed?: boolean;
27
+ install_ref?: string;
28
+ }
29
+
30
+ export interface ListCapabilitiesOptions {
31
+ /** Include marketplace browse (network). Default true. */
32
+ includeDiscoverable?: boolean;
33
+ apiBase?: string;
34
+ /** Max marketplace skills to scan. Default 50. */
35
+ marketplaceLimit?: number;
36
+ /** Soft-fail: on network error return installed-only. Default true. */
37
+ softFail?: boolean;
38
+ }
39
+
40
+ interface MarketplaceSkillDoc {
41
+ marketplaceRef?: string;
42
+ ownerLogin?: string;
43
+ skillSlug?: string;
44
+ slug?: string;
45
+ capabilities?: Array<Record<string, unknown>>;
46
+ visibility?: string;
47
+ }
48
+
49
+ function installRefOf(s: MarketplaceSkillDoc): string | null {
50
+ if (s.marketplaceRef && typeof s.marketplaceRef === "string") {
51
+ return s.marketplaceRef.toLowerCase();
52
+ }
53
+ if (s.ownerLogin && s.skillSlug) {
54
+ return `${s.ownerLogin}/${s.skillSlug}`.toLowerCase();
55
+ }
56
+ if (s.slug && s.slug.includes("/")) return s.slug.toLowerCase();
57
+ return null;
58
+ }
59
+
60
+ function fieldMap(
61
+ raw: unknown,
62
+ ): Record<string, CapabilityField> | undefined {
63
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
64
+ const out: Record<string, CapabilityField> = {};
65
+ for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
66
+ if (typeof v === "string") {
67
+ out[k] = { type: v };
68
+ } else if (v && typeof v === "object") {
69
+ const o = v as Record<string, unknown>;
70
+ out[k] = {
71
+ type: typeof o.type === "string" ? o.type : "unknown",
72
+ description: typeof o.description === "string" ? o.description : undefined,
73
+ optional: o.optional === true,
74
+ };
75
+ }
76
+ }
77
+ return Object.keys(out).length ? out : undefined;
78
+ }
79
+
80
+ function normalizeMarketplaceCap(
81
+ raw: Record<string, unknown>,
82
+ installRef: string,
83
+ packageName: string,
84
+ ): DiscoverableCapability | null {
85
+ const id = typeof raw.id === "string" ? raw.id : null;
86
+ const verb = typeof raw.verb === "string" ? raw.verb : null;
87
+ const description =
88
+ typeof raw.description === "string" ? raw.description : id ? `Marketplace capability ${id}` : null;
89
+ if (!id || !verb || !description) return null;
90
+ const source: CapabilitySource = {
91
+ kind: "skill",
92
+ skillId: installRef.split("/")[1] ?? installRef,
93
+ package: packageName,
94
+ };
95
+ return {
96
+ id,
97
+ verb,
98
+ description,
99
+ inputs: fieldMap(raw.inputs),
100
+ outputs: fieldMap(raw.outputs),
101
+ interruptible: raw.interruptible === true ? true : raw.interruptible === false ? false : undefined,
102
+ blocks_base: raw.blocks_base === true ? true : undefined,
103
+ source,
104
+ discoverable: true,
105
+ installed: false,
106
+ install_ref: installRef,
107
+ };
108
+ }
109
+
110
+ export async function fetchMarketplaceSkills(
111
+ opts: { apiBase?: string; limit?: number } = {},
112
+ ): Promise<MarketplaceSkillDoc[]> {
113
+ const base = skillsApiBase(opts.apiBase);
114
+ const limit = opts.limit ?? 50;
115
+ const u = new URL(`${base}/skills`);
116
+ u.searchParams.set("limit", String(limit));
117
+ u.searchParams.set("sort", "popular");
118
+ const res = await fetch(u.toString(), { headers: { Accept: "application/json" } });
119
+ if (!res.ok) {
120
+ throw new Error(`Marketplace list ${res.status}`);
121
+ }
122
+ const body = (await res.json()) as { skills?: MarketplaceSkillDoc[] };
123
+ return body.skills ?? [];
124
+ }
125
+
126
+ /**
127
+ * Merge installed capabilities with discoverable marketplace capabilities
128
+ * (ids not already present locally). Soft-fails offline.
129
+ */
130
+ export async function listCapabilitiesWithDiscoverable(
131
+ config: AgenticROSConfig,
132
+ opts: ListCapabilitiesOptions = {},
133
+ ): Promise<ListedCapability[]> {
134
+ const installed = listAllCapabilities(config).map((c) => {
135
+ const listed: ListedCapability = {
136
+ ...c,
137
+ installed: true,
138
+ discoverable: false,
139
+ };
140
+ return listed;
141
+ });
142
+ if (opts.includeDiscoverable === false) {
143
+ return installed;
144
+ }
145
+
146
+ const installedIds = new Set(installed.map((c) => c.id));
147
+ try {
148
+ const skills = await fetchMarketplaceSkills({
149
+ apiBase: opts.apiBase,
150
+ limit: opts.marketplaceLimit ?? 50,
151
+ });
152
+ const extra: DiscoverableCapability[] = [];
153
+ const seenDiscoverable = new Set<string>();
154
+ for (const s of skills) {
155
+ if (s.visibility && s.visibility !== "public") continue;
156
+ const ref = installRefOf(s);
157
+ if (!ref) continue;
158
+ const caps = Array.isArray(s.capabilities) ? s.capabilities : [];
159
+ for (const raw of caps) {
160
+ if (!raw || typeof raw !== "object") continue;
161
+ const id = typeof (raw as { id?: unknown }).id === "string" ? (raw as { id: string }).id : null;
162
+ if (!id || installedIds.has(id) || seenDiscoverable.has(id)) continue;
163
+ const norm = normalizeMarketplaceCap(
164
+ raw as Record<string, unknown>,
165
+ ref,
166
+ s.slug ?? ref,
167
+ );
168
+ if (!norm) continue;
169
+ seenDiscoverable.add(id);
170
+ extra.push(norm);
171
+ }
172
+ }
173
+ return [...installed, ...extra];
174
+ } catch (e) {
175
+ if (opts.softFail === false) throw e;
176
+ return installed;
177
+ }
178
+ }
@@ -156,12 +156,25 @@ export async function executeExternalCapability(
156
156
  }
157
157
 
158
158
  if (impl.topic) {
159
- const topic = resolveName(impl.topic, ns);
159
+ const topicName =
160
+ typeof inputs.topic === "string" && inputs.topic.trim()
161
+ ? inputs.topic.trim()
162
+ : impl.topic;
163
+ const topic = resolveName(topicName, ns);
160
164
  const msgType = impl.msg_type ?? "";
161
- const mode = typeof inputs.mode === "string" ? inputs.mode : "publish";
165
+ // Detection / sensor skills default to subscribe_once; publishers must pass mode: "publish".
166
+ const mode =
167
+ typeof inputs.mode === "string"
168
+ ? inputs.mode
169
+ : "subscribe";
162
170
  if (mode === "subscribe" || mode === "subscribe_once") {
163
171
  const timeout =
164
- options.timeoutMs ?? (typeof inputs.timeout === "number" ? inputs.timeout : 5000);
172
+ options.timeoutMs ??
173
+ (typeof inputs.timeout_ms === "number"
174
+ ? inputs.timeout_ms
175
+ : typeof inputs.timeout === "number"
176
+ ? inputs.timeout
177
+ : 5000);
165
178
  const msg = await new Promise<Record<string, unknown>>((resolve, reject) => {
166
179
  let settled = false;
167
180
  const timer = setTimeout(() => {
@@ -171,5 +171,35 @@ export type {
171
171
  FindRobotsForResult,
172
172
  } from "./find-robots-for.js";
173
173
 
174
+ export {
175
+ DEFAULT_SKILLS_API,
176
+ DEFAULT_SKILLS_CACHE_DIR,
177
+ ensureSkillRefCached,
178
+ fetchInstallDescriptor,
179
+ githubRepoBasename,
180
+ parseSkillRef,
181
+ resolveSkillRefs,
182
+ skillsApiBase,
183
+ skillsCacheDir,
184
+ withResolvedSkillRefs,
185
+ applyCachedSkillRefs,
186
+ } from "./skill-refs.js";
187
+ export type {
188
+ InstallDescriptor,
189
+ ParsedSkillRef,
190
+ ResolveSkillRefsOptions,
191
+ ResolveSkillRefsResult,
192
+ } from "./skill-refs.js";
193
+
194
+ export {
195
+ fetchMarketplaceSkills,
196
+ listCapabilitiesWithDiscoverable,
197
+ } from "./discoverable-capabilities.js";
198
+ export type {
199
+ DiscoverableCapability,
200
+ ListCapabilitiesOptions,
201
+ ListedCapability,
202
+ } from "./discoverable-capabilities.js";
203
+
174
204
  export { TransportPool, TRANSPORT_POOL_GLOBAL_KEY } from "./transport-pool.js";
175
205
  export type { TransportFactory } from "./transport-pool.js";