@anaclumos/taal 1.1.2

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 (42) hide show
  1. package/.claude/settings.json +15 -0
  2. package/.github/workflows/ci.yml +28 -0
  3. package/.github/workflows/publish.yml +42 -0
  4. package/AGENTS.md +123 -0
  5. package/README.md +568 -0
  6. package/biome.jsonc +24 -0
  7. package/lefthook.yml +12 -0
  8. package/package.json +52 -0
  9. package/src/commands/collect.ts +172 -0
  10. package/src/commands/diff.ts +127 -0
  11. package/src/commands/init.ts +66 -0
  12. package/src/commands/list.ts +80 -0
  13. package/src/commands/providers.ts +46 -0
  14. package/src/commands/sync.ts +111 -0
  15. package/src/commands/validate.ts +17 -0
  16. package/src/config/env.ts +49 -0
  17. package/src/config/loader.ts +88 -0
  18. package/src/config/parser.ts +44 -0
  19. package/src/config/schema.ts +67 -0
  20. package/src/errors/index.ts +43 -0
  21. package/src/index.ts +301 -0
  22. package/src/providers/antigravity.ts +24 -0
  23. package/src/providers/base.ts +70 -0
  24. package/src/providers/claude-code.ts +12 -0
  25. package/src/providers/claude-desktop.ts +19 -0
  26. package/src/providers/codex.ts +61 -0
  27. package/src/providers/continue.ts +62 -0
  28. package/src/providers/cursor.ts +25 -0
  29. package/src/providers/index.ts +34 -0
  30. package/src/providers/opencode.ts +42 -0
  31. package/src/providers/registry.ts +74 -0
  32. package/src/providers/types.ts +99 -0
  33. package/src/providers/utils.ts +106 -0
  34. package/src/providers/windsurf.ts +50 -0
  35. package/src/providers/zed.ts +35 -0
  36. package/src/scripts/generate-schema.ts +17 -0
  37. package/src/skills/copy.ts +58 -0
  38. package/src/skills/discovery.ts +87 -0
  39. package/src/skills/validator.ts +95 -0
  40. package/src/utils/atomic-write.ts +35 -0
  41. package/src/utils/backup.ts +27 -0
  42. package/taal.schema.json +91 -0
