dxai-cli 1.0.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 +172 -0
- package/bin/cli.js +272 -0
- package/package.json +64 -0
- package/src/auto-update.js +106 -0
- package/src/branding.js +80 -0
- package/src/cleanup.js +615 -0
- package/src/config-remover.js +325 -0
- package/src/config-writer.js +781 -0
- package/src/detect-project.js +316 -0
- package/src/detect.js +587 -0
- package/src/fs-atomic.js +35 -0
- package/src/handshake.js +123 -0
- package/src/index.js +966 -0
- package/src/inspect.js +283 -0
- package/src/manifest.js +179 -0
- package/src/mcp-cmd.js +282 -0
- package/src/net.js +72 -0
- package/src/profile.js +139 -0
- package/src/registry/automation-tools.js +6 -0
- package/src/registry/data/automation-tools.json +37 -0
- package/src/registry/data/mcp-servers.json +451 -0
- package/src/registry/data/skills.json +132 -0
- package/src/registry/loader.js +102 -0
- package/src/registry/mcp-registry.js +292 -0
- package/src/registry/mcp-servers.js +72 -0
- package/src/registry/skills.js +10 -0
- package/src/registry/stacks.js +769 -0
- package/src/registry/validate.js +209 -0
- package/src/rollback.js +182 -0
- package/src/runtime.js +40 -0
- package/src/select.js +72 -0
- package/src/update.js +126 -0
package/src/inspect.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
printBanner, sectionHeader, successMsg, warnMsg, errorMsg, infoMsg, theme,
|
|
6
|
+
} from './branding.js';
|
|
7
|
+
import { detectOS, AGENT_DEFINITIONS, commandExists } from './detect.js';
|
|
8
|
+
import { MCP_SERVERS } from './registry/mcp-servers.js';
|
|
9
|
+
import {
|
|
10
|
+
readManifest, SYSTEM_MANIFEST_PATH, PROJECT_MANIFEST_PATH,
|
|
11
|
+
} from './manifest.js';
|
|
12
|
+
import {
|
|
13
|
+
scanJsonMcpConfig, scanTomlMcpConfig, scanClaudeCodeMcpServers,
|
|
14
|
+
} from './config-remover.js';
|
|
15
|
+
import { handshakeServer, resolveSpawnSpec } from './handshake.js';
|
|
16
|
+
|
|
17
|
+
const KNOWN_MCP_IDS = MCP_SERVERS.map((s) => s.id);
|
|
18
|
+
|
|
19
|
+
function loadBoth(cwd = process.cwd()) {
|
|
20
|
+
const system = readManifest(SYSTEM_MANIFEST_PATH);
|
|
21
|
+
const project = readManifest(path.join(cwd, PROJECT_MANIFEST_PATH));
|
|
22
|
+
return { system, project };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── list ──
|
|
26
|
+
export async function listCmd(opts = {}) {
|
|
27
|
+
const { system, project } = loadBoth();
|
|
28
|
+
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
process.stdout.write(JSON.stringify({ ok: true, system, project }, null, 2) + '\n');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
printBanner();
|
|
35
|
+
|
|
36
|
+
sectionHeader('System (~/.dxai/manifest.json)');
|
|
37
|
+
if (system.agents.length === 0 && Object.keys(system.mcp).length === 0 && Object.keys(system.skills).length === 0) {
|
|
38
|
+
infoMsg('No system-level dxai installs recorded.');
|
|
39
|
+
} else {
|
|
40
|
+
if (system.agents.length > 0) successMsg(`Agents: ${system.agents.join(', ')}`);
|
|
41
|
+
for (const [agentId, servers] of Object.entries(system.mcp)) {
|
|
42
|
+
const list = Object.keys(servers).join(', ');
|
|
43
|
+
console.log(` ${theme.label(agentId.padEnd(14))} ${theme.dim('MCP:')} ${list}`);
|
|
44
|
+
}
|
|
45
|
+
if (Object.keys(system.skills).length > 0) {
|
|
46
|
+
console.log(` ${theme.label('skills'.padEnd(14))} ${theme.dim('→')} ${Object.keys(system.skills).join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log();
|
|
51
|
+
sectionHeader('Project (./.dxai/manifest.json)');
|
|
52
|
+
if (project.agents.length === 0 && Object.keys(project.mcp).length === 0 && project.files.length === 0) {
|
|
53
|
+
infoMsg('No project-level dxai installs recorded in cwd.');
|
|
54
|
+
} else {
|
|
55
|
+
if (project.agents.length > 0) successMsg(`Agents: ${project.agents.join(', ')}`);
|
|
56
|
+
for (const [agentId, servers] of Object.entries(project.mcp)) {
|
|
57
|
+
const list = Object.keys(servers).join(', ');
|
|
58
|
+
console.log(` ${theme.label(agentId.padEnd(14))} ${theme.dim('MCP:')} ${list}`);
|
|
59
|
+
}
|
|
60
|
+
if (project.files.length > 0) {
|
|
61
|
+
console.log(` ${theme.label('files'.padEnd(14))} ${theme.dim('→')} ${project.files.map((f) => f.relativePath).join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
console.log();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── status ──
|
|
68
|
+
// Diff manifest vs actual config files. Reports drift.
|
|
69
|
+
// `ids` is the catalogue plus whatever the manifest recorded — servers added
|
|
70
|
+
// live by registry name are not in the catalogue but must still be scanned.
|
|
71
|
+
function readActualMcp(agent, home, cwd, ids = KNOWN_MCP_IDS) {
|
|
72
|
+
const out = { global: [], project: [] };
|
|
73
|
+
|
|
74
|
+
if (agent.configFormat === 'json' && typeof agent.globalMcpPath === 'function') {
|
|
75
|
+
const legacy = typeof agent.legacyGlobalMcpPaths === 'function' ? agent.legacyGlobalMcpPaths(home) : [];
|
|
76
|
+
out.global = [...new Set([agent.globalMcpPath(home), ...legacy].flatMap((p) => scanJsonMcpConfig(p, agent.mcpKey, ids)))];
|
|
77
|
+
} else if (agent.configFormat === 'toml' && typeof agent.globalMcpPath === 'function') {
|
|
78
|
+
out.global = scanTomlMcpConfig(agent.globalMcpPath(home), ids);
|
|
79
|
+
} else if (agent.configFormat === 'cli') {
|
|
80
|
+
out.global = scanClaudeCodeMcpServers(ids);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof agent.projectMcpPath === 'function') {
|
|
84
|
+
const projPath = path.join(cwd, agent.projectMcpPath());
|
|
85
|
+
if (fs.existsSync(projPath)) {
|
|
86
|
+
out.project = (agent.projectConfigFormat || agent.configFormat) === 'toml'
|
|
87
|
+
? scanTomlMcpConfig(projPath, ids)
|
|
88
|
+
: scanJsonMcpConfig(projPath, agent.projectMcpKey || agent.mcpKey, ids);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function statusCmd(opts = {}) {
|
|
96
|
+
const { home } = detectOS();
|
|
97
|
+
const cwd = process.cwd();
|
|
98
|
+
const { system, project } = loadBoth(cwd);
|
|
99
|
+
|
|
100
|
+
const drift = { system: {}, project: {} };
|
|
101
|
+
|
|
102
|
+
for (const agent of AGENT_DEFINITIONS) {
|
|
103
|
+
const recordedSystem = Object.keys(system.mcp[agent.id] || {});
|
|
104
|
+
const recordedProject = Object.keys(project.mcp[agent.id] || {});
|
|
105
|
+
if (recordedSystem.length === 0 && recordedProject.length === 0) continue;
|
|
106
|
+
|
|
107
|
+
const actual = readActualMcp(agent, home, cwd, [...new Set([...KNOWN_MCP_IDS, ...recordedSystem, ...recordedProject])]);
|
|
108
|
+
|
|
109
|
+
// Missing: in manifest but not in config (someone removed it).
|
|
110
|
+
// Extra: in config but not in manifest (added outside dxai or by another tool).
|
|
111
|
+
const sysMissing = recordedSystem.filter((id) => !actual.global.includes(id));
|
|
112
|
+
const sysExtra = actual.global.filter((id) => !recordedSystem.includes(id));
|
|
113
|
+
const projMissing = recordedProject.filter((id) => !actual.project.includes(id));
|
|
114
|
+
const projExtra = actual.project.filter((id) => !recordedProject.includes(id));
|
|
115
|
+
|
|
116
|
+
if (sysMissing.length || sysExtra.length) {
|
|
117
|
+
drift.system[agent.id] = { missing: sysMissing, extra: sysExtra };
|
|
118
|
+
}
|
|
119
|
+
if (projMissing.length || projExtra.length) {
|
|
120
|
+
drift.project[agent.id] = { missing: projMissing, extra: projExtra };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Project files: did manifest-recorded files actually survive?
|
|
125
|
+
const fileDrift = [];
|
|
126
|
+
for (const f of project.files || []) {
|
|
127
|
+
const abs = path.join(cwd, f.relativePath);
|
|
128
|
+
if (!fs.existsSync(abs)) fileDrift.push(f.relativePath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const clean =
|
|
132
|
+
Object.keys(drift.system).length === 0 &&
|
|
133
|
+
Object.keys(drift.project).length === 0 &&
|
|
134
|
+
fileDrift.length === 0;
|
|
135
|
+
|
|
136
|
+
if (opts.json) {
|
|
137
|
+
process.stdout.write(JSON.stringify({ ok: true, clean, drift, fileDrift }, null, 2) + '\n');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
printBanner();
|
|
142
|
+
sectionHeader('Status — Manifest vs Actual Config');
|
|
143
|
+
|
|
144
|
+
if (clean) {
|
|
145
|
+
console.log();
|
|
146
|
+
successMsg('In sync. Manifest and live config agree.');
|
|
147
|
+
console.log();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const [agentId, d] of Object.entries(drift.system)) {
|
|
152
|
+
console.log();
|
|
153
|
+
console.log(theme.label(` System / ${agentId}`));
|
|
154
|
+
if (d.missing.length) warnMsg(`Missing in config (manifest expected): ${d.missing.join(', ')}`);
|
|
155
|
+
if (d.extra.length) infoMsg(`Extra in config (not added by dxai): ${d.extra.join(', ')}`);
|
|
156
|
+
}
|
|
157
|
+
for (const [agentId, d] of Object.entries(drift.project)) {
|
|
158
|
+
console.log();
|
|
159
|
+
console.log(theme.label(` Project / ${agentId}`));
|
|
160
|
+
if (d.missing.length) warnMsg(`Missing in config (manifest expected): ${d.missing.join(', ')}`);
|
|
161
|
+
if (d.extra.length) infoMsg(`Extra in config (not added by dxai): ${d.extra.join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
if (fileDrift.length > 0) {
|
|
164
|
+
console.log();
|
|
165
|
+
console.log(theme.label(' Project files'));
|
|
166
|
+
warnMsg(`Removed since install: ${fileDrift.join(', ')}`);
|
|
167
|
+
}
|
|
168
|
+
console.log();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── doctor ──
|
|
172
|
+
// Validate that configs parse, env vars are set, and (best-effort) MCP commands exist.
|
|
173
|
+
export async function doctorCmd(opts = {}) {
|
|
174
|
+
const { home } = detectOS();
|
|
175
|
+
const cwd = process.cwd();
|
|
176
|
+
const { system, project } = loadBoth(cwd);
|
|
177
|
+
|
|
178
|
+
const findings = [];
|
|
179
|
+
const ok = (msg) => findings.push({ severity: 'ok', msg });
|
|
180
|
+
const warn = (msg) => findings.push({ severity: 'warn', msg });
|
|
181
|
+
const fail = (msg) => findings.push({ severity: 'error', msg });
|
|
182
|
+
const info = (msg) => findings.push({ severity: 'info', msg });
|
|
183
|
+
|
|
184
|
+
// Config files parse.
|
|
185
|
+
for (const agent of AGENT_DEFINITIONS) {
|
|
186
|
+
if (typeof agent.globalMcpPath !== 'function') continue;
|
|
187
|
+
if (agent.configFormat === 'cli') continue; // no file to parse
|
|
188
|
+
const p = agent.globalMcpPath(home);
|
|
189
|
+
if (!fs.existsSync(p)) continue;
|
|
190
|
+
|
|
191
|
+
if (agent.configFormat === 'json') {
|
|
192
|
+
try {
|
|
193
|
+
fs.readJsonSync(p);
|
|
194
|
+
ok(`${agent.name}: config parses (${p})`);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
fail(`${agent.name}: config malformed at ${p} — ${err.message}`);
|
|
197
|
+
}
|
|
198
|
+
} else if (agent.configFormat === 'toml') {
|
|
199
|
+
try {
|
|
200
|
+
fs.readFileSync(p, 'utf-8'); // shallow check; deeper TOML parse optional
|
|
201
|
+
ok(`${agent.name}: config readable (${p})`);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
fail(`${agent.name}: config unreadable at ${p} — ${err.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Env vars for installed servers. Catalogue entries carry requiresEnv; servers
|
|
209
|
+
// added live by registry name carry the env var names in their manifest record.
|
|
210
|
+
const installed = new Map();
|
|
211
|
+
for (const servers of [...Object.values(system.mcp), ...Object.values(project.mcp)]) {
|
|
212
|
+
for (const [id, rec] of Object.entries(servers)) if (!installed.has(id)) installed.set(id, rec);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const [id, rec] of installed) {
|
|
216
|
+
const meta = MCP_SERVERS.find((s) => s.id === id);
|
|
217
|
+
const label = meta?.name || rec.registry || id;
|
|
218
|
+
const envEntries = meta?.requiresEnv
|
|
219
|
+
? Object.entries(meta.requiresEnv)
|
|
220
|
+
: (rec.requiresEnv || []).map((v) => [v, `required by ${label}`]);
|
|
221
|
+
for (const [envVar, desc] of envEntries) {
|
|
222
|
+
if (process.env[envVar]) ok(`env: ${envVar} set (${label})`);
|
|
223
|
+
else warn(`env: ${envVar} not set — needed by ${label} (${desc})`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Most MCP servers spawn via npx.
|
|
228
|
+
if (commandExists('npx')) ok('npx is on PATH');
|
|
229
|
+
else fail('npx not found on PATH — most MCP servers spawn via `npx`.');
|
|
230
|
+
|
|
231
|
+
// Project files referenced in manifest still exist.
|
|
232
|
+
for (const f of project.files || []) {
|
|
233
|
+
const abs = path.join(cwd, f.relativePath);
|
|
234
|
+
if (fs.existsSync(abs)) ok(`project file present: ${f.relativePath}`);
|
|
235
|
+
else warn(`project file missing: ${f.relativePath} (recorded in manifest)`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handshake (opt-in) — spawn each installed MCP server and verify JSON-RPC.
|
|
239
|
+
if (opts.handshake) {
|
|
240
|
+
for (const id of installed.keys()) {
|
|
241
|
+
const meta = MCP_SERVERS.find((s) => s.id === id);
|
|
242
|
+
if (!meta) { info(`handshake: ${id} is not in the catalogue — skipped`); continue; }
|
|
243
|
+
const spec = resolveSpawnSpec(meta);
|
|
244
|
+
if (!spec) {
|
|
245
|
+
info(`handshake: ${meta.name} is a remote/URL server — skipped`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (meta.requiresEnv && Object.keys(meta.requiresEnv).some((v) => !process.env[v])) {
|
|
249
|
+
warn(`handshake: ${meta.name} skipped — required env not set`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const res = await handshakeServer(spec, { timeoutMs: 10000 });
|
|
253
|
+
if (res.ok) ok(`handshake: ${meta.name} responded to initialize`);
|
|
254
|
+
else fail(`handshake: ${meta.name} failed — ${res.error}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const summary = {
|
|
259
|
+
ok: findings.filter((f) => f.severity === 'ok').length,
|
|
260
|
+
warn: findings.filter((f) => f.severity === 'warn').length,
|
|
261
|
+
error: findings.filter((f) => f.severity === 'error').length,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (opts.json) {
|
|
265
|
+
process.stdout.write(JSON.stringify({ ok: summary.error === 0, summary, findings }, null, 2) + '\n');
|
|
266
|
+
if (summary.error > 0) process.exit(1);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
printBanner();
|
|
271
|
+
sectionHeader('Doctor — Health Check');
|
|
272
|
+
console.log();
|
|
273
|
+
for (const f of findings) {
|
|
274
|
+
if (f.severity === 'ok') successMsg(f.msg);
|
|
275
|
+
else if (f.severity === 'warn') warnMsg(f.msg);
|
|
276
|
+
else if (f.severity === 'info') infoMsg(f.msg);
|
|
277
|
+
else errorMsg(f.msg);
|
|
278
|
+
}
|
|
279
|
+
console.log();
|
|
280
|
+
infoMsg(`${summary.ok} ok · ${summary.warn} warn · ${summary.error} error`);
|
|
281
|
+
console.log();
|
|
282
|
+
if (summary.error > 0) process.exit(1);
|
|
283
|
+
}
|
package/src/manifest.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { warnMsg } from './branding.js';
|
|
5
|
+
import { writeJsonAtomic } from './fs-atomic.js';
|
|
6
|
+
import { AGENT_ID_ALIASES } from './detect.js';
|
|
7
|
+
|
|
8
|
+
const HOME = os.homedir();
|
|
9
|
+
|
|
10
|
+
export const SYSTEM_MANIFEST_PATH = path.join(HOME, '.dxai', 'manifest.json');
|
|
11
|
+
export const PROJECT_MANIFEST_PATH = path.join('.dxai', 'manifest.json');
|
|
12
|
+
export const MANIFEST_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
export function emptyManifest() {
|
|
15
|
+
return {
|
|
16
|
+
version: MANIFEST_VERSION,
|
|
17
|
+
createdAt: null,
|
|
18
|
+
updatedAt: null,
|
|
19
|
+
agents: [],
|
|
20
|
+
mcp: {}, // { [agentId]: { [serverId]: { addedAt, configPath } } }
|
|
21
|
+
skills: {}, // { [skillId]: { addedAt, path } }
|
|
22
|
+
tools: {}, // { [toolId]: { addedAt } }
|
|
23
|
+
files: [], // [{ relativePath, addedAt }]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readManifest(filePath) {
|
|
28
|
+
if (!fs.existsSync(filePath)) return emptyManifest();
|
|
29
|
+
try {
|
|
30
|
+
const data = fs.readJsonSync(filePath);
|
|
31
|
+
// Future migrations would go here, gated on data.version.
|
|
32
|
+
return migrateAgentIds({ ...emptyManifest(), ...data });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// A corrupt manifest must not silently read as "nothing installed" — that
|
|
35
|
+
// would hide real installs from `list`/`status` and let a subsequent write
|
|
36
|
+
// clobber recoverable data. Preserve the bad file and warn loudly.
|
|
37
|
+
try {
|
|
38
|
+
const salvage = `${filePath}.corrupt`;
|
|
39
|
+
if (!fs.existsSync(salvage)) fs.copySync(filePath, salvage);
|
|
40
|
+
warnMsg(`Manifest at ${filePath} is unreadable (${err.message}); preserved a copy at ${path.basename(salvage)}.`);
|
|
41
|
+
} catch { /* best-effort salvage */ }
|
|
42
|
+
return emptyManifest();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fold entries recorded under a former agent id (e.g. `windsurf`) into the
|
|
47
|
+
// current id so list/status/cleanup keep seeing them after a rename. The next
|
|
48
|
+
// write persists the migrated shape.
|
|
49
|
+
function migrateAgentIds(manifest) {
|
|
50
|
+
for (const [alias, target] of Object.entries(AGENT_ID_ALIASES)) {
|
|
51
|
+
if (manifest.mcp && manifest.mcp[alias]) {
|
|
52
|
+
manifest.mcp[target] = { ...(manifest.mcp[alias]), ...(manifest.mcp[target] || {}) };
|
|
53
|
+
delete manifest.mcp[alias];
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(manifest.agents) && manifest.agents.includes(alias)) {
|
|
56
|
+
manifest.agents = [...new Set(manifest.agents.map((id) => (id === alias ? target : id)))];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return manifest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function writeManifest(filePath, manifest) {
|
|
63
|
+
const now = new Date().toISOString();
|
|
64
|
+
const out = {
|
|
65
|
+
...manifest,
|
|
66
|
+
version: MANIFEST_VERSION,
|
|
67
|
+
createdAt: manifest.createdAt || now,
|
|
68
|
+
updatedAt: now,
|
|
69
|
+
};
|
|
70
|
+
writeJsonAtomic(filePath, out, { spaces: 2 });
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Convenience: load → mutate → save.
|
|
75
|
+
function updateManifest(filePath, mutator) {
|
|
76
|
+
const m = readManifest(filePath);
|
|
77
|
+
mutator(m);
|
|
78
|
+
return writeManifest(filePath, m);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Record MCP installs from a writeMcpConfigs / writeProjectMcpConfigs result map.
|
|
82
|
+
// `mcpResults` is { [agentId]: { agent, added, skipped, errors, path, addedIds } }.
|
|
83
|
+
// Only the IDs each agent actually merged (r.addedIds) are recorded, so the
|
|
84
|
+
// manifest never claims servers that were skipped because they were already present.
|
|
85
|
+
// `meta` ({ [serverId]: { registry, requiresEnv } }) carries provenance for servers
|
|
86
|
+
// that are not in the bundled catalogue (added live by registry name), so
|
|
87
|
+
// `doctor`/`status` can still reason about them later.
|
|
88
|
+
function recordMcp(filePath, mcpResults, meta = {}) {
|
|
89
|
+
if (!mcpResults) return;
|
|
90
|
+
updateManifest(filePath, (m) => {
|
|
91
|
+
const now = new Date().toISOString();
|
|
92
|
+
for (const [agentId, r] of Object.entries(mcpResults)) {
|
|
93
|
+
const ids = r.addedIds || [];
|
|
94
|
+
if (ids.length === 0) continue;
|
|
95
|
+
if (!m.mcp[agentId]) m.mcp[agentId] = {};
|
|
96
|
+
for (const serverId of ids) {
|
|
97
|
+
m.mcp[agentId][serverId] = { addedAt: now, configPath: r.path || null, ...(meta[serverId] || {}) };
|
|
98
|
+
}
|
|
99
|
+
if (!m.agents.includes(agentId)) m.agents.push(agentId);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// `skillResults.installed` holds skill IDs (which are also the on-disk
|
|
105
|
+
// directory names), so manifest keys line up with what cleanup scans for.
|
|
106
|
+
function recordSkills(filePath, skillResults) {
|
|
107
|
+
if (!skillResults || skillResults.installed.length === 0) return;
|
|
108
|
+
updateManifest(filePath, (m) => {
|
|
109
|
+
const now = new Date().toISOString();
|
|
110
|
+
for (const skillId of skillResults.installed) {
|
|
111
|
+
m.skills[skillId] = { addedAt: now, path: skillResults.directory };
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function recordSystemMcp(mcpResults, meta) {
|
|
117
|
+
recordMcp(SYSTEM_MANIFEST_PATH, mcpResults, meta);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function recordProjectMcp(mcpResults, cwd = process.cwd(), meta) {
|
|
121
|
+
recordMcp(path.join(cwd, PROJECT_MANIFEST_PATH), mcpResults, meta);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function recordSystemSkills(skillResults) {
|
|
125
|
+
recordSkills(SYSTEM_MANIFEST_PATH, skillResults);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function recordProjectSkills(skillResults, cwd = process.cwd()) {
|
|
129
|
+
recordSkills(path.join(cwd, PROJECT_MANIFEST_PATH), skillResults);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function recordSystemTools(toolResults) {
|
|
133
|
+
if (!toolResults || toolResults.installed.length === 0) return;
|
|
134
|
+
updateManifest(SYSTEM_MANIFEST_PATH, (m) => {
|
|
135
|
+
if (!m.tools) m.tools = {};
|
|
136
|
+
const now = new Date().toISOString();
|
|
137
|
+
for (const toolId of toolResults.installed) {
|
|
138
|
+
m.tools[toolId] = { addedAt: now };
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Remove specific MCP server IDs for one agent from a manifest file, cleaning up
|
|
144
|
+
// an emptied agent bucket. No-op (and never creates the file) when the manifest
|
|
145
|
+
// doesn't exist. Returns the count actually removed.
|
|
146
|
+
export function unrecordMcp(filePath, agentId, ids) {
|
|
147
|
+
if (!fs.existsSync(filePath)) return 0;
|
|
148
|
+
let removed = 0;
|
|
149
|
+
updateManifest(filePath, (m) => {
|
|
150
|
+
if (!m.mcp[agentId]) return;
|
|
151
|
+
for (const id of ids) {
|
|
152
|
+
if (m.mcp[agentId][id]) { delete m.mcp[agentId][id]; removed++; }
|
|
153
|
+
}
|
|
154
|
+
if (Object.keys(m.mcp[agentId]).length === 0) delete m.mcp[agentId];
|
|
155
|
+
});
|
|
156
|
+
return removed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function unrecordSystemMcp(agentId, ids) {
|
|
160
|
+
return unrecordMcp(SYSTEM_MANIFEST_PATH, agentId, ids);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function unrecordProjectMcp(agentId, ids, cwd = process.cwd()) {
|
|
164
|
+
return unrecordMcp(path.join(cwd, PROJECT_MANIFEST_PATH), agentId, ids);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function recordProjectFiles(filePaths, cwd = process.cwd()) {
|
|
168
|
+
if (!filePaths || filePaths.length === 0) return;
|
|
169
|
+
const filePath = path.join(cwd, PROJECT_MANIFEST_PATH);
|
|
170
|
+
updateManifest(filePath, (m) => {
|
|
171
|
+
const now = new Date().toISOString();
|
|
172
|
+
const have = new Set(m.files.map((f) => f.relativePath));
|
|
173
|
+
for (const rel of filePaths) {
|
|
174
|
+
if (!have.has(rel)) {
|
|
175
|
+
m.files.push({ relativePath: rel, addedAt: now });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|