facult 1.0.1
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 +383 -0
- package/bin/facult.cjs +302 -0
- package/package.json +78 -0
- package/src/adapters/claude-cli.ts +18 -0
- package/src/adapters/claude-desktop.ts +15 -0
- package/src/adapters/clawdbot.ts +18 -0
- package/src/adapters/codex.ts +19 -0
- package/src/adapters/cursor.ts +18 -0
- package/src/adapters/index.ts +69 -0
- package/src/adapters/mcp.ts +270 -0
- package/src/adapters/reference.ts +9 -0
- package/src/adapters/skills.ts +47 -0
- package/src/adapters/types.ts +42 -0
- package/src/adapters/version.ts +18 -0
- package/src/audit/agent.ts +1071 -0
- package/src/audit/index.ts +74 -0
- package/src/audit/static.ts +1130 -0
- package/src/audit/tui.ts +704 -0
- package/src/audit/types.ts +68 -0
- package/src/audit/update-index.ts +115 -0
- package/src/conflicts.ts +135 -0
- package/src/consolidate-conflict-action.ts +57 -0
- package/src/consolidate.ts +1637 -0
- package/src/enable-disable.ts +349 -0
- package/src/index-builder.ts +562 -0
- package/src/index.ts +589 -0
- package/src/manage.ts +894 -0
- package/src/migrate.ts +272 -0
- package/src/paths.ts +238 -0
- package/src/quarantine.ts +217 -0
- package/src/query.ts +186 -0
- package/src/remote-manifest-integrity.ts +367 -0
- package/src/remote-providers.ts +905 -0
- package/src/remote-source-policy.ts +237 -0
- package/src/remote-sources.ts +162 -0
- package/src/remote-types.ts +136 -0
- package/src/remote.ts +1970 -0
- package/src/scan.ts +2427 -0
- package/src/schema.ts +39 -0
- package/src/self-update.ts +408 -0
- package/src/snippets-cli.ts +293 -0
- package/src/snippets.ts +706 -0
- package/src/source-trust.ts +203 -0
- package/src/trust-list.ts +232 -0
- package/src/trust.ts +170 -0
- package/src/tui.ts +118 -0
- package/src/util/codex-toml.ts +126 -0
- package/src/util/json.ts +32 -0
- package/src/util/skills.ts +55 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { FacultIndex } from "./index-builder";
|
|
4
|
+
import { loadManagedState, syncManagedTools } from "./manage";
|
|
5
|
+
import { facultRootDir } from "./paths";
|
|
6
|
+
|
|
7
|
+
type EntryKind = "skills" | "mcp";
|
|
8
|
+
|
|
9
|
+
type CommandMode = "enable" | "disable";
|
|
10
|
+
|
|
11
|
+
const TOOL_LIST_SEPARATOR = ",";
|
|
12
|
+
|
|
13
|
+
function parseToolList(raw: string): string[] {
|
|
14
|
+
return raw
|
|
15
|
+
.split(TOOL_LIST_SEPARATOR)
|
|
16
|
+
.map((tool) => tool.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function uniqueSorted(values: string[]): string[] {
|
|
21
|
+
return Array.from(new Set(values)).sort();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseEntryName(raw: string): { kind: EntryKind; name: string } {
|
|
25
|
+
if (raw.startsWith("mcp:")) {
|
|
26
|
+
return { kind: "mcp", name: raw.slice("mcp:".length) };
|
|
27
|
+
}
|
|
28
|
+
return { kind: "skills", name: raw };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ensureIndexStructure(index: FacultIndex): FacultIndex {
|
|
32
|
+
return {
|
|
33
|
+
version: index.version ?? 1,
|
|
34
|
+
updatedAt: index.updatedAt ?? new Date().toISOString(),
|
|
35
|
+
skills: index.skills ?? {},
|
|
36
|
+
mcp: index.mcp ?? { servers: {} },
|
|
37
|
+
agents: index.agents ?? {},
|
|
38
|
+
snippets: index.snippets ?? {},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function computeNextEnabledFor({
|
|
43
|
+
current,
|
|
44
|
+
allTools,
|
|
45
|
+
targetTools,
|
|
46
|
+
mode,
|
|
47
|
+
}: {
|
|
48
|
+
current: unknown;
|
|
49
|
+
allTools: string[];
|
|
50
|
+
targetTools: string[];
|
|
51
|
+
mode: CommandMode;
|
|
52
|
+
}): string[] {
|
|
53
|
+
const base = Array.isArray(current)
|
|
54
|
+
? current.map((t) => String(t))
|
|
55
|
+
: mode === "disable"
|
|
56
|
+
? [...allTools]
|
|
57
|
+
: [];
|
|
58
|
+
if (mode === "enable") {
|
|
59
|
+
return uniqueSorted([...base, ...targetTools]);
|
|
60
|
+
}
|
|
61
|
+
return uniqueSorted(base.filter((tool) => !targetTools.includes(tool)));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function loadIndex(rootDir: string): Promise<FacultIndex> {
|
|
65
|
+
const indexPath = join(rootDir, "index.json");
|
|
66
|
+
const file = Bun.file(indexPath);
|
|
67
|
+
if (!(await file.exists())) {
|
|
68
|
+
throw new Error(`Index not found at ${indexPath}. Run "facult index".`);
|
|
69
|
+
}
|
|
70
|
+
const raw = await file.text();
|
|
71
|
+
return JSON.parse(raw) as FacultIndex;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function writeIndex(rootDir: string, index: FacultIndex) {
|
|
75
|
+
const indexPath = join(rootDir, "index.json");
|
|
76
|
+
await Bun.write(indexPath, `${JSON.stringify(index, null, 2)}\n`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractServersObject(parsed: Record<string, unknown>): {
|
|
80
|
+
servers: Record<string, unknown>;
|
|
81
|
+
set: (servers: Record<string, unknown>) => void;
|
|
82
|
+
} {
|
|
83
|
+
if (parsed.servers && typeof parsed.servers === "object") {
|
|
84
|
+
return {
|
|
85
|
+
servers: parsed.servers as Record<string, unknown>,
|
|
86
|
+
set: (servers) => {
|
|
87
|
+
parsed.servers = servers;
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (parsed.mcpServers && typeof parsed.mcpServers === "object") {
|
|
92
|
+
return {
|
|
93
|
+
servers: parsed.mcpServers as Record<string, unknown>,
|
|
94
|
+
set: (servers) => {
|
|
95
|
+
parsed.mcpServers = servers;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
parsed.mcp &&
|
|
101
|
+
typeof parsed.mcp === "object" &&
|
|
102
|
+
(parsed.mcp as Record<string, unknown>).servers &&
|
|
103
|
+
typeof (parsed.mcp as Record<string, unknown>).servers === "object"
|
|
104
|
+
) {
|
|
105
|
+
return {
|
|
106
|
+
servers: (parsed.mcp as Record<string, unknown>).servers as Record<
|
|
107
|
+
string,
|
|
108
|
+
unknown
|
|
109
|
+
>,
|
|
110
|
+
set: (servers) => {
|
|
111
|
+
(parsed.mcp as Record<string, unknown>).servers = servers;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
parsed.servers = {};
|
|
116
|
+
return {
|
|
117
|
+
servers: parsed.servers as Record<string, unknown>,
|
|
118
|
+
set: (servers) => {
|
|
119
|
+
parsed.servers = servers;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function updateCanonicalServers({
|
|
125
|
+
rootDir,
|
|
126
|
+
updates,
|
|
127
|
+
allTools,
|
|
128
|
+
targetTools,
|
|
129
|
+
mode,
|
|
130
|
+
}: {
|
|
131
|
+
rootDir: string;
|
|
132
|
+
updates: string[];
|
|
133
|
+
allTools: string[];
|
|
134
|
+
targetTools: string[];
|
|
135
|
+
mode: CommandMode;
|
|
136
|
+
}) {
|
|
137
|
+
if (updates.length === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const serversPath = join(rootDir, "mcp", "servers.json");
|
|
142
|
+
const mcpPath = join(rootDir, "mcp", "mcp.json");
|
|
143
|
+
let sourcePath: string | null = null;
|
|
144
|
+
|
|
145
|
+
if (await Bun.file(serversPath).exists()) {
|
|
146
|
+
sourcePath = serversPath;
|
|
147
|
+
} else if (await Bun.file(mcpPath).exists()) {
|
|
148
|
+
sourcePath = mcpPath;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!sourcePath) {
|
|
152
|
+
throw new Error("No canonical MCP servers.json or mcp.json found.");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const raw = await Bun.file(sourcePath).text();
|
|
156
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
157
|
+
const container = extractServersObject(parsed);
|
|
158
|
+
|
|
159
|
+
for (const name of updates) {
|
|
160
|
+
const entry = container.servers[name];
|
|
161
|
+
if (!entry || typeof entry !== "object") {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const next = computeNextEnabledFor({
|
|
165
|
+
current: (entry as Record<string, unknown>).enabledFor,
|
|
166
|
+
allTools,
|
|
167
|
+
targetTools,
|
|
168
|
+
mode,
|
|
169
|
+
});
|
|
170
|
+
(entry as Record<string, unknown>).enabledFor = next;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
container.set(container.servers);
|
|
174
|
+
await Bun.write(sourcePath, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function applyEnableDisable({
|
|
178
|
+
names,
|
|
179
|
+
mode,
|
|
180
|
+
tools,
|
|
181
|
+
homeDir,
|
|
182
|
+
rootDir,
|
|
183
|
+
}: {
|
|
184
|
+
names: string[];
|
|
185
|
+
mode: CommandMode;
|
|
186
|
+
tools?: string[];
|
|
187
|
+
homeDir?: string;
|
|
188
|
+
rootDir?: string;
|
|
189
|
+
}) {
|
|
190
|
+
const home = homeDir ?? homedir();
|
|
191
|
+
const root = rootDir ?? facultRootDir(home);
|
|
192
|
+
const managedState = await loadManagedState(home);
|
|
193
|
+
const managedTools = Object.keys(managedState.tools).sort();
|
|
194
|
+
const targetTools =
|
|
195
|
+
tools && tools.length > 0 ? uniqueSorted(tools) : managedTools;
|
|
196
|
+
|
|
197
|
+
if (!targetTools.length) {
|
|
198
|
+
throw new Error("No tools specified (and no managed tools found).");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const allTools = managedTools.length ? managedTools : targetTools;
|
|
202
|
+
|
|
203
|
+
const index = ensureIndexStructure(await loadIndex(root));
|
|
204
|
+
const missing: string[] = [];
|
|
205
|
+
const mcpUpdates: string[] = [];
|
|
206
|
+
|
|
207
|
+
for (const raw of names) {
|
|
208
|
+
const { kind, name } = parseEntryName(raw);
|
|
209
|
+
if (kind === "skills") {
|
|
210
|
+
const entry = index.skills[name] as Record<string, unknown> | undefined;
|
|
211
|
+
if (!entry) {
|
|
212
|
+
missing.push(raw);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
entry.enabledFor = computeNextEnabledFor({
|
|
216
|
+
current: entry.enabledFor,
|
|
217
|
+
allTools,
|
|
218
|
+
targetTools,
|
|
219
|
+
mode,
|
|
220
|
+
});
|
|
221
|
+
} else {
|
|
222
|
+
const entry = index.mcp?.servers?.[name] as
|
|
223
|
+
| Record<string, unknown>
|
|
224
|
+
| undefined;
|
|
225
|
+
if (!entry) {
|
|
226
|
+
missing.push(raw);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
entry.enabledFor = computeNextEnabledFor({
|
|
230
|
+
current: entry.enabledFor,
|
|
231
|
+
allTools,
|
|
232
|
+
targetTools,
|
|
233
|
+
mode,
|
|
234
|
+
});
|
|
235
|
+
mcpUpdates.push(name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (missing.length) {
|
|
240
|
+
throw new Error(`Entries not found: ${missing.join(", ")}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
index.updatedAt = new Date().toISOString();
|
|
244
|
+
await writeIndex(root, index);
|
|
245
|
+
|
|
246
|
+
await updateCanonicalServers({
|
|
247
|
+
rootDir: root,
|
|
248
|
+
updates: mcpUpdates,
|
|
249
|
+
allTools,
|
|
250
|
+
targetTools,
|
|
251
|
+
mode,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const toolsToSync = targetTools.filter((tool) => managedState.tools[tool]);
|
|
255
|
+
if (toolsToSync.length) {
|
|
256
|
+
for (const tool of toolsToSync) {
|
|
257
|
+
await syncManagedTools({
|
|
258
|
+
homeDir: home,
|
|
259
|
+
rootDir: root,
|
|
260
|
+
tool,
|
|
261
|
+
dryRun: false,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parseEnableDisableArgs(argv: string[]): {
|
|
268
|
+
names: string[];
|
|
269
|
+
tools?: string[];
|
|
270
|
+
} {
|
|
271
|
+
const names: string[] = [];
|
|
272
|
+
let tools: string[] | undefined;
|
|
273
|
+
|
|
274
|
+
for (let i = 0; i < argv.length; i++) {
|
|
275
|
+
const arg = argv[i];
|
|
276
|
+
if (!arg) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (arg === "--for") {
|
|
280
|
+
const next = argv[i + 1];
|
|
281
|
+
if (!next) {
|
|
282
|
+
throw new Error("--for requires a comma-separated list of tools");
|
|
283
|
+
}
|
|
284
|
+
tools = parseToolList(next);
|
|
285
|
+
i += 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (arg.startsWith("--for=")) {
|
|
289
|
+
tools = parseToolList(arg.slice("--for=".length));
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (arg.startsWith("-")) {
|
|
293
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
294
|
+
}
|
|
295
|
+
names.push(arg);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!names.length) {
|
|
299
|
+
throw new Error("At least one name is required.");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return { names, tools };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function enableCommand(argv: string[]) {
|
|
306
|
+
if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
|
|
307
|
+
console.log(`facult enable — enable skills or MCP servers for tools
|
|
308
|
+
|
|
309
|
+
Usage:
|
|
310
|
+
facult enable <name> [moreNames...] [--for <tool1,tool2,...>]
|
|
311
|
+
facult enable mcp:<name> [--for <tools>]
|
|
312
|
+
|
|
313
|
+
Options:
|
|
314
|
+
--for Comma-separated list of tools (defaults to all managed tools)
|
|
315
|
+
`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const { names, tools } = parseEnableDisableArgs(argv);
|
|
320
|
+
await applyEnableDisable({ names, tools, mode: "enable" });
|
|
321
|
+
console.log(`Enabled ${names.join(", ")}`);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
324
|
+
process.exitCode = 1;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function disableCommand(argv: string[]) {
|
|
329
|
+
if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
|
|
330
|
+
console.log(`facult disable — disable skills or MCP servers for tools
|
|
331
|
+
|
|
332
|
+
Usage:
|
|
333
|
+
facult disable <name> [moreNames...] [--for <tool1,tool2,...>]
|
|
334
|
+
facult disable mcp:<name> [--for <tools>]
|
|
335
|
+
|
|
336
|
+
Options:
|
|
337
|
+
--for Comma-separated list of tools (defaults to all managed tools)
|
|
338
|
+
`);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
const { names, tools } = parseEnableDisableArgs(argv);
|
|
343
|
+
await applyEnableDisable({ names, tools, mode: "disable" });
|
|
344
|
+
console.log(`Disabled ${names.join(", ")}`);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
347
|
+
process.exitCode = 1;
|
|
348
|
+
}
|
|
349
|
+
}
|