agenticros 0.3.5 → 0.4.1

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 (39) 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 +24 -0
  39. package/runtime/scripts/check-cli-tarball-install.mjs +41 -8
@@ -0,0 +1,330 @@
1
+ /**
2
+ * skillRefs → ~/.agenticros/skills-cache resolver (marketplace auto-fetch v1).
3
+ *
4
+ * Declarative refs like `agenticros/navigate-to` or `owner/skill@main` are
5
+ * resolved via the skills marketplace install API, cloned into the cache,
6
+ * built if needed, and returned as absolute skillPaths for capability
7
+ * reading / OpenClaw loading.
8
+ *
9
+ * Never auto-upgrades: if the cache dir for a pin already exists, reuse it
10
+ * (optional pull when AGENTICROS_SKILLS_CACHE_PULL=1).
11
+ */
12
+
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { basename, join } from "node:path";
16
+ import { execFileSync } from "node:child_process";
17
+ import type { AgenticROSConfig } from "./config.js";
18
+
19
+ export const DEFAULT_SKILLS_API = "https://skills.agenticros.com/api";
20
+ export const DEFAULT_SKILLS_CACHE_DIR = join(homedir(), ".agenticros", "skills-cache");
21
+
22
+ export interface ParsedSkillRef {
23
+ /** owner/skill (lowercase) */
24
+ marketplaceRef: string;
25
+ owner: string;
26
+ skill: string;
27
+ /** git ref / branch pin; default main */
28
+ gitRef: string;
29
+ }
30
+
31
+ export interface InstallDescriptor {
32
+ slug: string;
33
+ marketplaceRef?: string;
34
+ skillId: string;
35
+ packageName: string;
36
+ githubUrl: string;
37
+ ref: string;
38
+ buildCmd: string;
39
+ }
40
+
41
+ export interface ResolveSkillRefsOptions {
42
+ apiBase?: string;
43
+ cacheDir?: string;
44
+ /** When true, git pull --ff-only if cache already exists. */
45
+ pullIfPresent?: boolean;
46
+ /** Skip network / clone (return only paths that already exist in cache). */
47
+ offline?: boolean;
48
+ onLog?: (msg: string) => void;
49
+ }
50
+
51
+ export interface ResolveSkillRefsResult {
52
+ /** Absolute directories ready to merge into skillPaths. */
53
+ paths: string[];
54
+ errors: Array<{ ref: string; error: string }>;
55
+ }
56
+
57
+ /** Parse `owner/skill` or `owner/skill@branch`. */
58
+ export function parseSkillRef(raw: string): ParsedSkillRef | null {
59
+ const trimmed = raw.trim();
60
+ if (!trimmed) return null;
61
+ const at = trimmed.lastIndexOf("@");
62
+ let body = trimmed;
63
+ let gitRef = "main";
64
+ if (at > 0) {
65
+ body = trimmed.slice(0, at);
66
+ gitRef = trimmed.slice(at + 1).trim() || "main";
67
+ }
68
+ const parts = body.split("/").filter(Boolean);
69
+ if (parts.length < 2) return null;
70
+ const owner = parts[0]!.toLowerCase();
71
+ const skill = parts.slice(1).join("/").toLowerCase();
72
+ if (!owner || !skill) return null;
73
+ return {
74
+ marketplaceRef: `${owner}/${skill}`,
75
+ owner,
76
+ skill,
77
+ gitRef,
78
+ };
79
+ }
80
+
81
+ export function skillsApiBase(override?: string): string {
82
+ return (override || process.env.AGENTICROS_SKILLS_API || DEFAULT_SKILLS_API).replace(
83
+ /\/+$/,
84
+ "",
85
+ );
86
+ }
87
+
88
+ export function skillsCacheDir(override?: string): string {
89
+ return override || process.env.AGENTICROS_SKILLS_CACHE || DEFAULT_SKILLS_CACHE_DIR;
90
+ }
91
+
92
+ function skillApiPath(apiBase: string, ref: string): string {
93
+ const [owner, ...rest] = ref.split("/");
94
+ const skill = rest.join("/");
95
+ return `${apiBase}/skills/${encodeURIComponent(owner!)}/${encodeURIComponent(skill)}`;
96
+ }
97
+
98
+ export async function fetchInstallDescriptor(
99
+ marketplaceRef: string,
100
+ apiBase?: string,
101
+ ): Promise<InstallDescriptor> {
102
+ const base = skillsApiBase(apiBase);
103
+ const url = `${skillApiPath(base, marketplaceRef)}/install`;
104
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
105
+ if (!res.ok) {
106
+ const body = await res.text().catch(() => "");
107
+ throw new Error(`Marketplace install ${res.status}: ${body.slice(0, 200)}`);
108
+ }
109
+ const desc = (await res.json()) as InstallDescriptor;
110
+ if (!desc.githubUrl) {
111
+ throw new Error(`Install descriptor for ${marketplaceRef} has no githubUrl`);
112
+ }
113
+ return desc;
114
+ }
115
+
116
+ function cachePathFor(
117
+ cacheDir: string,
118
+ owner: string,
119
+ skill: string,
120
+ gitRef: string,
121
+ ): string {
122
+ const safeRef = gitRef.replace(/[^a-zA-Z0-9._-]+/g, "_");
123
+ return join(cacheDir, owner, skill, safeRef);
124
+ }
125
+
126
+ function hasBuiltEntry(dir: string): boolean {
127
+ try {
128
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
129
+ main?: string;
130
+ };
131
+ const main = pkg.main ?? "dist/index.js";
132
+ return existsSync(join(dir, main));
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+
138
+ function run(cmd: string, args: string[], cwd: string, log: (m: string) => void): void {
139
+ log(`$ ${cmd} ${args.join(" ")} (in ${cwd})`);
140
+ execFileSync(cmd, args, { cwd, stdio: "inherit" });
141
+ }
142
+
143
+ function hasBin(name: string): boolean {
144
+ try {
145
+ execFileSync(name, ["--version"], { stdio: "ignore" });
146
+ return true;
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Ensure one skillRef is present under the cache. Returns absolute path or throws.
154
+ */
155
+ export async function ensureSkillRefCached(
156
+ rawRef: string,
157
+ opts: ResolveSkillRefsOptions = {},
158
+ ): Promise<string> {
159
+ const parsed = parseSkillRef(rawRef);
160
+ if (!parsed) {
161
+ throw new Error(`Invalid skillRef "${rawRef}" (expected owner/skill or owner/skill@ref)`);
162
+ }
163
+ const log = opts.onLog ?? (() => undefined);
164
+ const cacheRoot = skillsCacheDir(opts.cacheDir);
165
+ const dir = cachePathFor(cacheRoot, parsed.owner, parsed.skill, parsed.gitRef);
166
+
167
+ if (existsSync(join(dir, "package.json"))) {
168
+ if (opts.pullIfPresent || process.env.AGENTICROS_SKILLS_CACHE_PULL === "1") {
169
+ try {
170
+ run("git", ["pull", "--ff-only"], dir, log);
171
+ } catch (e) {
172
+ log(`git pull failed (keeping cache): ${e instanceof Error ? e.message : String(e)}`);
173
+ }
174
+ }
175
+ if (!hasBuiltEntry(dir) && !opts.offline) {
176
+ await buildInPlace(dir, log);
177
+ }
178
+ return dir;
179
+ }
180
+
181
+ if (opts.offline) {
182
+ throw new Error(`skillRef ${parsed.marketplaceRef} not in cache (offline)`);
183
+ }
184
+
185
+ const descriptor = await fetchInstallDescriptor(parsed.marketplaceRef, opts.apiBase);
186
+ const gitRef = parsed.gitRef !== "main" ? parsed.gitRef : descriptor.ref || "main";
187
+ const repoUrl = descriptor.githubUrl.endsWith(".git")
188
+ ? descriptor.githubUrl
189
+ : `${descriptor.githubUrl}.git`;
190
+
191
+ mkdirSync(join(cacheRoot, parsed.owner, parsed.skill), { recursive: true });
192
+ if (!existsSync(join(dir, ".git"))) {
193
+ log(`Cloning ${repoUrl} (${gitRef}) → ${dir}`);
194
+ try {
195
+ run(
196
+ "git",
197
+ ["clone", "--branch", gitRef, "--single-branch", "--depth", "1", repoUrl, dir],
198
+ join(cacheRoot, parsed.owner, parsed.skill),
199
+ log,
200
+ );
201
+ } catch {
202
+ // Branch pin may be a tag or default branch name mismatch — full clone + checkout.
203
+ run("git", ["clone", "--depth", "1", repoUrl, dir], join(cacheRoot, parsed.owner, parsed.skill), log);
204
+ try {
205
+ run("git", ["checkout", gitRef], dir, log);
206
+ } catch {
207
+ /* stay on default branch */
208
+ }
209
+ }
210
+ }
211
+
212
+ // Marker for which marketplace ref produced this cache entry
213
+ try {
214
+ writeFileSync(
215
+ join(dir, ".agenticros-skill-ref.json"),
216
+ JSON.stringify(
217
+ {
218
+ marketplaceRef: parsed.marketplaceRef,
219
+ gitRef,
220
+ githubUrl: descriptor.githubUrl,
221
+ cachedAt: new Date().toISOString(),
222
+ },
223
+ null,
224
+ 2,
225
+ ),
226
+ );
227
+ } catch {
228
+ /* ignore */
229
+ }
230
+
231
+ await buildInPlace(dir, log);
232
+ return dir;
233
+ }
234
+
235
+ async function buildInPlace(dir: string, log: (m: string) => void): Promise<void> {
236
+ if (hasBuiltEntry(dir)) return;
237
+ const useNpm = !hasBin("pnpm");
238
+ if (useNpm) {
239
+ run("npm", ["install"], dir, log);
240
+ run("npm", ["run", "build"], dir, log);
241
+ } else {
242
+ run("pnpm", ["install"], dir, log);
243
+ run("pnpm", ["run", "build"], dir, log);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Resolve all config.skillRefs into absolute directories.
249
+ * Does not mutate config — caller merges into skillPaths.
250
+ */
251
+ export async function resolveSkillRefs(
252
+ refs: string[],
253
+ opts: ResolveSkillRefsOptions = {},
254
+ ): Promise<ResolveSkillRefsResult> {
255
+ const paths: string[] = [];
256
+ const errors: ResolveSkillRefsResult["errors"] = [];
257
+ const seen = new Set<string>();
258
+ for (const raw of refs) {
259
+ try {
260
+ const dir = await ensureSkillRefCached(raw, opts);
261
+ if (!seen.has(dir)) {
262
+ seen.add(dir);
263
+ paths.push(dir);
264
+ }
265
+ } catch (e) {
266
+ errors.push({
267
+ ref: raw,
268
+ error: e instanceof Error ? e.message : String(e),
269
+ });
270
+ }
271
+ }
272
+ return { paths, errors };
273
+ }
274
+
275
+ /**
276
+ * Return a config copy with skillRefs resolved into skillPaths (deduped).
277
+ * Sync-friendly path: if offline or empty refs, returns config unchanged
278
+ * except merging any already-cached paths when possible without network.
279
+ */
280
+ export async function withResolvedSkillRefs(
281
+ config: AgenticROSConfig,
282
+ opts: ResolveSkillRefsOptions = {},
283
+ ): Promise<{ config: AgenticROSConfig; errors: ResolveSkillRefsResult["errors"] }> {
284
+ const refs = config.skillRefs ?? [];
285
+ if (refs.length === 0) {
286
+ return { config, errors: [] };
287
+ }
288
+ const { paths, errors } = await resolveSkillRefs(refs, opts);
289
+ const merged = [...(config.skillPaths ?? [])];
290
+ const have = new Set(merged.map((p) => p));
291
+ for (const p of paths) {
292
+ if (!have.has(p)) {
293
+ have.add(p);
294
+ merged.push(p);
295
+ }
296
+ }
297
+ return {
298
+ config: { ...config, skillPaths: merged },
299
+ errors,
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Sync: merge skillRefs that are already present under the skills-cache
305
+ * into skillPaths. No network — use CLI install / ensureSkillRefCached to
306
+ * populate the cache first. Safe for OpenClaw's synchronous register().
307
+ */
308
+ export function applyCachedSkillRefs(config: AgenticROSConfig): AgenticROSConfig {
309
+ const refs = config.skillRefs ?? [];
310
+ if (refs.length === 0) return config;
311
+ const cacheRoot = skillsCacheDir();
312
+ const merged = [...(config.skillPaths ?? [])];
313
+ const have = new Set(merged);
314
+ for (const raw of refs) {
315
+ const parsed = parseSkillRef(raw);
316
+ if (!parsed) continue;
317
+ const dir = cachePathFor(cacheRoot, parsed.owner, parsed.skill, parsed.gitRef);
318
+ if (existsSync(join(dir, "package.json")) && !have.has(dir)) {
319
+ have.add(dir);
320
+ merged.push(dir);
321
+ }
322
+ }
323
+ if (merged.length === (config.skillPaths ?? []).length) return config;
324
+ return { ...config, skillPaths: merged };
325
+ }
326
+
327
+ /** Repo name hint from github URL (for logging). */
328
+ export function githubRepoBasename(githubUrl: string): string {
329
+ return basename(githubUrl.replace(/\.git$/, "").replace(/\/+$/, ""));
330
+ }
@@ -65,6 +65,9 @@ importers:
65
65
 
66
66
  packages/agenticros-cli:
67
67
  dependencies:
68
+ '@agenticros/core':
69
+ specifier: ^0.7.0
70
+ version: 0.7.0
68
71
  '@inquirer/prompts':
69
72
  specifier: ^7.0.0
70
73
  version: 7.10.1(@types/node@20.19.35)
@@ -175,6 +178,10 @@ importers:
175
178
 
176
179
  packages:
177
180
 
181
+ '@agenticros/core@0.7.0':
182
+ resolution: {integrity: sha512-tbcKSf34DVD+pO51goHw1zbu/L12nP3oPvOHoFJK48qaZ4kuejEepljo5xUNB84AtA/QnH9quMPUZbc8rVF8zQ==}
183
+ engines: {node: '>=20'}
184
+
178
185
  '@eclipse-zenoh/zenoh-ts@1.9.0':
179
186
  resolution: {integrity: sha512-qG3VR9Yt81+KjH0WOybyKcNVS0vtBmmhyUJ7dCc5n+8AeAEYTpEvMBTh43Lwu8P+imzMYvFqjaMAO2HFK+x9hA==}
180
187
 
@@ -1974,6 +1981,23 @@ packages:
1974
1981
 
1975
1982
  snapshots:
1976
1983
 
1984
+ '@agenticros/core@0.7.0':
1985
+ dependencies:
1986
+ '@eclipse-zenoh/zenoh-ts': 1.9.0(patch_hash=pdjq6ms3zvqwikn6vpqzasydsy)
1987
+ '@foxglove/cdr': 3.5.0
1988
+ '@foxglove/rosmsg': 4.2.2
1989
+ '@foxglove/rosmsg2-serialization': 3.1.0
1990
+ ws: 8.19.0
1991
+ zod: 3.25.76
1992
+ optionalDependencies:
1993
+ node-datachannel: 0.12.0
1994
+ rclnodejs: 1.8.2
1995
+ transitivePeerDependencies:
1996
+ - bufferutil
1997
+ - jiti
1998
+ - supports-color
1999
+ - utf-8-validate
2000
+
1977
2001
  '@eclipse-zenoh/zenoh-ts@1.9.0(patch_hash=pdjq6ms3zvqwikn6vpqzasydsy)':
1978
2002
  dependencies:
1979
2003
  '@thi.ng/leb128': 3.1.79
@@ -32,7 +32,16 @@
32
32
  */
33
33
 
34
34
  import { execSync, spawnSync } from "node:child_process";
35
- import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
35
+ import {
36
+ existsSync,
37
+ mkdirSync,
38
+ mkdtempSync,
39
+ readdirSync,
40
+ readFileSync,
41
+ rmSync,
42
+ statSync,
43
+ writeFileSync,
44
+ } from "node:fs";
36
45
  import { tmpdir } from "node:os";
37
46
  import { dirname, join, resolve } from "node:path";
38
47
  import { fileURLToPath } from "node:url";
@@ -132,6 +141,31 @@ try {
132
141
  }
133
142
  ok(`Extracted to ${pkgDir}`);
134
143
 
144
+ // 3a. published package.json must not contain pnpm workspace: protocol
145
+ // (npm / npx consumers cannot resolve workspace:* — broke agenticros@0.4.0).
146
+ step("Checking published package.json has no workspace: dependencies...");
147
+ const publishedPkg = JSON.parse(
148
+ readFileSync(join(pkgDir, "package.json"), "utf8"),
149
+ );
150
+ const badDeps = [];
151
+ for (const section of ["dependencies", "optionalDependencies", "peerDependencies"]) {
152
+ const block = publishedPkg[section];
153
+ if (!block || typeof block !== "object") continue;
154
+ for (const [name, range] of Object.entries(block)) {
155
+ if (typeof range === "string" && range.startsWith("workspace:")) {
156
+ badDeps.push(`${section}.${name}=${range}`);
157
+ }
158
+ }
159
+ }
160
+ if (badDeps.length > 0) {
161
+ fail(
162
+ `Published package.json still has workspace: protocol (npm cannot install this):\n ${badDeps.join("\n ")}\n` +
163
+ `Use a semver range against the published @agenticros/core (e.g. "^0.7.0").`,
164
+ );
165
+ process.exit(1);
166
+ }
167
+ ok("No workspace: protocol in published dependencies.");
168
+
135
169
  // 3. critical files present in the SHIPPED tarball
136
170
  step("Verifying critical runtime files are in the tarball...");
137
171
  const runtimeInTar = join(pkgDir, "runtime");
@@ -150,8 +184,7 @@ try {
150
184
 
151
185
  // 5. write inline .npmrc (mirrors init.ts writeInitNpmrcInline)
152
186
  step("Writing inline .npmrc (mirroring writeInitNpmrcInline)...");
153
- const fs = await import("node:fs");
154
- fs.writeFileSync(
187
+ writeFileSync(
155
188
  join(installDir, ".npmrc"),
156
189
  [
157
190
  "shamefully-hoist=false",
@@ -220,9 +253,9 @@ try {
220
253
  step("Simulating refresh over a pre-existing snapshot (regression test for 0.1.7)...");
221
254
  const fakeOld = join(work, "old-install");
222
255
  mkdirSync(fakeOld);
223
- fs.writeFileSync(join(fakeOld, "tsconfig.base.json"), '{"old":"to-be-overwritten"}');
256
+ writeFileSync(join(fakeOld, "tsconfig.base.json"), '{"old":"to-be-overwritten"}');
224
257
  mkdirSync(join(fakeOld, "packages"), { recursive: true });
225
- fs.writeFileSync(
258
+ writeFileSync(
226
259
  join(fakeOld, "packages", "preserved.txt"),
227
260
  "must survive overlay refresh",
228
261
  );
@@ -234,7 +267,7 @@ try {
234
267
  const src = join(runtimeInTar, rel);
235
268
  const dst = join(fakeOld, rel);
236
269
  if (!existsSync(src)) continue;
237
- const srcIsDir = fs.statSync(src).isDirectory();
270
+ const srcIsDir = statSync(src).isDirectory();
238
271
  if (srcIsDir && existsSync(dst)) {
239
272
  run("cp", ["-a", `${src}/.`, `${dst}/`]);
240
273
  } else {
@@ -243,12 +276,12 @@ try {
243
276
  }
244
277
  }
245
278
 
246
- const tsStat = fs.statSync(join(fakeOld, "tsconfig.base.json"));
279
+ const tsStat = statSync(join(fakeOld, "tsconfig.base.json"));
247
280
  if (!tsStat.isFile()) {
248
281
  fail("Refresh turned tsconfig.base.json into a non-file. cp branching is wrong.");
249
282
  process.exit(1);
250
283
  }
251
- const tsContent = fs.readFileSync(join(fakeOld, "tsconfig.base.json"), "utf8");
284
+ const tsContent = readFileSync(join(fakeOld, "tsconfig.base.json"), "utf8");
252
285
  if (tsContent.includes('"old":"to-be-overwritten"')) {
253
286
  fail("Refresh failed to overwrite tsconfig.base.json with the bundle's version.");
254
287
  process.exit(1);