agenticros 0.1.18 → 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/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/sync-skill-tools.mjs +67 -10
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* refresh-skill-deps.mjs
|
|
4
|
+
*
|
|
5
|
+
* Prevents the pnpm-hardlink-cascade we've hit three times running:
|
|
6
|
+
*
|
|
7
|
+
* 1. Add a new file to packages/core/src/ (e.g. capabilities.ts, mission.ts).
|
|
8
|
+
* 2. Build core. The workspace's dist/ has the new file.
|
|
9
|
+
* 3. External skill repos consume @agenticros/core via `file:` deps, so
|
|
10
|
+
* pnpm hardlinks them through a virtual store snapshot
|
|
11
|
+
* (.pnpm/@agenticros+core@file+..+agenticros+packages+core/...).
|
|
12
|
+
* 4. That snapshot is taken once at install time and is NEVER refreshed
|
|
13
|
+
* automatically when the source's dist/ gains new files.
|
|
14
|
+
* 5. The skill repo's @agenticros/core now silently lags behind. Imports
|
|
15
|
+
* that reference the new file fail with ERR_MODULE_NOT_FOUND.
|
|
16
|
+
* 6. `sync-skill-tools.mjs` runs, fails to import the skill, and (prior
|
|
17
|
+
* to the hardening done alongside this script) silently strips the
|
|
18
|
+
* skill's tools from contracts.tools + tools.alsoAllow.
|
|
19
|
+
* 7. The chat agent loses access to follow_robot, find_object, etc. The
|
|
20
|
+
* failure mode is silent until somebody asks "why can't the robot
|
|
21
|
+
* follow me anymore?"
|
|
22
|
+
*
|
|
23
|
+
* This script detects + repairs that cascade. It compares the workspace's
|
|
24
|
+
* packages/core/dist/ file list against each skill repo's resolved
|
|
25
|
+
* @agenticros/core/dist/. If anything is missing, it refreshes the skill
|
|
26
|
+
* repo's node_modules (pnpm install, escalating to a clean reinstall if
|
|
27
|
+
* pnpm decides to no-op).
|
|
28
|
+
*
|
|
29
|
+
* Usage
|
|
30
|
+
* -----
|
|
31
|
+
* node scripts/refresh-skill-deps.mjs # refresh stale skills
|
|
32
|
+
* node scripts/refresh-skill-deps.mjs --dry-run # check + report only
|
|
33
|
+
* node scripts/refresh-skill-deps.mjs --verbose # extra logging
|
|
34
|
+
* node scripts/refresh-skill-deps.mjs --force # always reinstall
|
|
35
|
+
* node scripts/refresh-skill-deps.mjs --skill <path> # extra path (repeatable)
|
|
36
|
+
* node scripts/refresh-skill-deps.mjs --quiet # suppress non-error log
|
|
37
|
+
*
|
|
38
|
+
* Discovery order for skill paths (deduped):
|
|
39
|
+
* 1. --skill <path> CLI flags
|
|
40
|
+
* 2. AGENTICROS_SKILL_PATHS env var (colon-separated)
|
|
41
|
+
* 3. OpenClaw config (OPENCLAW_CONFIG or ~/.openclaw/openclaw.json)
|
|
42
|
+
* under plugins.entries.agenticros.config.skillPaths
|
|
43
|
+
* 4. AgenticROS standalone config (AGENTICROS_CONFIG_PATH or
|
|
44
|
+
* ~/.agenticros/config.json) under skillPaths
|
|
45
|
+
*
|
|
46
|
+
* Exit codes:
|
|
47
|
+
* 0 — nothing was stale, OR every stale repo was refreshed successfully
|
|
48
|
+
* 1 — at least one stale repo could not be refreshed
|
|
49
|
+
* 2 — invalid arguments / unexpected error
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { readFileSync, existsSync, readdirSync, statSync, realpathSync, rmSync } from "node:fs";
|
|
53
|
+
import { join, resolve, dirname, relative, isAbsolute } from "node:path";
|
|
54
|
+
import { homedir } from "node:os";
|
|
55
|
+
import { fileURLToPath } from "node:url";
|
|
56
|
+
import { spawnSync } from "node:child_process";
|
|
57
|
+
|
|
58
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
59
|
+
const __dirname = dirname(__filename);
|
|
60
|
+
const REPO_ROOT = resolve(__dirname, "..");
|
|
61
|
+
const CORE_DIST = join(REPO_ROOT, "packages", "core", "dist");
|
|
62
|
+
|
|
63
|
+
// ---------- CLI parsing ----------
|
|
64
|
+
|
|
65
|
+
function parseArgs(argv) {
|
|
66
|
+
const opts = {
|
|
67
|
+
dryRun: false,
|
|
68
|
+
verbose: false,
|
|
69
|
+
quiet: false,
|
|
70
|
+
force: false,
|
|
71
|
+
extraSkillPaths: [],
|
|
72
|
+
};
|
|
73
|
+
for (let i = 0; i < argv.length; i++) {
|
|
74
|
+
const a = argv[i];
|
|
75
|
+
if (a === "--dry-run" || a === "-n") opts.dryRun = true;
|
|
76
|
+
else if (a === "--verbose" || a === "-v") opts.verbose = true;
|
|
77
|
+
else if (a === "--quiet" || a === "-q") opts.quiet = true;
|
|
78
|
+
else if (a === "--force") opts.force = true;
|
|
79
|
+
else if (a === "--skill") opts.extraSkillPaths.push(argv[++i]);
|
|
80
|
+
else if (a.startsWith("--skill=")) opts.extraSkillPaths.push(a.slice("--skill=".length));
|
|
81
|
+
else if (a === "--help" || a === "-h") {
|
|
82
|
+
printHelp();
|
|
83
|
+
process.exit(0);
|
|
84
|
+
} else if (a.startsWith("-")) {
|
|
85
|
+
console.error(`[refresh-skill-deps] Unknown arg: ${a}`);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return opts;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function printHelp() {
|
|
93
|
+
console.log(`refresh-skill-deps — repair stale pnpm hardlinks in external skill repos.
|
|
94
|
+
|
|
95
|
+
Usage:
|
|
96
|
+
node scripts/refresh-skill-deps.mjs [options]
|
|
97
|
+
|
|
98
|
+
Options:
|
|
99
|
+
--dry-run, -n Report what's stale, don't refresh.
|
|
100
|
+
--verbose, -v Print per-file comparison details.
|
|
101
|
+
--quiet, -q Suppress all output except errors and summary.
|
|
102
|
+
--force Always reinstall, even if not stale.
|
|
103
|
+
--skill <path> Additional skill path to check (repeatable).
|
|
104
|
+
--help, -h Show this help.
|
|
105
|
+
|
|
106
|
+
Exit codes:
|
|
107
|
+
0 — nothing was stale, OR every stale repo was refreshed.
|
|
108
|
+
1 — at least one skill could not be refreshed.
|
|
109
|
+
2 — invalid arguments.
|
|
110
|
+
`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------- Skill discovery ----------
|
|
114
|
+
|
|
115
|
+
function readJsonSafe(path) {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function discoverSkillPaths({ extraSkillPaths = [] } = {}) {
|
|
124
|
+
const out = new Set();
|
|
125
|
+
for (const p of extraSkillPaths) if (p) out.add(resolve(p));
|
|
126
|
+
|
|
127
|
+
if (process.env.AGENTICROS_SKILL_PATHS) {
|
|
128
|
+
for (const p of process.env.AGENTICROS_SKILL_PATHS.split(":")) {
|
|
129
|
+
if (p) out.add(resolve(p));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const openclawConfigPath =
|
|
134
|
+
process.env.OPENCLAW_CONFIG ||
|
|
135
|
+
join(homedir(), ".openclaw", "openclaw.json");
|
|
136
|
+
const openclawConfig = readJsonSafe(openclawConfigPath);
|
|
137
|
+
const pluginCfg =
|
|
138
|
+
openclawConfig?.plugins?.entries?.agenticros?.config ??
|
|
139
|
+
openclawConfig?.plugins?.agenticros?.config ??
|
|
140
|
+
null;
|
|
141
|
+
if (pluginCfg?.skillPaths && Array.isArray(pluginCfg.skillPaths)) {
|
|
142
|
+
for (const p of pluginCfg.skillPaths) if (p) out.add(resolve(p));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const agenticrosConfigPath =
|
|
146
|
+
process.env.AGENTICROS_CONFIG_PATH ||
|
|
147
|
+
join(homedir(), ".agenticros", "config.json");
|
|
148
|
+
const agenticrosConfig = readJsonSafe(agenticrosConfigPath);
|
|
149
|
+
if (agenticrosConfig?.skillPaths && Array.isArray(agenticrosConfig.skillPaths)) {
|
|
150
|
+
for (const p of agenticrosConfig.skillPaths) if (p) out.add(resolve(p));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [...out];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------- Staleness detection ----------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Recursively list relative file paths under a directory. Returns a sorted
|
|
160
|
+
* array. Returns an empty array if the directory doesn't exist.
|
|
161
|
+
*/
|
|
162
|
+
export function listFilesRecursive(dir) {
|
|
163
|
+
const out = [];
|
|
164
|
+
if (!existsSync(dir)) return out;
|
|
165
|
+
const stack = [dir];
|
|
166
|
+
while (stack.length > 0) {
|
|
167
|
+
const cur = stack.pop();
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = readdirSync(cur, { withFileTypes: true });
|
|
171
|
+
} catch {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
const full = join(cur, entry.name);
|
|
176
|
+
if (entry.isDirectory()) stack.push(full);
|
|
177
|
+
else if (entry.isFile()) out.push(relative(dir, full));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
out.sort();
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Compare a source file list (the truth) against a target file list (the
|
|
186
|
+
* skill's hardlinked copy) and return any files present in source but
|
|
187
|
+
* missing in target.
|
|
188
|
+
*
|
|
189
|
+
* Exported so unit tests can pin the comparison logic without touching fs.
|
|
190
|
+
*/
|
|
191
|
+
export function findMissingFiles(sourceFiles, targetFiles) {
|
|
192
|
+
const targetSet = new Set(targetFiles);
|
|
193
|
+
return sourceFiles.filter((f) => !targetSet.has(f));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check one skill repo for staleness against the workspace core's dist/.
|
|
198
|
+
*
|
|
199
|
+
* Returns: { skillPath, hasCoreLink, missingFiles, sourceCount, targetCount }
|
|
200
|
+
* - hasCoreLink: false → skill doesn't depend on @agenticros/core (skip).
|
|
201
|
+
* - missingFiles: non-empty → STALE; needs refresh.
|
|
202
|
+
*/
|
|
203
|
+
export function checkSkillStaleness(skillPath, opts = {}) {
|
|
204
|
+
const sourceCoreDist = opts.sourceCoreDist ?? CORE_DIST;
|
|
205
|
+
const coreLink = join(skillPath, "node_modules", "@agenticros", "core");
|
|
206
|
+
if (!existsSync(coreLink)) {
|
|
207
|
+
return {
|
|
208
|
+
skillPath,
|
|
209
|
+
hasCoreLink: false,
|
|
210
|
+
missingFiles: [],
|
|
211
|
+
sourceCount: 0,
|
|
212
|
+
targetCount: 0,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const skillDist = join(coreLink, "dist");
|
|
216
|
+
const sourceFiles = listFilesRecursive(sourceCoreDist);
|
|
217
|
+
const targetFiles = listFilesRecursive(skillDist);
|
|
218
|
+
const missingFiles = findMissingFiles(sourceFiles, targetFiles);
|
|
219
|
+
return {
|
|
220
|
+
skillPath,
|
|
221
|
+
hasCoreLink: true,
|
|
222
|
+
missingFiles,
|
|
223
|
+
sourceCount: sourceFiles.length,
|
|
224
|
+
targetCount: targetFiles.length,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------- Refresh logic ----------
|
|
229
|
+
|
|
230
|
+
function log(opts, level, ...m) {
|
|
231
|
+
if (level === "error") {
|
|
232
|
+
console.error("[refresh-skill-deps]", ...m);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (opts.quiet) return;
|
|
236
|
+
if (level === "verbose" && !opts.verbose) return;
|
|
237
|
+
console.log("[refresh-skill-deps]", ...m);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function runPnpm(args, cwd, opts) {
|
|
241
|
+
// --ignore-workspace prevents pnpm from picking up the agenticros
|
|
242
|
+
// workspace's pnpm-workspace.yaml when the skill repo sits next to it on
|
|
243
|
+
// disk. Without it pnpm errors with "packages field missing or empty"
|
|
244
|
+
// because it thinks the skill is part of a workspace that doesn't
|
|
245
|
+
// declare it. The skill repos are standalone projects with their own
|
|
246
|
+
// package.json + lockfile, so workspace mode is incorrect for them.
|
|
247
|
+
const finalArgs = ["--ignore-workspace", ...args];
|
|
248
|
+
log(opts, "verbose", `→ pnpm ${finalArgs.join(" ")} (cwd=${cwd})`);
|
|
249
|
+
const res = spawnSync("pnpm", finalArgs, {
|
|
250
|
+
cwd,
|
|
251
|
+
stdio: opts.verbose ? "inherit" : ["ignore", "pipe", "pipe"],
|
|
252
|
+
encoding: "utf8",
|
|
253
|
+
});
|
|
254
|
+
return { code: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function refreshSkill(skillPath, opts) {
|
|
258
|
+
// First try: plain pnpm install. Cheap, no-op when up to date.
|
|
259
|
+
const r1 = runPnpm(["install"], skillPath, opts);
|
|
260
|
+
// ERR_PNPM_IGNORED_BUILDS is just a warning about postinstall scripts;
|
|
261
|
+
// treat any non-zero with that marker as success.
|
|
262
|
+
const isBenignWarn = (out) =>
|
|
263
|
+
typeof out === "string" && out.includes("ERR_PNPM_IGNORED_BUILDS");
|
|
264
|
+
if (r1.code !== 0 && !isBenignWarn(r1.stderr) && !isBenignWarn(r1.stdout)) {
|
|
265
|
+
log(opts, "error", `pnpm install failed in ${skillPath}`);
|
|
266
|
+
if (r1.stderr) log(opts, "error", r1.stderr.trim());
|
|
267
|
+
return { ok: false, escalated: false };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const after = checkSkillStaleness(skillPath);
|
|
271
|
+
if (after.missingFiles.length === 0) {
|
|
272
|
+
log(opts, "info", ` ✓ refreshed via pnpm install`);
|
|
273
|
+
return { ok: true, escalated: false };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// pnpm decided it was already up to date but the hardlinks are still
|
|
277
|
+
// stale. This happens when the .pnpm virtual store hash matches even
|
|
278
|
+
// though dist/ contents changed. Escalate to a clean reinstall.
|
|
279
|
+
log(opts, "info", ` pnpm install no-op'd; escalating to clean reinstall`);
|
|
280
|
+
const nm = join(skillPath, "node_modules");
|
|
281
|
+
if (existsSync(nm)) {
|
|
282
|
+
try {
|
|
283
|
+
rmSync(nm, { recursive: true, force: true });
|
|
284
|
+
} catch (e) {
|
|
285
|
+
log(opts, "error", `failed to rm -rf ${nm}: ${e.message}`);
|
|
286
|
+
return { ok: false, escalated: true };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const r2 = runPnpm(["install"], skillPath, opts);
|
|
290
|
+
if (r2.code !== 0 && !isBenignWarn(r2.stderr) && !isBenignWarn(r2.stdout)) {
|
|
291
|
+
log(opts, "error", `clean pnpm install failed in ${skillPath}`);
|
|
292
|
+
if (r2.stderr) log(opts, "error", r2.stderr.trim());
|
|
293
|
+
return { ok: false, escalated: true };
|
|
294
|
+
}
|
|
295
|
+
const final = checkSkillStaleness(skillPath);
|
|
296
|
+
if (final.missingFiles.length > 0) {
|
|
297
|
+
log(opts, "error", `${skillPath} STILL stale after clean reinstall; missing ${final.missingFiles.length} file(s)`);
|
|
298
|
+
return { ok: false, escalated: true };
|
|
299
|
+
}
|
|
300
|
+
log(opts, "info", ` ✓ refreshed via clean reinstall`);
|
|
301
|
+
return { ok: true, escalated: true };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---------- Main ----------
|
|
305
|
+
|
|
306
|
+
async function main(argv) {
|
|
307
|
+
const opts = parseArgs(argv);
|
|
308
|
+
const skillPaths = discoverSkillPaths({ extraSkillPaths: opts.extraSkillPaths });
|
|
309
|
+
if (skillPaths.length === 0) {
|
|
310
|
+
log(opts, "info", "No skill paths discovered (checked --skill, AGENTICROS_SKILL_PATHS, ~/.openclaw/openclaw.json, ~/.agenticros/config.json).");
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
log(opts, "info", `Checking ${skillPaths.length} skill path(s) against ${CORE_DIST}`);
|
|
315
|
+
if (!existsSync(CORE_DIST)) {
|
|
316
|
+
log(opts, "error", `packages/core/dist/ does not exist. Run \`pnpm --filter @agenticros/core build\` first.`);
|
|
317
|
+
return 2;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const sourceCount = listFilesRecursive(CORE_DIST).length;
|
|
321
|
+
let stale = 0;
|
|
322
|
+
let refreshed = 0;
|
|
323
|
+
let failed = 0;
|
|
324
|
+
let skipped = 0;
|
|
325
|
+
|
|
326
|
+
for (const skillPath of skillPaths) {
|
|
327
|
+
if (!existsSync(skillPath)) {
|
|
328
|
+
log(opts, "info", `· ${skillPath}: directory does not exist; skipping`);
|
|
329
|
+
skipped++;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const status = checkSkillStaleness(skillPath);
|
|
333
|
+
if (!status.hasCoreLink && !opts.force) {
|
|
334
|
+
log(opts, "verbose", `· ${skillPath}: no @agenticros/core dep; skipping`);
|
|
335
|
+
skipped++;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const isStale = status.missingFiles.length > 0;
|
|
339
|
+
if (!isStale && !opts.force) {
|
|
340
|
+
log(opts, "verbose", `· ${skillPath}: up to date (${status.targetCount}/${status.sourceCount} files)`);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
stale++;
|
|
344
|
+
if (isStale) {
|
|
345
|
+
log(
|
|
346
|
+
opts,
|
|
347
|
+
"info",
|
|
348
|
+
`· ${skillPath}: STALE — missing ${status.missingFiles.length} file(s) in @agenticros/core/dist` +
|
|
349
|
+
(opts.verbose ? `: ${status.missingFiles.join(", ")}` : ""),
|
|
350
|
+
);
|
|
351
|
+
} else {
|
|
352
|
+
log(opts, "info", `· ${skillPath}: --force refresh`);
|
|
353
|
+
}
|
|
354
|
+
if (opts.dryRun) continue;
|
|
355
|
+
const result = refreshSkill(skillPath, opts);
|
|
356
|
+
if (result.ok) refreshed++;
|
|
357
|
+
else failed++;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const sum = `Summary: ${skillPaths.length} skill(s) checked — ${stale} stale, ${refreshed} refreshed, ${failed} failed, ${skipped} skipped. Core source has ${sourceCount} file(s).`;
|
|
361
|
+
if (opts.dryRun && stale > 0) {
|
|
362
|
+
log(opts, "info", sum);
|
|
363
|
+
log(opts, "info", "Re-run without --dry-run to refresh.");
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
log(opts, "info", sum);
|
|
367
|
+
return failed > 0 ? 1 : 0;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Run only when invoked directly (preserves importability for tests).
|
|
371
|
+
if (
|
|
372
|
+
import.meta.url === `file://${process.argv[1]}` ||
|
|
373
|
+
process.argv[1]?.endsWith("refresh-skill-deps.mjs")
|
|
374
|
+
) {
|
|
375
|
+
main(process.argv.slice(2))
|
|
376
|
+
.then((code) => process.exit(code))
|
|
377
|
+
.catch((err) => {
|
|
378
|
+
console.error("[refresh-skill-deps] Unexpected error:", err);
|
|
379
|
+
process.exit(2);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
# --robot-namespace N ROS2 namespace for cmd_vel (e.g. robot3946b404c33e4aa39a8d16deb1c5c593)
|
|
30
30
|
# --camera-topic T Camera topic for ros2_camera_snapshot / teleop
|
|
31
31
|
# --skip-build Skip `pnpm build` (assume it's already done)
|
|
32
|
+
# --skip-refresh-skills
|
|
33
|
+
# Skip the skill-hardlink refresh step. By default this
|
|
34
|
+
# script runs scripts/refresh-skill-deps.mjs to keep
|
|
35
|
+
# external skill repos in lockstep with packages/core
|
|
36
|
+
# (prevents the pnpm cascade where new core exports
|
|
37
|
+
# silently disappear from the chat agent's tool list).
|
|
32
38
|
# --no-systemd Skip systemd service tweaks
|
|
33
39
|
# --no-restart Don't restart the gateway at the end
|
|
34
40
|
# -h, --help Show this help
|
|
@@ -43,6 +49,7 @@ ZENOH_ENDPOINT=""
|
|
|
43
49
|
ROBOT_NAMESPACE=""
|
|
44
50
|
CAMERA_TOPIC=""
|
|
45
51
|
SKIP_BUILD=false
|
|
52
|
+
SKIP_REFRESH_SKILLS=false
|
|
46
53
|
NO_SYSTEMD=false
|
|
47
54
|
NO_RESTART=false
|
|
48
55
|
|
|
@@ -58,9 +65,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
58
65
|
--robot-namespace) ROBOT_NAMESPACE="$2"; shift 2 ;;
|
|
59
66
|
--camera-topic) CAMERA_TOPIC="$2"; shift 2 ;;
|
|
60
67
|
--skip-build) SKIP_BUILD=true; shift ;;
|
|
68
|
+
--skip-refresh-skills) SKIP_REFRESH_SKILLS=true; shift ;;
|
|
61
69
|
--no-systemd) NO_SYSTEMD=true; shift ;;
|
|
62
70
|
--no-restart) NO_RESTART=true; shift ;;
|
|
63
|
-
-h|--help) sed -n '2,
|
|
71
|
+
-h|--help) sed -n '2,45p' "$0"; exit 0 ;;
|
|
64
72
|
*) echo "Unknown option: $1"; exit 1 ;;
|
|
65
73
|
esac
|
|
66
74
|
done
|
|
@@ -105,17 +113,36 @@ else
|
|
|
105
113
|
fi
|
|
106
114
|
echo ""
|
|
107
115
|
|
|
116
|
+
# 2b. Refresh external skill repos so they pick up new @agenticros/core
|
|
117
|
+
# exports. Skips itself when no skill paths are configured. See
|
|
118
|
+
# scripts/refresh-skill-deps.mjs for the cascade this prevents.
|
|
119
|
+
if [[ "$SKIP_REFRESH_SKILLS" != true ]]; then
|
|
120
|
+
echo "[2b/5] Refreshing external skill repos (use --skip-refresh-skills to opt out)..."
|
|
121
|
+
if ! node "$REPO_ROOT/scripts/refresh-skill-deps.mjs"; then
|
|
122
|
+
echo "WARN: skill refresh reported failures; continuing with deploy. Re-run \`pnpm refresh:skills\` manually to investigate." >&2
|
|
123
|
+
fi
|
|
124
|
+
echo ""
|
|
125
|
+
fi
|
|
126
|
+
|
|
108
127
|
# 3. Flatten the plugin into a deploy directory.
|
|
109
128
|
# OpenClaw 2026.6+ rejects `node_modules/<dep>` symlinks that resolve outside
|
|
110
129
|
# the plugin install root. `pnpm deploy --prod` materialises every dep inside
|
|
111
130
|
# the deploy directory, so all symlinks are safely contained.
|
|
131
|
+
#
|
|
132
|
+
# --config.strict-peer-dependencies=false: mem0ai has unmet peer ranges for
|
|
133
|
+
# pg / redis / @qdrant/js-client-rest that don't affect us (we only use the
|
|
134
|
+
# in-memory + JSON local backends and ship our own pinned mem0ai). Without
|
|
135
|
+
# this flag pnpm fails the deploy on what is effectively a non-issue.
|
|
112
136
|
echo "[3/5] Building flat plugin deployment at $DEPLOY_DIR ..."
|
|
113
137
|
mkdir -p "$(dirname "$DEPLOY_DIR")"
|
|
114
138
|
rm -rf "$DEPLOY_DIR"
|
|
115
|
-
(cd "$REPO_ROOT" && pnpm --filter ./packages/agenticros deploy --prod "$DEPLOY_DIR")
|
|
116
|
-
# pnpm leaves
|
|
117
|
-
#
|
|
118
|
-
|
|
139
|
+
(cd "$REPO_ROOT" && pnpm --config.strict-peer-dependencies=false --filter ./packages/agenticros deploy --prod "$DEPLOY_DIR")
|
|
140
|
+
# pnpm leaves a self-reference symlink (.pnpm/node_modules/<pkg> → the source
|
|
141
|
+
# path) that OpenClaw's safety scan will reject. The plugin's npm name is
|
|
142
|
+
# `@agenticros/openclaw` (scoped), so the symlink lives under @agenticros/.
|
|
143
|
+
# Older builds used the bare name `agenticros`; clear both to be tolerant.
|
|
144
|
+
rm -rf "$DEPLOY_DIR/node_modules/.pnpm/node_modules/@agenticros/openclaw"
|
|
145
|
+
rm -f "$DEPLOY_DIR/node_modules/.pnpm/node_modules/agenticros"
|
|
119
146
|
|
|
120
147
|
# `pnpm deploy --prod` skips lifecycle scripts, so rclnodejs's postinstall
|
|
121
148
|
# (which runs `node scripts/generate_messages.js` to materialise ROS message
|
|
@@ -56,6 +56,14 @@ const CORE_TOOLS = [
|
|
|
56
56
|
"ros2_param_get",
|
|
57
57
|
"ros2_param_set",
|
|
58
58
|
"ros2_list_topics",
|
|
59
|
+
"ros2_list_capabilities",
|
|
60
|
+
"ros2_list_robots",
|
|
61
|
+
"ros2_discover_robots",
|
|
62
|
+
// Phase 1.e fleet-capability filter — `ros2_find_robots_for(capability, kind, online)`.
|
|
63
|
+
"ros2_find_robots_for",
|
|
64
|
+
"run_mission",
|
|
65
|
+
// Phase 1.f mission control — cancel a running mission by id.
|
|
66
|
+
"mission_cancel",
|
|
59
67
|
"ros2_camera_snapshot",
|
|
60
68
|
"ros2_depth_distance",
|
|
61
69
|
// Memory tools (registered conditionally at runtime when memory.enabled=true).
|
|
@@ -206,14 +214,21 @@ async function discoverSkillTools(entryPath, packageName) {
|
|
|
206
214
|
try {
|
|
207
215
|
mod = await import(url);
|
|
208
216
|
} catch (e) {
|
|
209
|
-
|
|
210
|
-
|
|
217
|
+
const errMsg = e?.message ?? String(e);
|
|
218
|
+
warn(`Failed to import ${packageName} (${entryPath}): ${errMsg}`);
|
|
219
|
+
// Detect the classic pnpm-hardlink-cascade signature: an
|
|
220
|
+
// ERR_MODULE_NOT_FOUND for something inside @agenticros/core/dist/.
|
|
221
|
+
// When we see it, surface it as a structured failure so the caller can
|
|
222
|
+
// preserve the skill's previously-known tools instead of silently
|
|
223
|
+
// stripping them.
|
|
224
|
+
const isCoreCascade = /@agenticros\+?core.*dist\/.+\.js/i.test(errMsg);
|
|
225
|
+
return { tools: [], importFailed: true, cascadeSuspected: isCoreCascade };
|
|
211
226
|
}
|
|
212
227
|
const registerSkill =
|
|
213
228
|
mod.registerSkill ?? mod.default?.registerSkill ?? mod.default;
|
|
214
229
|
if (typeof registerSkill !== "function") {
|
|
215
230
|
warn(`${packageName}: no registerSkill export — skipping discovery`);
|
|
216
|
-
return [];
|
|
231
|
+
return { tools: [], importFailed: false };
|
|
217
232
|
}
|
|
218
233
|
const { tools, api } = createSpyApi();
|
|
219
234
|
const ctx = createStubContext();
|
|
@@ -235,7 +250,7 @@ async function discoverSkillTools(entryPath, packageName) {
|
|
|
235
250
|
`${packageName} registerSkill threw during discovery — captured ${tools.length} tools before error: ${e?.message ?? e}`,
|
|
236
251
|
);
|
|
237
252
|
}
|
|
238
|
-
return tools;
|
|
253
|
+
return { tools, importFailed: false };
|
|
239
254
|
}
|
|
240
255
|
|
|
241
256
|
function uniqueOrdered(...lists) {
|
|
@@ -277,13 +292,18 @@ async function main() {
|
|
|
277
292
|
log(`skillPackages: ${skillPackages.length === 0 ? "(none)" : skillPackages.join(", ")}`);
|
|
278
293
|
|
|
279
294
|
const discovered = new Map(); // skillId/packageName -> tools[]
|
|
295
|
+
const importFailures = []; // [{ name, cascadeSuspected }]
|
|
280
296
|
|
|
281
297
|
for (const pkg of skillPackages) {
|
|
282
298
|
const entry = resolvePackageEntry(pkg);
|
|
283
299
|
if (!entry) continue;
|
|
284
300
|
vlog(`Resolving package "${pkg}" → ${entry}`);
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
301
|
+
const result = await discoverSkillTools(entry, pkg);
|
|
302
|
+
if (result.importFailed) {
|
|
303
|
+
importFailures.push({ name: pkg, cascadeSuspected: result.cascadeSuspected });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (result.tools.length > 0) discovered.set(pkg, result.tools);
|
|
287
307
|
else vlog(` no tools discovered for ${pkg}`);
|
|
288
308
|
}
|
|
289
309
|
|
|
@@ -292,8 +312,12 @@ async function main() {
|
|
|
292
312
|
const resolved = findSkillEntryFromPath(absDir);
|
|
293
313
|
if (!resolved) continue;
|
|
294
314
|
vlog(`Resolving path "${absDir}" → ${resolved.entry} (${resolved.packageName})`);
|
|
295
|
-
const
|
|
296
|
-
if (
|
|
315
|
+
const result = await discoverSkillTools(resolved.entry, resolved.packageName);
|
|
316
|
+
if (result.importFailed) {
|
|
317
|
+
importFailures.push({ name: resolved.packageName, cascadeSuspected: result.cascadeSuspected });
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (result.tools.length > 0) discovered.set(resolved.packageName, result.tools);
|
|
297
321
|
else vlog(` no tools discovered for ${resolved.packageName}`);
|
|
298
322
|
}
|
|
299
323
|
|
|
@@ -305,12 +329,45 @@ async function main() {
|
|
|
305
329
|
}
|
|
306
330
|
}
|
|
307
331
|
|
|
308
|
-
const skillTools = [...discovered.values()].flat();
|
|
309
|
-
const merged = uniqueOrdered(CORE_TOOLS, skillTools);
|
|
310
332
|
const previous = Array.isArray(manifest?.contracts?.tools)
|
|
311
333
|
? manifest.contracts.tools.slice()
|
|
312
334
|
: [];
|
|
313
335
|
|
|
336
|
+
// If any skill failed to import, preserve its presumed entries from the
|
|
337
|
+
// previous manifest instead of silently stripping them. We can't tell
|
|
338
|
+
// exactly which tools belonged to that specific skill, but we know any
|
|
339
|
+
// tool in `previous` that's not in CORE_TOOLS and not in the freshly
|
|
340
|
+
// discovered set must have come from one of these skills — and dropping
|
|
341
|
+
// them silently was exactly the regression we hit on 2026-06-10.
|
|
342
|
+
const coreSet = new Set(CORE_TOOLS);
|
|
343
|
+
const freshSkillTools = [...discovered.values()].flat();
|
|
344
|
+
const freshSet = new Set(freshSkillTools);
|
|
345
|
+
let preservedTools = [];
|
|
346
|
+
if (importFailures.length > 0) {
|
|
347
|
+
preservedTools = previous.filter(
|
|
348
|
+
(t) => typeof t === "string" && !coreSet.has(t) && !freshSet.has(t),
|
|
349
|
+
);
|
|
350
|
+
const failedNames = importFailures.map((f) => f.name).join(", ");
|
|
351
|
+
warn(`Some skill(s) failed to import: ${failedNames}`);
|
|
352
|
+
if (preservedTools.length > 0) {
|
|
353
|
+
warn(
|
|
354
|
+
`Preserving ${preservedTools.length} previously-known tool(s) from contracts.tools so the chat agent does not silently lose them: ${preservedTools.join(", ")}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
const cascade = importFailures.some((f) => f.cascadeSuspected);
|
|
358
|
+
if (cascade) {
|
|
359
|
+
warn(
|
|
360
|
+
"Cascade signature detected (ERR_MODULE_NOT_FOUND inside @agenticros/core/dist). " +
|
|
361
|
+
"Run `pnpm refresh:skills` to repair the stale pnpm hardlinks, then re-run this sync.",
|
|
362
|
+
);
|
|
363
|
+
} else {
|
|
364
|
+
warn("Re-run this sync after fixing the import error to refresh the manifest.");
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const skillTools = [...freshSkillTools, ...preservedTools];
|
|
369
|
+
const merged = uniqueOrdered(CORE_TOOLS, skillTools);
|
|
370
|
+
|
|
314
371
|
if (arraysEqual(previous, merged)) {
|
|
315
372
|
log("Manifest is already up to date.");
|
|
316
373
|
return;
|