agentradar-cli 0.2.6 → 0.2.8
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/index.js +4 -4
- package/dist/setup.js +7 -12
- package/dist/skill-install.js +3 -97
- package/dist/tool-run.js +46 -5
- package/dist/vendor-runner/feeder.mjs +1 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export const runArgs = (raw) => raw[0] !== "run" ? raw
|
|
|
15
15
|
async function main() {
|
|
16
16
|
const args = runArgs(process.argv.slice(2));
|
|
17
17
|
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
18
|
-
console.log("Usage: agentradar run <
|
|
18
|
+
console.log("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] [--harness claude|codex] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat <runner-id> --args '<json>' | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
if (args[0] === "leave" && args.length === 2) {
|
|
@@ -65,7 +65,7 @@ async function main() {
|
|
|
65
65
|
urls.push(args[i]);
|
|
66
66
|
}
|
|
67
67
|
if (urls.length !== 1 || [urls[0], ...Object.values(named)].some((value) => value.startsWith("-"))) {
|
|
68
|
-
console.error("Usage: agentradar join <
|
|
68
|
+
console.error("Usage: agentradar join <join-link> [--name <your name>] [--model <your model id>] [--trust]");
|
|
69
69
|
process.exitCode = 1;
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
@@ -89,7 +89,7 @@ async function main() {
|
|
|
89
89
|
const call = args[0] === "call";
|
|
90
90
|
const parsed = parseCallArgs(args.slice(call ? 1 : 2));
|
|
91
91
|
if (!parsed || (!call && parsed.flags.length)) {
|
|
92
|
-
console.error(call ? "Usage: agentradar
|
|
92
|
+
console.error(call ? "Usage: agentradar run <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar run <runner-id> [<tool>] --help" : "Usage: agentradar tool run <runner-id> <tool-key> [--args '<json>']");
|
|
93
93
|
process.exitCode = 1;
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
@@ -129,7 +129,7 @@ async function main() {
|
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
const help = args.length === 1 && (args[0] === "--help" || args[0] === "-h");
|
|
132
|
-
(help ? console.log : console.error)("Usage: agentradar run <
|
|
132
|
+
(help ? console.log : console.error)("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat <runner-id> --args '<json>' | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
|
|
133
133
|
if (!help)
|
|
134
134
|
process.exitCode = 1;
|
|
135
135
|
}
|
package/dist/setup.js
CHANGED
|
@@ -7,7 +7,6 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import * as tls from "node:tls";
|
|
9
9
|
import { promisify } from "node:util";
|
|
10
|
-
import { syncJoinedSkills } from "./skill-install.js";
|
|
11
10
|
const DEFAULT_API_URL = "https://vendor.agentradar.com/api";
|
|
12
11
|
/** The service's CLI build (the CLI itself and the agent-guard plugin): `installCli` keeps it current, never the npx cache. */
|
|
13
12
|
const cliDist = (home) => join(home, ".agentradar", "cli", "current", "dist");
|
|
@@ -556,7 +555,7 @@ async function installAgentService(runnerId, home, env) {
|
|
|
556
555
|
});
|
|
557
556
|
}
|
|
558
557
|
/**
|
|
559
|
-
* Mode A — the organisation's own agent, on its own compute, joining through
|
|
558
|
+
* Mode A — the organisation's own agent, on its own compute, joining through a join link.
|
|
560
559
|
*
|
|
561
560
|
* accept → wait for a human to approve → claim the key once → install the runner and start it as a service
|
|
562
561
|
* (LaunchAgent / systemd user unit) that owns the agent's persistent session. The one-time claim secret is
|
|
@@ -605,10 +604,6 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
605
604
|
const runnerEnv = await installRunner(home, projectPath, { status: "approved", agentId, runnerId, installation, harness }, { sessionId, env });
|
|
606
605
|
await saved?.();
|
|
607
606
|
log(`Runner installed → ${runnerEnv}`);
|
|
608
|
-
// Best effort: a failed first sync never stops the join; the service's feeder syncs them again.
|
|
609
|
-
await syncJoinedSkills(runnerId, { home, fetch: fetchImpl, log }).catch((error) => {
|
|
610
|
-
log(`Your AgentRadar skills are not installed yet (${String(error?.message ?? error).split("\n")[0]}); the runner installs them once it is running.`);
|
|
611
|
-
});
|
|
612
607
|
try {
|
|
613
608
|
await (deps.installService ?? ((id) => installAgentService(id, home, env)))(runnerId);
|
|
614
609
|
}
|
|
@@ -625,11 +620,11 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
625
620
|
link = new URL(enrolmentUrl);
|
|
626
621
|
}
|
|
627
622
|
catch {
|
|
628
|
-
throw new Error("Pass the
|
|
623
|
+
throw new Error("Pass the join link your organisation gave you, e.g. https://…/invite/<token>");
|
|
629
624
|
}
|
|
630
625
|
const token = /^\/(?:api\/invites|invite)\/([^/]+?)(?:\.md)?(?:\/|$)/.exec(link.pathname)?.[1];
|
|
631
626
|
if (!token || !trustedMcpEndpoint(link.origin)) {
|
|
632
|
-
throw new Error("That is not an AgentRadar
|
|
627
|
+
throw new Error("That is not an AgentRadar join link (expected https://…/invite/<token>)");
|
|
633
628
|
}
|
|
634
629
|
const api = (path) => new URL(`/api/${path}`, link.origin);
|
|
635
630
|
const mcpEndpoint = env.AWP_MCP_URL || api("mcp").toString();
|
|
@@ -645,12 +640,12 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
645
640
|
throw error;
|
|
646
641
|
}
|
|
647
642
|
if (state?.agentId) {
|
|
648
|
-
log("This
|
|
643
|
+
log("This machine already joined with this join link.");
|
|
649
644
|
const saved = await readFile(join(home, ".agentradar", "agents", `join-${state.requestId}.env`), "utf8").catch(() => "");
|
|
650
645
|
const { envValue } = await import("./tool-run.js");
|
|
651
646
|
const bearer = envValue(saved, "AWP_BEARER");
|
|
652
647
|
if (!bearer)
|
|
653
|
-
throw new Error("This machine left that organisation (agentradar leave). A key is issued once; ask your organisation for a new
|
|
648
|
+
throw new Error("This machine left that organisation (agentradar leave). A key is issued once; ask your organisation for a new join link.");
|
|
654
649
|
return startJoined(state.requestId, state.agentId, { mcpEndpoint: envValue(saved, "AWP_MCP_URL") ?? mcpEndpoint, bearer });
|
|
655
650
|
}
|
|
656
651
|
if (!state) {
|
|
@@ -712,10 +707,10 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
|
|
|
712
707
|
continue;
|
|
713
708
|
}
|
|
714
709
|
if (response.status === 409 && said.includes("rejected")) {
|
|
715
|
-
throw new Error("Your organisation declined this join request. Ask it for a new
|
|
710
|
+
throw new Error("Your organisation declined this join request. Ask it for a new join link.");
|
|
716
711
|
}
|
|
717
712
|
if (response.status === 409 && /already (?:used|claimed)/.test(said)) {
|
|
718
|
-
throw new Error("This
|
|
713
|
+
throw new Error("This join link's key was already claimed. A key is issued once and cannot be claimed again; ask your organisation for a new join link.");
|
|
719
714
|
}
|
|
720
715
|
throw new Error(said);
|
|
721
716
|
}
|
package/dist/skill-install.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { chmod, lstat, mkdir, readFile,
|
|
1
|
+
import { chmod, lstat, mkdir, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import { awpRequest, envValue } from "./tool-run.js";
|
|
5
|
-
const MANAGED_MARK = ".agentradar-managed";
|
|
6
|
-
const SKILL_NAME = /^[a-z0-9][a-z0-9-]{0,99}$/;
|
|
3
|
+
import { join } from "node:path";
|
|
7
4
|
async function ensureDirectory(path) {
|
|
8
5
|
try {
|
|
9
6
|
const info = await lstat(path);
|
|
@@ -41,98 +38,7 @@ export async function installVendorSkill(deps = {}) {
|
|
|
41
38
|
});
|
|
42
39
|
}
|
|
43
40
|
// The runner keeps this skill current from AgentRadar; the marker says the folder is AgentRadar's to replace.
|
|
44
|
-
await writeFile(join(skillDir,
|
|
41
|
+
await writeFile(join(skillDir, ".agentradar-managed"), "Written by AgentRadar; replaced on every sync.\n");
|
|
45
42
|
(deps.log ?? console.log)("AgentRadar vendor skill installed for this project.");
|
|
46
43
|
}
|
|
47
|
-
const isManaged = async (dir) => (await lstat(dir).then((info) => info.isDirectory(), () => false)) && (await lstat(join(dir, MANAGED_MARK)).then(() => true, () => false));
|
|
48
|
-
/**
|
|
49
|
-
* The feeder's `writeManagedSkills` (templates/vendor-runner/feeder.mjs), for the CLI: the server's answer is
|
|
50
|
-
* untrusted input to this box, so names and paths are checked, each skill is staged then renamed into place,
|
|
51
|
-
* a folder without the marker (the vendor's own) is never touched, and a managed skill no longer listed goes.
|
|
52
|
-
*/
|
|
53
|
-
const sameSkill = async (dir, files) => {
|
|
54
|
-
try {
|
|
55
|
-
const have = [];
|
|
56
|
-
for (const p of await readdir(dir, { recursive: true }))
|
|
57
|
-
if (p !== MANAGED_MARK && (await stat(join(dir, p))).isFile())
|
|
58
|
-
have.push(p);
|
|
59
|
-
for (const f of files)
|
|
60
|
-
if (await readFile(join(dir, f.path), "utf8") !== f.content)
|
|
61
|
-
return false;
|
|
62
|
-
return have.length === files.length;
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
export async function writeManagedSkills(skillsDir, skills) {
|
|
69
|
-
await mkdir(skillsDir, { recursive: true });
|
|
70
|
-
const written = new Set();
|
|
71
|
-
for (const skill of skills) {
|
|
72
|
-
const name = String(skill?.name ?? "");
|
|
73
|
-
const target = join(skillsDir, name);
|
|
74
|
-
if (!SKILL_NAME.test(name))
|
|
75
|
-
continue;
|
|
76
|
-
if (await lstat(target).then(() => true, () => false) && !(await isManaged(target)))
|
|
77
|
-
continue;
|
|
78
|
-
const files = (Array.isArray(skill.files) ? skill.files : []);
|
|
79
|
-
if (files.some((f) => typeof f?.path !== "string" || typeof f?.content !== "string" || isAbsolute(f.path)
|
|
80
|
-
|| normalize(f.path).split(sep).includes("..") || [".", MANAGED_MARK].includes(normalize(f.path))))
|
|
81
|
-
continue;
|
|
82
|
-
// An unchanged skill is not rewritten: the agent's CLI may be reading it.
|
|
83
|
-
if (await sameSkill(target, files)) {
|
|
84
|
-
written.add(name);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
const staging = join(skillsDir, `.${name}.sync-${process.pid}`);
|
|
88
|
-
const old = join(skillsDir, `.${name}.old-${process.pid}`);
|
|
89
|
-
try {
|
|
90
|
-
await rm(staging, { recursive: true, force: true });
|
|
91
|
-
for (const f of files) {
|
|
92
|
-
await mkdir(dirname(join(staging, f.path)), { recursive: true });
|
|
93
|
-
await writeFile(join(staging, f.path), f.content);
|
|
94
|
-
}
|
|
95
|
-
await writeFile(join(staging, MANAGED_MARK), "Written by the AgentRadar runner; replaced on every sync.\n");
|
|
96
|
-
// Rename aside, then into place: the skill is missing for two renames, not for a recursive delete.
|
|
97
|
-
await rm(old, { recursive: true, force: true });
|
|
98
|
-
await rename(target, old).catch((err) => { if (err?.code !== "ENOENT")
|
|
99
|
-
throw err; });
|
|
100
|
-
await rename(staging, target);
|
|
101
|
-
await rm(old, { recursive: true, force: true });
|
|
102
|
-
written.add(name);
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
await rm(staging, { recursive: true, force: true });
|
|
106
|
-
if (!(await lstat(target).then(() => true, () => false)))
|
|
107
|
-
await rename(old, target).catch(() => { });
|
|
108
|
-
if (await isManaged(target))
|
|
109
|
-
written.add(name);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
for (const entry of await readdir(skillsDir)) {
|
|
113
|
-
if (!written.has(entry) && await isManaged(join(skillsDir, entry)))
|
|
114
|
-
await rm(join(skillsDir, entry), { recursive: true, force: true });
|
|
115
|
-
}
|
|
116
|
-
return [...written];
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* R650 — a joined agent's AgentRadar skills (`my_skills`: the bundled set plus the ones linked to it), written
|
|
120
|
-
* at join into the directory its CLI reads, the feeder's `skillsDirFor`: Claude `$AR_BASE/.claude/skills`,
|
|
121
|
-
* Codex `$CODEX_HOME/skills`. Done here because a session that acts only through `agentradar run` has no
|
|
122
|
-
* feeder; where the service runs, its feeder keeps them current.
|
|
123
|
-
*/
|
|
124
|
-
export async function syncJoinedSkills(runnerId, deps) {
|
|
125
|
-
const env = await readFile(join(deps.home, ".agentradar", "agents", `${runnerId}.env`), "utf8");
|
|
126
|
-
const root = envValue(env, "AR_AGENT_HARNESS") === "codex" ? envValue(env, "CODEX_HOME") : envValue(env, "AR_BASE");
|
|
127
|
-
if (!root)
|
|
128
|
-
return;
|
|
129
|
-
const skillsDir = envValue(env, "AR_AGENT_HARNESS") === "codex" ? join(root, "skills") : join(root, ".claude", "skills");
|
|
130
|
-
const body = await awpRequest(runnerId, "tools/call", { name: "my_skills", arguments: {} }, { home: deps.home, fetch: deps.fetch, logError: deps.log });
|
|
131
|
-
const text = body?.result?.content?.find((c) => c.type === "text")?.text;
|
|
132
|
-
const answer = (text ? JSON.parse(text) : {});
|
|
133
|
-
if (!answer.ok || !Array.isArray(answer.skills))
|
|
134
|
-
throw new Error("AgentRadar did not return your skills");
|
|
135
|
-
const written = await writeManagedSkills(skillsDir, answer.skills);
|
|
136
|
-
deps.log(`Installed ${written.length} AgentRadar skill(s) in ${skillsDir}.`);
|
|
137
|
-
}
|
|
138
44
|
//# sourceMappingURL=skill-install.js.map
|
package/dist/tool-run.js
CHANGED
|
@@ -21,6 +21,8 @@ export async function runServiceToolOverCli(runnerId, toolKey, argumentsJson, de
|
|
|
21
21
|
}
|
|
22
22
|
/** `agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>']`: any AWP tool from a shell, so a just-joined agent works before its harness reloads MCP tools. */
|
|
23
23
|
export async function callToolOverCli(runnerId, tool, argumentsJson, deps = {}, flags = []) {
|
|
24
|
+
if (flags.some(([flag]) => flag === "help"))
|
|
25
|
+
return await describeTools(runnerId, tool, deps);
|
|
24
26
|
const parameters = argumentsJson === undefined ? {} : parseArgumentsObject(argumentsJson, deps.logError ?? console.error);
|
|
25
27
|
if (!parameters)
|
|
26
28
|
return 1;
|
|
@@ -65,6 +67,10 @@ export function parseCallArgs(rest) {
|
|
|
65
67
|
positional.push(arg);
|
|
66
68
|
continue;
|
|
67
69
|
}
|
|
70
|
+
if (arg === "--help") {
|
|
71
|
+
flags.push(["help", ""]);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
68
74
|
const value = rest[++i];
|
|
69
75
|
if (value === undefined)
|
|
70
76
|
return null;
|
|
@@ -73,7 +79,37 @@ export function parseCallArgs(rest) {
|
|
|
73
79
|
else
|
|
74
80
|
flags.push([arg.slice(2), value]);
|
|
75
81
|
}
|
|
76
|
-
|
|
82
|
+
const help = flags.some(([flag]) => flag === "help");
|
|
83
|
+
return positional.length === 2 || (help && positional.length === 1)
|
|
84
|
+
? { runnerId: positional[0], tool: positional[1] ?? "", json, flags } : null;
|
|
85
|
+
}
|
|
86
|
+
async function listedTools(runnerId, deps) {
|
|
87
|
+
const listed = await awpRequest(runnerId, "tools/list", {}, deps);
|
|
88
|
+
return listed ? listed.result?.tools ?? [] : null;
|
|
89
|
+
}
|
|
90
|
+
// An agent that guessed a tool name from memory needs the real list, or it goes looking with commands
|
|
91
|
+
// its harness has not allowed and the session stalls on a permission prompt.
|
|
92
|
+
const unknownTool = (runnerId, tool, tools) => `${tool} is not a tool on this server. Its tools: ${tools.map((entry) => entry.name).join(" ")}. `
|
|
93
|
+
+ `\`agentradar run ${runnerId} <tool> --help\` shows a tool's arguments.`;
|
|
94
|
+
/** `agentradar run <runner-id> [<tool>] --help`: the server's own tool list or one tool's arguments. */
|
|
95
|
+
async function describeTools(runnerId, tool, deps) {
|
|
96
|
+
const tools = await listedTools(runnerId, deps);
|
|
97
|
+
if (!tools)
|
|
98
|
+
return 1;
|
|
99
|
+
const log = deps.log ?? console.log;
|
|
100
|
+
if (!tool) {
|
|
101
|
+
log(`Tools: ${tools.map((entry) => entry.name).join(" ")}. \`agentradar run ${runnerId} <tool> --help\` shows a tool's arguments.`);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
const entry = tools.find((candidate) => candidate.name === tool);
|
|
105
|
+
if (!entry) {
|
|
106
|
+
(deps.logError ?? console.error)(unknownTool(runnerId, tool, tools));
|
|
107
|
+
return 1;
|
|
108
|
+
}
|
|
109
|
+
const required = entry.inputSchema?.required ?? [];
|
|
110
|
+
const names = Object.keys(entry.inputSchema?.properties ?? {}).map((name) => `--${name}${required.includes(name) ? " (required)" : ""}`);
|
|
111
|
+
log(`${tool}: ${entry.description ?? ""}\nArguments: ${names.join(" ") || "none"}`);
|
|
112
|
+
return 0;
|
|
77
113
|
}
|
|
78
114
|
/**
|
|
79
115
|
* `--text "hi" --conversation-id X` as the tool's own arguments, typed by the inputSchema the server
|
|
@@ -81,10 +117,15 @@ export function parseCallArgs(rest) {
|
|
|
81
117
|
*/
|
|
82
118
|
async function flagArguments(runnerId, tool, flags, deps) {
|
|
83
119
|
const logError = deps.logError ?? console.error;
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
120
|
+
const tools = await listedTools(runnerId, deps);
|
|
121
|
+
if (!tools)
|
|
122
|
+
return null;
|
|
123
|
+
const entry = tools.find((candidate) => candidate.name === tool);
|
|
124
|
+
if (!entry) {
|
|
125
|
+
logError(unknownTool(runnerId, tool, tools));
|
|
86
126
|
return null;
|
|
87
|
-
|
|
127
|
+
}
|
|
128
|
+
const schema = entry.inputSchema?.properties ?? {};
|
|
88
129
|
const out = {};
|
|
89
130
|
const camel = (flag) => flag.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
90
131
|
for (let [flag, value] of flags) {
|
|
@@ -203,7 +244,7 @@ export async function callAwpTool(runnerId, name, toolArguments, deps) {
|
|
|
203
244
|
}
|
|
204
245
|
const RUNNER_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
205
246
|
/** One JSON-RPC request on the runner's AWP MCP endpoint with the runner's own key; null after logging why it failed. */
|
|
206
|
-
|
|
247
|
+
async function awpRequest(runnerId, method, params, deps) {
|
|
207
248
|
const logError = deps.logError ?? console.error;
|
|
208
249
|
if (!RUNNER_ID.test(runnerId)) {
|
|
209
250
|
logError("Invalid runner ID");
|
|
@@ -663,14 +663,6 @@ const isManaged = (dir) => {
|
|
|
663
663
|
const st = fs.lstatSync(dir, { throwIfNoEntry: false });
|
|
664
664
|
return Boolean(st?.isDirectory() && fs.existsSync(join(dir, MANAGED_MARK)));
|
|
665
665
|
};
|
|
666
|
-
const sameSkill = (dir, files) => {
|
|
667
|
-
try {
|
|
668
|
-
const have = fs.readdirSync(dir, { recursive: true }).filter((p) => p !== MANAGED_MARK && fs.statSync(join(dir, p)).isFile());
|
|
669
|
-
return have.length === files.length && files.every((f) => fs.readFileSync(join(dir, f.path), "utf8") === f.content);
|
|
670
|
-
} catch {
|
|
671
|
-
return false;
|
|
672
|
-
}
|
|
673
|
-
};
|
|
674
666
|
|
|
675
667
|
export function writeManagedSkills(skillsDir, skills) {
|
|
676
668
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
@@ -687,11 +679,7 @@ export function writeManagedSkills(skillsDir, skills) {
|
|
|
687
679
|
const bad = files.find((f) => typeof f?.path !== "string" || typeof f?.content !== "string" || isAbsolute(f.path)
|
|
688
680
|
|| normalize(f.path).split(sep).includes("..") || [".", MANAGED_MARK].includes(normalize(f.path)));
|
|
689
681
|
if (bad) { console.warn(`[feeder] skill sync: ${name} has an unsafe file path, skipped`); continue; }
|
|
690
|
-
// An unchanged skill is not rewritten: the agent's CLI may be reading it (Codex skipped a SKILL.md that a
|
|
691
|
-
// periodic resync had just removed).
|
|
692
|
-
if (sameSkill(target, files)) { written.add(name); continue; }
|
|
693
682
|
const staging = join(skillsDir, `.${name}.sync-${process.pid}`);
|
|
694
|
-
const old = join(skillsDir, `.${name}.old-${process.pid}`);
|
|
695
683
|
try {
|
|
696
684
|
fs.rmSync(staging, { recursive: true, force: true });
|
|
697
685
|
for (const f of files) {
|
|
@@ -699,16 +687,12 @@ export function writeManagedSkills(skillsDir, skills) {
|
|
|
699
687
|
fs.writeFileSync(join(staging, f.path), f.content);
|
|
700
688
|
}
|
|
701
689
|
fs.writeFileSync(join(staging, MANAGED_MARK), "Written by the AgentRadar runner; replaced on every sync.\n");
|
|
702
|
-
|
|
703
|
-
fs.rmSync(old, { recursive: true, force: true });
|
|
704
|
-
if (fs.existsSync(target)) fs.renameSync(target, old);
|
|
690
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
705
691
|
fs.renameSync(staging, target);
|
|
706
|
-
fs.rmSync(old, { recursive: true, force: true });
|
|
707
692
|
written.add(name);
|
|
708
693
|
} catch (err) {
|
|
709
694
|
// One skill that cannot be written keeps its previous copy and does not stop the rest of the sync.
|
|
710
695
|
fs.rmSync(staging, { recursive: true, force: true });
|
|
711
|
-
if (!fs.existsSync(target) && fs.existsSync(old)) fs.renameSync(old, target);
|
|
712
696
|
if (isManaged(target)) written.add(name);
|
|
713
697
|
console.warn(`[feeder] skill sync: ${name} not written: ${err instanceof Error ? err.message : String(err)}`);
|
|
714
698
|
}
|