@paradigma-inc/flywheel 0.1.4 → 0.1.9
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/README.md +129 -24
- package/package.json +10 -8
- package/skills/flywheel/SKILL.md +52 -0
- package/skills/flywheel/agents/openai.yaml +15 -0
- package/skills/flywheel/campaigns/participating-in-a-campaign.md +18 -0
- package/skills/flywheel/compute/credits-and-billing.md +11 -0
- package/skills/flywheel/compute/managed-compute.md +44 -0
- package/skills/flywheel/example-workflows/organizing-exploring-and-iterating-on-a-research-topic.md +255 -0
- package/skills/flywheel/example-workflows/reproducing-papers-on-a-budget.md +151 -0
- package/skills/flywheel/getting-started/account-access.md +8 -0
- package/skills/flywheel/getting-started/flywheel-quickstart.md +23 -0
- package/skills/flywheel/getting-started/flywheel-tutorial-overview.md +30 -0
- package/skills/flywheel/reference/experiment-design-protocol.md +200 -0
- package/skills/flywheel/reference/flywheel-mcp-tool-map.md +160 -0
- package/skills/flywheel/setting-up-flywheel/claude-code-cli-installation.md +16 -0
- package/skills/flywheel/setting-up-flywheel/codex-cli-installation.md +16 -0
- package/skills/flywheel/setting-up-flywheel/how-can-i-get-an-authorized-client_id-for-the-oauth-flow.md +50 -0
- package/skills/flywheel/setting-up-flywheel/installation-overview.md +26 -0
- package/skills/flywheel/setting-up-flywheel/other-hosts-installation.md +40 -0
- package/skills/flywheel/setting-up-flywheel/updating-flywheel-mcp.md +21 -0
- package/skills/flywheel/usage-and-workflows/using-local-hardware-with-flywheel.md +57 -0
- package/skills/flywheel/usage-and-workflows/what-to-do-with-flywheel.md +34 -0
- package/skills/flywheel/web-ui/flywheel-webui-map.md +28 -0
- package/skills/flywheel/web-ui/the-flywheel-web-ui.md +17 -0
- package/src/cli.mjs +508 -54
- package/src/mcp-writer.mjs +128 -3
- package/src/setup-auth.mjs +231 -27
- package/src/skill-installer.mjs +542 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
access,
|
|
5
|
+
cp,
|
|
6
|
+
lstat,
|
|
7
|
+
mkdir,
|
|
8
|
+
mkdtemp,
|
|
9
|
+
readFile,
|
|
10
|
+
readlink,
|
|
11
|
+
readdir,
|
|
12
|
+
rm,
|
|
13
|
+
symlink,
|
|
14
|
+
writeFile,
|
|
15
|
+
} from "node:fs/promises";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { promisify } from "node:util";
|
|
21
|
+
import { dump as dumpYaml, load as loadYaml } from "js-yaml";
|
|
22
|
+
|
|
23
|
+
const execFile = promisify(execFileCb);
|
|
24
|
+
const requireFromInstaller = createRequire(import.meta.url);
|
|
25
|
+
const DEFAULT_SKILL_MCP_SERVER_NAME = "flywheel";
|
|
26
|
+
const DEFAULT_SKILL_MCP_SERVER_URL = "https://flywheel.paradigma.inc/mcp-server";
|
|
27
|
+
const OPENAI_AGENT_METADATA_RELATIVE_PATH = path.join("agents", "openai.yaml");
|
|
28
|
+
|
|
29
|
+
export const INSTALL_SKILLS_AGENT_BY_HOST = Object.freeze({
|
|
30
|
+
claude: "claude-code",
|
|
31
|
+
codex: "codex",
|
|
32
|
+
opencode: "opencode",
|
|
33
|
+
cursor: "cursor",
|
|
34
|
+
openclaw: "openclaw",
|
|
35
|
+
"pi-mono": "pi",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const SUPPORTED_INSTALL_SKILL_HOSTS = Object.freeze([
|
|
39
|
+
"codex",
|
|
40
|
+
"claude",
|
|
41
|
+
"opencode",
|
|
42
|
+
"cursor",
|
|
43
|
+
"openclaw",
|
|
44
|
+
"pi-mono",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export function resolvePackageRoot(moduleUrl = import.meta.url) {
|
|
48
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
49
|
+
const moduleDir = path.dirname(modulePath);
|
|
50
|
+
return path.resolve(moduleDir, "..");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function listBundledSkills(packageRoot) {
|
|
54
|
+
let skillEntries;
|
|
55
|
+
try {
|
|
56
|
+
skillEntries = await readdir(path.join(packageRoot, "skills"), {
|
|
57
|
+
withFileTypes: true,
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
const bundledSkillNames = [];
|
|
66
|
+
|
|
67
|
+
for (const entry of skillEntries) {
|
|
68
|
+
if (!entry.isDirectory()) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const skillPath = path.join(packageRoot, "skills", entry.name, "SKILL.md");
|
|
73
|
+
if (await pathExists(skillPath)) {
|
|
74
|
+
bundledSkillNames.push(entry.name);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return bundledSkillNames.sort();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeBundledSkillName(skillName) {
|
|
82
|
+
const normalizedSkillName = String(skillName ?? "").trim();
|
|
83
|
+
if (normalizedSkillName.length === 0) {
|
|
84
|
+
throw new Error("Bundled skill name is required.");
|
|
85
|
+
}
|
|
86
|
+
return normalizedSkillName;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function resolveBundledSkillDir(packageRoot, skillName) {
|
|
90
|
+
return path.join(packageRoot, "skills", normalizeBundledSkillName(skillName));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function resolveSkillsCliPath(packageRoot) {
|
|
94
|
+
try {
|
|
95
|
+
return requireFromInstaller.resolve("skills/bin/cli.mjs", {
|
|
96
|
+
paths: [packageRoot],
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Unable to resolve 'skills/bin/cli.mjs' from package root '${packageRoot}'. Ensure dependency 'skills' is installed. (${reason})`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function resolveSkillsAgentName(hostName) {
|
|
107
|
+
const agentName = INSTALL_SKILLS_AGENT_BY_HOST[hostName];
|
|
108
|
+
if (!agentName) {
|
|
109
|
+
throw new Error(`--install-skill is not supported for host '${hostName}'.`);
|
|
110
|
+
}
|
|
111
|
+
return agentName;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function formatSupportedInstallSkillHosts() {
|
|
115
|
+
return SUPPORTED_INSTALL_SKILL_HOSTS.join(", ");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function uniqueValues(values) {
|
|
119
|
+
return [...new Set(values)];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeBundledSkillNames(skillNames) {
|
|
123
|
+
if (!Array.isArray(skillNames) || skillNames.length === 0) {
|
|
124
|
+
throw new Error("At least one bundled skill name is required.");
|
|
125
|
+
}
|
|
126
|
+
return uniqueValues(skillNames.map(normalizeBundledSkillName)).sort();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeInstallScope(scope) {
|
|
130
|
+
return scope === "global" ? "global" : "project";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveScopeRoot(projectRoot, scope) {
|
|
134
|
+
return normalizeInstallScope(scope) === "global" ? os.homedir() : projectRoot;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function resolveOpenClawGlobalSkillsRoot(homeDir) {
|
|
138
|
+
// Must stay aligned with pinned skills runtime precedence.
|
|
139
|
+
// skills@1.4.7 getOpenClawGlobalSkillsDir:
|
|
140
|
+
// .openclaw -> .clawdbot -> .moltbot -> default .openclaw.
|
|
141
|
+
if (existsSync(path.join(homeDir, ".openclaw"))) {
|
|
142
|
+
return path.join(homeDir, ".openclaw", "skills");
|
|
143
|
+
}
|
|
144
|
+
if (existsSync(path.join(homeDir, ".clawdbot"))) {
|
|
145
|
+
return path.join(homeDir, ".clawdbot", "skills");
|
|
146
|
+
}
|
|
147
|
+
if (existsSync(path.join(homeDir, ".moltbot"))) {
|
|
148
|
+
return path.join(homeDir, ".moltbot", "skills");
|
|
149
|
+
}
|
|
150
|
+
return path.join(homeDir, ".openclaw", "skills");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveInstalledSkillDirsForHosts(
|
|
154
|
+
projectRoot,
|
|
155
|
+
hostNames = [],
|
|
156
|
+
scope = "project",
|
|
157
|
+
skillName,
|
|
158
|
+
) {
|
|
159
|
+
return uniqueValues(
|
|
160
|
+
hostNames.map((hostName) =>
|
|
161
|
+
resolveInstalledSkillDir(projectRoot, hostName, scope, skillName),
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function partitionInstallSkillHosts(hostNames) {
|
|
167
|
+
const supportedHosts = [];
|
|
168
|
+
const unsupportedHosts = [];
|
|
169
|
+
|
|
170
|
+
for (const hostName of uniqueValues(hostNames)) {
|
|
171
|
+
if (hostName in INSTALL_SKILLS_AGENT_BY_HOST) {
|
|
172
|
+
supportedHosts.push(hostName);
|
|
173
|
+
} else {
|
|
174
|
+
unsupportedHosts.push(hostName);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { supportedHosts, unsupportedHosts };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function resolveInstalledSkillDir(
|
|
182
|
+
projectRoot,
|
|
183
|
+
hostName,
|
|
184
|
+
scope = "project",
|
|
185
|
+
skillName,
|
|
186
|
+
) {
|
|
187
|
+
const normalizedScope = normalizeInstallScope(scope);
|
|
188
|
+
const normalizedSkillName = normalizeBundledSkillName(skillName);
|
|
189
|
+
const root = resolveScopeRoot(projectRoot, normalizedScope);
|
|
190
|
+
switch (hostName) {
|
|
191
|
+
case "claude":
|
|
192
|
+
return path.join(root, ".claude", "skills", normalizedSkillName);
|
|
193
|
+
case "codex":
|
|
194
|
+
case "opencode":
|
|
195
|
+
case "cursor":
|
|
196
|
+
return path.join(root, ".agents", "skills", normalizedSkillName);
|
|
197
|
+
case "openclaw": {
|
|
198
|
+
if (normalizedScope === "global") {
|
|
199
|
+
return path.join(
|
|
200
|
+
resolveOpenClawGlobalSkillsRoot(root),
|
|
201
|
+
normalizedSkillName,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return path.join(root, "skills", normalizedSkillName);
|
|
205
|
+
}
|
|
206
|
+
case "pi-mono":
|
|
207
|
+
if (normalizedScope === "global") {
|
|
208
|
+
return path.join(root, ".pi", "agent", "skills", normalizedSkillName);
|
|
209
|
+
}
|
|
210
|
+
return path.join(root, ".pi", "skills", normalizedSkillName);
|
|
211
|
+
default:
|
|
212
|
+
throw new Error(`--install-skill is not supported for host '${hostName}'.`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveProjectSkillsLockPath(projectRoot) {
|
|
217
|
+
return path.join(projectRoot, "skills-lock.json");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function pathExists(targetPath) {
|
|
221
|
+
try {
|
|
222
|
+
await access(targetPath);
|
|
223
|
+
return true;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function createBackupDirPath(backupRoot) {
|
|
230
|
+
const backupSnapshotRoot = await mkdtemp(path.join(backupRoot, "snapshot-"));
|
|
231
|
+
return path.join(backupSnapshotRoot, "artifact");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function inspectInstalledSkillDir({
|
|
235
|
+
backupRoot,
|
|
236
|
+
installedSkillDir,
|
|
237
|
+
}) {
|
|
238
|
+
try {
|
|
239
|
+
const stats = await lstat(installedSkillDir);
|
|
240
|
+
if (stats.isSymbolicLink()) {
|
|
241
|
+
return {
|
|
242
|
+
installedSkillDir,
|
|
243
|
+
hadInstalledSkillDir: true,
|
|
244
|
+
priorArtifactKind: "symlink",
|
|
245
|
+
backupDir: null,
|
|
246
|
+
symlinkTarget: await readlink(installedSkillDir),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (!stats.isDirectory()) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Installed skill path '${installedSkillDir}' must be a directory or symlink.`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const backupDir = await createBackupDirPath(backupRoot);
|
|
256
|
+
await cp(installedSkillDir, backupDir, { recursive: true });
|
|
257
|
+
return {
|
|
258
|
+
installedSkillDir,
|
|
259
|
+
hadInstalledSkillDir: true,
|
|
260
|
+
priorArtifactKind: "directory",
|
|
261
|
+
backupDir,
|
|
262
|
+
symlinkTarget: null,
|
|
263
|
+
};
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
266
|
+
return {
|
|
267
|
+
installedSkillDir,
|
|
268
|
+
hadInstalledSkillDir: false,
|
|
269
|
+
priorArtifactKind: null,
|
|
270
|
+
backupDir: null,
|
|
271
|
+
symlinkTarget: null,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function inspectInstalledSkillArtifactsForHosts({
|
|
279
|
+
projectRoot = process.cwd(),
|
|
280
|
+
hostNames = [],
|
|
281
|
+
scope = "project",
|
|
282
|
+
skillNames,
|
|
283
|
+
} = {}) {
|
|
284
|
+
const normalizedScope = normalizeInstallScope(scope);
|
|
285
|
+
const normalizedSkillNames = normalizeBundledSkillNames(skillNames);
|
|
286
|
+
const skillsLockPath =
|
|
287
|
+
normalizedScope === "project"
|
|
288
|
+
? resolveProjectSkillsLockPath(projectRoot)
|
|
289
|
+
: null;
|
|
290
|
+
const hadSkillsLock = skillsLockPath ? await pathExists(skillsLockPath) : false;
|
|
291
|
+
const skillsLockContents =
|
|
292
|
+
hadSkillsLock && skillsLockPath
|
|
293
|
+
? await readFile(skillsLockPath, "utf8").catch(() => null)
|
|
294
|
+
: null;
|
|
295
|
+
const backupRoot = await mkdtemp(
|
|
296
|
+
path.join(os.tmpdir(), "flywheel-install-skill-backup-"),
|
|
297
|
+
);
|
|
298
|
+
const skillStates = new Map();
|
|
299
|
+
|
|
300
|
+
for (const skillName of normalizedSkillNames) {
|
|
301
|
+
const installedSkillDirs = resolveInstalledSkillDirsForHosts(
|
|
302
|
+
projectRoot,
|
|
303
|
+
hostNames,
|
|
304
|
+
normalizedScope,
|
|
305
|
+
skillName,
|
|
306
|
+
);
|
|
307
|
+
const skillState = {
|
|
308
|
+
installedSkillDirs: [],
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
for (const installedSkillDir of installedSkillDirs) {
|
|
312
|
+
// eslint-disable-next-line no-await-in-loop
|
|
313
|
+
skillState.installedSkillDirs.push(
|
|
314
|
+
await inspectInstalledSkillDir({
|
|
315
|
+
backupRoot,
|
|
316
|
+
installedSkillDir,
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
skillStates.set(skillName, skillState);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
backupRoot,
|
|
326
|
+
skillStates,
|
|
327
|
+
skillsLockPath,
|
|
328
|
+
hadSkillsLock,
|
|
329
|
+
skillsLockContents,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function installBundledSkillForHosts({
|
|
334
|
+
projectRoot = process.cwd(),
|
|
335
|
+
hostNames = [],
|
|
336
|
+
scope = "project",
|
|
337
|
+
serverName = DEFAULT_SKILL_MCP_SERVER_NAME,
|
|
338
|
+
serverUrl = DEFAULT_SKILL_MCP_SERVER_URL,
|
|
339
|
+
skillNames,
|
|
340
|
+
} = {}) {
|
|
341
|
+
const packageRoot = resolvePackageRoot();
|
|
342
|
+
const skillsCliPath = resolveSkillsCliPath(packageRoot);
|
|
343
|
+
const normalizedScope = normalizeInstallScope(scope);
|
|
344
|
+
const normalizedSkillNames = normalizeBundledSkillNames(skillNames);
|
|
345
|
+
const resolvedServerName =
|
|
346
|
+
String(serverName || "").trim() || DEFAULT_SKILL_MCP_SERVER_NAME;
|
|
347
|
+
const resolvedServerUrl =
|
|
348
|
+
String(serverUrl || "").trim() || DEFAULT_SKILL_MCP_SERVER_URL;
|
|
349
|
+
const shouldStageSkillSource =
|
|
350
|
+
resolvedServerName !== DEFAULT_SKILL_MCP_SERVER_NAME ||
|
|
351
|
+
resolvedServerUrl !== DEFAULT_SKILL_MCP_SERVER_URL;
|
|
352
|
+
|
|
353
|
+
for (const skillName of normalizedSkillNames) {
|
|
354
|
+
const bundledSkillDir = resolveBundledSkillDir(packageRoot, skillName);
|
|
355
|
+
let installSkillSourceDir = bundledSkillDir;
|
|
356
|
+
let stagedSkillSourceRoot = null;
|
|
357
|
+
if (shouldStageSkillSource) {
|
|
358
|
+
// The stage root must be unique per skill so rewrites do not overlap.
|
|
359
|
+
stagedSkillSourceRoot = await mkdtemp(
|
|
360
|
+
path.join(os.tmpdir(), `flywheel-install-skill-${skillName}-`),
|
|
361
|
+
);
|
|
362
|
+
installSkillSourceDir = path.join(stagedSkillSourceRoot, skillName);
|
|
363
|
+
await cp(bundledSkillDir, installSkillSourceDir, { recursive: true });
|
|
364
|
+
await rewriteOpenAiAgentMetadataMcpTarget({
|
|
365
|
+
skillDir: installSkillSourceDir,
|
|
366
|
+
serverName: resolvedServerName,
|
|
367
|
+
serverUrl: resolvedServerUrl,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
for (const hostName of uniqueValues(hostNames)) {
|
|
373
|
+
const agentName = resolveSkillsAgentName(hostName);
|
|
374
|
+
const addArgs = [
|
|
375
|
+
skillsCliPath,
|
|
376
|
+
"add",
|
|
377
|
+
installSkillSourceDir,
|
|
378
|
+
"--agent",
|
|
379
|
+
agentName,
|
|
380
|
+
"--copy",
|
|
381
|
+
"--yes",
|
|
382
|
+
];
|
|
383
|
+
if (normalizedScope === "global") {
|
|
384
|
+
addArgs.push("--global");
|
|
385
|
+
}
|
|
386
|
+
// eslint-disable-next-line no-await-in-loop
|
|
387
|
+
await execFile(process.execPath, addArgs, {
|
|
388
|
+
cwd: projectRoot,
|
|
389
|
+
env: process.env,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
} finally {
|
|
393
|
+
if (stagedSkillSourceRoot) {
|
|
394
|
+
await rm(stagedSkillSourceRoot, { force: true, recursive: true });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function rewriteOpenAiAgentMetadataMcpTarget({
|
|
401
|
+
skillDir,
|
|
402
|
+
serverName,
|
|
403
|
+
serverUrl,
|
|
404
|
+
}) {
|
|
405
|
+
const openAiAgentMetadataPath = path.join(
|
|
406
|
+
skillDir,
|
|
407
|
+
OPENAI_AGENT_METADATA_RELATIVE_PATH,
|
|
408
|
+
);
|
|
409
|
+
let metadataRaw = "";
|
|
410
|
+
try {
|
|
411
|
+
metadataRaw = await readFile(openAiAgentMetadataPath, "utf8");
|
|
412
|
+
} catch {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const metadata = loadYaml(metadataRaw);
|
|
417
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const dependencies = metadata.dependencies;
|
|
421
|
+
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const tools = dependencies.tools;
|
|
425
|
+
if (!Array.isArray(tools)) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let didRewrite = false;
|
|
430
|
+
for (const tool of tools) {
|
|
431
|
+
if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (tool.type !== "mcp") {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (tool.value !== serverName) {
|
|
439
|
+
tool.value = serverName;
|
|
440
|
+
didRewrite = true;
|
|
441
|
+
}
|
|
442
|
+
if (tool.url !== serverUrl) {
|
|
443
|
+
tool.url = serverUrl;
|
|
444
|
+
didRewrite = true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!didRewrite) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
await writeFile(openAiAgentMetadataPath, dumpYaml(metadata, { noRefs: true }), {
|
|
453
|
+
encoding: "utf8",
|
|
454
|
+
mode: 0o600,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export async function cleanupInstalledSkillArtifactsForHosts({
|
|
459
|
+
projectRoot = process.cwd(),
|
|
460
|
+
hostNames = [],
|
|
461
|
+
priorState = null,
|
|
462
|
+
scope = "project",
|
|
463
|
+
skillNames,
|
|
464
|
+
} = {}) {
|
|
465
|
+
const normalizedScope = normalizeInstallScope(scope);
|
|
466
|
+
const normalizedSkillNames = normalizeBundledSkillNames(skillNames);
|
|
467
|
+
const skillStates =
|
|
468
|
+
priorState?.skillStates ??
|
|
469
|
+
new Map(
|
|
470
|
+
normalizedSkillNames.map((skillName) => [
|
|
471
|
+
skillName,
|
|
472
|
+
{
|
|
473
|
+
installedSkillDirs: resolveInstalledSkillDirsForHosts(
|
|
474
|
+
projectRoot,
|
|
475
|
+
hostNames,
|
|
476
|
+
normalizedScope,
|
|
477
|
+
skillName,
|
|
478
|
+
).map((installedSkillDir) => ({
|
|
479
|
+
installedSkillDir,
|
|
480
|
+
hadInstalledSkillDir: false,
|
|
481
|
+
priorArtifactKind: null,
|
|
482
|
+
backupDir: null,
|
|
483
|
+
symlinkTarget: null,
|
|
484
|
+
})),
|
|
485
|
+
},
|
|
486
|
+
]),
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
for (const skillName of normalizedSkillNames) {
|
|
490
|
+
const skillState = skillStates.get(skillName) ?? { installedSkillDirs: [] };
|
|
491
|
+
for (const installedSkillState of skillState.installedSkillDirs) {
|
|
492
|
+
const {
|
|
493
|
+
installedSkillDir,
|
|
494
|
+
priorArtifactKind,
|
|
495
|
+
backupDir,
|
|
496
|
+
symlinkTarget,
|
|
497
|
+
} = installedSkillState;
|
|
498
|
+
if (priorArtifactKind === "directory") {
|
|
499
|
+
// eslint-disable-next-line no-await-in-loop
|
|
500
|
+
await rm(installedSkillDir, { force: true, recursive: true });
|
|
501
|
+
// eslint-disable-next-line no-await-in-loop
|
|
502
|
+
await mkdir(path.dirname(installedSkillDir), { recursive: true });
|
|
503
|
+
// eslint-disable-next-line no-await-in-loop
|
|
504
|
+
await cp(backupDir, installedSkillDir, { recursive: true });
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (priorArtifactKind === "symlink") {
|
|
508
|
+
// eslint-disable-next-line no-await-in-loop
|
|
509
|
+
await rm(installedSkillDir, { force: true, recursive: true });
|
|
510
|
+
// eslint-disable-next-line no-await-in-loop
|
|
511
|
+
await mkdir(path.dirname(installedSkillDir), { recursive: true });
|
|
512
|
+
// eslint-disable-next-line no-await-in-loop
|
|
513
|
+
await symlink(symlinkTarget, installedSkillDir);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
// eslint-disable-next-line no-await-in-loop
|
|
517
|
+
await rm(installedSkillDir, { force: true, recursive: true });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const removeSkillsLock =
|
|
522
|
+
normalizedScope === "project" && !(priorState?.hadSkillsLock ?? false);
|
|
523
|
+
if (
|
|
524
|
+
normalizedScope === "project" &&
|
|
525
|
+
(priorState?.hadSkillsLock ?? false) &&
|
|
526
|
+
priorState?.skillsLockPath &&
|
|
527
|
+
typeof priorState.skillsLockContents === "string"
|
|
528
|
+
) {
|
|
529
|
+
await writeFile(priorState.skillsLockPath, priorState.skillsLockContents, {
|
|
530
|
+
encoding: "utf8",
|
|
531
|
+
mode: 0o600,
|
|
532
|
+
});
|
|
533
|
+
} else if (removeSkillsLock && priorState?.skillsLockPath) {
|
|
534
|
+
await rm(priorState.skillsLockPath, { force: true });
|
|
535
|
+
} else if (removeSkillsLock) {
|
|
536
|
+
await rm(resolveProjectSkillsLockPath(projectRoot), { force: true });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (priorState?.backupRoot) {
|
|
540
|
+
await rm(priorState.backupRoot, { force: true, recursive: true });
|
|
541
|
+
}
|
|
542
|
+
}
|