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
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "facult",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Manage coding-agent skills and MCP configs across tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/hack-dance/facult.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/hack-dance/facult#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/hack-dance/facult/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cli",
|
|
17
|
+
"agents",
|
|
18
|
+
"mcp",
|
|
19
|
+
"skills",
|
|
20
|
+
"bun"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18",
|
|
24
|
+
"bun": ">=1.3.0"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"bin/facult.cjs",
|
|
31
|
+
"src/**/*.ts",
|
|
32
|
+
"!src/**/*.test.ts",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"bin": {
|
|
37
|
+
"facult": "bin/facult.cjs"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"facult": "bun run ./src/index.ts",
|
|
41
|
+
"scan": "bun run ./src/index.ts scan",
|
|
42
|
+
"build": "bun run scripts/build-binary.ts",
|
|
43
|
+
"build:verify": "./dist/facult --help",
|
|
44
|
+
"install:dev": "bun run scripts/install-cli.ts --mode=dev --force",
|
|
45
|
+
"install:bin": "bun run build && bun run scripts/install-cli.ts --mode=bin --force",
|
|
46
|
+
"install:status": "bun run scripts/install-status.ts",
|
|
47
|
+
"check": "ultracite check",
|
|
48
|
+
"fix": "ultracite fix",
|
|
49
|
+
"type-check": "tsc --noEmit",
|
|
50
|
+
"test": "bun test",
|
|
51
|
+
"test:ci": "bun run scripts/test-ci.ts",
|
|
52
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
53
|
+
"release:dry-run": "bunx semantic-release --dry-run --no-ci",
|
|
54
|
+
"release": "bunx semantic-release",
|
|
55
|
+
"prepare": "husky"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@biomejs/biome": "2.3.13",
|
|
59
|
+
"@semantic-release/changelog": "6.0.3",
|
|
60
|
+
"@semantic-release/commit-analyzer": "13.0.1",
|
|
61
|
+
"@semantic-release/git": "10.0.1",
|
|
62
|
+
"@semantic-release/github": "11.0.6",
|
|
63
|
+
"@semantic-release/npm": "12.0.2",
|
|
64
|
+
"@semantic-release/release-notes-generator": "14.1.0",
|
|
65
|
+
"@types/bun": "latest",
|
|
66
|
+
"conventional-changelog-conventionalcommits": "^8.0.0",
|
|
67
|
+
"husky": "^9.1.7",
|
|
68
|
+
"semantic-release": "25.0.2",
|
|
69
|
+
"typescript": "5.9.3",
|
|
70
|
+
"ultracite": "7.1.1"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@clack/prompts": "^1.0.0",
|
|
74
|
+
"@opentui/core": "^0.1.75",
|
|
75
|
+
"jsonc-parser": "^3.3.1",
|
|
76
|
+
"yaml": "^2.8.2"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { generateMcpConfig, parseMcpConfig } from "./mcp";
|
|
2
|
+
import { parseSkillsDir } from "./skills";
|
|
3
|
+
import type { ToolAdapter } from "./types";
|
|
4
|
+
import { detectExplicitVersion } from "./version";
|
|
5
|
+
|
|
6
|
+
export const claudeCliAdapter: ToolAdapter = {
|
|
7
|
+
id: "claude",
|
|
8
|
+
name: "Claude CLI",
|
|
9
|
+
versions: ["v1"],
|
|
10
|
+
detectVersion: detectExplicitVersion,
|
|
11
|
+
getDefaultPaths: () => ({
|
|
12
|
+
mcp: "~/.claude.json",
|
|
13
|
+
skills: "~/.claude/skills",
|
|
14
|
+
}),
|
|
15
|
+
parseMcp: (config) => parseMcpConfig(config),
|
|
16
|
+
generateMcp: (canonical) => generateMcpConfig(canonical, "mcpServers"),
|
|
17
|
+
parseSkills: async (skillsDir) => await parseSkillsDir(skillsDir),
|
|
18
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { generateMcpConfig, parseMcpConfig } from "./mcp";
|
|
2
|
+
import type { ToolAdapter } from "./types";
|
|
3
|
+
import { detectExplicitVersion } from "./version";
|
|
4
|
+
|
|
5
|
+
export const claudeDesktopAdapter: ToolAdapter = {
|
|
6
|
+
id: "claude-desktop",
|
|
7
|
+
name: "Claude Desktop",
|
|
8
|
+
versions: ["v1"],
|
|
9
|
+
detectVersion: detectExplicitVersion,
|
|
10
|
+
getDefaultPaths: () => ({
|
|
11
|
+
mcp: "~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
12
|
+
}),
|
|
13
|
+
parseMcp: (config) => parseMcpConfig(config),
|
|
14
|
+
generateMcp: (canonical) => generateMcpConfig(canonical, "mcpServers"),
|
|
15
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { generateMcpConfig, parseMcpConfig } from "./mcp";
|
|
2
|
+
import { parseSkillsDir } from "./skills";
|
|
3
|
+
import type { ToolAdapter } from "./types";
|
|
4
|
+
import { detectExplicitVersion } from "./version";
|
|
5
|
+
|
|
6
|
+
export const clawdbotAdapter: ToolAdapter = {
|
|
7
|
+
id: "clawdbot",
|
|
8
|
+
name: "Clawdbot",
|
|
9
|
+
versions: ["v1"],
|
|
10
|
+
detectVersion: detectExplicitVersion,
|
|
11
|
+
getDefaultPaths: () => ({
|
|
12
|
+
mcp: "~/.clawdbot/mcp.json",
|
|
13
|
+
skills: "~/.clawdbot/skills",
|
|
14
|
+
}),
|
|
15
|
+
parseMcp: (config) => parseMcpConfig(config),
|
|
16
|
+
generateMcp: (canonical) => generateMcpConfig(canonical, "mcpServers"),
|
|
17
|
+
parseSkills: async (skillsDir) => await parseSkillsDir(skillsDir),
|
|
18
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { generateMcpConfig, parseMcpConfig } from "./mcp";
|
|
2
|
+
import { parseSkillsDir } from "./skills";
|
|
3
|
+
import type { ToolAdapter } from "./types";
|
|
4
|
+
import { detectExplicitVersion } from "./version";
|
|
5
|
+
|
|
6
|
+
export const codexAdapter: ToolAdapter = {
|
|
7
|
+
id: "codex",
|
|
8
|
+
name: "Codex",
|
|
9
|
+
versions: ["v1"],
|
|
10
|
+
detectVersion: detectExplicitVersion,
|
|
11
|
+
getDefaultPaths: () => ({
|
|
12
|
+
mcp: "~/.codex/mcp.json",
|
|
13
|
+
skills: "~/.codex/skills",
|
|
14
|
+
config: "~/.config/openai/codex.json",
|
|
15
|
+
}),
|
|
16
|
+
parseMcp: (config) => parseMcpConfig(config),
|
|
17
|
+
generateMcp: (canonical) => generateMcpConfig(canonical, "mcpServers"),
|
|
18
|
+
parseSkills: async (skillsDir) => await parseSkillsDir(skillsDir),
|
|
19
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { generateMcpConfig, parseMcpConfig } from "./mcp";
|
|
2
|
+
import { parseSkillsDir } from "./skills";
|
|
3
|
+
import type { ToolAdapter } from "./types";
|
|
4
|
+
import { detectExplicitVersion } from "./version";
|
|
5
|
+
|
|
6
|
+
export const cursorAdapter: ToolAdapter = {
|
|
7
|
+
id: "cursor",
|
|
8
|
+
name: "Cursor",
|
|
9
|
+
versions: ["v1"],
|
|
10
|
+
detectVersion: detectExplicitVersion,
|
|
11
|
+
getDefaultPaths: () => ({
|
|
12
|
+
mcp: "~/.cursor/mcp.json",
|
|
13
|
+
skills: "~/.cursor/skills",
|
|
14
|
+
}),
|
|
15
|
+
parseMcp: (config) => parseMcpConfig(config),
|
|
16
|
+
generateMcp: (canonical) => generateMcpConfig(canonical, "mcpServers"),
|
|
17
|
+
parseSkills: async (skillsDir) => await parseSkillsDir(skillsDir),
|
|
18
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { claudeCliAdapter } from "./claude-cli";
|
|
2
|
+
import { claudeDesktopAdapter } from "./claude-desktop";
|
|
3
|
+
import { clawdbotAdapter } from "./clawdbot";
|
|
4
|
+
import { codexAdapter } from "./codex";
|
|
5
|
+
import { cursorAdapter } from "./cursor";
|
|
6
|
+
import { referenceAdapter } from "./reference";
|
|
7
|
+
import type { ResolveVersionOptions, ToolAdapter } from "./types";
|
|
8
|
+
|
|
9
|
+
const registry = new Map<string, ToolAdapter>();
|
|
10
|
+
|
|
11
|
+
export function registerAdapter(adapter: ToolAdapter) {
|
|
12
|
+
if (registry.has(adapter.id)) {
|
|
13
|
+
throw new Error(`Adapter already registered: ${adapter.id}`);
|
|
14
|
+
}
|
|
15
|
+
registry.set(adapter.id, adapter);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getAdapter(id: string): ToolAdapter | undefined {
|
|
19
|
+
return registry.get(id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getAllAdapters(): ToolAdapter[] {
|
|
23
|
+
return Array.from(registry.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function clearAdapters() {
|
|
27
|
+
registry.clear();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function resolveAdapterVersion(
|
|
31
|
+
adapter: ToolAdapter,
|
|
32
|
+
configPath: string,
|
|
33
|
+
options: ResolveVersionOptions = {}
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
const warn = options.warn ?? console.warn;
|
|
36
|
+
const fallback =
|
|
37
|
+
options.fallbackVersion ?? adapter.versions.at(-1) ?? "unknown";
|
|
38
|
+
|
|
39
|
+
if (!adapter.detectVersion) {
|
|
40
|
+
warn(
|
|
41
|
+
`Adapter ${adapter.id} has no version detection; using fallback ${fallback}.`
|
|
42
|
+
);
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const detected = await adapter.detectVersion(configPath);
|
|
47
|
+
if (detected && adapter.versions.includes(detected)) {
|
|
48
|
+
return detected;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (detected && !adapter.versions.includes(detected)) {
|
|
52
|
+
warn(
|
|
53
|
+
`Adapter ${adapter.id} detected unsupported version ${detected}; using fallback ${fallback}.`
|
|
54
|
+
);
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
warn(
|
|
59
|
+
`Adapter ${adapter.id} could not detect a version; using fallback ${fallback}.`
|
|
60
|
+
);
|
|
61
|
+
return fallback;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
registerAdapter(referenceAdapter);
|
|
65
|
+
registerAdapter(cursorAdapter);
|
|
66
|
+
registerAdapter(codexAdapter);
|
|
67
|
+
registerAdapter(claudeCliAdapter);
|
|
68
|
+
registerAdapter(claudeDesktopAdapter);
|
|
69
|
+
registerAdapter(clawdbotAdapter);
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { CanonicalMcpConfig, CanonicalMcpServer } from "./types";
|
|
2
|
+
|
|
3
|
+
type McpContainer = "mcpServers" | "servers" | "mcp";
|
|
4
|
+
|
|
5
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
6
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isStringArray(v: unknown): v is string[] {
|
|
10
|
+
return Array.isArray(v) && v.every((item) => typeof item === "string");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isStringRecord(v: unknown): v is Record<string, string> {
|
|
14
|
+
if (!isPlainObject(v)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return Object.values(v).every((val) => typeof val === "string");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SERVER_KNOWN_KEYS = new Set([
|
|
21
|
+
"transport",
|
|
22
|
+
"command",
|
|
23
|
+
"args",
|
|
24
|
+
"url",
|
|
25
|
+
"env",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function normalizeArgs(value: unknown): string[] | undefined {
|
|
29
|
+
if (isStringArray(value)) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.map(String);
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeEnv(value: unknown): Record<string, string> | undefined {
|
|
39
|
+
if (isStringRecord(value)) {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
if (!isPlainObject(value)) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const entries = Object.entries(value).filter(
|
|
46
|
+
([, v]) => typeof v === "string"
|
|
47
|
+
);
|
|
48
|
+
if (!entries.length) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return Object.fromEntries(entries.map(([k, v]) => [k, String(v)]));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setKnownServerField(
|
|
55
|
+
out: CanonicalMcpServer,
|
|
56
|
+
key: string,
|
|
57
|
+
value: unknown
|
|
58
|
+
): boolean {
|
|
59
|
+
switch (key) {
|
|
60
|
+
case "transport":
|
|
61
|
+
out.transport = typeof value === "string" ? value : out.transport;
|
|
62
|
+
return true;
|
|
63
|
+
case "command":
|
|
64
|
+
out.command = typeof value === "string" ? value : out.command;
|
|
65
|
+
return true;
|
|
66
|
+
case "args":
|
|
67
|
+
out.args = normalizeArgs(value);
|
|
68
|
+
return true;
|
|
69
|
+
case "url":
|
|
70
|
+
out.url = typeof value === "string" ? value : out.url;
|
|
71
|
+
return true;
|
|
72
|
+
case "env":
|
|
73
|
+
out.env = normalizeEnv(value);
|
|
74
|
+
return true;
|
|
75
|
+
default:
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function canonicalizeServer(config: unknown): CanonicalMcpServer {
|
|
81
|
+
if (!isPlainObject(config)) {
|
|
82
|
+
return {
|
|
83
|
+
vendorExtensions: { raw: config },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const out: CanonicalMcpServer = {};
|
|
88
|
+
const vendor: Record<string, unknown> = {};
|
|
89
|
+
|
|
90
|
+
for (const [k, v] of Object.entries(config)) {
|
|
91
|
+
if (!(SERVER_KNOWN_KEYS.has(k) && setKnownServerField(out, k, v))) {
|
|
92
|
+
vendor[k] = v;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (Object.keys(vendor).length) {
|
|
97
|
+
out.vendorExtensions = vendor;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function detectContainer(
|
|
104
|
+
obj: Record<string, unknown>
|
|
105
|
+
): { container: McpContainer; servers: Record<string, unknown> } | null {
|
|
106
|
+
if (isPlainObject(obj.mcpServers)) {
|
|
107
|
+
return { container: "mcpServers", servers: obj.mcpServers };
|
|
108
|
+
}
|
|
109
|
+
if (isPlainObject(obj.mcp)) {
|
|
110
|
+
const mcp = obj.mcp as Record<string, unknown>;
|
|
111
|
+
if (isPlainObject(mcp.servers)) {
|
|
112
|
+
return { container: "mcp", servers: mcp.servers };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (isPlainObject(obj.servers)) {
|
|
116
|
+
return { container: "servers", servers: obj.servers };
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractMcpVendorExtensions(value: unknown): Record<string, unknown> {
|
|
122
|
+
if (!isPlainObject(value)) {
|
|
123
|
+
return { mcp: value };
|
|
124
|
+
}
|
|
125
|
+
const { servers: _servers, ...rest } = value as Record<string, unknown>;
|
|
126
|
+
if (Object.keys(rest).length) {
|
|
127
|
+
return { mcp: rest };
|
|
128
|
+
}
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractServers(config: unknown): {
|
|
133
|
+
servers: Record<string, unknown>;
|
|
134
|
+
vendorExtensions: Record<string, unknown>;
|
|
135
|
+
container: McpContainer | null;
|
|
136
|
+
} {
|
|
137
|
+
if (!isPlainObject(config)) {
|
|
138
|
+
return { servers: {}, vendorExtensions: {}, container: null };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const obj = config as Record<string, unknown>;
|
|
142
|
+
const detected = detectContainer(obj);
|
|
143
|
+
const container = detected?.container ?? null;
|
|
144
|
+
const servers = detected?.servers ?? {};
|
|
145
|
+
|
|
146
|
+
const vendorExtensions: Record<string, unknown> = {};
|
|
147
|
+
|
|
148
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
149
|
+
if (container === "mcpServers" && k === "mcpServers") {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (container === "servers" && k === "servers") {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (container === "mcp" && k === "mcp") {
|
|
156
|
+
Object.assign(vendorExtensions, extractMcpVendorExtensions(v));
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
vendorExtensions[k] = v;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { servers, vendorExtensions, container };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function parseMcpConfig(config: unknown): CanonicalMcpConfig {
|
|
166
|
+
const { servers, vendorExtensions } = extractServers(config);
|
|
167
|
+
const canonical: CanonicalMcpConfig = {
|
|
168
|
+
servers: {},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
172
|
+
canonical.servers[name] = canonicalizeServer(cfg);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (Object.keys(vendorExtensions).length) {
|
|
176
|
+
canonical.vendorExtensions = vendorExtensions;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return canonical;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function generateServerConfig(server: CanonicalMcpServer): unknown {
|
|
183
|
+
const known: Record<string, unknown> = {};
|
|
184
|
+
const hasKnownKeys = (key: keyof CanonicalMcpServer) =>
|
|
185
|
+
server[key] !== undefined;
|
|
186
|
+
|
|
187
|
+
if (hasKnownKeys("transport")) {
|
|
188
|
+
known.transport = server.transport;
|
|
189
|
+
}
|
|
190
|
+
if (hasKnownKeys("command")) {
|
|
191
|
+
known.command = server.command;
|
|
192
|
+
}
|
|
193
|
+
if (hasKnownKeys("args")) {
|
|
194
|
+
known.args = server.args;
|
|
195
|
+
}
|
|
196
|
+
if (hasKnownKeys("url")) {
|
|
197
|
+
known.url = server.url;
|
|
198
|
+
}
|
|
199
|
+
if (hasKnownKeys("env")) {
|
|
200
|
+
known.env = server.env;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const vendor = isPlainObject(server.vendorExtensions)
|
|
204
|
+
? (server.vendorExtensions as Record<string, unknown>)
|
|
205
|
+
: null;
|
|
206
|
+
|
|
207
|
+
if (vendor && "raw" in vendor && Object.keys(known).length === 0) {
|
|
208
|
+
const rawValue = vendor.raw;
|
|
209
|
+
if (Object.keys(vendor).length === 1) {
|
|
210
|
+
return rawValue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (vendor) {
|
|
215
|
+
for (const [k, v] of Object.entries(vendor)) {
|
|
216
|
+
if (!(k in known)) {
|
|
217
|
+
known[k] = v;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return known;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function generateMcpConfig(
|
|
226
|
+
canonical: CanonicalMcpConfig,
|
|
227
|
+
container: McpContainer = "mcpServers"
|
|
228
|
+
): Record<string, unknown> {
|
|
229
|
+
const servers: Record<string, unknown> = {};
|
|
230
|
+
for (const [name, server] of Object.entries(canonical.servers ?? {})) {
|
|
231
|
+
servers[name] = generateServerConfig(server);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const vendor = isPlainObject(canonical.vendorExtensions)
|
|
235
|
+
? (canonical.vendorExtensions as Record<string, unknown>)
|
|
236
|
+
: {};
|
|
237
|
+
|
|
238
|
+
const out: Record<string, unknown> = {};
|
|
239
|
+
|
|
240
|
+
if (container === "mcp") {
|
|
241
|
+
const mcpObj: Record<string, unknown> = { servers };
|
|
242
|
+
if (isPlainObject(vendor.mcp)) {
|
|
243
|
+
Object.assign(mcpObj, vendor.mcp as Record<string, unknown>);
|
|
244
|
+
}
|
|
245
|
+
out.mcp = mcpObj;
|
|
246
|
+
} else if (container === "servers") {
|
|
247
|
+
out.servers = servers;
|
|
248
|
+
} else {
|
|
249
|
+
out.mcpServers = servers;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const [k, v] of Object.entries(vendor)) {
|
|
253
|
+
if (k === "mcp" && container === "mcp") {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (k === "servers" && container === "servers") {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (k === "mcpServers" && container === "mcpServers") {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
out[k] = v;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function isPlainObjectValue(v: unknown): v is Record<string, unknown> {
|
|
269
|
+
return isPlainObject(v);
|
|
270
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import type { CanonicalSkill } from "./types";
|
|
4
|
+
|
|
5
|
+
async function listSubdirs(dir: string): Promise<string[]> {
|
|
6
|
+
try {
|
|
7
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
8
|
+
return entries
|
|
9
|
+
.filter((entry) => entry.isDirectory())
|
|
10
|
+
.map((entry) => join(dir, entry.name));
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function skillFromDir(dir: string): Promise<CanonicalSkill | null> {
|
|
17
|
+
const path = join(dir, "SKILL.md");
|
|
18
|
+
try {
|
|
19
|
+
const file = Bun.file(path);
|
|
20
|
+
const st = await file.stat();
|
|
21
|
+
if (!st.isFile()) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const body = await file.text();
|
|
25
|
+
return {
|
|
26
|
+
name: basename(dir),
|
|
27
|
+
body,
|
|
28
|
+
path: dir,
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function parseSkillsDir(
|
|
36
|
+
skillsDir: string
|
|
37
|
+
): Promise<CanonicalSkill[]> {
|
|
38
|
+
const dirs = await listSubdirs(skillsDir);
|
|
39
|
+
const out: CanonicalSkill[] = [];
|
|
40
|
+
for (const dir of dirs) {
|
|
41
|
+
const skill = await skillFromDir(dir);
|
|
42
|
+
if (skill) {
|
|
43
|
+
out.push(skill);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface CanonicalMcpServer {
|
|
2
|
+
transport?: string;
|
|
3
|
+
command?: string;
|
|
4
|
+
args?: string[];
|
|
5
|
+
url?: string;
|
|
6
|
+
env?: Record<string, string>;
|
|
7
|
+
vendorExtensions?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CanonicalMcpConfig {
|
|
11
|
+
servers: Record<string, CanonicalMcpServer>;
|
|
12
|
+
vendorExtensions?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CanonicalSkill {
|
|
16
|
+
name: string;
|
|
17
|
+
body?: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AdapterDefaultPaths {
|
|
22
|
+
mcp?: string;
|
|
23
|
+
skills?: string | string[];
|
|
24
|
+
config?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ToolAdapter {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
versions: string[];
|
|
31
|
+
detectVersion?: (configPath: string) => Promise<string | null>;
|
|
32
|
+
parseMcp?: (config: unknown, version?: string) => CanonicalMcpConfig;
|
|
33
|
+
parseSkills?: (skillsDir: string) => Promise<CanonicalSkill[]>;
|
|
34
|
+
generateMcp?: (canonical: CanonicalMcpConfig, version?: string) => unknown;
|
|
35
|
+
generateSkillsDir?: (skills: CanonicalSkill[]) => Promise<void>;
|
|
36
|
+
getDefaultPaths?: () => AdapterDefaultPaths;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ResolveVersionOptions {
|
|
40
|
+
fallbackVersion?: string;
|
|
41
|
+
warn?: (message: string) => void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function detectExplicitVersion(
|
|
2
|
+
configPath: string
|
|
3
|
+
): Promise<string | null> {
|
|
4
|
+
try {
|
|
5
|
+
const raw = await Bun.file(configPath).text();
|
|
6
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
7
|
+
const version = parsed.version;
|
|
8
|
+
if (typeof version === "string") {
|
|
9
|
+
return version;
|
|
10
|
+
}
|
|
11
|
+
if (typeof version === "number") {
|
|
12
|
+
return String(version);
|
|
13
|
+
}
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|