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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Official MCP Registry resolver.
|
|
2
|
+
//
|
|
3
|
+
// A catalog entry may carry a `registry` block naming its canonical record in
|
|
4
|
+
// the official registry (registry.modelcontextprotocol.io). This module turns
|
|
5
|
+
// that record into the fields dxai already understands — `transport`,
|
|
6
|
+
// `requiresEnv`, `requiresInput`, `version`, `stale` — so the rest of the CLI
|
|
7
|
+
// (config derivation, writers, doctor) never has to know the registry exists.
|
|
8
|
+
//
|
|
9
|
+
// Three callers share it: the maintainer-side sync script (keeps the bundled
|
|
10
|
+
// JSON current), `dxai update` (re-resolves live into the user's cache), and
|
|
11
|
+
// `dxai add <registry-name>` (resolves one server on the spot).
|
|
12
|
+
//
|
|
13
|
+
// Ownership rule: the resolver only writes the fields listed in
|
|
14
|
+
// `registry.resolved.fields`. On first resolution it claims every resolvable
|
|
15
|
+
// field the curated entry does not define. Delete a field to hand it back;
|
|
16
|
+
// to hand-curate an owned field, edit it and drop it from that list.
|
|
17
|
+
|
|
18
|
+
import { fetchJson } from '../net.js';
|
|
19
|
+
import {
|
|
20
|
+
isPackageSpec, isHttpsUrl, isValidRegistryName, RESOLVABLE_FIELDS,
|
|
21
|
+
} from './validate.js';
|
|
22
|
+
|
|
23
|
+
export { RESOLVABLE_FIELDS };
|
|
24
|
+
|
|
25
|
+
export const OFFICIAL_MCP_REGISTRY =
|
|
26
|
+
process.env.DXAI_MCP_REGISTRY_URL || 'https://registry.modelcontextprotocol.io';
|
|
27
|
+
|
|
28
|
+
const OFFICIAL_META_KEY = 'io.modelcontextprotocol.registry/official';
|
|
29
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
30
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
31
|
+
|
|
32
|
+
// ── Fetch ──
|
|
33
|
+
|
|
34
|
+
export function registryServerUrl(name, base = OFFICIAL_MCP_REGISTRY) {
|
|
35
|
+
return `${base.replace(/\/$/, '')}/v0/servers/${encodeURIComponent(name)}/versions/latest`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The latest record for `name`, or null when the registry has never heard of it.
|
|
39
|
+
// Any other failure (network, 5xx, malformed) propagates.
|
|
40
|
+
export async function fetchRegistryServer(name, { base, fetchImpl = fetchJson, timeoutMs = DEFAULT_TIMEOUT_MS, retries } = {}) {
|
|
41
|
+
if (!isValidRegistryName(name)) throw new Error(`Invalid registry server name: ${name}`);
|
|
42
|
+
try {
|
|
43
|
+
return await fetchImpl(registryServerUrl(name, base), { timeoutMs, retries });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err?.status === 404) return null;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Substring search on server names (the only search the registry offers),
|
|
51
|
+
// latest versions only. Returns the `{ server, _meta }` envelopes.
|
|
52
|
+
export async function fetchRegistrySearch(query, { base = OFFICIAL_MCP_REGISTRY, fetchImpl = fetchJson, timeoutMs = DEFAULT_TIMEOUT_MS, retries, limit = 20 } = {}) {
|
|
53
|
+
const url = `${base.replace(/\/$/, '')}/v0/servers?search=${encodeURIComponent(query)}&version=latest&limit=${limit}`;
|
|
54
|
+
const data = await fetchImpl(url, { timeoutMs, retries });
|
|
55
|
+
return Array.isArray(data?.servers) ? data.servers : [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Pick a transport out of a server.json record ──
|
|
59
|
+
|
|
60
|
+
const REMOTE_TYPES = ['streamable-http', 'sse'];
|
|
61
|
+
const PACKAGE_LAUNCHERS = {
|
|
62
|
+
npm: (identifier) => ({ command: 'npx', args: ['-y', identifier] }),
|
|
63
|
+
pypi: (identifier) => ({ command: 'uvx', args: [identifier] }),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function usableRemotes(remotes, prefer, warnings) {
|
|
67
|
+
const valid = [];
|
|
68
|
+
for (const r of remotes || []) {
|
|
69
|
+
if (!REMOTE_TYPES.includes(r?.type)) continue;
|
|
70
|
+
if (!isHttpsUrl(r.url)) { warnings.push(`remote ${r.url} skipped: not https`); continue; }
|
|
71
|
+
valid.push(r);
|
|
72
|
+
}
|
|
73
|
+
valid.sort((a, b) => REMOTE_TYPES.indexOf(a.type) - REMOTE_TYPES.indexOf(b.type));
|
|
74
|
+
if (prefer?.remote) {
|
|
75
|
+
const matched = valid.filter((r) => r.url.includes(prefer.remote));
|
|
76
|
+
if (matched.length) return matched;
|
|
77
|
+
warnings.push(`no remote matches prefer.remote "${prefer.remote}"; using the first available`);
|
|
78
|
+
}
|
|
79
|
+
return valid;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function usablePackages(packages, warnings, unsupported) {
|
|
83
|
+
const valid = [];
|
|
84
|
+
for (const p of packages || []) {
|
|
85
|
+
if (!PACKAGE_LAUNCHERS[p?.registryType]) {
|
|
86
|
+
if (p?.registryType) unsupported.push(`package type "${p.registryType}" (${p.identifier}) is not supported yet`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!isPackageSpec(p.identifier)) { warnings.push(`package identifier "${p.identifier}" rejected`); continue; }
|
|
90
|
+
if (p.transport && p.transport.type !== 'stdio') continue;
|
|
91
|
+
valid.push(p);
|
|
92
|
+
}
|
|
93
|
+
return valid;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Positional/named package arguments → argv tokens plus the inputs a user must
|
|
97
|
+
// supply. A required argument becomes a `{key}` placeholder that config-writer
|
|
98
|
+
// substitutes at write time (the same mechanism `filesystem` uses).
|
|
99
|
+
function packageArgv(args, requiresInput) {
|
|
100
|
+
const argv = [];
|
|
101
|
+
for (const a of args || []) {
|
|
102
|
+
if (!a || typeof a !== 'object') continue;
|
|
103
|
+
const key = a.valueHint || (a.name || '').replace(/^-+/, '');
|
|
104
|
+
if (a.type === 'positional') {
|
|
105
|
+
if (a.value && !/\{[^}]+\}/.test(a.value)) argv.push(a.value);
|
|
106
|
+
else if (a.isRequired && key) {
|
|
107
|
+
const placeholder = `{${key}}`;
|
|
108
|
+
requiresInput[key] = { prompt: a.description || key, default: a.default, placeholder };
|
|
109
|
+
argv.push(placeholder);
|
|
110
|
+
}
|
|
111
|
+
} else if (a.type === 'named' && a.name) {
|
|
112
|
+
if (a.value && !/\{[^}]+\}/.test(a.value)) argv.push(a.name, a.value);
|
|
113
|
+
else if (a.isRequired && key) {
|
|
114
|
+
const placeholder = `{${key}}`;
|
|
115
|
+
requiresInput[key] = { prompt: a.description || key, default: a.default, placeholder };
|
|
116
|
+
argv.push(a.name, placeholder);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return argv;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function requiredEnv(pkg) {
|
|
124
|
+
const out = {};
|
|
125
|
+
for (const e of pkg.environmentVariables || []) {
|
|
126
|
+
if (e?.name && e.isRequired === true) out[e.name] = e.description || e.name;
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function requiredHeaders(remote) {
|
|
132
|
+
return (remote.headers || []).filter((h) => h?.name && h.isRequired === true).map((h) => h.name);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Map a registry record (the `{ server, _meta }` envelope or a bare server.json)
|
|
136
|
+
// onto dxai catalog fields. Remote-first unless `prefer.transport` says
|
|
137
|
+
// otherwise; never throws — problems land in `warnings`. `version` (a pin, in
|
|
138
|
+
// catalog terms) is only written when `prefer.pin` is set — the policy is to
|
|
139
|
+
// pin minimally and let npx float; the record's version is kept in
|
|
140
|
+
// `registry.resolved.version` for provenance either way.
|
|
141
|
+
export function pickTransport(record, prefer = {}) {
|
|
142
|
+
const server = record?.server || record || {};
|
|
143
|
+
const official = record?._meta?.[OFFICIAL_META_KEY] || {};
|
|
144
|
+
const warnings = [];
|
|
145
|
+
const out = {
|
|
146
|
+
name: server.name,
|
|
147
|
+
title: server.title,
|
|
148
|
+
description: server.description,
|
|
149
|
+
registryVersion: server.version,
|
|
150
|
+
status: official.status || 'active',
|
|
151
|
+
transport: null,
|
|
152
|
+
requiresEnv: {},
|
|
153
|
+
requiresInput: {},
|
|
154
|
+
version: undefined,
|
|
155
|
+
stale: false,
|
|
156
|
+
staleReason: undefined,
|
|
157
|
+
source: null,
|
|
158
|
+
warnings,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const remotes = usableRemotes(server.remotes, prefer, warnings);
|
|
162
|
+
const unsupported = [];
|
|
163
|
+
const packages = usablePackages(server.packages, warnings, unsupported);
|
|
164
|
+
const wantPackage = prefer.transport === 'package';
|
|
165
|
+
|
|
166
|
+
let useRemote = wantPackage ? (packages.length === 0 && remotes.length > 0) : remotes.length > 0;
|
|
167
|
+
if (useRemote && requiredHeaders(remotes[0]).length && prefer.transport !== 'remote' && packages.length) {
|
|
168
|
+
warnings.push(`remote ${remotes[0].url} requires header(s) ${requiredHeaders(remotes[0]).join(', ')}; using the package instead`);
|
|
169
|
+
useRemote = false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (useRemote) {
|
|
173
|
+
const remote = remotes[0];
|
|
174
|
+
out.transport = { type: 'http', url: remote.url };
|
|
175
|
+
out.source = 'remote';
|
|
176
|
+
if (remote.type === 'sse') warnings.push(`remote ${remote.url} is SSE; some clients need a transport hint`);
|
|
177
|
+
const headers = requiredHeaders(remote);
|
|
178
|
+
if (headers.length) warnings.push(`remote ${remote.url} requires header(s) ${headers.join(', ')} (not rendered)`);
|
|
179
|
+
} else if (packages.length) {
|
|
180
|
+
const pkg = packages[0];
|
|
181
|
+
const launch = PACKAGE_LAUNCHERS[pkg.registryType](pkg.identifier);
|
|
182
|
+
const argv = packageArgv(pkg.packageArguments, out.requiresInput);
|
|
183
|
+
out.transport = { type: 'stdio', command: launch.command, args: [...launch.args, ...argv] };
|
|
184
|
+
out.source = 'package';
|
|
185
|
+
if (prefer.pin) out.version = pkg.version || server.version;
|
|
186
|
+
out.requiresEnv = requiredEnv(pkg);
|
|
187
|
+
} else {
|
|
188
|
+
// Only worth mentioning the package types we skipped when they were the
|
|
189
|
+
// only option — a picked remote makes them irrelevant.
|
|
190
|
+
warnings.push(...unsupported, 'no usable transport (no https remote, no npm/pypi package)');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (official.status === 'deprecated' || official.status === 'deleted') {
|
|
194
|
+
out.stale = true;
|
|
195
|
+
out.staleReason = official.statusMessage || `Marked ${official.status} in the MCP registry`;
|
|
196
|
+
warnings.push(`registry status is ${official.status}`);
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Apply a resolution to a catalog entry ──
|
|
202
|
+
|
|
203
|
+
function ownedFields(entry) {
|
|
204
|
+
const owned = new Set(entry.registry?.resolved?.fields || []);
|
|
205
|
+
for (const f of RESOLVABLE_FIELDS) if (!(f in entry)) owned.add(f);
|
|
206
|
+
return owned;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isEmpty(value) {
|
|
210
|
+
if (value === undefined || value === null || value === false) return true;
|
|
211
|
+
if (typeof value === 'object') return Object.keys(value).length === 0;
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function stripResolvedAt(entry) {
|
|
216
|
+
const { registry, ...rest } = entry;
|
|
217
|
+
if (!registry?.resolved) return rest;
|
|
218
|
+
const { at: _at, ...resolved } = registry.resolved;
|
|
219
|
+
return { ...rest, registry: { ...registry, resolved } };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Return a new entry with the resolver-owned fields replaced by `resolution`
|
|
223
|
+
// and the `registry.resolved` bookkeeping refreshed. `changed` ignores the
|
|
224
|
+
// timestamp so a no-op sync produces no diff.
|
|
225
|
+
export function applyResolution(entry, resolution, { now = new Date() } = {}) {
|
|
226
|
+
const owned = ownedFields(entry);
|
|
227
|
+
const next = { ...entry };
|
|
228
|
+
for (const field of RESOLVABLE_FIELDS) {
|
|
229
|
+
if (!owned.has(field)) continue;
|
|
230
|
+
if (isEmpty(resolution[field])) delete next[field];
|
|
231
|
+
else next[field] = resolution[field];
|
|
232
|
+
}
|
|
233
|
+
next.registry = {
|
|
234
|
+
...entry.registry,
|
|
235
|
+
resolved: {
|
|
236
|
+
version: resolution.registryVersion,
|
|
237
|
+
at: now.toISOString(),
|
|
238
|
+
fields: RESOLVABLE_FIELDS.filter((f) => owned.has(f)),
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
const changed = JSON.stringify(stripResolvedAt(entry)) !== JSON.stringify(stripResolvedAt(next));
|
|
242
|
+
return { entry: next, changed };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Catalog id derived from a registry name: the last path segment, slugified.
|
|
246
|
+
// "io.github.upstash/context7" → "context7", "com.figma.mcp/mcp" → "mcp".
|
|
247
|
+
export function slugForRegistryName(name) {
|
|
248
|
+
const tail = String(name).split('/').pop() || '';
|
|
249
|
+
return tail.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Resolve a whole catalog ──
|
|
253
|
+
|
|
254
|
+
async function mapLimit(items, limit, fn) {
|
|
255
|
+
const out = new Array(items.length);
|
|
256
|
+
let next = 0;
|
|
257
|
+
const worker = async () => {
|
|
258
|
+
while (next < items.length) {
|
|
259
|
+
const i = next++;
|
|
260
|
+
out[i] = await fn(items[i], i);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Re-resolve every entry that names a registry server. Entries without a
|
|
268
|
+
// `registry` block pass through untouched. Per-entry failures are captured in
|
|
269
|
+
// `results` and leave that entry as-is, so one bad record never sinks the
|
|
270
|
+
// catalog. Returns { entries, results }.
|
|
271
|
+
export async function resolveEntries(entries, opts = {}) {
|
|
272
|
+
const { concurrency = DEFAULT_CONCURRENCY, now = new Date(), ...fetchOpts } = opts;
|
|
273
|
+
const targets = entries.map((e, i) => ({ e, i })).filter(({ e }) => e.registry?.name);
|
|
274
|
+
const out = [...entries];
|
|
275
|
+
const results = await mapLimit(targets, concurrency, async ({ e, i }) => {
|
|
276
|
+
const name = e.registry.name;
|
|
277
|
+
try {
|
|
278
|
+
const record = await fetchRegistryServer(name, fetchOpts);
|
|
279
|
+
if (!record) return { id: e.id, name, ok: false, error: 'not found in registry' };
|
|
280
|
+
const resolution = pickTransport(record, e.registry.prefer);
|
|
281
|
+
if (!resolution.transport && !('transport' in e)) {
|
|
282
|
+
return { id: e.id, name, ok: false, error: 'no usable transport', warnings: resolution.warnings };
|
|
283
|
+
}
|
|
284
|
+
const { entry, changed } = applyResolution(e, resolution, { now });
|
|
285
|
+
out[i] = entry;
|
|
286
|
+
return { id: e.id, name, ok: true, changed, source: resolution.source, version: resolution.registryVersion, warnings: resolution.warnings };
|
|
287
|
+
} catch (err) {
|
|
288
|
+
return { id: e.id, name, ok: false, error: err.message };
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return { entries: out, results };
|
|
292
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// MCP server registry — data lives in data/mcp-servers.json.
|
|
2
|
+
// At import time we use the cache (if present) or fall back to bundled JSON.
|
|
3
|
+
// Run `dxai update` to refresh the cache from a remote source.
|
|
4
|
+
|
|
5
|
+
import { loadRegistry } from './loader.js';
|
|
6
|
+
import { AGENT_DEFINITIONS, renderAgentConfig, MCP_CONFIG_ALIASES, normalizeAgentIds } from '../detect.js';
|
|
7
|
+
|
|
8
|
+
const data = loadRegistry('mcp-servers');
|
|
9
|
+
|
|
10
|
+
// Expand config-key aliases (e.g. `antigravity` → every Antigravity agent) in
|
|
11
|
+
// place, without clobbering an explicit per-agent key if one already exists. An
|
|
12
|
+
// alias may share its name with a real agent id, in which case that agent keeps
|
|
13
|
+
// the block and the other targets receive a copy.
|
|
14
|
+
function expandAliases(configs) {
|
|
15
|
+
for (const [alias, targets] of Object.entries(MCP_CONFIG_ALIASES)) {
|
|
16
|
+
if (!(alias in configs)) continue;
|
|
17
|
+
const block = configs[alias];
|
|
18
|
+
if (!targets.includes(alias)) delete configs[alias];
|
|
19
|
+
for (const target of targets) {
|
|
20
|
+
if (!(target in configs)) configs[target] = block;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return configs;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Resolve a server's effective per-agent configs. A server may declare a
|
|
27
|
+
// canonical `transport` (derived into a block for every agent that has a
|
|
28
|
+
// dialect) and/or explicit per-agent `configs`. Explicit configs win on a
|
|
29
|
+
// per-agent basis, so they remain an escape hatch for servers that don't fit
|
|
30
|
+
// the common shapes. A server with neither transport nor configs resolves to {}.
|
|
31
|
+
// A server may opt specific agents out of derivation (e.g. "Claude Code as an
|
|
32
|
+
// MCP server" makes no sense inside Claude Code itself).
|
|
33
|
+
function isExcluded(server, agentId) {
|
|
34
|
+
return normalizeAgentIds(server.excludeAgents).includes(agentId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function deriveConfigs(server, agents = AGENT_DEFINITIONS) {
|
|
38
|
+
const derived = {};
|
|
39
|
+
if (server.transport) {
|
|
40
|
+
for (const agent of agents) {
|
|
41
|
+
if (isExcluded(server, agent.id)) continue;
|
|
42
|
+
const cfg = renderAgentConfig(agent, server);
|
|
43
|
+
if (cfg) derived[agent.id] = cfg;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return expandAliases({ ...derived, ...(server.configs || {}) });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Per-agent blocks for the *project-level* file. Agents whose project file uses
|
|
50
|
+
// a different dialect from their global config (Claude Code: CLI globally,
|
|
51
|
+
// .mcp.json in projects) are rendered from `transport` with that dialect;
|
|
52
|
+
// everyone else reuses their global block.
|
|
53
|
+
export function deriveProjectConfigs(server, configs, agents = AGENT_DEFINITIONS) {
|
|
54
|
+
const out = {};
|
|
55
|
+
for (const agent of agents) {
|
|
56
|
+
if (typeof agent.projectMcpPath !== 'function') continue;
|
|
57
|
+
if (agent.projectMcpDialect) {
|
|
58
|
+
if (!server.transport || isExcluded(server, agent.id)) continue;
|
|
59
|
+
const cfg = renderAgentConfig(agent, server, { project: true });
|
|
60
|
+
if (cfg) out[agent.id] = cfg;
|
|
61
|
+
} else if (configs[agent.id]) {
|
|
62
|
+
out[agent.id] = configs[agent.id];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const MCP_CATEGORIES = data.categories;
|
|
69
|
+
export const MCP_SERVERS = data.servers.map((server) => {
|
|
70
|
+
const configs = deriveConfigs(server);
|
|
71
|
+
return { ...server, configs, projectConfigs: deriveProjectConfigs(server, configs) };
|
|
72
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Agent skills registry — data lives in data/skills.json.
|
|
2
|
+
// At import time we use the cache (if present) or fall back to bundled JSON.
|
|
3
|
+
// Run `dxai update` to refresh the cache from a remote source.
|
|
4
|
+
|
|
5
|
+
import { loadRegistry } from './loader.js';
|
|
6
|
+
|
|
7
|
+
const data = loadRegistry('skills');
|
|
8
|
+
|
|
9
|
+
export const SKILL_CATEGORIES = data.categories;
|
|
10
|
+
export const SKILLS = data.skills;
|