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
@@ -0,0 +1,126 @@
1
+ const SECTION_RE = /^\s*\[([^\]]+)\]\s*$/;
2
+ const DOUBLE_QUOTED_KEY_RE = /^"([^"]+)"(.*)$/;
3
+ const SINGLE_QUOTED_KEY_RE = /^'([^']+)'(.*)$/;
4
+
5
+ const SECRETY_STRING_RE =
6
+ /\b(sk-[A-Za-z0-9]{10,}|ghp_[A-Za-z0-9]{10,}|github_pat_[A-Za-z0-9_]{10,})\b/g;
7
+ const SECRET_KEY_RE = /(TOKEN|KEY|SECRET|PASSWORD|PASS|BEARER)/i;
8
+
9
+ function normalizeNewlines(text: string): string {
10
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
11
+ }
12
+
13
+ function parseCodexMcpServerNameFromSection(section: string): string | null {
14
+ const prefix = "mcp_servers.";
15
+ if (!section.startsWith(prefix)) {
16
+ return null;
17
+ }
18
+ const rest = section.slice(prefix.length).trim();
19
+ if (!rest) {
20
+ return null;
21
+ }
22
+
23
+ // Quoted key: mcp_servers."Sequential_Thinking"
24
+ if (rest.startsWith('"')) {
25
+ const m = DOUBLE_QUOTED_KEY_RE.exec(rest);
26
+ if (!m) {
27
+ return null;
28
+ }
29
+ return m[1] || null;
30
+ }
31
+
32
+ // Single-quoted keys are unusual in TOML, but handle best-effort.
33
+ if (rest.startsWith("'")) {
34
+ const m = SINGLE_QUOTED_KEY_RE.exec(rest);
35
+ if (!m) {
36
+ return null;
37
+ }
38
+ return m[1] || null;
39
+ }
40
+
41
+ // Unquoted: mcp_servers.github.env -> github
42
+ const first = rest.split(".")[0];
43
+ return first || null;
44
+ }
45
+
46
+ export function extractCodexTomlMcpServerBlocks(
47
+ text: string
48
+ ): Record<string, string> {
49
+ const lines = normalizeNewlines(text).split("\n");
50
+ const out = new Map<string, string[]>();
51
+ let current: string | null = null;
52
+
53
+ for (const line of lines) {
54
+ const m = SECTION_RE.exec(line);
55
+ if (m) {
56
+ const section = m[1] ?? "";
57
+ const server = parseCodexMcpServerNameFromSection(section);
58
+ if (server) {
59
+ current = server;
60
+ const arr = out.get(server) ?? [];
61
+ arr.push(line);
62
+ out.set(server, arr);
63
+ } else {
64
+ current = null;
65
+ }
66
+ continue;
67
+ }
68
+
69
+ if (current) {
70
+ const arr = out.get(current) ?? [];
71
+ arr.push(line);
72
+ out.set(current, arr);
73
+ }
74
+ }
75
+
76
+ const obj: Record<string, string> = {};
77
+ for (const [name, lines] of out.entries()) {
78
+ obj[name] = `${lines.join("\n")}\n`;
79
+ }
80
+ return obj;
81
+ }
82
+
83
+ export function extractCodexTomlMcpServerNames(text: string): string[] {
84
+ return Object.keys(extractCodexTomlMcpServerBlocks(text)).sort();
85
+ }
86
+
87
+ export function sanitizeCodexTomlMcpText(text: string): string {
88
+ const normalized = normalizeNewlines(text);
89
+
90
+ // 1) Redact obvious token formats.
91
+ let out = normalized.replace(SECRETY_STRING_RE, "<redacted>");
92
+
93
+ // 2) Redact TOML assignments for secret-ish keys: FOO_TOKEN = "..."
94
+ out = out.replace(
95
+ /^(\s*[A-Za-z0-9_]*(TOKEN|KEY|SECRET|PASSWORD|PASS|BEARER)[A-Za-z0-9_]*\s*=\s*).+$/gim,
96
+ '$1"<redacted>"'
97
+ );
98
+
99
+ // 3) Redact inline env assignments embedded in strings: API_KEY="..."
100
+ out = out.replace(
101
+ /\b([A-Z0-9_]*(TOKEN|KEY|SECRET|PASSWORD|PASS|BEARER)[A-Z0-9_]*)="[^"]*"/gi,
102
+ '$1="<redacted>"'
103
+ );
104
+
105
+ // 4) As a last pass, redact any remaining long quoted values for secret-ish keys
106
+ // inside nested tables (best-effort).
107
+ out = out.replace(
108
+ /^(\s*[A-Za-z0-9_]*(TOKEN|KEY|SECRET|PASSWORD|PASS|BEARER)[A-Za-z0-9_]*\s*=\s*)'.*'$/gim,
109
+ "$1'<redacted>'"
110
+ );
111
+
112
+ return out;
113
+ }
114
+
115
+ export function sanitizeCodexTomlForAudit(value: unknown): unknown {
116
+ // Keep this file focused; callers can use sanitizeCodexTomlMcpText() on strings.
117
+ if (typeof value === "string") {
118
+ // Avoid leaking token-like substrings when embedding in other payloads.
119
+ const redacted = value.replace(SECRETY_STRING_RE, "<redacted>");
120
+ if (SECRET_KEY_RE.test(value)) {
121
+ return "<redacted>";
122
+ }
123
+ return redacted;
124
+ }
125
+ return value;
126
+ }
@@ -0,0 +1,32 @@
1
+ import { parse as parseJsonc } from "jsonc-parser";
2
+
3
+ type JsoncParseError = { error: number; offset: number; length: number };
4
+
5
+ /**
6
+ * Parse JSON, falling back to JSONC (comments + trailing commas).
7
+ *
8
+ * This is mainly needed for VS Code-like settings files (`settings.json`) which
9
+ * are commonly JSONC.
10
+ */
11
+ export function parseJsonLenient(text: string): unknown {
12
+ try {
13
+ return JSON.parse(text) as unknown;
14
+ } catch {
15
+ // fall through to JSONC
16
+ }
17
+
18
+ const errors: JsoncParseError[] = [];
19
+ const value = parseJsonc(text, errors, {
20
+ allowTrailingComma: true,
21
+ disallowComments: false,
22
+ });
23
+
24
+ if (errors.length) {
25
+ const first = errors[0];
26
+ throw new Error(
27
+ `JSONC Parse error at offset ${first?.offset ?? 0} (code ${first?.error ?? "unknown"})`
28
+ );
29
+ }
30
+
31
+ return value as unknown;
32
+ }
@@ -0,0 +1,55 @@
1
+ import { basename } from "node:path";
2
+ import type { ScanResult } from "../scan";
3
+
4
+ export interface SkillOccurrence {
5
+ name: string;
6
+ count: number;
7
+ // One entry per appearance, formatted as "<sourceId>:<entryDir>".
8
+ locations: string[];
9
+ }
10
+
11
+ function skillNameFromEntryDir(entryDir: string): string {
12
+ // The skill name is derived from the directory that contains SKILL.md.
13
+ // e.g. /path/to/skills/my-skill -> my-skill
14
+ return basename(entryDir);
15
+ }
16
+
17
+ export function computeSkillOccurrences(res: ScanResult): SkillOccurrence[] {
18
+ const byName = new Map<string, { count: number; locations: Set<string> }>();
19
+
20
+ for (const src of res.sources) {
21
+ for (const entryDir of src.skills.entries) {
22
+ const name = skillNameFromEntryDir(entryDir);
23
+ const cur = byName.get(name) ?? {
24
+ count: 0,
25
+ locations: new Set<string>(),
26
+ };
27
+ cur.count += 1;
28
+ cur.locations.add(`${src.id}:${entryDir}`);
29
+ byName.set(name, cur);
30
+ }
31
+ }
32
+
33
+ return [...byName.entries()]
34
+ .map(([name, v]) => ({
35
+ name,
36
+ count: v.count,
37
+ locations: [...v.locations].sort(),
38
+ }))
39
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
40
+ }
41
+
42
+ export async function lastModified(p: string): Promise<Date | null> {
43
+ try {
44
+ const st = await Bun.file(p).stat();
45
+ if (st.mtime instanceof Date) {
46
+ return st.mtime;
47
+ }
48
+ if (typeof st.mtimeMs === "number") {
49
+ return new Date(st.mtimeMs);
50
+ }
51
+ return null;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }