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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +383 -0
  3. package/bin/facult.cjs +302 -0
  4. package/package.json +78 -0
  5. package/src/adapters/claude-cli.ts +18 -0
  6. package/src/adapters/claude-desktop.ts +15 -0
  7. package/src/adapters/clawdbot.ts +18 -0
  8. package/src/adapters/codex.ts +19 -0
  9. package/src/adapters/cursor.ts +18 -0
  10. package/src/adapters/index.ts +69 -0
  11. package/src/adapters/mcp.ts +270 -0
  12. package/src/adapters/reference.ts +9 -0
  13. package/src/adapters/skills.ts +47 -0
  14. package/src/adapters/types.ts +42 -0
  15. package/src/adapters/version.ts +18 -0
  16. package/src/audit/agent.ts +1071 -0
  17. package/src/audit/index.ts +74 -0
  18. package/src/audit/static.ts +1130 -0
  19. package/src/audit/tui.ts +704 -0
  20. package/src/audit/types.ts +68 -0
  21. package/src/audit/update-index.ts +115 -0
  22. package/src/conflicts.ts +135 -0
  23. package/src/consolidate-conflict-action.ts +57 -0
  24. package/src/consolidate.ts +1637 -0
  25. package/src/enable-disable.ts +349 -0
  26. package/src/index-builder.ts +562 -0
  27. package/src/index.ts +589 -0
  28. package/src/manage.ts +894 -0
  29. package/src/migrate.ts +272 -0
  30. package/src/paths.ts +238 -0
  31. package/src/quarantine.ts +217 -0
  32. package/src/query.ts +186 -0
  33. package/src/remote-manifest-integrity.ts +367 -0
  34. package/src/remote-providers.ts +905 -0
  35. package/src/remote-source-policy.ts +237 -0
  36. package/src/remote-sources.ts +162 -0
  37. package/src/remote-types.ts +136 -0
  38. package/src/remote.ts +1970 -0
  39. package/src/scan.ts +2427 -0
  40. package/src/schema.ts +39 -0
  41. package/src/self-update.ts +408 -0
  42. package/src/snippets-cli.ts +293 -0
  43. package/src/snippets.ts +706 -0
  44. package/src/source-trust.ts +203 -0
  45. package/src/trust-list.ts +232 -0
  46. package/src/trust.ts +170 -0
  47. package/src/tui.ts +118 -0
  48. package/src/util/codex-toml.ts +126 -0
  49. package/src/util/json.ts +32 -0
  50. package/src/util/skills.ts +55 -0
