agentloom 0.1.0 → 0.1.2
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 +91 -72
- package/bin/cli.mjs +3 -2
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +149 -17
- package/dist/commands/add.d.ts +7 -0
- package/dist/commands/add.js +122 -31
- package/dist/commands/agent.d.ts +2 -0
- package/dist/commands/agent.js +85 -0
- package/dist/commands/command.d.ts +2 -0
- package/dist/commands/command.js +98 -0
- package/dist/commands/delete.d.ts +9 -0
- package/dist/commands/delete.js +444 -0
- package/dist/commands/entity-utils.d.ts +13 -0
- package/dist/commands/entity-utils.js +58 -0
- package/dist/commands/find.d.ts +21 -0
- package/dist/commands/find.js +944 -0
- package/dist/commands/mcp.js +133 -55
- package/dist/commands/skills.d.ts +2 -1
- package/dist/commands/skills.js +105 -9
- package/dist/commands/sync.d.ts +6 -0
- package/dist/commands/sync.js +12 -10
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +286 -21
- package/dist/core/argv.d.ts +2 -1
- package/dist/core/argv.js +42 -2
- package/dist/core/commands.d.ts +13 -0
- package/dist/core/commands.js +65 -0
- package/dist/core/copy.d.ts +6 -0
- package/dist/core/copy.js +126 -65
- package/dist/core/importer.d.ts +28 -1
- package/dist/core/importer.js +1104 -41
- package/dist/core/lockfile.js +86 -3
- package/dist/core/manage-agents-bootstrap.d.ts +10 -0
- package/dist/core/manage-agents-bootstrap.js +40 -0
- package/dist/core/manifest.js +7 -1
- package/dist/core/router.d.ts +16 -0
- package/dist/core/router.js +66 -0
- package/dist/core/scope.d.ts +1 -1
- package/dist/core/scope.js +12 -8
- package/dist/core/settings.d.ts +4 -3
- package/dist/core/settings.js +10 -8
- package/dist/core/skills.d.ts +23 -0
- package/dist/core/skills.js +328 -0
- package/dist/core/sources.d.ts +3 -1
- package/dist/core/sources.js +31 -1
- package/dist/core/telemetry.d.ts +26 -0
- package/dist/core/telemetry.js +124 -0
- package/dist/core/version-notifier.d.ts +1 -0
- package/dist/core/version-notifier.js +22 -4
- package/dist/sync/index.d.ts +7 -1
- package/dist/sync/index.js +395 -131
- package/dist/types.d.ts +16 -1
- package/package.json +5 -4
package/dist/sync/index.js
CHANGED
|
@@ -1,49 +1,127 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { cancel, isCancel, multiselect } from "@clack/prompts";
|
|
5
5
|
import TOML from "@iarna/toml";
|
|
6
6
|
import YAML from "yaml";
|
|
7
|
+
import { ALL_PROVIDERS } from "../types.js";
|
|
7
8
|
import { getProviderConfig, isProviderEnabled, parseAgentsDir, } from "../core/agents.js";
|
|
9
|
+
import { parseCommandsDir } from "../core/commands.js";
|
|
8
10
|
import { ensureDir, isObject, readJsonIfExists, relativePosix, removeFileIfExists, slugify, toPosixPath, writeJsonAtomic, writeTextAtomic, } from "../core/fs.js";
|
|
9
11
|
import { readManifest, writeManifest } from "../core/manifest.js";
|
|
10
12
|
import { readCanonicalMcp, resolveMcpForProvider } from "../core/mcp.js";
|
|
11
|
-
import { getGlobalSettingsPath, readSettings, updateLastScope, } from "../core/settings.js";
|
|
13
|
+
import { getGlobalSettingsPath, readSettings, updateLastScope, updateLastScopeBestEffort, } from "../core/settings.js";
|
|
14
|
+
export async function resolveProvidersForSync(options) {
|
|
15
|
+
const settings = readSettings(options.paths.settingsPath);
|
|
16
|
+
return resolveProviders({
|
|
17
|
+
explicitProviders: options.explicitProviders,
|
|
18
|
+
settings,
|
|
19
|
+
nonInteractive: options.nonInteractive,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
12
22
|
export async function syncFromCanonical(options) {
|
|
13
23
|
const agents = parseAgentsDir(options.paths.agentsDir);
|
|
24
|
+
const commands = parseCommandsDir(options.paths.commandsDir);
|
|
14
25
|
const mcp = readCanonicalMcp(options.paths);
|
|
15
26
|
const manifest = readManifest(options.paths);
|
|
16
|
-
const
|
|
17
|
-
|
|
27
|
+
const effectiveManifest = {
|
|
28
|
+
...manifest,
|
|
29
|
+
generatedByEntity: normalizeGeneratedByEntity(manifest),
|
|
30
|
+
};
|
|
31
|
+
const providers = await resolveProvidersForSync({
|
|
32
|
+
paths: options.paths,
|
|
33
|
+
explicitProviders: options.providers,
|
|
34
|
+
nonInteractive: options.nonInteractive,
|
|
35
|
+
});
|
|
36
|
+
const target = options.target ?? "all";
|
|
18
37
|
const nextManifest = {
|
|
19
38
|
version: 1,
|
|
20
39
|
generatedFiles: [],
|
|
40
|
+
generatedByEntity: {},
|
|
21
41
|
codex: {
|
|
22
|
-
roles: [],
|
|
23
|
-
mcpServers: [],
|
|
42
|
+
roles: [...(effectiveManifest.codex?.roles ?? [])],
|
|
43
|
+
mcpServers: [...(effectiveManifest.codex?.mcpServers ?? [])],
|
|
24
44
|
},
|
|
25
45
|
};
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
const generatedAgents = new Set();
|
|
47
|
+
const generatedCommands = new Set();
|
|
48
|
+
const generatedMcp = new Set();
|
|
49
|
+
if (target === "all" || target === "agent") {
|
|
50
|
+
for (const provider of providers) {
|
|
51
|
+
syncProviderAgents({
|
|
52
|
+
provider,
|
|
53
|
+
paths: options.paths,
|
|
54
|
+
agents,
|
|
55
|
+
generated: generatedAgents,
|
|
56
|
+
dryRun: !!options.dryRun,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (target === "all" || target === "command") {
|
|
61
|
+
for (const provider of providers) {
|
|
62
|
+
syncProviderCommands({
|
|
63
|
+
provider,
|
|
64
|
+
paths: options.paths,
|
|
65
|
+
commands,
|
|
66
|
+
generated: generatedCommands,
|
|
67
|
+
dryRun: !!options.dryRun,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (target === "all" || target === "mcp") {
|
|
72
|
+
syncProviderMcp({
|
|
73
|
+
providers,
|
|
30
74
|
paths: options.paths,
|
|
31
|
-
|
|
32
|
-
generated,
|
|
75
|
+
mcp,
|
|
76
|
+
generated: generatedMcp,
|
|
33
77
|
dryRun: !!options.dryRun,
|
|
34
78
|
});
|
|
35
79
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
80
|
+
if (providers.includes("codex")) {
|
|
81
|
+
const includeRoles = target === "all" || target === "agent";
|
|
82
|
+
const includeMcp = target === "all" || target === "mcp";
|
|
83
|
+
if (includeRoles || includeMcp) {
|
|
84
|
+
syncCodex({
|
|
85
|
+
paths: options.paths,
|
|
86
|
+
agents,
|
|
87
|
+
resolvedMcp: resolveMcpForProvider(mcp, "codex"),
|
|
88
|
+
generated: includeRoles ? generatedAgents : generatedMcp,
|
|
89
|
+
manifest: effectiveManifest,
|
|
90
|
+
nextManifest,
|
|
91
|
+
dryRun: !!options.dryRun,
|
|
92
|
+
includeRoles,
|
|
93
|
+
includeMcp,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
nextManifest.codex = {
|
|
98
|
+
roles: [...(effectiveManifest.codex?.roles ?? [])],
|
|
99
|
+
mcpServers: [...(effectiveManifest.codex?.mcpServers ?? [])],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const previousByEntity = normalizeGeneratedByEntity(effectiveManifest);
|
|
104
|
+
const nextByEntity = {
|
|
105
|
+
...previousByEntity,
|
|
106
|
+
};
|
|
107
|
+
if (target === "all" || target === "agent") {
|
|
108
|
+
nextByEntity.agent = [...generatedAgents].sort();
|
|
109
|
+
}
|
|
110
|
+
if (target === "all" || target === "command") {
|
|
111
|
+
nextByEntity.command = [...generatedCommands].sort();
|
|
112
|
+
}
|
|
113
|
+
if (target === "all" || target === "mcp") {
|
|
114
|
+
nextByEntity.mcp = [...generatedMcp].sort();
|
|
115
|
+
}
|
|
116
|
+
nextManifest.generatedByEntity = pruneGeneratedByEntity(nextByEntity);
|
|
117
|
+
nextManifest.generatedFiles = [
|
|
118
|
+
...new Set([
|
|
119
|
+
...(nextManifest.generatedByEntity.agent ?? []),
|
|
120
|
+
...(nextManifest.generatedByEntity.command ?? []),
|
|
121
|
+
...(nextManifest.generatedByEntity.mcp ?? []),
|
|
122
|
+
...(nextManifest.generatedByEntity.skill ?? []),
|
|
123
|
+
]),
|
|
124
|
+
].sort();
|
|
47
125
|
const removedFiles = await removeStaleGeneratedFiles({
|
|
48
126
|
oldManifest: manifest,
|
|
49
127
|
newManifest: nextManifest,
|
|
@@ -54,7 +132,10 @@ export async function syncFromCanonical(options) {
|
|
|
54
132
|
if (!options.dryRun) {
|
|
55
133
|
writeManifest(options.paths, nextManifest);
|
|
56
134
|
updateLastScope(options.paths.settingsPath, options.paths.scope, providers);
|
|
57
|
-
|
|
135
|
+
const globalSettingsPath = getGlobalSettingsPath(options.paths.homeDir);
|
|
136
|
+
if (options.paths.settingsPath !== globalSettingsPath) {
|
|
137
|
+
updateLastScopeBestEffort(globalSettingsPath, options.paths.scope, providers);
|
|
138
|
+
}
|
|
58
139
|
}
|
|
59
140
|
return {
|
|
60
141
|
providers,
|
|
@@ -62,14 +143,56 @@ export async function syncFromCanonical(options) {
|
|
|
62
143
|
removedFiles,
|
|
63
144
|
};
|
|
64
145
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
146
|
+
const PROVIDER_LABELS = {
|
|
147
|
+
cursor: "Cursor",
|
|
148
|
+
claude: "Claude",
|
|
149
|
+
codex: "Codex",
|
|
150
|
+
opencode: "OpenCode",
|
|
151
|
+
gemini: "Gemini",
|
|
152
|
+
copilot: "Copilot",
|
|
153
|
+
};
|
|
154
|
+
const MULTISELECT_HELP_TEXT = "↑↓ move, space select, enter confirm";
|
|
155
|
+
function withMultiselectHelp(message) {
|
|
156
|
+
return `${message}\n${MULTISELECT_HELP_TEXT}`;
|
|
157
|
+
}
|
|
158
|
+
async function resolveProviders(options) {
|
|
159
|
+
if (options.explicitProviders && options.explicitProviders.length > 0) {
|
|
160
|
+
return normalizeProviderSelection(options.explicitProviders);
|
|
68
161
|
}
|
|
69
|
-
|
|
70
|
-
|
|
162
|
+
const defaults = normalizeProviderSelection(options.settings.defaultProviders);
|
|
163
|
+
const initialSelection = defaults.length > 0 ? defaults : [...ALL_PROVIDERS];
|
|
164
|
+
const nonInteractive = options.nonInteractive ?? !(process.stdin.isTTY && process.stdout.isTTY);
|
|
165
|
+
if (nonInteractive) {
|
|
166
|
+
return initialSelection;
|
|
71
167
|
}
|
|
72
|
-
|
|
168
|
+
const selected = await multiselect({
|
|
169
|
+
message: withMultiselectHelp("Select providers to sync"),
|
|
170
|
+
options: ALL_PROVIDERS.map((provider) => ({
|
|
171
|
+
value: provider,
|
|
172
|
+
label: PROVIDER_LABELS[provider],
|
|
173
|
+
})),
|
|
174
|
+
initialValues: initialSelection,
|
|
175
|
+
required: true,
|
|
176
|
+
});
|
|
177
|
+
if (isCancel(selected)) {
|
|
178
|
+
cancel("Operation cancelled.");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
const normalized = normalizeProviderSelection(Array.isArray(selected) ? selected : []);
|
|
182
|
+
if (normalized.length === 0) {
|
|
183
|
+
throw new Error("At least one provider must be selected.");
|
|
184
|
+
}
|
|
185
|
+
return normalized;
|
|
186
|
+
}
|
|
187
|
+
function normalizeProviderSelection(providers) {
|
|
188
|
+
const selected = new Set();
|
|
189
|
+
for (const provider of providers ?? []) {
|
|
190
|
+
const normalized = provider.trim().toLowerCase();
|
|
191
|
+
if (ALL_PROVIDERS.includes(normalized)) {
|
|
192
|
+
selected.add(normalized);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return [...selected];
|
|
73
196
|
}
|
|
74
197
|
function syncProviderAgents(options) {
|
|
75
198
|
const providerDir = getProviderAgentsDir(options.paths, options.provider);
|
|
@@ -84,9 +207,7 @@ function syncProviderAgents(options) {
|
|
|
84
207
|
}
|
|
85
208
|
const fileName = options.provider === "copilot"
|
|
86
209
|
? `${slugify(agent.name) || "agent"}.agent.md`
|
|
87
|
-
:
|
|
88
|
-
? `${slugify(agent.name) || "agent"}.mdc`
|
|
89
|
-
: `${slugify(agent.name) || "agent"}.md`;
|
|
210
|
+
: `${slugify(agent.name) || "agent"}.md`;
|
|
90
211
|
const outputPath = path.join(providerDir, fileName);
|
|
91
212
|
const content = buildProviderAgentContent(options.provider, agent, providerConfig ?? {});
|
|
92
213
|
if (!options.dryRun) {
|
|
@@ -97,15 +218,6 @@ function syncProviderAgents(options) {
|
|
|
97
218
|
}
|
|
98
219
|
}
|
|
99
220
|
function buildProviderAgentContent(provider, agent, providerConfig) {
|
|
100
|
-
if (provider === "cursor") {
|
|
101
|
-
const frontmatter = {
|
|
102
|
-
description: agent.description,
|
|
103
|
-
alwaysApply: false,
|
|
104
|
-
...providerConfig,
|
|
105
|
-
};
|
|
106
|
-
const fm = YAML.stringify(frontmatter).trimEnd();
|
|
107
|
-
return `---\n${fm}\n---\n\n${agent.body.trimStart()}${agent.body.endsWith("\n") ? "" : "\n"}`;
|
|
108
|
-
}
|
|
109
221
|
const frontmatter = {
|
|
110
222
|
name: agent.name,
|
|
111
223
|
description: agent.description,
|
|
@@ -120,8 +232,8 @@ function getProviderAgentsDir(paths, provider) {
|
|
|
120
232
|
switch (provider) {
|
|
121
233
|
case "cursor":
|
|
122
234
|
return paths.scope === "local"
|
|
123
|
-
? path.join(workspaceRoot, ".cursor", "
|
|
124
|
-
: path.join(home, ".cursor", "
|
|
235
|
+
? path.join(workspaceRoot, ".cursor", "agents")
|
|
236
|
+
: path.join(home, ".cursor", "agents");
|
|
125
237
|
case "claude":
|
|
126
238
|
return paths.scope === "local"
|
|
127
239
|
? path.join(workspaceRoot, ".claude", "agents")
|
|
@@ -146,8 +258,74 @@ function getProviderAgentsDir(paths, provider) {
|
|
|
146
258
|
return path.join(workspaceRoot, ".agents", "unknown");
|
|
147
259
|
}
|
|
148
260
|
}
|
|
261
|
+
function syncProviderCommands(options) {
|
|
262
|
+
const providerDir = getProviderCommandsDir(options.paths, options.provider);
|
|
263
|
+
for (const command of options.commands) {
|
|
264
|
+
const fileName = mapProviderCommandFileName(options.provider, command.fileName);
|
|
265
|
+
const outputPath = path.join(providerDir, fileName);
|
|
266
|
+
if (!options.dryRun) {
|
|
267
|
+
ensureDir(path.dirname(outputPath));
|
|
268
|
+
writeTextAtomic(outputPath, command.content);
|
|
269
|
+
}
|
|
270
|
+
options.generated.add(outputPath);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function mapProviderCommandFileName(provider, fileName) {
|
|
274
|
+
const lower = fileName.toLowerCase();
|
|
275
|
+
if (provider === "copilot") {
|
|
276
|
+
if (lower.endsWith(".prompt.md"))
|
|
277
|
+
return fileName;
|
|
278
|
+
if (lower.endsWith(".md")) {
|
|
279
|
+
return `${fileName.slice(0, -3)}.prompt.md`;
|
|
280
|
+
}
|
|
281
|
+
if (lower.endsWith(".mdc")) {
|
|
282
|
+
return `${fileName.slice(0, -4)}.prompt.md`;
|
|
283
|
+
}
|
|
284
|
+
const ext = path.extname(fileName);
|
|
285
|
+
if (ext) {
|
|
286
|
+
return `${fileName.slice(0, -ext.length)}.prompt.md`;
|
|
287
|
+
}
|
|
288
|
+
return `${fileName}.prompt.md`;
|
|
289
|
+
}
|
|
290
|
+
if (lower.endsWith(".mdc")) {
|
|
291
|
+
return `${fileName.slice(0, -4)}.md`;
|
|
292
|
+
}
|
|
293
|
+
return fileName;
|
|
294
|
+
}
|
|
295
|
+
function getProviderCommandsDir(paths, provider) {
|
|
296
|
+
const workspaceRoot = paths.workspaceRoot;
|
|
297
|
+
const home = paths.homeDir;
|
|
298
|
+
switch (provider) {
|
|
299
|
+
case "cursor":
|
|
300
|
+
return paths.scope === "local"
|
|
301
|
+
? path.join(workspaceRoot, ".cursor", "commands")
|
|
302
|
+
: path.join(home, ".cursor", "commands");
|
|
303
|
+
case "claude":
|
|
304
|
+
return paths.scope === "local"
|
|
305
|
+
? path.join(workspaceRoot, ".claude", "commands")
|
|
306
|
+
: path.join(home, ".claude", "commands");
|
|
307
|
+
case "codex":
|
|
308
|
+
return path.join(home, ".codex", "prompts");
|
|
309
|
+
case "opencode":
|
|
310
|
+
return paths.scope === "local"
|
|
311
|
+
? path.join(workspaceRoot, ".opencode", "commands")
|
|
312
|
+
: path.join(home, ".config", "opencode", "commands");
|
|
313
|
+
case "gemini":
|
|
314
|
+
return paths.scope === "local"
|
|
315
|
+
? path.join(workspaceRoot, ".gemini", "commands")
|
|
316
|
+
: path.join(home, ".gemini", "commands");
|
|
317
|
+
case "copilot":
|
|
318
|
+
return paths.scope === "local"
|
|
319
|
+
? path.join(workspaceRoot, ".github", "prompts")
|
|
320
|
+
: path.join(home, ".github", "prompts");
|
|
321
|
+
default:
|
|
322
|
+
return path.join(workspaceRoot, ".agents", "unknown", "commands");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
149
325
|
function syncProviderMcp(options) {
|
|
150
326
|
for (const provider of options.providers) {
|
|
327
|
+
if (provider === "codex")
|
|
328
|
+
continue;
|
|
151
329
|
const resolved = resolveMcpForProvider(options.mcp, provider);
|
|
152
330
|
if (provider === "cursor") {
|
|
153
331
|
const outputPath = options.paths.scope === "local"
|
|
@@ -187,18 +365,6 @@ function syncProviderMcp(options) {
|
|
|
187
365
|
options.generated.add(settingsPath);
|
|
188
366
|
continue;
|
|
189
367
|
}
|
|
190
|
-
if (provider === "codex") {
|
|
191
|
-
syncCodex({
|
|
192
|
-
paths: options.paths,
|
|
193
|
-
agents: options.agents,
|
|
194
|
-
resolvedMcp: resolved,
|
|
195
|
-
generated: options.generated,
|
|
196
|
-
manifest: options.manifest,
|
|
197
|
-
nextManifest: options.nextManifest,
|
|
198
|
-
dryRun: options.dryRun,
|
|
199
|
-
});
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
368
|
if (provider === "opencode") {
|
|
203
369
|
const outputPath = options.paths.scope === "local"
|
|
204
370
|
? path.join(options.paths.workspaceRoot, ".opencode", "opencode.json")
|
|
@@ -310,63 +476,72 @@ function syncCodex(options) {
|
|
|
310
476
|
features.multi_agent = true;
|
|
311
477
|
parsed.features = features;
|
|
312
478
|
const agentsTable = isObject(parsed.agents) ? { ...parsed.agents } : {};
|
|
313
|
-
const
|
|
314
|
-
const nextRoles = [];
|
|
315
|
-
const enabledCodexRoles = new Set(options.agents
|
|
316
|
-
.filter((agent) => isProviderEnabled(agent.frontmatter, "codex"))
|
|
317
|
-
.map((agent) => slugify(agent.name))
|
|
318
|
-
.filter((role) => role.length > 0));
|
|
319
|
-
for (const oldRole of previousRoles) {
|
|
320
|
-
if (!enabledCodexRoles.has(oldRole)) {
|
|
321
|
-
delete agentsTable[oldRole];
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
for (const agent of options.agents) {
|
|
325
|
-
if (!isProviderEnabled(agent.frontmatter, "codex"))
|
|
326
|
-
continue;
|
|
327
|
-
const codexConfig = getProviderConfig(agent.frontmatter, "codex") ?? {};
|
|
328
|
-
const role = slugify(agent.name);
|
|
329
|
-
if (!role)
|
|
330
|
-
continue;
|
|
331
|
-
const roleTomlPath = path.join(codexAgentsDir, `${role}.toml`);
|
|
332
|
-
const roleInstructionsPath = path.join(codexAgentsDir, `${role}.instructions.md`);
|
|
333
|
-
const roleToml = buildCodexRoleToml(roleInstructionsPath, codexConfig);
|
|
334
|
-
if (!options.dryRun) {
|
|
335
|
-
ensureDir(codexAgentsDir);
|
|
336
|
-
writeTextAtomic(roleInstructionsPath, `${agent.body.trimStart()}\n`);
|
|
337
|
-
writeTextAtomic(roleTomlPath, TOML.stringify(roleToml));
|
|
338
|
-
}
|
|
339
|
-
options.generated.add(roleTomlPath);
|
|
340
|
-
options.generated.add(roleInstructionsPath);
|
|
341
|
-
agentsTable[role] = {
|
|
342
|
-
description: agent.description,
|
|
343
|
-
config_file: `./agents/${role}.toml`,
|
|
344
|
-
};
|
|
345
|
-
nextRoles.push(role);
|
|
346
|
-
}
|
|
347
|
-
parsed.agents = agentsTable;
|
|
348
|
-
const previousServers = new Set(options.manifest.codex?.mcpServers ?? []);
|
|
479
|
+
const trackedRoles = resolveTrackedCodexEntries(options.manifest.codex?.roles, Object.keys(agentsTable));
|
|
349
480
|
const mcpServers = isObject(parsed.mcp_servers)
|
|
350
481
|
? { ...parsed.mcp_servers }
|
|
351
482
|
: {};
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
483
|
+
const trackedServers = resolveTrackedCodexEntries(options.manifest.codex?.mcpServers, Object.keys(mcpServers));
|
|
484
|
+
let nextRoles = [...trackedRoles];
|
|
485
|
+
if (options.includeRoles) {
|
|
486
|
+
const previousRoles = new Set(trackedRoles);
|
|
487
|
+
nextRoles = [];
|
|
488
|
+
const enabledCodexRoles = new Set(options.agents
|
|
489
|
+
.filter((agent) => isProviderEnabled(agent.frontmatter, "codex"))
|
|
490
|
+
.map((agent) => slugify(agent.name))
|
|
491
|
+
.filter((role) => role.length > 0));
|
|
492
|
+
for (const oldRole of previousRoles) {
|
|
493
|
+
if (!enabledCodexRoles.has(oldRole)) {
|
|
494
|
+
delete agentsTable[oldRole];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (const agent of options.agents) {
|
|
498
|
+
if (!isProviderEnabled(agent.frontmatter, "codex"))
|
|
499
|
+
continue;
|
|
500
|
+
const codexConfig = getProviderConfig(agent.frontmatter, "codex") ?? {};
|
|
501
|
+
const role = slugify(agent.name);
|
|
502
|
+
if (!role)
|
|
503
|
+
continue;
|
|
504
|
+
const roleTomlPath = path.join(codexAgentsDir, `${role}.toml`);
|
|
505
|
+
const roleInstructionsPath = path.join(codexAgentsDir, `${role}.instructions.md`);
|
|
506
|
+
const roleToml = buildCodexRoleToml(roleInstructionsPath, codexConfig);
|
|
507
|
+
if (!options.dryRun) {
|
|
508
|
+
ensureDir(codexAgentsDir);
|
|
509
|
+
writeTextAtomic(roleInstructionsPath, `${agent.body.trimStart()}\n`);
|
|
510
|
+
writeTextAtomic(roleTomlPath, TOML.stringify(roleToml));
|
|
511
|
+
}
|
|
512
|
+
options.generated.add(roleTomlPath);
|
|
513
|
+
options.generated.add(roleInstructionsPath);
|
|
514
|
+
agentsTable[role] = {
|
|
515
|
+
description: agent.description,
|
|
516
|
+
config_file: `./agents/${role}.toml`,
|
|
517
|
+
};
|
|
518
|
+
nextRoles.push(role);
|
|
519
|
+
}
|
|
520
|
+
parsed.agents = agentsTable;
|
|
521
|
+
}
|
|
522
|
+
let nextServers = [...trackedServers];
|
|
523
|
+
if (options.includeMcp) {
|
|
524
|
+
const previousServers = new Set(trackedServers);
|
|
525
|
+
for (const oldServer of previousServers) {
|
|
526
|
+
if (!Object.prototype.hasOwnProperty.call(options.resolvedMcp, oldServer)) {
|
|
527
|
+
delete mcpServers[oldServer];
|
|
528
|
+
}
|
|
355
529
|
}
|
|
530
|
+
for (const [serverName, config] of Object.entries(options.resolvedMcp)) {
|
|
531
|
+
const mapped = {};
|
|
532
|
+
if (typeof config.url === "string")
|
|
533
|
+
mapped.url = config.url;
|
|
534
|
+
if (typeof config.command === "string")
|
|
535
|
+
mapped.command = config.command;
|
|
536
|
+
if (Array.isArray(config.args))
|
|
537
|
+
mapped.args = config.args;
|
|
538
|
+
if (isObject(config.env))
|
|
539
|
+
mapped.env = config.env;
|
|
540
|
+
mcpServers[serverName] = mapped;
|
|
541
|
+
}
|
|
542
|
+
parsed.mcp_servers = mcpServers;
|
|
543
|
+
nextServers = Object.keys(options.resolvedMcp).sort();
|
|
356
544
|
}
|
|
357
|
-
for (const [serverName, config] of Object.entries(options.resolvedMcp)) {
|
|
358
|
-
const mapped = {};
|
|
359
|
-
if (typeof config.url === "string")
|
|
360
|
-
mapped.url = config.url;
|
|
361
|
-
if (typeof config.command === "string")
|
|
362
|
-
mapped.command = config.command;
|
|
363
|
-
if (Array.isArray(config.args))
|
|
364
|
-
mapped.args = config.args;
|
|
365
|
-
if (isObject(config.env))
|
|
366
|
-
mapped.env = config.env;
|
|
367
|
-
mcpServers[serverName] = mapped;
|
|
368
|
-
}
|
|
369
|
-
parsed.mcp_servers = mcpServers;
|
|
370
545
|
if (!options.dryRun) {
|
|
371
546
|
ensureDir(codexDir);
|
|
372
547
|
writeTextAtomic(codexConfigPath, TOML.stringify(parsed));
|
|
@@ -374,9 +549,13 @@ function syncCodex(options) {
|
|
|
374
549
|
options.generated.add(codexConfigPath);
|
|
375
550
|
options.nextManifest.codex = {
|
|
376
551
|
roles: nextRoles.sort(),
|
|
377
|
-
mcpServers:
|
|
552
|
+
mcpServers: nextServers.sort(),
|
|
378
553
|
};
|
|
379
554
|
}
|
|
555
|
+
function resolveTrackedCodexEntries(trackedEntries, fallbackEntries) {
|
|
556
|
+
const tracked = Array.isArray(trackedEntries) ? trackedEntries : [];
|
|
557
|
+
return [...new Set([...tracked, ...fallbackEntries])].sort();
|
|
558
|
+
}
|
|
380
559
|
function buildCodexRoleToml(roleInstructionsPath, providerConfig) {
|
|
381
560
|
const roleToml = {
|
|
382
561
|
model_instructions_file: `./${path.basename(roleInstructionsPath)}`,
|
|
@@ -423,31 +602,34 @@ function maybeWriteJson(filePath, payload, dryRun) {
|
|
|
423
602
|
async function removeStaleGeneratedFiles(options) {
|
|
424
603
|
const oldSet = new Set(options.oldManifest.generatedFiles);
|
|
425
604
|
const newSet = new Set(options.newManifest.generatedFiles);
|
|
426
|
-
const stale = [...oldSet]
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
605
|
+
const stale = [...oldSet]
|
|
606
|
+
.filter((filePath) => !newSet.has(filePath))
|
|
607
|
+
.filter((filePath) => fs.existsSync(filePath));
|
|
608
|
+
if (stale.length === 0)
|
|
609
|
+
return [];
|
|
610
|
+
if (options.dryRun)
|
|
611
|
+
return stale;
|
|
612
|
+
if (!options.yes && !options.nonInteractive) {
|
|
613
|
+
const selected = await multiselect({
|
|
614
|
+
message: withMultiselectHelp("Remove stale generated files?"),
|
|
615
|
+
options: stale.map((filePath) => ({
|
|
616
|
+
value: filePath,
|
|
617
|
+
label: toPosixPath(filePath),
|
|
618
|
+
})),
|
|
619
|
+
initialValues: stale,
|
|
620
|
+
});
|
|
621
|
+
if (isCancel(selected))
|
|
622
|
+
return [];
|
|
623
|
+
const toRemove = Array.isArray(selected) ? selected : [];
|
|
624
|
+
for (const filePath of toRemove) {
|
|
625
|
+
removeFileIfExists(filePath);
|
|
446
626
|
}
|
|
627
|
+
return toRemove;
|
|
628
|
+
}
|
|
629
|
+
for (const filePath of stale) {
|
|
447
630
|
removeFileIfExists(filePath);
|
|
448
|
-
removed.push(filePath);
|
|
449
631
|
}
|
|
450
|
-
return
|
|
632
|
+
return stale;
|
|
451
633
|
}
|
|
452
634
|
function getVsCodeSettingsPath(homeDir) {
|
|
453
635
|
switch (os.platform()) {
|
|
@@ -464,6 +646,88 @@ function getVsCodeSettingsPath(homeDir) {
|
|
|
464
646
|
return path.join(homeDir, ".config", "Code", "User", "settings.json");
|
|
465
647
|
}
|
|
466
648
|
}
|
|
649
|
+
function normalizeGeneratedByEntity(manifest) {
|
|
650
|
+
const source = manifest.generatedByEntity;
|
|
651
|
+
if (!source || typeof source !== "object") {
|
|
652
|
+
return inferGeneratedByEntityFromLegacyFiles(manifest.generatedFiles);
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
agent: Array.isArray(source.agent) ? [...source.agent] : [],
|
|
656
|
+
command: Array.isArray(source.command) ? [...source.command] : [],
|
|
657
|
+
mcp: Array.isArray(source.mcp) ? [...source.mcp] : [],
|
|
658
|
+
skill: Array.isArray(source.skill) ? [...source.skill] : [],
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function inferGeneratedByEntityFromLegacyFiles(generatedFiles) {
|
|
662
|
+
const byEntity = {
|
|
663
|
+
agent: [],
|
|
664
|
+
command: [],
|
|
665
|
+
mcp: [],
|
|
666
|
+
skill: [],
|
|
667
|
+
};
|
|
668
|
+
for (const filePath of generatedFiles) {
|
|
669
|
+
for (const entity of classifyLegacyGeneratedFile(filePath)) {
|
|
670
|
+
byEntity[entity]?.push(filePath);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return pruneGeneratedByEntity(byEntity);
|
|
674
|
+
}
|
|
675
|
+
function classifyLegacyGeneratedFile(filePath) {
|
|
676
|
+
const normalized = toPosixPath(filePath).toLowerCase();
|
|
677
|
+
if (isLegacyCodexConfigPath(normalized)) {
|
|
678
|
+
return ["agent", "mcp"];
|
|
679
|
+
}
|
|
680
|
+
if (isLegacyCommandOutputPath(normalized)) {
|
|
681
|
+
return ["command"];
|
|
682
|
+
}
|
|
683
|
+
if (isLegacyAgentOutputPath(normalized)) {
|
|
684
|
+
return ["agent"];
|
|
685
|
+
}
|
|
686
|
+
if (isLegacyMcpOutputPath(normalized)) {
|
|
687
|
+
return ["mcp"];
|
|
688
|
+
}
|
|
689
|
+
// Preserve unknown generated paths during scoped syncs.
|
|
690
|
+
return ["agent", "command", "mcp"];
|
|
691
|
+
}
|
|
692
|
+
function isLegacyCommandOutputPath(normalizedPath) {
|
|
693
|
+
return (normalizedPath.includes("/.cursor/commands/") ||
|
|
694
|
+
normalizedPath.includes("/.claude/commands/") ||
|
|
695
|
+
normalizedPath.includes("/.opencode/commands/") ||
|
|
696
|
+
normalizedPath.includes("/.gemini/commands/") ||
|
|
697
|
+
normalizedPath.includes("/.github/prompts/") ||
|
|
698
|
+
normalizedPath.includes("/.codex/prompts/"));
|
|
699
|
+
}
|
|
700
|
+
function isLegacyAgentOutputPath(normalizedPath) {
|
|
701
|
+
return (normalizedPath.includes("/.cursor/agents/") ||
|
|
702
|
+
normalizedPath.includes("/.cursor/rules/") ||
|
|
703
|
+
normalizedPath.includes("/.claude/agents/") ||
|
|
704
|
+
normalizedPath.includes("/.opencode/agents/") ||
|
|
705
|
+
normalizedPath.includes("/.gemini/agents/") ||
|
|
706
|
+
normalizedPath.includes("/.github/agents/") ||
|
|
707
|
+
normalizedPath.includes("/.codex/agents/"));
|
|
708
|
+
}
|
|
709
|
+
function isLegacyMcpOutputPath(normalizedPath) {
|
|
710
|
+
return (normalizedPath.endsWith("/.cursor/mcp.json") ||
|
|
711
|
+
normalizedPath.endsWith("/.mcp.json") ||
|
|
712
|
+
normalizedPath.endsWith("/.claude/settings.json") ||
|
|
713
|
+
normalizedPath.endsWith("/.opencode/opencode.json") ||
|
|
714
|
+
normalizedPath.endsWith("/.gemini/settings.json") ||
|
|
715
|
+
normalizedPath.endsWith("/.vscode/mcp.json") ||
|
|
716
|
+
normalizedPath.endsWith("/code/user/settings.json"));
|
|
717
|
+
}
|
|
718
|
+
function isLegacyCodexConfigPath(normalizedPath) {
|
|
719
|
+
return normalizedPath.endsWith("/.codex/config.toml");
|
|
720
|
+
}
|
|
721
|
+
function pruneGeneratedByEntity(value) {
|
|
722
|
+
const next = {};
|
|
723
|
+
for (const entity of ["agent", "command", "mcp", "skill"]) {
|
|
724
|
+
const files = value[entity];
|
|
725
|
+
if (!files || files.length === 0)
|
|
726
|
+
continue;
|
|
727
|
+
next[entity] = [...new Set(files)].sort();
|
|
728
|
+
}
|
|
729
|
+
return next;
|
|
730
|
+
}
|
|
467
731
|
export function formatSyncSummary(summary, agentsRoot) {
|
|
468
732
|
const generated = summary.generatedFiles
|
|
469
733
|
.map((filePath) => relativePosix(agentsRoot, filePath))
|