agentloom 0.1.0
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/LICENSE +21 -0
- package/README.md +234 -0
- package/ThirdPartyNoticeText.txt +3 -0
- package/bin/cli.mjs +8 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +61 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +62 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +188 -0
- package/dist/commands/skills.d.ts +1 -0
- package/dist/commands/skills.js +11 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +25 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +71 -0
- package/dist/core/agents.d.ts +7 -0
- package/dist/core/agents.js +67 -0
- package/dist/core/argv.d.ts +5 -0
- package/dist/core/argv.js +52 -0
- package/dist/core/copy.d.ts +16 -0
- package/dist/core/copy.js +167 -0
- package/dist/core/fs.d.ts +13 -0
- package/dist/core/fs.js +70 -0
- package/dist/core/importer.d.ts +21 -0
- package/dist/core/importer.js +201 -0
- package/dist/core/lockfile.d.ts +4 -0
- package/dist/core/lockfile.js +25 -0
- package/dist/core/manifest.d.ts +3 -0
- package/dist/core/manifest.js +17 -0
- package/dist/core/mcp.d.ts +4 -0
- package/dist/core/mcp.js +73 -0
- package/dist/core/scope.d.ts +9 -0
- package/dist/core/scope.js +64 -0
- package/dist/core/settings.d.ts +6 -0
- package/dist/core/settings.js +54 -0
- package/dist/core/sources.d.ts +20 -0
- package/dist/core/sources.js +162 -0
- package/dist/core/version-notifier.d.ts +8 -0
- package/dist/core/version-notifier.js +142 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/core/version.js +25 -0
- package/dist/sync/index.d.ts +15 -0
- package/dist/sync/index.js +482 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +8 -0
- package/package.json +60 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ensureDir, writeJsonAtomic } from "./fs.js";
|
|
6
|
+
const UPDATE_CACHE_PATH = path.join(os.homedir(), ".agents", ".agentloom-version-cache.json");
|
|
7
|
+
const CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
8
|
+
const REQUEST_TIMEOUT_MS = 1800;
|
|
9
|
+
export async function maybeNotifyVersionUpdate(options) {
|
|
10
|
+
if (process.env.AGENTLOOM_DISABLE_UPDATE_NOTIFIER === "1")
|
|
11
|
+
return;
|
|
12
|
+
if (!process.stdout.isTTY || !process.stderr.isTTY)
|
|
13
|
+
return;
|
|
14
|
+
const loweredCommand = options.command.toLowerCase();
|
|
15
|
+
if (loweredCommand === "help" ||
|
|
16
|
+
loweredCommand === "--help" ||
|
|
17
|
+
loweredCommand === "-h" ||
|
|
18
|
+
loweredCommand === "version" ||
|
|
19
|
+
loweredCommand === "--version" ||
|
|
20
|
+
loweredCommand === "-v") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const packageName = options.packageName ?? "agentloom";
|
|
24
|
+
const cache = readVersionCache();
|
|
25
|
+
if (cache.latestVersion &&
|
|
26
|
+
isNewerVersion(cache.latestVersion, options.currentVersion) &&
|
|
27
|
+
cache.lastNotifiedVersion !== cache.latestVersion) {
|
|
28
|
+
printNotice(options.currentVersion, cache.latestVersion);
|
|
29
|
+
cache.lastNotifiedVersion = cache.latestVersion;
|
|
30
|
+
writeVersionCache(cache);
|
|
31
|
+
}
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const lastChecked = parseTime(cache.lastCheckedAt);
|
|
34
|
+
if (lastChecked && now - lastChecked < CHECK_INTERVAL_MS) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const latest = await fetchLatestVersion(packageName);
|
|
38
|
+
if (!latest) {
|
|
39
|
+
cache.lastCheckedAt = new Date(now).toISOString();
|
|
40
|
+
writeVersionCache(cache);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
cache.lastCheckedAt = new Date(now).toISOString();
|
|
44
|
+
cache.latestVersion = latest;
|
|
45
|
+
if (isNewerVersion(latest, options.currentVersion) &&
|
|
46
|
+
cache.lastNotifiedVersion !== latest) {
|
|
47
|
+
printNotice(options.currentVersion, latest);
|
|
48
|
+
cache.lastNotifiedVersion = latest;
|
|
49
|
+
}
|
|
50
|
+
writeVersionCache(cache);
|
|
51
|
+
}
|
|
52
|
+
export function isNewerVersion(candidate, current) {
|
|
53
|
+
const next = parseSemver(candidate);
|
|
54
|
+
const base = parseSemver(current);
|
|
55
|
+
if (!next || !base)
|
|
56
|
+
return false;
|
|
57
|
+
for (let index = 0; index < 3; index += 1) {
|
|
58
|
+
if (next[index] > base[index])
|
|
59
|
+
return true;
|
|
60
|
+
if (next[index] < base[index])
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
function parseSemver(input) {
|
|
66
|
+
const normalized = input.replace(/^v/i, "").split("-")[0];
|
|
67
|
+
const parts = normalized.split(".");
|
|
68
|
+
if (parts.length < 3)
|
|
69
|
+
return null;
|
|
70
|
+
const numbers = parts.slice(0, 3).map((part) => Number(part));
|
|
71
|
+
if (numbers.some((num) => Number.isNaN(num) || num < 0))
|
|
72
|
+
return null;
|
|
73
|
+
return [numbers[0], numbers[1], numbers[2]];
|
|
74
|
+
}
|
|
75
|
+
function fetchLatestVersion(packageName) {
|
|
76
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const req = https.get(url, {
|
|
79
|
+
headers: {
|
|
80
|
+
Accept: "application/json",
|
|
81
|
+
},
|
|
82
|
+
}, (res) => {
|
|
83
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
84
|
+
resolve(null);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const chunks = [];
|
|
88
|
+
res.on("data", (chunk) => {
|
|
89
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
90
|
+
});
|
|
91
|
+
res.on("end", () => {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
94
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
95
|
+
resolve(parsed.version.trim());
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// ignore parse errors
|
|
101
|
+
}
|
|
102
|
+
resolve(null);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
req.setTimeout(REQUEST_TIMEOUT_MS, () => {
|
|
106
|
+
req.destroy();
|
|
107
|
+
resolve(null);
|
|
108
|
+
});
|
|
109
|
+
req.on("error", () => resolve(null));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function printNotice(current, latest) {
|
|
113
|
+
console.error(`\nUpdate available for agentloom: ${current} -> ${latest}\nRun: npm i -g agentloom\n`);
|
|
114
|
+
}
|
|
115
|
+
function readVersionCache() {
|
|
116
|
+
try {
|
|
117
|
+
if (!fs.existsSync(UPDATE_CACHE_PATH))
|
|
118
|
+
return {};
|
|
119
|
+
const parsed = JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, "utf8"));
|
|
120
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function writeVersionCache(cache) {
|
|
127
|
+
try {
|
|
128
|
+
ensureDir(path.dirname(UPDATE_CACHE_PATH));
|
|
129
|
+
writeJsonAtomic(UPDATE_CACHE_PATH, cache);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// best-effort only
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function parseTime(value) {
|
|
136
|
+
if (!value)
|
|
137
|
+
return null;
|
|
138
|
+
const parsed = Date.parse(value);
|
|
139
|
+
if (Number.isNaN(parsed))
|
|
140
|
+
return null;
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getCliVersion(): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
let cachedVersion = null;
|
|
3
|
+
function readPackageVersion() {
|
|
4
|
+
try {
|
|
5
|
+
const packageUrl = new URL("../../package.json", import.meta.url);
|
|
6
|
+
const raw = fs.readFileSync(packageUrl, "utf8");
|
|
7
|
+
const parsed = JSON.parse(raw);
|
|
8
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
9
|
+
return parsed.version.trim();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// fall through
|
|
14
|
+
}
|
|
15
|
+
return "0.0.0";
|
|
16
|
+
}
|
|
17
|
+
export function getCliVersion() {
|
|
18
|
+
if (typeof process.env.npm_package_version === "string") {
|
|
19
|
+
return process.env.npm_package_version;
|
|
20
|
+
}
|
|
21
|
+
if (cachedVersion === null) {
|
|
22
|
+
cachedVersion = readPackageVersion();
|
|
23
|
+
}
|
|
24
|
+
return cachedVersion;
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Provider, ScopePaths } from "../types.js";
|
|
2
|
+
export interface SyncOptions {
|
|
3
|
+
paths: ScopePaths;
|
|
4
|
+
providers?: Provider[];
|
|
5
|
+
yes?: boolean;
|
|
6
|
+
nonInteractive?: boolean;
|
|
7
|
+
dryRun?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface SyncSummary {
|
|
10
|
+
providers: Provider[];
|
|
11
|
+
generatedFiles: string[];
|
|
12
|
+
removedFiles: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function syncFromCanonical(options: SyncOptions): Promise<SyncSummary>;
|
|
15
|
+
export declare function formatSyncSummary(summary: SyncSummary, agentsRoot: string): string;
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { confirm, isCancel } from "@clack/prompts";
|
|
5
|
+
import TOML from "@iarna/toml";
|
|
6
|
+
import YAML from "yaml";
|
|
7
|
+
import { getProviderConfig, isProviderEnabled, parseAgentsDir, } from "../core/agents.js";
|
|
8
|
+
import { ensureDir, isObject, readJsonIfExists, relativePosix, removeFileIfExists, slugify, toPosixPath, writeJsonAtomic, writeTextAtomic, } from "../core/fs.js";
|
|
9
|
+
import { readManifest, writeManifest } from "../core/manifest.js";
|
|
10
|
+
import { readCanonicalMcp, resolveMcpForProvider } from "../core/mcp.js";
|
|
11
|
+
import { getGlobalSettingsPath, readSettings, updateLastScope, } from "../core/settings.js";
|
|
12
|
+
export async function syncFromCanonical(options) {
|
|
13
|
+
const agents = parseAgentsDir(options.paths.agentsDir);
|
|
14
|
+
const mcp = readCanonicalMcp(options.paths);
|
|
15
|
+
const manifest = readManifest(options.paths);
|
|
16
|
+
const settings = readSettings(options.paths.settingsPath);
|
|
17
|
+
const providers = resolveProviders(options.providers, settings);
|
|
18
|
+
const nextManifest = {
|
|
19
|
+
version: 1,
|
|
20
|
+
generatedFiles: [],
|
|
21
|
+
codex: {
|
|
22
|
+
roles: [],
|
|
23
|
+
mcpServers: [],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
const generated = new Set();
|
|
27
|
+
for (const provider of providers) {
|
|
28
|
+
syncProviderAgents({
|
|
29
|
+
provider,
|
|
30
|
+
paths: options.paths,
|
|
31
|
+
agents,
|
|
32
|
+
generated,
|
|
33
|
+
dryRun: !!options.dryRun,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
syncProviderMcp({
|
|
37
|
+
providers,
|
|
38
|
+
paths: options.paths,
|
|
39
|
+
agents,
|
|
40
|
+
mcp,
|
|
41
|
+
generated,
|
|
42
|
+
manifest,
|
|
43
|
+
nextManifest,
|
|
44
|
+
dryRun: !!options.dryRun,
|
|
45
|
+
});
|
|
46
|
+
nextManifest.generatedFiles = [...generated].sort();
|
|
47
|
+
const removedFiles = await removeStaleGeneratedFiles({
|
|
48
|
+
oldManifest: manifest,
|
|
49
|
+
newManifest: nextManifest,
|
|
50
|
+
dryRun: !!options.dryRun,
|
|
51
|
+
yes: !!options.yes,
|
|
52
|
+
nonInteractive: !!options.nonInteractive,
|
|
53
|
+
});
|
|
54
|
+
if (!options.dryRun) {
|
|
55
|
+
writeManifest(options.paths, nextManifest);
|
|
56
|
+
updateLastScope(options.paths.settingsPath, options.paths.scope, providers);
|
|
57
|
+
updateLastScope(getGlobalSettingsPath(options.paths.homeDir), options.paths.scope);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
providers,
|
|
61
|
+
generatedFiles: nextManifest.generatedFiles,
|
|
62
|
+
removedFiles,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function resolveProviders(explicitProviders, settings) {
|
|
66
|
+
if (explicitProviders && explicitProviders.length > 0) {
|
|
67
|
+
return [...new Set(explicitProviders)];
|
|
68
|
+
}
|
|
69
|
+
if (settings.defaultProviders && settings.defaultProviders.length > 0) {
|
|
70
|
+
return [...new Set(settings.defaultProviders)];
|
|
71
|
+
}
|
|
72
|
+
return ["cursor", "claude", "codex", "opencode", "gemini", "copilot"];
|
|
73
|
+
}
|
|
74
|
+
function syncProviderAgents(options) {
|
|
75
|
+
const providerDir = getProviderAgentsDir(options.paths, options.provider);
|
|
76
|
+
for (const agent of options.agents) {
|
|
77
|
+
if (!isProviderEnabled(agent.frontmatter, options.provider))
|
|
78
|
+
continue;
|
|
79
|
+
const providerConfig = getProviderConfig(agent.frontmatter, options.provider);
|
|
80
|
+
if (providerConfig === null)
|
|
81
|
+
continue;
|
|
82
|
+
if (options.provider === "codex") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const fileName = options.provider === "copilot"
|
|
86
|
+
? `${slugify(agent.name) || "agent"}.agent.md`
|
|
87
|
+
: options.provider === "cursor"
|
|
88
|
+
? `${slugify(agent.name) || "agent"}.mdc`
|
|
89
|
+
: `${slugify(agent.name) || "agent"}.md`;
|
|
90
|
+
const outputPath = path.join(providerDir, fileName);
|
|
91
|
+
const content = buildProviderAgentContent(options.provider, agent, providerConfig ?? {});
|
|
92
|
+
if (!options.dryRun) {
|
|
93
|
+
ensureDir(path.dirname(outputPath));
|
|
94
|
+
writeTextAtomic(outputPath, content);
|
|
95
|
+
}
|
|
96
|
+
options.generated.add(outputPath);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
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
|
+
const frontmatter = {
|
|
110
|
+
name: agent.name,
|
|
111
|
+
description: agent.description,
|
|
112
|
+
...providerConfig,
|
|
113
|
+
};
|
|
114
|
+
const fm = YAML.stringify(frontmatter).trimEnd();
|
|
115
|
+
return `---\n${fm}\n---\n\n${agent.body.trimStart()}${agent.body.endsWith("\n") ? "" : "\n"}`;
|
|
116
|
+
}
|
|
117
|
+
function getProviderAgentsDir(paths, provider) {
|
|
118
|
+
const workspaceRoot = paths.workspaceRoot;
|
|
119
|
+
const home = paths.homeDir;
|
|
120
|
+
switch (provider) {
|
|
121
|
+
case "cursor":
|
|
122
|
+
return paths.scope === "local"
|
|
123
|
+
? path.join(workspaceRoot, ".cursor", "rules")
|
|
124
|
+
: path.join(home, ".cursor", "rules");
|
|
125
|
+
case "claude":
|
|
126
|
+
return paths.scope === "local"
|
|
127
|
+
? path.join(workspaceRoot, ".claude", "agents")
|
|
128
|
+
: path.join(home, ".claude", "agents");
|
|
129
|
+
case "codex":
|
|
130
|
+
return paths.scope === "local"
|
|
131
|
+
? path.join(workspaceRoot, ".codex", "agents")
|
|
132
|
+
: path.join(home, ".codex", "agents");
|
|
133
|
+
case "opencode":
|
|
134
|
+
return paths.scope === "local"
|
|
135
|
+
? path.join(workspaceRoot, ".opencode", "agents")
|
|
136
|
+
: path.join(home, ".config", "opencode", "agents");
|
|
137
|
+
case "gemini":
|
|
138
|
+
return paths.scope === "local"
|
|
139
|
+
? path.join(workspaceRoot, ".gemini", "agents")
|
|
140
|
+
: path.join(home, ".gemini", "agents");
|
|
141
|
+
case "copilot":
|
|
142
|
+
return paths.scope === "local"
|
|
143
|
+
? path.join(workspaceRoot, ".github", "agents")
|
|
144
|
+
: path.join(home, ".vscode", "chatmodes");
|
|
145
|
+
default:
|
|
146
|
+
return path.join(workspaceRoot, ".agents", "unknown");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function syncProviderMcp(options) {
|
|
150
|
+
for (const provider of options.providers) {
|
|
151
|
+
const resolved = resolveMcpForProvider(options.mcp, provider);
|
|
152
|
+
if (provider === "cursor") {
|
|
153
|
+
const outputPath = options.paths.scope === "local"
|
|
154
|
+
? path.join(options.paths.workspaceRoot, ".cursor", "mcp.json")
|
|
155
|
+
: path.join(options.paths.homeDir, ".cursor", "mcp.json");
|
|
156
|
+
const payload = {
|
|
157
|
+
mcpServers: mapMcpServers(resolved, ["url", "command", "args", "env"]),
|
|
158
|
+
};
|
|
159
|
+
maybeWriteJson(outputPath, payload, options.dryRun);
|
|
160
|
+
options.generated.add(outputPath);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (provider === "claude") {
|
|
164
|
+
const mcpPath = options.paths.scope === "local"
|
|
165
|
+
? path.join(options.paths.workspaceRoot, ".mcp.json")
|
|
166
|
+
: path.join(options.paths.homeDir, ".mcp.json");
|
|
167
|
+
const settingsPath = options.paths.scope === "local"
|
|
168
|
+
? path.join(options.paths.workspaceRoot, ".claude", "settings.json")
|
|
169
|
+
: path.join(options.paths.homeDir, ".claude.json");
|
|
170
|
+
const claudeServers = mapMcpServers(resolved, [
|
|
171
|
+
"type",
|
|
172
|
+
"url",
|
|
173
|
+
"command",
|
|
174
|
+
"args",
|
|
175
|
+
"env",
|
|
176
|
+
]);
|
|
177
|
+
for (const [serverName, config] of Object.entries(claudeServers)) {
|
|
178
|
+
if (!("type" in config) && typeof config.url === "string") {
|
|
179
|
+
config.type = "http";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
maybeWriteJson(mcpPath, { mcpServers: claudeServers }, options.dryRun);
|
|
183
|
+
options.generated.add(mcpPath);
|
|
184
|
+
const settings = readJsonIfExists(settingsPath) ?? {};
|
|
185
|
+
settings.enabledMcpjsonServers = Object.keys(claudeServers).sort();
|
|
186
|
+
maybeWriteJson(settingsPath, settings, options.dryRun);
|
|
187
|
+
options.generated.add(settingsPath);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
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
|
+
if (provider === "opencode") {
|
|
203
|
+
const outputPath = options.paths.scope === "local"
|
|
204
|
+
? path.join(options.paths.workspaceRoot, ".opencode", "opencode.json")
|
|
205
|
+
: path.join(options.paths.homeDir, ".config", "opencode", "opencode.json");
|
|
206
|
+
const existing = readJsonIfExists(outputPath) ?? {};
|
|
207
|
+
const mcp = {};
|
|
208
|
+
for (const [serverName, config] of Object.entries(resolved)) {
|
|
209
|
+
if (typeof config.url === "string") {
|
|
210
|
+
mcp[serverName] = {
|
|
211
|
+
type: "remote",
|
|
212
|
+
url: config.url,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
mcp[serverName] = {
|
|
217
|
+
type: "local",
|
|
218
|
+
command: config.command,
|
|
219
|
+
args: Array.isArray(config.args) ? config.args : undefined,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (isObject(config.env)) {
|
|
223
|
+
mcp[serverName].environment = config.env;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const payload = {
|
|
227
|
+
...existing,
|
|
228
|
+
mcp,
|
|
229
|
+
};
|
|
230
|
+
maybeWriteJson(outputPath, payload, options.dryRun);
|
|
231
|
+
options.generated.add(outputPath);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (provider === "gemini") {
|
|
235
|
+
const outputPath = options.paths.scope === "local"
|
|
236
|
+
? path.join(options.paths.workspaceRoot, ".gemini", "settings.json")
|
|
237
|
+
: path.join(options.paths.homeDir, ".gemini", "settings.json");
|
|
238
|
+
const existing = readJsonIfExists(outputPath) ?? {};
|
|
239
|
+
const experimental = isObject(existing.experimental)
|
|
240
|
+
? { ...existing.experimental }
|
|
241
|
+
: {};
|
|
242
|
+
experimental.enableAgents = true;
|
|
243
|
+
const mcpServers = {};
|
|
244
|
+
for (const [serverName, config] of Object.entries(resolved)) {
|
|
245
|
+
const mapped = {};
|
|
246
|
+
if (typeof config.url === "string")
|
|
247
|
+
mapped.httpUrl = config.url;
|
|
248
|
+
if (typeof config.command === "string")
|
|
249
|
+
mapped.command = config.command;
|
|
250
|
+
if (Array.isArray(config.args))
|
|
251
|
+
mapped.args = config.args;
|
|
252
|
+
if (isObject(config.env))
|
|
253
|
+
mapped.env = config.env;
|
|
254
|
+
mcpServers[serverName] = mapped;
|
|
255
|
+
}
|
|
256
|
+
const payload = {
|
|
257
|
+
...existing,
|
|
258
|
+
experimental,
|
|
259
|
+
mcpServers,
|
|
260
|
+
};
|
|
261
|
+
maybeWriteJson(outputPath, payload, options.dryRun);
|
|
262
|
+
options.generated.add(outputPath);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (provider === "copilot") {
|
|
266
|
+
const profileMcpPath = options.paths.scope === "local"
|
|
267
|
+
? path.join(options.paths.workspaceRoot, ".vscode", "mcp.json")
|
|
268
|
+
: path.join(options.paths.homeDir, ".vscode", "mcp.json");
|
|
269
|
+
const copilotServers = mapMcpServers(resolved, [
|
|
270
|
+
"type",
|
|
271
|
+
"url",
|
|
272
|
+
"command",
|
|
273
|
+
"args",
|
|
274
|
+
"env",
|
|
275
|
+
"tools",
|
|
276
|
+
]);
|
|
277
|
+
for (const config of Object.values(copilotServers)) {
|
|
278
|
+
if (!Array.isArray(config.tools)) {
|
|
279
|
+
config.tools = ["*"];
|
|
280
|
+
}
|
|
281
|
+
if (!config.type) {
|
|
282
|
+
config.type = config.url ? "http" : "local";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
maybeWriteJson(profileMcpPath, { mcpServers: copilotServers }, options.dryRun);
|
|
286
|
+
options.generated.add(profileMcpPath);
|
|
287
|
+
if (options.paths.scope === "global") {
|
|
288
|
+
const settingsPath = getVsCodeSettingsPath(options.paths.homeDir);
|
|
289
|
+
const settings = readJsonIfExists(settingsPath) ?? {};
|
|
290
|
+
settings["mcp.servers"] = copilotServers;
|
|
291
|
+
maybeWriteJson(settingsPath, settings, options.dryRun);
|
|
292
|
+
options.generated.add(settingsPath);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function syncCodex(options) {
|
|
298
|
+
const codexDir = options.paths.scope === "local"
|
|
299
|
+
? path.join(options.paths.workspaceRoot, ".codex")
|
|
300
|
+
: path.join(options.paths.homeDir, ".codex");
|
|
301
|
+
const codexConfigPath = path.join(codexDir, "config.toml");
|
|
302
|
+
const codexAgentsDir = path.join(codexDir, "agents");
|
|
303
|
+
const rawConfig = fs.existsSync(codexConfigPath)
|
|
304
|
+
? fs.readFileSync(codexConfigPath, "utf8")
|
|
305
|
+
: "";
|
|
306
|
+
const parsed = rawConfig.trim()
|
|
307
|
+
? TOML.parse(rawConfig)
|
|
308
|
+
: {};
|
|
309
|
+
const features = isObject(parsed.features) ? { ...parsed.features } : {};
|
|
310
|
+
features.multi_agent = true;
|
|
311
|
+
parsed.features = features;
|
|
312
|
+
const agentsTable = isObject(parsed.agents) ? { ...parsed.agents } : {};
|
|
313
|
+
const previousRoles = new Set(options.manifest.codex?.roles ?? []);
|
|
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 ?? []);
|
|
349
|
+
const mcpServers = isObject(parsed.mcp_servers)
|
|
350
|
+
? { ...parsed.mcp_servers }
|
|
351
|
+
: {};
|
|
352
|
+
for (const oldServer of previousServers) {
|
|
353
|
+
if (!Object.prototype.hasOwnProperty.call(options.resolvedMcp, oldServer)) {
|
|
354
|
+
delete mcpServers[oldServer];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
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
|
+
if (!options.dryRun) {
|
|
371
|
+
ensureDir(codexDir);
|
|
372
|
+
writeTextAtomic(codexConfigPath, TOML.stringify(parsed));
|
|
373
|
+
}
|
|
374
|
+
options.generated.add(codexConfigPath);
|
|
375
|
+
options.nextManifest.codex = {
|
|
376
|
+
roles: nextRoles.sort(),
|
|
377
|
+
mcpServers: Object.keys(options.resolvedMcp).sort(),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function buildCodexRoleToml(roleInstructionsPath, providerConfig) {
|
|
381
|
+
const roleToml = {
|
|
382
|
+
model_instructions_file: `./${path.basename(roleInstructionsPath)}`,
|
|
383
|
+
};
|
|
384
|
+
if (typeof providerConfig.model === "string") {
|
|
385
|
+
roleToml.model = providerConfig.model;
|
|
386
|
+
}
|
|
387
|
+
if (typeof providerConfig.reasoningEffort === "string") {
|
|
388
|
+
roleToml.model_reasoning_effort = providerConfig.reasoningEffort;
|
|
389
|
+
}
|
|
390
|
+
if (typeof providerConfig.approvalPolicy === "string") {
|
|
391
|
+
roleToml.approval_policy = providerConfig.approvalPolicy;
|
|
392
|
+
}
|
|
393
|
+
if (typeof providerConfig.sandboxMode === "string") {
|
|
394
|
+
roleToml.sandbox_mode = providerConfig.sandboxMode;
|
|
395
|
+
}
|
|
396
|
+
if (typeof providerConfig.webSearch === "boolean") {
|
|
397
|
+
roleToml.tools = {
|
|
398
|
+
web_search: providerConfig.webSearch,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return roleToml;
|
|
402
|
+
}
|
|
403
|
+
function mapMcpServers(servers, allowedKeys) {
|
|
404
|
+
const allowed = new Set(allowedKeys);
|
|
405
|
+
const mapped = {};
|
|
406
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
407
|
+
const next = {};
|
|
408
|
+
for (const [key, value] of Object.entries(config)) {
|
|
409
|
+
if (allowed.has(key)) {
|
|
410
|
+
next[key] = value;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
mapped[name] = next;
|
|
414
|
+
}
|
|
415
|
+
return mapped;
|
|
416
|
+
}
|
|
417
|
+
function maybeWriteJson(filePath, payload, dryRun) {
|
|
418
|
+
if (dryRun)
|
|
419
|
+
return;
|
|
420
|
+
ensureDir(path.dirname(filePath));
|
|
421
|
+
writeJsonAtomic(filePath, payload);
|
|
422
|
+
}
|
|
423
|
+
async function removeStaleGeneratedFiles(options) {
|
|
424
|
+
const oldSet = new Set(options.oldManifest.generatedFiles);
|
|
425
|
+
const newSet = new Set(options.newManifest.generatedFiles);
|
|
426
|
+
const stale = [...oldSet].filter((filePath) => !newSet.has(filePath));
|
|
427
|
+
const removed = [];
|
|
428
|
+
for (const filePath of stale) {
|
|
429
|
+
if (!fs.existsSync(filePath))
|
|
430
|
+
continue;
|
|
431
|
+
if (options.dryRun) {
|
|
432
|
+
removed.push(filePath);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (!options.yes && !options.nonInteractive) {
|
|
436
|
+
const shouldDelete = await confirm({
|
|
437
|
+
message: `Remove stale generated file ${toPosixPath(filePath)}?`,
|
|
438
|
+
initialValue: true,
|
|
439
|
+
});
|
|
440
|
+
if (isCancel(shouldDelete)) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (!shouldDelete) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
removeFileIfExists(filePath);
|
|
448
|
+
removed.push(filePath);
|
|
449
|
+
}
|
|
450
|
+
return removed;
|
|
451
|
+
}
|
|
452
|
+
function getVsCodeSettingsPath(homeDir) {
|
|
453
|
+
switch (os.platform()) {
|
|
454
|
+
case "darwin":
|
|
455
|
+
return path.join(homeDir, "Library", "Application Support", "Code", "User", "settings.json");
|
|
456
|
+
case "win32": {
|
|
457
|
+
const appData = process.env.APPDATA;
|
|
458
|
+
if (!appData) {
|
|
459
|
+
return path.join(homeDir, "AppData", "Roaming", "Code", "User", "settings.json");
|
|
460
|
+
}
|
|
461
|
+
return path.join(appData, "Code", "User", "settings.json");
|
|
462
|
+
}
|
|
463
|
+
default:
|
|
464
|
+
return path.join(homeDir, ".config", "Code", "User", "settings.json");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
export function formatSyncSummary(summary, agentsRoot) {
|
|
468
|
+
const generated = summary.generatedFiles
|
|
469
|
+
.map((filePath) => relativePosix(agentsRoot, filePath))
|
|
470
|
+
.sort();
|
|
471
|
+
const removed = summary.removedFiles
|
|
472
|
+
.map((filePath) => relativePosix(agentsRoot, filePath))
|
|
473
|
+
.sort();
|
|
474
|
+
const lines = [
|
|
475
|
+
`Providers: ${summary.providers.join(", ")}`,
|
|
476
|
+
`Generated/updated files: ${generated.length}`,
|
|
477
|
+
];
|
|
478
|
+
if (removed.length > 0) {
|
|
479
|
+
lines.push(`Removed stale files: ${removed.length}`);
|
|
480
|
+
}
|
|
481
|
+
return lines.join("\n");
|
|
482
|
+
}
|