package/src/index.ts ADDED
@@ -0,0 +1,589 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { join } from "node:path";
4
+ import { getAllAdapters } from "./adapters";
5
+ import { auditCommand } from "./audit";
6
+ import { consolidateCommand } from "./consolidate";
7
+ import { disableCommand, enableCommand } from "./enable-disable";
8
+ import type {
9
+ AgentEntry,
10
+ FacultIndex,
11
+ McpEntry,
12
+ SkillEntry,
13
+ SnippetEntry,
14
+ } from "./index-builder";
15
+ import { indexCommand } from "./index-builder";
16
+ import {
17
+ manageCommand,
18
+ managedCommand,
19
+ syncCommand,
20
+ unmanageCommand,
21
+ } from "./manage";
22
+ import { migrateCommand } from "./migrate";
23
+ import type { QueryFilters } from "./query";
24
+ import {
25
+ filterAgents,
26
+ filterMcp,
27
+ filterSkills,
28
+ filterSnippets,
29
+ loadIndex,
30
+ } from "./query";
31
+ import {
32
+ installCommand,
33
+ searchCommand,
34
+ sourcesCommand,
35
+ templatesCommand,
36
+ updateCommand,
37
+ verifySourceCommand,
38
+ } from "./remote";
39
+ import { scanCommand } from "./scan";
40
+ import { selfUpdateCommand } from "./self-update";
41
+ import { snippetsCommand } from "./snippets-cli";
42
+ import { trustCommand, untrustCommand } from "./trust";
43
+ import { parseJsonLenient } from "./util/json";
44
+
45
+ type ListKind = "skills" | "mcp" | "agents" | "snippets";
46
+
47
+ const LIST_KINDS: ListKind[] = ["skills", "mcp", "agents", "snippets"];
48
+
49
+ export interface ListCommandOptions {
50
+ kind: ListKind;
51
+ filters: QueryFilters;
52
+ json: boolean;
53
+ }
54
+
55
+ function printHelp() {
56
+ console.log(`facult — inspect local agent configs for skills + MCP servers
57
+
58
+ Usage:
59
+ facult scan [--json] [--show-duplicates] [--tui] [--from <path>]
60
+ facult audit [--from <path>]
61
+ facult audit --non-interactive [name|mcp:<name>] [--severity <level>] [--rules <path>] [--from <path>] [--json]
62
+ facult audit --non-interactive [name|mcp:<name>] --with <claude|codex> [--from <path>] [--max-items <n|all>] [--json]
63
+ facult migrate [--from <path>] [--dry-run] [--move] [--write-config]
64
+ facult consolidate [--force] [--auto <mode>] [scan options]
65
+ facult index [--force]
66
+ facult list [skills|mcp|agents|snippets] [--enabled-for TOOL] [--untrusted] [--flagged] [--pending] [--json]
67
+ facult show <name>
68
+ facult show mcp:<name> [--show-secrets]
69
+ facult adapters
70
+ facult trust <name> [moreNames...]
71
+ facult untrust <name> [moreNames...]
72
+ facult manage <tool>
73
+ facult unmanage <tool>
74
+ facult managed
75
+ facult enable <name> [moreNames...] [--for <tools>]
76
+ facult disable <name> [moreNames...] [--for <tools>]
77
+ facult sync [tool] [--dry-run]
78
+ facult search <query> [--index <name>] [--limit <n>]
79
+ facult install <index:item> [--as <name>] [--dry-run] [--force] [--strict-source-trust]
80
+ facult update [--apply] [--strict-source-trust]
81
+ facult update --self [--version <x.y.z|latest>] [--dry-run]
82
+ facult self-update [--version <x.y.z|latest>] [--dry-run]
83
+ facult verify-source <name> [--json]
84
+ facult sources <cmd> [args...]
85
+ facult templates <cmd> [args...]
86
+ facult snippets <cmd> [args...]
87
+ facult --show-duplicates
88
+
89
+ Commands:
90
+ scan Scan common config locations (Cursor, Claude, Claude Desktop, etc.)
91
+ audit Security audits (interactive by default; use --non-interactive for scripts)
92
+ migrate Copy/move a legacy canonical store to ~/agents/.facult
93
+ consolidate Deduplicate and copy skills + MCP configs (interactive or --auto)
94
+ index Build a queryable index from the canonical store (see FACULT_ROOT_DIR)
95
+ list List indexed skills, MCP servers, agents, or snippets
96
+ show Show a single indexed entry, including file contents
97
+ adapters List registered tool adapters
98
+ trust Mark a skill or MCP server as trusted (annotation only)
99
+ untrust Remove trusted annotation
100
+ manage Back up tool config and enter managed mode
101
+ unmanage Restore backups and exit managed mode
102
+ managed List tools in managed mode
103
+ enable Enable skills or MCP servers for tools
104
+ disable Disable skills or MCP servers for tools
105
+ sync Sync managed tools with canonical configs
106
+ search Search remote indices (builtin + provider aliases + configured)
107
+ install Install an item from a remote index
108
+ update Check/apply updates for remotely installed items
109
+ self-update Update facult itself based on install method
110
+ verify-source Verify source trust and manifest integrity/signature status
111
+ sources Manage source trust policy for remote indices
112
+ templates Scaffold DX-first templates (skills/instructions/MCP/snippets)
113
+ snippets Sync reusable snippet blocks into config files
114
+
115
+ Options:
116
+ --json Print full JSON (ScanResult or list output)
117
+ --show-duplicates Print duplicates for skills, MCP servers, and hook assets
118
+ --tui Render scan output in an interactive TUI (skills list)
119
+ --from Add one or more additional scan roots (repeatable): --from ~/dev
120
+ --from-ignore (scan) Ignore directories by basename under --from roots (repeatable)
121
+ --from-no-default-ignore (scan) Disable the default ignore list for --from scans
122
+ --from-max-visits (scan) Max directories visited per --from root before truncating
123
+ --from-max-results (scan) Max discovered paths per --from root before truncating
124
+ --non-interactive (audit) Run static/agent audit non-interactively (for scripts)
125
+ --severity Minimum severity to include in audit output (low|medium|high|critical)
126
+ --rules Path to an audit rules YAML file (default: ~/.facult/audit-rules.yaml)
127
+ --with (audit) Agent tool: claude|codex
128
+ --max-items (audit) Max items to send to the agent (n|all)
129
+ --force Re-copy items already consolidated OR rebuild index from scratch
130
+ --auto Auto-resolve consolidate conflicts: keep-newest, keep-current, keep-incoming
131
+ --enabled-for Filter list to entries enabled for a specific tool
132
+ --untrusted Filter list to entries that are not trusted
133
+ --flagged Filter list to entries flagged by audit
134
+ --pending Filter list to entries pending audit
135
+ --for Comma-separated list of tools for enable/disable
136
+ --dry-run Show what sync would change
137
+ --as Install/scaffold target name override
138
+ --limit Max results for search
139
+ --apply Apply updates (update command)
140
+ --self (update) run self-update flow instead of remote item updates
141
+ --strict-source-trust Enforce trust-only remote install/update actions
142
+ --show-secrets (show) Print raw secret values (unsafe)
143
+ `);
144
+ }
145
+
146
+ function printListHelp() {
147
+ console.log(`facult list — list indexed entries from the canonical store
148
+
149
+ Usage:
150
+ facult list [skills|mcp|agents|snippets] [options]
151
+
152
+ Options:
153
+ --enabled-for TOOL Only include entries enabled for a tool
154
+ --untrusted Only include entries that are not trusted
155
+ --flagged Only include entries flagged by audit
156
+ --pending Only include entries pending audit
157
+ --json Print JSON array
158
+ `);
159
+ }
160
+
161
+ function printShowHelp() {
162
+ console.log(`facult show — show a single indexed entry (and file contents)
163
+
164
+ Usage:
165
+ facult show <name>
166
+ facult show mcp:<name> [--show-secrets]
167
+
168
+ Options:
169
+ --show-secrets (mcp) Print raw secret values (unsafe)
170
+ `);
171
+ }
172
+
173
+ function parseListKind(argv: string[]): { kind: ListKind; startIndex: number } {
174
+ const first = argv[0];
175
+ if (!first || first.startsWith("-")) {
176
+ return { kind: "skills", startIndex: 0 };
177
+ }
178
+ if (LIST_KINDS.includes(first as ListKind)) {
179
+ return { kind: first as ListKind, startIndex: 1 };
180
+ }
181
+ throw new Error(`Unknown list type: ${first}`);
182
+ }
183
+
184
+ function parseEnabledForArg(
185
+ arg: string,
186
+ nextArg?: string
187
+ ): { tool: string; advance: number } | null {
188
+ if (arg === "--enabled-for") {
189
+ if (!nextArg) {
190
+ throw new Error("--enabled-for requires a tool name");
191
+ }
192
+ return { tool: nextArg, advance: 1 };
193
+ }
194
+ if (arg.startsWith("--enabled-for=")) {
195
+ const tool = arg.slice("--enabled-for=".length);
196
+ if (!tool) {
197
+ throw new Error("--enabled-for requires a tool name");
198
+ }
199
+ return { tool, advance: 0 };
200
+ }
201
+ return null;
202
+ }
203
+
204
+ export function parseListArgs(argv: string[]): ListCommandOptions {
205
+ const { kind, startIndex } = parseListKind(argv);
206
+ const filters: QueryFilters = {};
207
+ let json = false;
208
+
209
+ for (let i = startIndex; i < argv.length; i++) {
210
+ const arg = argv[i];
211
+ if (!arg) {
212
+ continue;
213
+ }
214
+ if (arg === "--json") {
215
+ json = true;
216
+ continue;
217
+ }
218
+ if (arg === "--untrusted") {
219
+ filters.untrusted = true;
220
+ continue;
221
+ }
222
+ if (arg === "--flagged") {
223
+ filters.flagged = true;
224
+ continue;
225
+ }
226
+ if (arg === "--pending") {
227
+ filters.pending = true;
228
+ continue;
229
+ }
230
+
231
+ const enabledFor = parseEnabledForArg(arg, argv[i + 1]);
232
+ if (enabledFor) {
233
+ filters.enabledFor = enabledFor.tool;
234
+ i += enabledFor.advance;
235
+ continue;
236
+ }
237
+
238
+ throw new Error(`Unknown option: ${arg}`);
239
+ }
240
+
241
+ return { kind, filters, json };
242
+ }
243
+
244
+ async function listCommand(argv: string[]) {
245
+ if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
246
+ printListHelp();
247
+ return;
248
+ }
249
+
250
+ let opts: ListCommandOptions;
251
+ try {
252
+ opts = parseListArgs(argv);
253
+ } catch (err) {
254
+ console.error(err instanceof Error ? err.message : String(err));
255
+ process.exitCode = 1;
256
+ return;
257
+ }
258
+
259
+ let index: FacultIndex;
260
+ try {
261
+ index = await loadIndex();
262
+ } catch (err) {
263
+ console.error(err instanceof Error ? err.message : String(err));
264
+ process.exitCode = 1;
265
+ return;
266
+ }
267
+
268
+ let entries: SkillEntry[] | McpEntry[] | AgentEntry[] | SnippetEntry[] = [];
269
+
270
+ switch (opts.kind) {
271
+ case "skills":
272
+ entries = filterSkills(index.skills, opts.filters);
273
+ break;
274
+ case "mcp":
275
+ entries = filterMcp(index.mcp?.servers ?? {}, opts.filters);
276
+ break;
277
+ case "agents":
278
+ entries = filterAgents(index.agents ?? {}, opts.filters);
279
+ break;
280
+ case "snippets":
281
+ entries = filterSnippets(index.snippets ?? {}, opts.filters);
282
+ break;
283
+ default:
284
+ entries = [];
285
+ break;
286
+ }
287
+
288
+ if (opts.json) {
289
+ console.log(`${JSON.stringify(entries, null, 2)}`);
290
+ return;
291
+ }
292
+
293
+ for (const entry of entries) {
294
+ if (opts.kind === "skills") {
295
+ const skill = entry as SkillEntry;
296
+ const desc = skill.description ? `\t${skill.description}` : "";
297
+ const meta = skill as SkillEntry & {
298
+ trusted?: boolean;
299
+ auditStatus?: string;
300
+ };
301
+ const trustedLabel = meta.trusted === true ? "trusted" : "untrusted";
302
+ const auditLabel = (meta.auditStatus ?? "pending").trim().toLowerCase();
303
+ console.log(
304
+ `${skill.name}${desc}\t[${trustedLabel}; audit=${auditLabel}]`
305
+ );
306
+ } else if (opts.kind === "mcp") {
307
+ const meta = entry as McpEntry & {
308
+ trusted?: boolean;
309
+ auditStatus?: string;
310
+ };
311
+ const trustedLabel = meta.trusted === true ? "trusted" : "untrusted";
312
+ const auditLabel = (meta.auditStatus ?? "pending").trim().toLowerCase();
313
+ console.log(`${entry.name}\t[${trustedLabel}; audit=${auditLabel}]`);
314
+ } else {
315
+ console.log(entry.name);
316
+ }
317
+ }
318
+ }
319
+
320
+ async function readEntryContents(entryPath: string): Promise<string> {
321
+ const file = Bun.file(entryPath);
322
+ if (!(await file.exists())) {
323
+ throw new Error(`File not found: ${entryPath}`);
324
+ }
325
+ return file.text();
326
+ }
327
+
328
+ const SECRET_KEY_RE = /(TOKEN|KEY|SECRET|PASSWORD|PASS|BEARER)/i;
329
+ const SECRETY_STRING_RE =
330
+ /\b(sk-[A-Za-z0-9]{10,}|ghp_[A-Za-z0-9]{10,}|github_pat_[A-Za-z0-9_]{10,})\b/g;
331
+
332
+ function redactPossibleSecrets(value: string): string {
333
+ return value.replace(SECRETY_STRING_RE, "<redacted>");
334
+ }
335
+
336
+ function sanitizeForDisplay(value: unknown): unknown {
337
+ if (typeof value === "string") {
338
+ return redactPossibleSecrets(value);
339
+ }
340
+ if (Array.isArray(value)) {
341
+ return value.map(sanitizeForDisplay);
342
+ }
343
+ if (!value || typeof value !== "object") {
344
+ return value;
345
+ }
346
+ const out: Record<string, unknown> = {};
347
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
348
+ if (SECRET_KEY_RE.test(k)) {
349
+ out[k] = "<redacted>";
350
+ } else {
351
+ out[k] = sanitizeForDisplay(v);
352
+ }
353
+ }
354
+ return out;
355
+ }
356
+
357
+ async function showCommand(argv: string[]) {
358
+ if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
359
+ printShowHelp();
360
+ return;
361
+ }
362
+
363
+ let showSecrets = false;
364
+ let raw: string | null = null;
365
+ for (const arg of argv) {
366
+ if (!arg) {
367
+ continue;
368
+ }
369
+ if (arg === "--show-secrets") {
370
+ showSecrets = true;
371
+ continue;
372
+ }
373
+ if (arg.startsWith("-")) {
374
+ console.error(`Unknown option: ${arg}`);
375
+ process.exitCode = 1;
376
+ return;
377
+ }
378
+ if (raw) {
379
+ console.error("show accepts a single name");
380
+ process.exitCode = 1;
381
+ return;
382
+ }
383
+ raw = arg;
384
+ }
385
+ if (!raw) {
386
+ console.error("show requires a name");
387
+ process.exitCode = 1;
388
+ return;
389
+ }
390
+
391
+ let index: FacultIndex;
392
+ try {
393
+ index = await loadIndex();
394
+ } catch (err) {
395
+ console.error(err instanceof Error ? err.message : String(err));
396
+ process.exitCode = 1;
397
+ return;
398
+ }
399
+
400
+ let kind: ListKind | "mcp" = "skills";
401
+ let name = raw;
402
+
403
+ if (raw.startsWith("mcp:")) {
404
+ kind = "mcp";
405
+ name = raw.slice("mcp:".length);
406
+ }
407
+
408
+ let entry: SkillEntry | McpEntry | AgentEntry | SnippetEntry | null = null;
409
+ const skill = index.skills[name];
410
+ const mcpServer = index.mcp?.servers?.[name];
411
+ const agent = index.agents?.[name];
412
+ const snippet = index.snippets?.[name];
413
+
414
+ if (kind === "skills" && skill) {
415
+ entry = skill;
416
+ } else if (kind === "mcp" && mcpServer) {
417
+ entry = mcpServer;
418
+ } else if (kind === "skills" && agent) {
419
+ kind = "agents";
420
+ entry = agent;
421
+ } else if (kind === "skills" && snippet) {
422
+ kind = "snippets";
423
+ entry = snippet;
424
+ } else if (kind === "skills" && mcpServer) {
425
+ kind = "mcp";
426
+ entry = mcpServer;
427
+ }
428
+
429
+ if (!entry) {
430
+ console.error(`Entry not found: ${raw}`);
431
+ process.exitCode = 1;
432
+ return;
433
+ }
434
+
435
+ let contentPath = entry.path;
436
+ if (kind === "skills") {
437
+ contentPath = join(entry.path, "SKILL.md");
438
+ }
439
+
440
+ let contents = "";
441
+ try {
442
+ contents = await readEntryContents(contentPath);
443
+ } catch (err) {
444
+ console.error(err instanceof Error ? err.message : String(err));
445
+ process.exitCode = 1;
446
+ return;
447
+ }
448
+
449
+ const displayEntry =
450
+ kind === "mcp" && !showSecrets ? sanitizeForDisplay(entry) : entry;
451
+ let displayContents = contents;
452
+ if (kind === "mcp" && !showSecrets) {
453
+ if (contentPath.endsWith(".json")) {
454
+ try {
455
+ const parsed = parseJsonLenient(contents);
456
+ displayContents = `${JSON.stringify(sanitizeForDisplay(parsed), null, 2)}\n`;
457
+ } catch {
458
+ displayContents = redactPossibleSecrets(contents);
459
+ }
460
+ } else {
461
+ displayContents = redactPossibleSecrets(contents);
462
+ }
463
+ }
464
+
465
+ console.log(`${kind}:${entry.name}`);
466
+ console.log(JSON.stringify(displayEntry, null, 2));
467
+ console.log("\n---\n");
468
+ console.log(displayContents);
469
+ }
470
+
471
+ function adaptersCommand(argv: string[]) {
472
+ if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
473
+ console.log(
474
+ "facult adapters — list registered tool adapters\n\nUsage:\n facult adapters\n"
475
+ );
476
+ return;
477
+ }
478
+ const adapters = getAllAdapters();
479
+ if (!adapters.length) {
480
+ console.log("No adapters registered.");
481
+ return;
482
+ }
483
+ for (const adapter of adapters) {
484
+ const versions = adapter.versions.join(", ");
485
+ console.log(`${adapter.id}\t${versions}`);
486
+ }
487
+ }
488
+
489
+ async function main(argv: string[]) {
490
+ const [cmd, ...rest] = argv;
491
+ if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
492
+ printHelp();
493
+ return;
494
+ }
495
+
496
+ // Convenience: allow `facult --show-duplicates` as shorthand for `facult scan --show-duplicates`.
497
+ if (cmd === "--show-duplicates") {
498
+ await scanCommand([cmd, ...rest]);
499
+ return;
500
+ }
501
+
502
+ switch (cmd) {
503
+ case "scan":
504
+ await scanCommand(rest);
505
+ return;
506
+ case "audit":
507
+ await auditCommand(rest);
508
+ return;
509
+ case "migrate":
510
+ await migrateCommand(rest);
511
+ return;
512
+ case "consolidate":
513
+ await consolidateCommand(rest);
514
+ return;
515
+ case "index":
516
+ await indexCommand(rest);
517
+ return;
518
+ case "list":
519
+ await listCommand(rest);
520
+ return;
521
+ case "show":
522
+ await showCommand(rest);
523
+ return;
524
+ case "adapters":
525
+ await adaptersCommand(rest);
526
+ return;
527
+ case "trust":
528
+ await trustCommand(rest);
529
+ return;
530
+ case "untrust":
531
+ await untrustCommand(rest);
532
+ return;
533
+ case "manage":
534
+ await manageCommand(rest);
535
+ return;
536
+ case "unmanage":
537
+ await unmanageCommand(rest);
538
+ return;
539
+ case "managed":
540
+ await managedCommand(rest);
541
+ return;
542
+ case "enable":
543
+ await enableCommand(rest);
544
+ return;
545
+ case "disable":
546
+ await disableCommand(rest);
547
+ return;
548
+ case "sync":
549
+ await syncCommand(rest);
550
+ return;
551
+ case "search":
552
+ await searchCommand(rest);
553
+ return;
554
+ case "install":
555
+ await installCommand(rest);
556
+ return;
557
+ case "update":
558
+ if (rest.includes("--self")) {
559
+ await selfUpdateCommand(rest.filter((arg) => arg !== "--self"));
560
+ return;
561
+ }
562
+ await updateCommand(rest);
563
+ return;
564
+ case "self-update":
565
+ await selfUpdateCommand(rest);
566
+ return;
567
+ case "verify-source":
568
+ await verifySourceCommand(rest);
569
+ return;
570
+ case "templates":
571
+ await templatesCommand(rest);
572
+ return;
573
+ case "sources":
574
+ await sourcesCommand(rest);
575
+ return;
576
+ case "snippets":
577
+ await snippetsCommand(rest);
578
+ return;
579
+ default:
580
+ console.error(`Unknown command: ${cmd}`);
581
+ printHelp();
582
+ process.exitCode = 1;
583
+ return;
584
+ }
585
+ }
586
+
587
+ if (import.meta.main) {
588
+ await main(process.argv.slice(2));
589
+ }