@@ -0,0 +1,172 @@
1
+ import { homedir } from "node:os";
2
+ import type { McpServer } from "../config/schema.js";
3
+ import { initializeProviders, registry } from "../providers/index.js";
4
+
5
+ export interface CollectConflict {
6
+ serverName: string;
7
+ providers: string[];
8
+ configs: unknown[];
9
+ }
10
+
11
+ export interface CollectResult {
12
+ servers: Record<string, McpServer>;
13
+ conflicts: CollectConflict[];
14
+ summary: {
15
+ totalServers: number;
16
+ providersScanned: number;
17
+ providersWithConfigs: number;
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Collect MCP server configs from all installed providers
23
+ */
24
+ export async function collect(baseDir?: string): Promise<CollectResult> {
25
+ initializeProviders();
26
+ const home = baseDir || homedir();
27
+ const servers: Record<string, McpServer> = {};
28
+ const conflicts: CollectConflict[] = [];
29
+ const serverSources = new Map<
30
+ string,
31
+ { provider: string; config: unknown }[]
32
+ >();
33
+
34
+ let providersScanned = 0;
35
+ let providersWithConfigs = 0;
36
+
37
+ const allProviders = registry.getAll();
38
+
39
+ for (const provider of allProviders) {
40
+ providersScanned++;
41
+
42
+ try {
43
+ // Check if provider is installed
44
+ const isInstalled = await provider.isInstalled(home);
45
+ if (!isInstalled) {
46
+ continue;
47
+ }
48
+
49
+ // Read provider config
50
+ const config = await provider.readConfig(home);
51
+ if (!config || typeof config !== "object") {
52
+ continue;
53
+ }
54
+
55
+ // Extract MCP servers from config
56
+ const mcpServers = (config as Record<string, unknown>)[provider.mcpKey];
57
+ if (!mcpServers || typeof mcpServers !== "object") {
58
+ continue;
59
+ }
60
+
61
+ providersWithConfigs++;
62
+
63
+ // Process each server
64
+ for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
65
+ // Track source for conflict detection
66
+ if (!serverSources.has(serverName)) {
67
+ serverSources.set(serverName, []);
68
+ }
69
+ serverSources.get(serverName)?.push({
70
+ provider: provider.name,
71
+ config: serverConfig,
72
+ });
73
+
74
+ // Convert to TAAL format
75
+ const taalServer = convertToTaalFormat(serverConfig);
76
+ if (taalServer) {
77
+ servers[serverName] = taalServer;
78
+ }
79
+ }
80
+ } catch (error) {
81
+ // Skip providers that fail to read
82
+ console.warn(
83
+ `Warning: Failed to read config from ${provider.name}:`,
84
+ error
85
+ );
86
+ }
87
+ }
88
+
89
+ // Detect conflicts
90
+ for (const [serverName, sources] of serverSources.entries()) {
91
+ if (sources.length > 1) {
92
+ // Check if configs are actually different
93
+ const configStrings = sources.map((s) => JSON.stringify(s.config));
94
+ const uniqueConfigs = new Set(configStrings);
95
+
96
+ if (uniqueConfigs.size > 1) {
97
+ conflicts.push({
98
+ serverName,
99
+ providers: sources.map((s) => s.provider),
100
+ configs: sources.map((s) => s.config),
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ return {
107
+ servers,
108
+ conflicts,
109
+ summary: {
110
+ totalServers: Object.keys(servers).length,
111
+ providersScanned,
112
+ providersWithConfigs,
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Convert provider-specific server config to TAAL format
119
+ */
120
+ function convertToTaalFormat(config: unknown): McpServer | null {
121
+ if (!config || typeof config !== "object") {
122
+ return null;
123
+ }
124
+
125
+ const obj = config as Record<string, unknown>;
126
+
127
+ // HTTP server
128
+ if (obj.url && typeof obj.url === "string") {
129
+ return {
130
+ url: obj.url,
131
+ headers:
132
+ (obj.headers as Record<string, string>) ||
133
+ (obj.http_headers as Record<string, string>),
134
+ };
135
+ }
136
+
137
+ // Stdio server
138
+ if (obj.command && typeof obj.command === "string") {
139
+ const server: McpServer = {
140
+ command: obj.command,
141
+ };
142
+
143
+ if (Array.isArray(obj.args)) {
144
+ server.args = obj.args as string[];
145
+ }
146
+
147
+ if (obj.env && typeof obj.env === "object") {
148
+ server.env = obj.env as Record<string, string>;
149
+ } else if (obj.environment && typeof obj.environment === "object") {
150
+ server.env = obj.environment as Record<string, string>;
151
+ }
152
+
153
+ return server;
154
+ }
155
+
156
+ // OpenCode format with command array
157
+ if (Array.isArray(obj.command) && obj.command.length > 0) {
158
+ const [cmd, ...args] = obj.command as string[];
159
+ const server: McpServer = {
160
+ command: cmd,
161
+ args,
162
+ };
163
+
164
+ if (obj.environment && typeof obj.environment === "object") {
165
+ server.env = obj.environment as Record<string, string>;
166
+ }
167
+
168
+ return server;
169
+ }
170
+
171
+ return null;
172
+ }
@@ -0,0 +1,127 @@
1
+ import { homedir } from "node:os";
2
+ import { loadTaalConfig } from "../config/loader.js";
3
+ import { initializeProviders, registry } from "../providers/index.js";
4
+
5
+ export interface DiffChange {
6
+ type: "add" | "remove" | "modify";
7
+ serverName: string;
8
+ provider: string;
9
+ oldValue?: unknown;
10
+ newValue?: unknown;
11
+ }
12
+
13
+ export interface DiffResult {
14
+ hasChanges: boolean;
15
+ changes: DiffChange[];
16
+ provider?: string;
17
+ error?: string;
18
+ }
19
+
20
+ export async function diff(
21
+ baseDir?: string,
22
+ providerName?: string
23
+ ): Promise<DiffResult> {
24
+ initializeProviders();
25
+ const home = baseDir || homedir();
26
+
27
+ const result = await loadTaalConfig(baseDir);
28
+
29
+ if (!result.config) {
30
+ return {
31
+ hasChanges: false,
32
+ changes: [],
33
+ error: result.errors[0] || "Config file not found",
34
+ };
35
+ }
36
+
37
+ const config = result.config;
38
+
39
+ try {
40
+ const changes: DiffChange[] = [];
41
+
42
+ const providers = providerName
43
+ ? [registry.get(providerName)].filter(Boolean)
44
+ : registry.getAll();
45
+
46
+ for (const provider of providers) {
47
+ if (!provider) {
48
+ continue;
49
+ }
50
+
51
+ const isInstalled = await provider.isInstalled(home);
52
+ if (!isInstalled) {
53
+ continue;
54
+ }
55
+
56
+ const enabledProviders = config.providers?.enabled || [];
57
+ if (!enabledProviders.includes(provider.name)) {
58
+ continue;
59
+ }
60
+
61
+ try {
62
+ const currentConfig = await provider.readConfig(home);
63
+ const currentServers: Record<string, unknown> =
64
+ ((currentConfig as Record<string, unknown>)?.[
65
+ provider.mcpKey
66
+ ] as Record<string, unknown>) || {};
67
+
68
+ const taalServers = config.mcp || {};
69
+ const transformedServers = provider.transformMcpServers(taalServers);
70
+
71
+ const currentKeys = new Set(Object.keys(currentServers));
72
+ const taalKeys = new Set(Object.keys(transformedServers as object));
73
+
74
+ for (const key of taalKeys) {
75
+ if (currentKeys.has(key)) {
76
+ const currentValue = JSON.stringify(currentServers[key]);
77
+ const newValue = JSON.stringify(
78
+ (transformedServers as Record<string, unknown>)[key]
79
+ );
80
+
81
+ if (currentValue !== newValue) {
82
+ changes.push({
83
+ type: "modify",
84
+ serverName: key,
85
+ provider: provider.name,
86
+ oldValue: currentServers[key],
87
+ newValue: (transformedServers as Record<string, unknown>)[key],
88
+ });
89
+ }
90
+ } else {
91
+ changes.push({
92
+ type: "add",
93
+ serverName: key,
94
+ provider: provider.name,
95
+ newValue: (transformedServers as Record<string, unknown>)[key],
96
+ });
97
+ }
98
+ }
99
+
100
+ for (const key of currentKeys) {
101
+ if (!taalKeys.has(key)) {
102
+ changes.push({
103
+ type: "remove",
104
+ serverName: key,
105
+ provider: provider.name,
106
+ oldValue: currentServers[key],
107
+ });
108
+ }
109
+ }
110
+ } catch (_error) {
111
+ // Ignore provider read errors - continue with other providers
112
+ }
113
+ }
114
+
115
+ return {
116
+ hasChanges: changes.length > 0,
117
+ changes,
118
+ provider: providerName,
119
+ };
120
+ } catch (error) {
121
+ return {
122
+ hasChanges: false,
123
+ changes: [],
124
+ error: error instanceof Error ? error.message : String(error),
125
+ };
126
+ }
127
+ }
@@ -0,0 +1,66 @@
1
+ import { exists, mkdir, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ const SAMPLE_CONFIG = `# TAAL Configuration
6
+ # https://github.com/user/taal
7
+
8
+ version: "1"
9
+
10
+ mcp: {}
11
+ # Example stdio server
12
+ # example-server:
13
+ # command: npx
14
+ # args: ["-y", "@example/mcp-server"]
15
+ # env:
16
+ # API_KEY: "\${API_KEY}"
17
+
18
+ # Example HTTP server
19
+ # context7:
20
+ # url: https://mcp.context7.com/mcp
21
+ # headers:
22
+ # CONTEXT7_API_KEY: "\${CONTEXT7_API_KEY}"
23
+
24
+ skills:
25
+ paths:
26
+ - ~/.taal/skills
27
+
28
+ providers:
29
+ enabled:
30
+ - claude-desktop
31
+ - claude-code
32
+ - cursor
33
+ - continue
34
+ - zed
35
+ - opencode
36
+ - codex
37
+ - windsurf
38
+ - antigravity
39
+ `;
40
+
41
+ export interface InitOptions {
42
+ force?: boolean;
43
+ }
44
+
45
+ export async function init(
46
+ baseDir?: string,
47
+ options: InitOptions = {}
48
+ ): Promise<void> {
49
+ const taalDir = join(baseDir || homedir(), ".taal");
50
+ const configPath = join(taalDir, "config.yaml");
51
+ const skillsDir = join(taalDir, "skills");
52
+ const backupsDir = join(taalDir, "backups");
53
+
54
+ // Check if already initialized
55
+ if ((await exists(configPath)) && !options.force) {
56
+ throw new Error("TAAL is already initialized. Use --force to overwrite.");
57
+ }
58
+
59
+ // Create directories
60
+ await mkdir(taalDir, { recursive: true });
61
+ await mkdir(skillsDir, { recursive: true });
62
+ await mkdir(backupsDir, { recursive: true });
63
+
64
+ // Write sample config
65
+ await writeFile(configPath, SAMPLE_CONFIG, "utf-8");
66
+ }
@@ -0,0 +1,80 @@
1
+ import { homedir } from "node:os";
2
+ import { loadTaalConfig } from "../config/loader.js";
3
+ import { discoverSkills } from "../skills/discovery.js";
4
+
5
+ export interface ServerInfo {
6
+ name: string;
7
+ type: "stdio" | "http";
8
+ command?: string;
9
+ url?: string;
10
+ }
11
+
12
+ export interface SkillInfo {
13
+ name: string;
14
+ path: string;
15
+ }
16
+
17
+ export interface ListResult {
18
+ servers: ServerInfo[];
19
+ skills: SkillInfo[];
20
+ enabledProviders: string[];
21
+ error?: string;
22
+ }
23
+
24
+ export async function list(baseDir?: string): Promise<ListResult> {
25
+ const home = baseDir || homedir();
26
+ const result = await loadTaalConfig(baseDir);
27
+
28
+ if (!result.config) {
29
+ return {
30
+ servers: [],
31
+ skills: [],
32
+ enabledProviders: [],
33
+ error: result.errors[0] || "Config file not found",
34
+ };
35
+ }
36
+
37
+ const config = result.config;
38
+
39
+ try {
40
+ const servers: ServerInfo[] = [];
41
+ for (const [name, server] of Object.entries(config.mcp || {})) {
42
+ if (server.url) {
43
+ servers.push({
44
+ name,
45
+ type: "http",
46
+ url: server.url,
47
+ });
48
+ } else if (server.command) {
49
+ servers.push({
50
+ name,
51
+ type: "stdio",
52
+ command: server.command,
53
+ });
54
+ }
55
+ }
56
+
57
+ const skillPaths = config.skills?.paths || [];
58
+ const discoveredSkills = await discoverSkills(skillPaths, home);
59
+
60
+ const skills: SkillInfo[] = discoveredSkills.map((skill) => ({
61
+ name: skill.name,
62
+ path: skill.path,
63
+ }));
64
+
65
+ const enabledProviders = config.providers?.enabled || [];
66
+
67
+ return {
68
+ servers,
69
+ skills,
70
+ enabledProviders,
71
+ };
72
+ } catch (error) {
73
+ return {
74
+ servers: [],
75
+ skills: [],
76
+ enabledProviders: [],
77
+ error: error instanceof Error ? error.message : String(error),
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,46 @@
1
+ import { homedir } from "node:os";
2
+ import { loadTaalConfig } from "../config/loader.js";
3
+ import { initializeProviders, registry } from "../providers/index.js";
4
+
5
+ export interface ProviderInfo {
6
+ name: string;
7
+ configPath: string;
8
+ format: string;
9
+ installed: boolean;
10
+ enabled: boolean;
11
+ }
12
+
13
+ export interface ProvidersResult {
14
+ providers: ProviderInfo[];
15
+ }
16
+
17
+ export async function providers(baseDir?: string): Promise<ProvidersResult> {
18
+ initializeProviders();
19
+ const home = baseDir || homedir();
20
+
21
+ const result = await loadTaalConfig(baseDir);
22
+ const enabledProviders = result.config?.providers?.enabled || [];
23
+
24
+ const allProviders = registry.getAll();
25
+ const providerInfos: ProviderInfo[] = [];
26
+
27
+ for (const provider of allProviders) {
28
+ const installed = await provider.isInstalled(home);
29
+ const configPathResolved =
30
+ typeof provider.configPath === "function"
31
+ ? provider.configPath(home)
32
+ : provider.configPath;
33
+
34
+ providerInfos.push({
35
+ name: provider.name,
36
+ configPath: configPathResolved,
37
+ format: provider.format,
38
+ installed,
39
+ enabled: enabledProviders.includes(provider.name),
40
+ });
41
+ }
42
+
43
+ return {
44
+ providers: providerInfos,
45
+ };
46
+ }
@@ -0,0 +1,111 @@
1
+ import { homedir } from "node:os";
2
+ import { loadTaalConfig } from "../config/loader.js";
3
+ import { initializeProviders, registry } from "../providers/index.js";
4
+ import { copySkillsToProvider } from "../skills/copy.js";
5
+ import { discoverSkills } from "../skills/discovery.js";
6
+ import { backupConfig } from "../utils/backup.js";
7
+
8
+ export interface SyncResult {
9
+ success: boolean;
10
+ synced: string[];
11
+ failed: Array<{ provider: string; error: string }>;
12
+ error?: string;
13
+ }
14
+
15
+ export async function sync(
16
+ baseDir?: string,
17
+ providerName?: string
18
+ ): Promise<SyncResult> {
19
+ initializeProviders();
20
+ const home = baseDir || homedir();
21
+
22
+ const result = await loadTaalConfig(baseDir);
23
+
24
+ if (!result.config) {
25
+ return {
26
+ success: false,
27
+ synced: [],
28
+ failed: [],
29
+ error: result.errors[0] || "Config file not found",
30
+ };
31
+ }
32
+
33
+ const config = result.config;
34
+
35
+ try {
36
+ const synced: string[] = [];
37
+ const failed: Array<{ provider: string; error: string }> = [];
38
+
39
+ const providers = providerName
40
+ ? [registry.get(providerName)].filter(Boolean)
41
+ : registry.getAll();
42
+
43
+ const enabledProviders = config.providers?.enabled || [];
44
+
45
+ for (const provider of providers) {
46
+ if (!provider) {
47
+ continue;
48
+ }
49
+
50
+ if (!enabledProviders.includes(provider.name)) {
51
+ continue;
52
+ }
53
+
54
+ try {
55
+ const isInstalled = await provider.isInstalled(home);
56
+ if (!isInstalled) {
57
+ continue;
58
+ }
59
+
60
+ const currentConfig = await provider.readConfig(home);
61
+
62
+ await backupConfig(
63
+ typeof provider.configPath === "function"
64
+ ? provider.configPath(home)
65
+ : provider.configPath,
66
+ home
67
+ );
68
+
69
+ const taalServers = config.mcp || {};
70
+ const transformedServers = provider.transformMcpServers(taalServers);
71
+
72
+ const newConfig = {
73
+ ...(currentConfig as object),
74
+ [provider.mcpKey]: transformedServers,
75
+ };
76
+
77
+ await provider.writeConfig(newConfig, home);
78
+
79
+ if (provider.skillsPath && config.skills?.paths) {
80
+ const allSkills = await discoverSkills(config.skills.paths, home);
81
+ const skillsPath =
82
+ typeof provider.skillsPath === "function"
83
+ ? provider.skillsPath(home)
84
+ : provider.skillsPath;
85
+
86
+ await copySkillsToProvider(allSkills, skillsPath);
87
+ }
88
+
89
+ synced.push(provider.name);
90
+ } catch (error) {
91
+ failed.push({
92
+ provider: provider.name,
93
+ error: error instanceof Error ? error.message : String(error),
94
+ });
95
+ }
96
+ }
97
+
98
+ return {
99
+ success: failed.length === 0,
100
+ synced,
101
+ failed,
102
+ };
103
+ } catch (error) {
104
+ return {
105
+ success: false,
106
+ synced: [],
107
+ failed: [],
108
+ error: error instanceof Error ? error.message : String(error),
109
+ };
110
+ }
111
+ }
@@ -0,0 +1,17 @@
1
+ import { loadTaalConfig } from "../config/loader.js";
2
+
3
+ export interface ValidationResult {
4
+ valid: boolean;
5
+ errors: string[];
6
+ warnings: string[];
7
+ }
8
+
9
+ export async function validate(baseDir?: string): Promise<ValidationResult> {
10
+ const result = await loadTaalConfig(baseDir);
11
+
12
+ return {
13
+ valid: result.config !== null && result.errors.length === 0,
14
+ errors: result.errors,
15
+ warnings: result.warnings,
16
+ };
17
+ }
@@ -0,0 +1,49 @@
1
+ export function findEnvVarReferences(value: unknown): string[] {
2
+ const refs = new Set<string>();
3
+
4
+ if (typeof value === "string") {
5
+ const matches = value.matchAll(/\$\{([^}]+)\}/g);
6
+ for (const match of matches) {
7
+ refs.add(match[1]);
8
+ }
9
+ } else if (Array.isArray(value)) {
10
+ for (const item of value) {
11
+ for (const ref of findEnvVarReferences(item)) {
12
+ refs.add(ref);
13
+ }
14
+ }
15
+ } else if (value && typeof value === "object") {
16
+ for (const v of Object.values(value)) {
17
+ for (const ref of findEnvVarReferences(v)) {
18
+ refs.add(ref);
19
+ }
20
+ }
21
+ }
22
+
23
+ return Array.from(refs);
24
+ }
25
+
26
+ export function substituteEnvVars(value: unknown): unknown {
27
+ if (typeof value === "string") {
28
+ return value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
29
+ const envValue = process.env[varName];
30
+ if (envValue === undefined) {
31
+ console.warn(`Warning: Environment variable ${varName} is not set`);
32
+ return match;
33
+ }
34
+ return envValue;
35
+ });
36
+ }
37
+
38
+ if (Array.isArray(value)) {
39
+ return value.map(substituteEnvVars);
40
+ }
41
+
42
+ if (value && typeof value === "object") {
43
+ return Object.fromEntries(
44
+ Object.entries(value).map(([k, v]) => [k, substituteEnvVars(v)])
45
+ );
46
+ }
47
+
48
+ return value;
49
+ }