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,237 @@
1
+ import { homedir } from "node:os";
2
+ import {
3
+ clearSourceTrustPolicy,
4
+ defaultSourceTrustLevel,
5
+ loadSourceTrustState,
6
+ type SourceTrustLevel,
7
+ type SourceTrustState,
8
+ setSourceTrustPolicy,
9
+ sourceTrustLevelFor,
10
+ } from "./source-trust";
11
+
12
+ const SOURCE_POLICY_ACTIONS = new Set(["trust", "review", "block", "clear"]);
13
+
14
+ export interface SourceIndexRef {
15
+ name: string;
16
+ url: string;
17
+ }
18
+
19
+ export interface SourcePolicyCommandContext {
20
+ homeDir?: string;
21
+ cwd?: string;
22
+ now?: () => Date;
23
+ }
24
+
25
+ export interface SourcePolicyRow {
26
+ source: string;
27
+ level: SourceTrustLevel;
28
+ explicit: boolean;
29
+ defaultLevel: SourceTrustLevel;
30
+ note?: string;
31
+ updatedAt?: string;
32
+ url?: string;
33
+ }
34
+
35
+ function parseLongFlag(argv: string[], flag: string): string | null {
36
+ for (let i = 0; i < argv.length; i += 1) {
37
+ const arg = argv[i];
38
+ if (!arg) {
39
+ continue;
40
+ }
41
+ if (arg === flag) {
42
+ return argv[i + 1] ?? null;
43
+ }
44
+ if (arg.startsWith(`${flag}=`)) {
45
+ return arg.slice(flag.length + 1);
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ function printSourcesHelp(args: { builtinIndexName: string }) {
52
+ console.log(`facult sources — manage source trust policy for remote indices
53
+
54
+ Usage:
55
+ facult sources list [--json]
56
+ facult sources trust <source> [--note <text>]
57
+ facult sources review <source> [--note <text>]
58
+ facult sources block <source> [--note <text>]
59
+ facult sources clear <source>
60
+
61
+ Notes:
62
+ - Default policy is "${args.builtinIndexName}=trusted", all other sources=review.
63
+ - Blocked sources are always denied for install/update.
64
+ - Review sources are allowed unless --strict-source-trust is enabled.
65
+ `);
66
+ }
67
+
68
+ export function evaluateSourceTrust(args: {
69
+ sourceName: string;
70
+ trustState: SourceTrustState;
71
+ }): {
72
+ level: SourceTrustLevel;
73
+ explicit: boolean;
74
+ note?: string;
75
+ updatedAt?: string;
76
+ } {
77
+ const trust = sourceTrustLevelFor({
78
+ sourceName: args.sourceName,
79
+ state: args.trustState,
80
+ });
81
+ return {
82
+ level: trust.level,
83
+ explicit: trust.explicit,
84
+ note: trust.policy?.note,
85
+ updatedAt: trust.policy?.updatedAt,
86
+ };
87
+ }
88
+
89
+ export function assertSourceAllowed(args: {
90
+ sourceName: string;
91
+ trustState: SourceTrustState;
92
+ strictSourceTrust: boolean;
93
+ }): SourceTrustLevel {
94
+ const trust = evaluateSourceTrust(args);
95
+ if (trust.level === "blocked") {
96
+ throw new Error(
97
+ `Source "${args.sourceName}" is blocked by policy. Use "facult sources clear ${args.sourceName}" to remove the block.`
98
+ );
99
+ }
100
+ if (args.strictSourceTrust && trust.level === "review") {
101
+ throw new Error(
102
+ `Source "${args.sourceName}" requires review (strict mode). Use "facult sources trust ${args.sourceName}" after review.`
103
+ );
104
+ }
105
+ return trust.level;
106
+ }
107
+
108
+ export async function sourcesCommand(args: {
109
+ argv: string[];
110
+ ctx?: SourcePolicyCommandContext;
111
+ readIndexSources: (home: string, cwd: string) => Promise<SourceIndexRef[]>;
112
+ builtinIndexName: string;
113
+ }) {
114
+ const [sub = "list", ...rest] = args.argv;
115
+ if (
116
+ sub === "--help" ||
117
+ sub === "-h" ||
118
+ sub === "help" ||
119
+ (sub !== "list" && !SOURCE_POLICY_ACTIONS.has(sub))
120
+ ) {
121
+ printSourcesHelp({ builtinIndexName: args.builtinIndexName });
122
+ return;
123
+ }
124
+
125
+ const home = args.ctx?.homeDir ?? homedir();
126
+ const cwd = args.ctx?.cwd ?? process.cwd();
127
+ const json = rest.includes("--json");
128
+
129
+ if (sub === "list") {
130
+ try {
131
+ const [sources, trustState] = await Promise.all([
132
+ args.readIndexSources(home, cwd),
133
+ loadSourceTrustState({ homeDir: home }),
134
+ ]);
135
+
136
+ const urlsByName = new Map<string, string>();
137
+ for (const source of sources) {
138
+ urlsByName.set(source.name, source.url);
139
+ }
140
+
141
+ const names = new Set<string>([
142
+ ...sources.map((source) => source.name),
143
+ ...Object.keys(trustState.sources),
144
+ ]);
145
+ const rows: SourcePolicyRow[] = Array.from(names)
146
+ .sort((a, b) => a.localeCompare(b))
147
+ .map((name) => {
148
+ const assessed = evaluateSourceTrust({
149
+ sourceName: name,
150
+ trustState,
151
+ });
152
+ return {
153
+ source: name,
154
+ level: assessed.level,
155
+ explicit: assessed.explicit,
156
+ defaultLevel: defaultSourceTrustLevel({ sourceName: name }),
157
+ note: assessed.note,
158
+ updatedAt: assessed.updatedAt,
159
+ url: urlsByName.get(name),
160
+ };
161
+ });
162
+
163
+ if (json) {
164
+ console.log(JSON.stringify(rows, null, 2));
165
+ return;
166
+ }
167
+
168
+ if (!rows.length) {
169
+ console.log("(no sources)");
170
+ return;
171
+ }
172
+ for (const row of rows) {
173
+ const origin = row.explicit ? "explicit" : "default";
174
+ const url = row.url ?? "-";
175
+ const note = row.note ? `\t${row.note}` : "";
176
+ console.log(`${row.source}\t${row.level}\t${origin}\t${url}${note}`);
177
+ }
178
+ } catch (err) {
179
+ console.error(err instanceof Error ? err.message : String(err));
180
+ process.exitCode = 1;
181
+ }
182
+ return;
183
+ }
184
+
185
+ const positional: string[] = [];
186
+ for (let i = 0; i < rest.length; i += 1) {
187
+ const arg = rest[i];
188
+ if (!arg) {
189
+ continue;
190
+ }
191
+ if (arg === "--note") {
192
+ i += 1;
193
+ continue;
194
+ }
195
+ if (arg.startsWith("--note=") || arg.startsWith("-")) {
196
+ continue;
197
+ }
198
+ positional.push(arg);
199
+ }
200
+ const sourceName = positional[0];
201
+ if (!sourceName) {
202
+ console.error(`${sub} requires a source name`);
203
+ process.exitCode = 1;
204
+ return;
205
+ }
206
+ const note = parseLongFlag(rest, "--note") ?? undefined;
207
+
208
+ try {
209
+ if (sub === "clear") {
210
+ await clearSourceTrustPolicy({
211
+ sourceName,
212
+ homeDir: home,
213
+ now: args.ctx?.now,
214
+ });
215
+ console.log(`Cleared source trust policy: ${sourceName}`);
216
+ return;
217
+ }
218
+
219
+ const level =
220
+ sub === "trust"
221
+ ? ("trusted" as const)
222
+ : sub === "review"
223
+ ? ("review" as const)
224
+ : ("blocked" as const);
225
+ await setSourceTrustPolicy({
226
+ sourceName,
227
+ level,
228
+ note,
229
+ homeDir: home,
230
+ now: args.ctx?.now,
231
+ });
232
+ console.log(`Set source policy: ${sourceName} -> ${level}`);
233
+ } catch (err) {
234
+ console.error(err instanceof Error ? err.message : String(err));
235
+ process.exitCode = 1;
236
+ }
237
+ }
@@ -0,0 +1,162 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { facultStateDir } from "./paths";
4
+ import {
5
+ type ManifestSignature,
6
+ type ManifestSignatureKey,
7
+ parseManifestIntegrity,
8
+ parseManifestSignature,
9
+ parseManifestSignatureKeys,
10
+ } from "./remote-manifest-integrity";
11
+ import {
12
+ BUILTIN_INDEX_NAME,
13
+ BUILTIN_INDEX_URL,
14
+ type IndexSource,
15
+ KNOWN_PROVIDER_SOURCES,
16
+ } from "./remote-types";
17
+ import { parseJsonLenient } from "./util/json";
18
+
19
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
20
+ return !!v && typeof v === "object" && !Array.isArray(v);
21
+ }
22
+
23
+ async function fileExists(path: string): Promise<boolean> {
24
+ try {
25
+ await Bun.file(path).stat();
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ function parseEntrySignature(
33
+ entry: Record<string, unknown>
34
+ ): ManifestSignature | undefined {
35
+ if (isPlainObject(entry.signature) || typeof entry.signature === "string") {
36
+ return parseManifestSignature(entry.signature);
37
+ }
38
+ const synthetic = {
39
+ algorithm: entry.signatureAlgorithm,
40
+ value: entry.sig ?? entry.signature,
41
+ keyId: entry.keyId ?? entry.signatureKeyId ?? entry.kid,
42
+ publicKey: entry.publicKey,
43
+ publicKeyPath: entry.publicKeyPath ?? entry.keyPath,
44
+ };
45
+ return parseManifestSignature(synthetic);
46
+ }
47
+
48
+ function mergeSignatureKeys(
49
+ globalKeys: ManifestSignatureKey[],
50
+ sourceKeys: ManifestSignatureKey[]
51
+ ): ManifestSignatureKey[] {
52
+ const merged = new Map<string, ManifestSignatureKey>();
53
+ for (const key of globalKeys) {
54
+ merged.set(key.id, key);
55
+ }
56
+ for (const key of sourceKeys) {
57
+ merged.set(key.id, key);
58
+ }
59
+ return Array.from(merged.values()).sort((a, b) => a.id.localeCompare(b.id));
60
+ }
61
+
62
+ export async function readIndexSources(
63
+ home: string,
64
+ cwd: string
65
+ ): Promise<IndexSource[]> {
66
+ const out: IndexSource[] = [
67
+ { name: BUILTIN_INDEX_NAME, url: BUILTIN_INDEX_URL, kind: "builtin" },
68
+ ];
69
+ const configPath = resolve(facultStateDir(home), "indices.json");
70
+ if (!(await fileExists(configPath))) {
71
+ return out;
72
+ }
73
+
74
+ try {
75
+ const parsed = parseJsonLenient(await readFile(configPath, "utf8"));
76
+ if (!isPlainObject(parsed)) {
77
+ return out;
78
+ }
79
+ const obj = parsed as Record<string, unknown>;
80
+ const globalKeys = parseManifestSignatureKeys(
81
+ obj.signatureKeys ?? obj.trustedKeys ?? obj.keys
82
+ );
83
+ const candidateLists: unknown[] = [obj.indices, obj.sources];
84
+ for (const list of candidateLists) {
85
+ if (!Array.isArray(list)) {
86
+ continue;
87
+ }
88
+ for (const entry of list) {
89
+ if (!isPlainObject(entry)) {
90
+ continue;
91
+ }
92
+ const name = typeof entry.name === "string" ? entry.name.trim() : "";
93
+ if (!name) {
94
+ continue;
95
+ }
96
+ const provider =
97
+ typeof entry.provider === "string" ? entry.provider.trim() : "";
98
+ const providerDefault = provider
99
+ ? KNOWN_PROVIDER_SOURCES[provider]
100
+ : undefined;
101
+ const rawUrl = typeof entry.url === "string" ? entry.url.trim() : "";
102
+ const integrity = parseManifestIntegrity(
103
+ entry.integrity ?? entry.checksum
104
+ );
105
+ const signature = parseEntrySignature(entry);
106
+ const sourceKeys = parseManifestSignatureKeys(
107
+ entry.signatureKeys ?? entry.trustedKeys ?? entry.keys
108
+ );
109
+ const signatureKeys = mergeSignatureKeys(globalKeys, sourceKeys);
110
+
111
+ if (providerDefault) {
112
+ out.push({
113
+ name,
114
+ kind: providerDefault.kind,
115
+ url: rawUrl || providerDefault.url,
116
+ integrity,
117
+ signature,
118
+ signatureKeys: signatureKeys.length ? signatureKeys : undefined,
119
+ });
120
+ continue;
121
+ }
122
+
123
+ if (!rawUrl) {
124
+ continue;
125
+ }
126
+ const resolvedUrl =
127
+ rawUrl.startsWith("http://") ||
128
+ rawUrl.startsWith("https://") ||
129
+ rawUrl.startsWith("file://") ||
130
+ isAbsolute(rawUrl)
131
+ ? rawUrl
132
+ : resolve(cwd, rawUrl);
133
+ out.push({
134
+ name,
135
+ url: resolvedUrl,
136
+ kind: "manifest",
137
+ integrity,
138
+ signature,
139
+ signatureKeys: signatureKeys.length ? signatureKeys : undefined,
140
+ });
141
+ }
142
+ }
143
+ } catch {
144
+ // Ignore malformed index config and keep builtin defaults.
145
+ }
146
+
147
+ const dedup = new Map<string, IndexSource>();
148
+ for (const source of out) {
149
+ dedup.set(source.name, source);
150
+ }
151
+ return Array.from(dedup.values()).sort((a, b) =>
152
+ a.name.localeCompare(b.name)
153
+ );
154
+ }
155
+
156
+ export function resolveKnownIndexSource(name: string): IndexSource | null {
157
+ const source = KNOWN_PROVIDER_SOURCES[name];
158
+ if (!source) {
159
+ return null;
160
+ }
161
+ return { ...source };
162
+ }
@@ -0,0 +1,136 @@
1
+ import type {
2
+ ManifestIntegrity,
3
+ ManifestSignature,
4
+ ManifestSignatureKey,
5
+ } from "./remote-manifest-integrity";
6
+
7
+ export type RemoteItemType = "skill" | "mcp" | "agent" | "snippet";
8
+
9
+ export interface RemoteSkillPayload {
10
+ name: string;
11
+ files: Record<string, string>;
12
+ }
13
+
14
+ export interface RemoteMcpPayload {
15
+ name: string;
16
+ definition: Record<string, unknown>;
17
+ }
18
+
19
+ export interface RemoteAgentPayload {
20
+ fileName: string;
21
+ content: string;
22
+ }
23
+
24
+ export interface RemoteSnippetPayload {
25
+ marker: string;
26
+ content: string;
27
+ }
28
+
29
+ export interface RemoteIndexItemBase {
30
+ id: string;
31
+ type: RemoteItemType;
32
+ title?: string;
33
+ description?: string;
34
+ version?: string;
35
+ tags?: string[];
36
+ sourceUrl?: string;
37
+ }
38
+
39
+ export interface RemoteSkillItem extends RemoteIndexItemBase {
40
+ type: "skill";
41
+ skill: RemoteSkillPayload;
42
+ }
43
+
44
+ export interface RemoteMcpItem extends RemoteIndexItemBase {
45
+ type: "mcp";
46
+ mcp: RemoteMcpPayload;
47
+ }
48
+
49
+ export interface RemoteAgentItem extends RemoteIndexItemBase {
50
+ type: "agent";
51
+ agent: RemoteAgentPayload;
52
+ }
53
+
54
+ export interface RemoteSnippetItem extends RemoteIndexItemBase {
55
+ type: "snippet";
56
+ snippet: RemoteSnippetPayload;
57
+ }
58
+
59
+ export type RemoteIndexItem =
60
+ | RemoteSkillItem
61
+ | RemoteMcpItem
62
+ | RemoteAgentItem
63
+ | RemoteSnippetItem;
64
+
65
+ export interface RemoteIndexManifest {
66
+ name: string;
67
+ url: string;
68
+ updatedAt?: string;
69
+ items: RemoteIndexItem[];
70
+ }
71
+
72
+ export type IndexSourceKind =
73
+ | "builtin"
74
+ | "manifest"
75
+ | "smithery"
76
+ | "glama"
77
+ | "skills-sh"
78
+ | "clawhub";
79
+
80
+ export interface IndexSource {
81
+ name: string;
82
+ url: string;
83
+ kind: IndexSourceKind;
84
+ integrity?: ManifestIntegrity;
85
+ signature?: ManifestSignature;
86
+ signatureKeys?: ManifestSignatureKey[];
87
+ }
88
+
89
+ export interface LoadManifestHints {
90
+ query?: string;
91
+ itemId?: string;
92
+ }
93
+
94
+ export const BUILTIN_INDEX_NAME = "facult";
95
+ export const BUILTIN_INDEX_URL = "builtin://facult";
96
+ export const SMITHERY_INDEX_NAME = "smithery";
97
+ export const GLAMA_INDEX_NAME = "glama";
98
+ export const SKILLS_SH_INDEX_NAME = "skills.sh";
99
+ export const CLAWHUB_INDEX_NAME = "clawhub";
100
+ export const SMITHERY_API_BASE = "https://api.smithery.ai";
101
+ export const GLAMA_API_BASE = "https://glama.ai/api/mcp/v1";
102
+ export const SKILLS_SH_WEB_BASE = "https://skills.sh";
103
+ export const CLAWHUB_API_BASE = "https://wry-manatee-359.convex.site/api/v1";
104
+
105
+ export const KNOWN_PROVIDER_SOURCES: Record<string, IndexSource> = {
106
+ [SMITHERY_INDEX_NAME]: {
107
+ name: SMITHERY_INDEX_NAME,
108
+ url: SMITHERY_API_BASE,
109
+ kind: "smithery",
110
+ },
111
+ [GLAMA_INDEX_NAME]: {
112
+ name: GLAMA_INDEX_NAME,
113
+ url: GLAMA_API_BASE,
114
+ kind: "glama",
115
+ },
116
+ [SKILLS_SH_INDEX_NAME]: {
117
+ name: SKILLS_SH_INDEX_NAME,
118
+ url: SKILLS_SH_WEB_BASE,
119
+ kind: "skills-sh",
120
+ },
121
+ [CLAWHUB_INDEX_NAME]: {
122
+ name: CLAWHUB_INDEX_NAME,
123
+ url: CLAWHUB_API_BASE,
124
+ kind: "clawhub",
125
+ },
126
+ "skills-sh": {
127
+ name: SKILLS_SH_INDEX_NAME,
128
+ url: SKILLS_SH_WEB_BASE,
129
+ kind: "skills-sh",
130
+ },
131
+ "clawhub.ai": {
132
+ name: CLAWHUB_INDEX_NAME,
133
+ url: CLAWHUB_API_BASE,
134
+ kind: "clawhub",
135
+ },
136
+ };