@oh-my-pi/pi-coding-agent 2.3.1337 → 3.1.1337

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 (117) hide show
  1. package/CHANGELOG.md +72 -34
  2. package/README.md +100 -100
  3. package/docs/compaction.md +8 -8
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +8 -8
  6. package/docs/extension-loading.md +58 -58
  7. package/docs/hooks.md +11 -11
  8. package/docs/rpc.md +4 -4
  9. package/docs/sdk.md +14 -14
  10. package/docs/session-tree-plan.md +1 -1
  11. package/docs/session.md +2 -2
  12. package/docs/skills.md +16 -16
  13. package/docs/theme.md +9 -9
  14. package/docs/tui.md +1 -1
  15. package/examples/README.md +1 -1
  16. package/examples/custom-tools/README.md +4 -4
  17. package/examples/custom-tools/subagent/README.md +13 -13
  18. package/examples/custom-tools/subagent/agents.ts +2 -2
  19. package/examples/custom-tools/subagent/index.ts +5 -5
  20. package/examples/hooks/README.md +3 -3
  21. package/examples/hooks/auto-commit-on-exit.ts +1 -1
  22. package/examples/hooks/custom-compaction.ts +1 -1
  23. package/examples/sdk/01-minimal.ts +1 -1
  24. package/examples/sdk/04-skills.ts +1 -1
  25. package/examples/sdk/05-tools.ts +1 -1
  26. package/examples/sdk/08-slash-commands.ts +1 -1
  27. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  28. package/examples/sdk/README.md +2 -2
  29. package/package.json +13 -11
  30. package/src/capability/context-file.ts +40 -0
  31. package/src/capability/extension.ts +48 -0
  32. package/src/capability/hook.ts +40 -0
  33. package/src/capability/index.ts +616 -0
  34. package/src/capability/instruction.ts +37 -0
  35. package/src/capability/mcp.ts +52 -0
  36. package/src/capability/prompt.ts +35 -0
  37. package/src/capability/rule.ts +52 -0
  38. package/src/capability/settings.ts +35 -0
  39. package/src/capability/skill.ts +49 -0
  40. package/src/capability/slash-command.ts +40 -0
  41. package/src/capability/system-prompt.ts +35 -0
  42. package/src/capability/tool.ts +38 -0
  43. package/src/capability/types.ts +166 -0
  44. package/src/cli/args.ts +2 -2
  45. package/src/cli/plugin-cli.ts +24 -19
  46. package/src/cli/update-cli.ts +10 -10
  47. package/src/config.ts +290 -6
  48. package/src/core/auth-storage.ts +32 -9
  49. package/src/core/bash-executor.ts +1 -1
  50. package/src/core/custom-commands/loader.ts +44 -50
  51. package/src/core/custom-tools/index.ts +1 -0
  52. package/src/core/custom-tools/loader.ts +67 -69
  53. package/src/core/custom-tools/types.ts +10 -1
  54. package/src/core/hooks/loader.ts +13 -42
  55. package/src/core/index.ts +0 -1
  56. package/src/core/logger.ts +7 -7
  57. package/src/core/mcp/client.ts +1 -1
  58. package/src/core/mcp/config.ts +94 -146
  59. package/src/core/mcp/index.ts +0 -4
  60. package/src/core/mcp/loader.ts +26 -22
  61. package/src/core/mcp/manager.ts +18 -23
  62. package/src/core/mcp/tool-bridge.ts +9 -1
  63. package/src/core/mcp/types.ts +2 -0
  64. package/src/core/model-registry.ts +25 -8
  65. package/src/core/plugins/installer.ts +1 -1
  66. package/src/core/plugins/loader.ts +17 -11
  67. package/src/core/plugins/manager.ts +2 -2
  68. package/src/core/plugins/paths.ts +12 -7
  69. package/src/core/plugins/types.ts +3 -3
  70. package/src/core/sdk.ts +48 -27
  71. package/src/core/session-manager.ts +4 -4
  72. package/src/core/settings-manager.ts +45 -21
  73. package/src/core/skills.ts +208 -293
  74. package/src/core/slash-commands.ts +34 -165
  75. package/src/core/system-prompt.ts +58 -65
  76. package/src/core/timings.ts +2 -2
  77. package/src/core/tools/lsp/config.ts +38 -17
  78. package/src/core/tools/task/agents.ts +21 -0
  79. package/src/core/tools/task/artifacts.ts +1 -1
  80. package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
  81. package/src/core/tools/task/bundled-agents/task.md +1 -0
  82. package/src/core/tools/task/commands.ts +30 -107
  83. package/src/core/tools/task/discovery.ts +75 -66
  84. package/src/core/tools/task/executor.ts +25 -10
  85. package/src/core/tools/task/index.ts +35 -10
  86. package/src/core/tools/task/model-resolver.ts +27 -25
  87. package/src/core/tools/task/types.ts +6 -2
  88. package/src/core/tools/web-fetch.ts +3 -3
  89. package/src/core/tools/web-search/auth.ts +40 -34
  90. package/src/core/tools/web-search/index.ts +1 -1
  91. package/src/core/tools/web-search/providers/anthropic.ts +1 -1
  92. package/src/discovery/agents-md.ts +75 -0
  93. package/src/discovery/builtin.ts +646 -0
  94. package/src/discovery/claude.ts +623 -0
  95. package/src/discovery/cline.ts +102 -0
  96. package/src/discovery/codex.ts +571 -0
  97. package/src/discovery/cursor.ts +264 -0
  98. package/src/discovery/gemini.ts +368 -0
  99. package/src/discovery/github.ts +120 -0
  100. package/src/discovery/helpers.test.ts +127 -0
  101. package/src/discovery/helpers.ts +249 -0
  102. package/src/discovery/index.ts +84 -0
  103. package/src/discovery/mcp-json.ts +127 -0
  104. package/src/discovery/vscode.ts +99 -0
  105. package/src/discovery/windsurf.ts +216 -0
  106. package/src/main.ts +14 -13
  107. package/src/migrations.ts +24 -3
  108. package/src/modes/interactive/components/hook-editor.ts +1 -1
  109. package/src/modes/interactive/components/plugin-settings.ts +1 -1
  110. package/src/modes/interactive/components/settings-defs.ts +38 -2
  111. package/src/modes/interactive/components/settings-selector.ts +1 -0
  112. package/src/modes/interactive/components/welcome.ts +2 -2
  113. package/src/modes/interactive/interactive-mode.ts +233 -16
  114. package/src/modes/interactive/theme/theme-schema.json +1 -1
  115. package/src/utils/clipboard.ts +1 -1
  116. package/src/utils/shell-snapshot.ts +2 -2
  117. package/src/utils/shell.ts +7 -7
@@ -0,0 +1,120 @@
1
+ /**
2
+ * GitHub Copilot Provider
3
+ *
4
+ * Loads configuration from GitHub Copilot's config directories.
5
+ * Priority: 30 (shared standard provider)
6
+ *
7
+ * Sources:
8
+ * - Project: .github/ (project-only, no user-level discovery)
9
+ *
10
+ * Capabilities:
11
+ * - context-files: copilot-instructions.md in .github/
12
+ * - instructions: *.instructions.md in .github/instructions/ with applyTo frontmatter
13
+ */
14
+
15
+ import { basename, dirname, sep } from "node:path";
16
+ import { type ContextFile, contextFileCapability } from "../capability/context-file";
17
+ import { registerProvider } from "../capability/index";
18
+ import { type Instruction, instructionCapability } from "../capability/instruction";
19
+ import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
20
+ import { calculateDepth, createSourceMeta, getProjectPath, loadFilesFromDir, parseFrontmatter } from "./helpers";
21
+
22
+ const PROVIDER_ID = "github";
23
+ const DISPLAY_NAME = "GitHub Copilot";
24
+ const PRIORITY = 30;
25
+
26
+ // =============================================================================
27
+ // Context Files
28
+ // =============================================================================
29
+
30
+ function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
31
+ const items: ContextFile[] = [];
32
+ const warnings: string[] = [];
33
+
34
+ // Project-level: .github/copilot-instructions.md
35
+ const copilotInstructionsPath = getProjectPath(ctx, "github", "copilot-instructions.md");
36
+ if (copilotInstructionsPath && ctx.fs.isFile(copilotInstructionsPath)) {
37
+ const content = ctx.fs.readFile(copilotInstructionsPath);
38
+ if (content) {
39
+ const fileDir = dirname(copilotInstructionsPath);
40
+ const depth = calculateDepth(ctx.cwd, fileDir, sep);
41
+
42
+ items.push({
43
+ path: copilotInstructionsPath,
44
+ content,
45
+ level: "project",
46
+ depth,
47
+ _source: createSourceMeta(PROVIDER_ID, copilotInstructionsPath, "project"),
48
+ });
49
+ } else {
50
+ warnings.push(`Failed to read ${copilotInstructionsPath}`);
51
+ }
52
+ }
53
+
54
+ return { items, warnings };
55
+ }
56
+
57
+ // =============================================================================
58
+ // Instructions
59
+ // =============================================================================
60
+
61
+ function loadInstructions(ctx: LoadContext): LoadResult<Instruction> {
62
+ const items: Instruction[] = [];
63
+ const warnings: string[] = [];
64
+
65
+ // Project-level: .github/instructions/*.instructions.md
66
+ const instructionsDir = getProjectPath(ctx, "github", "instructions");
67
+ if (instructionsDir && ctx.fs.isDir(instructionsDir)) {
68
+ const result = loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, "project", {
69
+ extensions: ["md"],
70
+ transform: transformInstruction,
71
+ });
72
+ items.push(...result.items);
73
+ if (result.warnings) warnings.push(...result.warnings);
74
+ }
75
+
76
+ return { items, warnings };
77
+ }
78
+
79
+ function transformInstruction(name: string, content: string, path: string, source: SourceMeta): Instruction | null {
80
+ // Only process .instructions.md files
81
+ if (!name.endsWith(".instructions.md")) {
82
+ return null;
83
+ }
84
+
85
+ const { frontmatter, body } = parseFrontmatter(content);
86
+
87
+ // Extract applyTo glob pattern from frontmatter
88
+ const applyTo = typeof frontmatter.applyTo === "string" ? frontmatter.applyTo : undefined;
89
+
90
+ // Derive name from filename (strip .instructions.md suffix)
91
+ const instructionName = basename(name, ".instructions.md");
92
+
93
+ return {
94
+ name: instructionName,
95
+ path,
96
+ content: body,
97
+ applyTo,
98
+ _source: source,
99
+ };
100
+ }
101
+
102
+ // =============================================================================
103
+ // Provider Registration
104
+ // =============================================================================
105
+
106
+ registerProvider(contextFileCapability.id, {
107
+ id: PROVIDER_ID,
108
+ displayName: DISPLAY_NAME,
109
+ description: "Load copilot-instructions.md from .github/",
110
+ priority: PRIORITY,
111
+ load: loadContextFiles,
112
+ });
113
+
114
+ registerProvider(instructionCapability.id, {
115
+ id: PROVIDER_ID,
116
+ displayName: DISPLAY_NAME,
117
+ description: "Load *.instructions.md from .github/instructions/ with applyTo frontmatter",
118
+ priority: PRIORITY,
119
+ load: loadInstructions,
120
+ });
@@ -0,0 +1,127 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { parseFrontmatter } from "./helpers";
3
+
4
+ describe("parseFrontmatter", () => {
5
+ test("parses simple key-value pairs", () => {
6
+ const content = `---
7
+ name: test
8
+ enabled: true
9
+ ---
10
+ Body content`;
11
+
12
+ const result = parseFrontmatter(content);
13
+ expect(result.frontmatter).toEqual({ name: "test", enabled: true });
14
+ expect(result.body).toBe("Body content");
15
+ });
16
+
17
+ test("parses YAML list syntax", () => {
18
+ const content = `---
19
+ tags:
20
+ - javascript
21
+ - typescript
22
+ - react
23
+ ---
24
+ Body content`;
25
+
26
+ const result = parseFrontmatter(content);
27
+ expect(result.frontmatter).toEqual({
28
+ tags: ["javascript", "typescript", "react"],
29
+ });
30
+ expect(result.body).toBe("Body content");
31
+ });
32
+
33
+ test("parses multi-line string values", () => {
34
+ const content = `---
35
+ description: |
36
+ This is a multi-line
37
+ description block
38
+ with several lines
39
+ ---
40
+ Body content`;
41
+
42
+ const result = parseFrontmatter(content);
43
+ expect(result.frontmatter).toEqual({
44
+ description: "This is a multi-line\ndescription block\nwith several lines\n",
45
+ });
46
+ expect(result.body).toBe("Body content");
47
+ });
48
+
49
+ test("parses nested objects", () => {
50
+ const content = `---
51
+ config:
52
+ server:
53
+ port: 3000
54
+ host: localhost
55
+ database:
56
+ name: mydb
57
+ ---
58
+ Body content`;
59
+
60
+ const result = parseFrontmatter(content);
61
+ expect(result.frontmatter).toEqual({
62
+ config: {
63
+ server: { port: 3000, host: "localhost" },
64
+ database: { name: "mydb" },
65
+ },
66
+ });
67
+ expect(result.body).toBe("Body content");
68
+ });
69
+
70
+ test("parses mixed complex YAML", () => {
71
+ const content = `---
72
+ name: complex-test
73
+ version: 1.0.0
74
+ tags:
75
+ - prod
76
+ - critical
77
+ metadata:
78
+ author: tester
79
+ created: 2024-01-01
80
+ description: |
81
+ Multi-line description
82
+ with formatting
83
+ ---
84
+ Body content`;
85
+
86
+ const result = parseFrontmatter(content);
87
+ expect(result.frontmatter).toEqual({
88
+ name: "complex-test",
89
+ version: "1.0.0",
90
+ tags: ["prod", "critical"],
91
+ metadata: {
92
+ author: "tester",
93
+ created: "2024-01-01",
94
+ },
95
+ description: "Multi-line description\nwith formatting\n",
96
+ });
97
+ expect(result.body).toBe("Body content");
98
+ });
99
+
100
+ test("handles missing frontmatter", () => {
101
+ const content = "Just body content";
102
+ const result = parseFrontmatter(content);
103
+ expect(result.frontmatter).toEqual({});
104
+ expect(result.body).toBe("Just body content");
105
+ });
106
+
107
+ test("handles invalid YAML in frontmatter", () => {
108
+ const content = `---
109
+ invalid: [unclosed array
110
+ ---
111
+ Body content`;
112
+
113
+ const result = parseFrontmatter(content);
114
+ expect(result.frontmatter).toEqual({}); // Fallback to empty
115
+ expect(result.body).toBe("Body content");
116
+ });
117
+
118
+ test("handles empty frontmatter", () => {
119
+ const content = `---
120
+ ---
121
+ Body content`;
122
+
123
+ const result = parseFrontmatter(content);
124
+ expect(result.frontmatter).toEqual({});
125
+ expect(result.body).toBe("Body content");
126
+ });
127
+ });
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Shared helpers for discovery providers.
3
+ */
4
+
5
+ import { join, resolve } from "node:path";
6
+ import { parse as parseYAML } from "yaml";
7
+ import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
8
+
9
+ /**
10
+ * Standard paths for each config source.
11
+ */
12
+ export const SOURCE_PATHS = {
13
+ native: {
14
+ userBase: ".omp",
15
+ userAgent: ".omp/agent",
16
+ projectDir: ".omp",
17
+ aliases: [".pi"], // .pi is an alias for backwards compat
18
+ },
19
+ claude: {
20
+ userBase: ".claude",
21
+ userAgent: ".claude",
22
+ projectDir: ".claude",
23
+ },
24
+ codex: {
25
+ userBase: ".codex",
26
+ userAgent: ".codex",
27
+ projectDir: ".codex",
28
+ },
29
+ gemini: {
30
+ userBase: ".gemini",
31
+ userAgent: ".gemini",
32
+ projectDir: ".gemini",
33
+ },
34
+ cursor: {
35
+ userBase: ".cursor",
36
+ userAgent: ".cursor",
37
+ projectDir: ".cursor",
38
+ },
39
+ windsurf: {
40
+ userBase: ".codeium/windsurf",
41
+ userAgent: ".codeium/windsurf",
42
+ projectDir: ".windsurf",
43
+ },
44
+ cline: {
45
+ userBase: ".cline",
46
+ userAgent: ".cline",
47
+ projectDir: null, // Cline uses root-level .clinerules
48
+ },
49
+ github: {
50
+ userBase: null,
51
+ userAgent: null,
52
+ projectDir: ".github",
53
+ },
54
+ vscode: {
55
+ userBase: ".vscode",
56
+ userAgent: ".vscode",
57
+ projectDir: ".vscode",
58
+ },
59
+ } as const;
60
+
61
+ export type SourceId = keyof typeof SOURCE_PATHS;
62
+
63
+ /**
64
+ * Get user-level path for a source.
65
+ */
66
+ export function getUserPath(ctx: LoadContext, source: SourceId, subpath: string): string | null {
67
+ const paths = SOURCE_PATHS[source];
68
+ if (!paths.userAgent) return null;
69
+ return join(ctx.home, paths.userAgent, subpath);
70
+ }
71
+
72
+ /**
73
+ * Get project-level path for a source (walks up from cwd).
74
+ */
75
+ export function getProjectPath(ctx: LoadContext, source: SourceId, subpath: string): string | null {
76
+ const paths = SOURCE_PATHS[source];
77
+ if (!paths.projectDir) return null;
78
+
79
+ const found = ctx.fs.walkUp(paths.projectDir, { dir: true });
80
+ if (!found) return null;
81
+
82
+ return join(found, subpath);
83
+ }
84
+
85
+ /**
86
+ * Create source metadata for an item.
87
+ */
88
+ export function createSourceMeta(provider: string, path: string, level: "user" | "project"): SourceMeta {
89
+ return {
90
+ provider,
91
+ providerName: "", // Filled in by registry
92
+ path: resolve(path),
93
+ level,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Strip YAML frontmatter from content.
99
+ * Returns { frontmatter, body, raw }
100
+ */
101
+ export function parseFrontmatter(content: string): {
102
+ frontmatter: Record<string, unknown>;
103
+ body: string;
104
+ raw: string;
105
+ } {
106
+ const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
107
+
108
+ if (!normalized.startsWith("---")) {
109
+ return { frontmatter: {}, body: normalized, raw: "" };
110
+ }
111
+
112
+ const endIndex = normalized.indexOf("\n---", 3);
113
+ if (endIndex === -1) {
114
+ return { frontmatter: {}, body: normalized, raw: "" };
115
+ }
116
+
117
+ const raw = normalized.slice(4, endIndex);
118
+ const body = normalized.slice(endIndex + 4).trim();
119
+
120
+ try {
121
+ const frontmatter = parseYAML(raw) as Record<string, unknown> | null;
122
+ return { frontmatter: frontmatter ?? {}, body, raw };
123
+ } catch {
124
+ // Fallback to empty frontmatter on parse error
125
+ return { frontmatter: {}, body, raw };
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Expand environment variables in a string.
131
+ * Supports ${VAR} and ${VAR:-default} syntax.
132
+ */
133
+ export function expandEnvVars(value: string, extraEnv?: Record<string, string>): string {
134
+ return value.replace(/\$\{([^}:]+)(?::-([^}]*))?\}/g, (_, varName: string, defaultValue?: string) => {
135
+ const envValue = extraEnv?.[varName] ?? process.env[varName];
136
+ if (envValue !== undefined) return envValue;
137
+ if (defaultValue !== undefined) return defaultValue;
138
+ return `\${${varName}}`;
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Recursively expand environment variables in an object.
144
+ */
145
+ export function expandEnvVarsDeep<T>(obj: T, extraEnv?: Record<string, string>): T {
146
+ if (typeof obj === "string") {
147
+ return expandEnvVars(obj, extraEnv) as T;
148
+ }
149
+ if (Array.isArray(obj)) {
150
+ return obj.map((item) => expandEnvVarsDeep(item, extraEnv)) as T;
151
+ }
152
+ if (obj !== null && typeof obj === "object") {
153
+ const result: Record<string, unknown> = {};
154
+ for (const [key, value] of Object.entries(obj)) {
155
+ result[key] = expandEnvVarsDeep(value, extraEnv);
156
+ }
157
+ return result as T;
158
+ }
159
+ return obj;
160
+ }
161
+
162
+ /**
163
+ * Load files from a directory matching a pattern.
164
+ */
165
+ export function loadFilesFromDir<T>(
166
+ ctx: LoadContext,
167
+ dir: string,
168
+ provider: string,
169
+ level: "user" | "project",
170
+ options: {
171
+ /** File extensions to match (without dot) */
172
+ extensions?: string[];
173
+ /** Transform file to item (return null to skip) */
174
+ transform: (name: string, content: string, path: string, source: SourceMeta) => T | null;
175
+ /** Whether to recurse into subdirectories */
176
+ recursive?: boolean;
177
+ },
178
+ ): LoadResult<T> {
179
+ const items: T[] = [];
180
+ const warnings: string[] = [];
181
+
182
+ if (!ctx.fs.isDir(dir)) {
183
+ return { items, warnings };
184
+ }
185
+
186
+ const files = ctx.fs.readDir(dir);
187
+
188
+ for (const name of files) {
189
+ if (name.startsWith(".")) continue;
190
+
191
+ const path = join(dir, name);
192
+
193
+ if (options.recursive && ctx.fs.isDir(path)) {
194
+ const subResult = loadFilesFromDir(ctx, path, provider, level, options);
195
+ items.push(...subResult.items);
196
+ if (subResult.warnings) warnings.push(...subResult.warnings);
197
+ continue;
198
+ }
199
+
200
+ if (!ctx.fs.isFile(path)) continue;
201
+
202
+ // Check extension
203
+ if (options.extensions) {
204
+ const hasMatch = options.extensions.some((ext) => name.endsWith(`.${ext}`));
205
+ if (!hasMatch) continue;
206
+ }
207
+
208
+ const content = ctx.fs.readFile(path);
209
+ if (content === null) {
210
+ warnings.push(`Failed to read file: ${path}`);
211
+ continue;
212
+ }
213
+
214
+ const source = createSourceMeta(provider, path, level);
215
+
216
+ try {
217
+ const item = options.transform(name, content, path, source);
218
+ if (item !== null) {
219
+ items.push(item);
220
+ }
221
+ } catch (err) {
222
+ warnings.push(`Failed to parse ${path}: ${err}`);
223
+ }
224
+ }
225
+
226
+ return { items, warnings };
227
+ }
228
+
229
+ /**
230
+ * Parse JSON safely.
231
+ */
232
+ export function parseJSON<T>(content: string): T | null {
233
+ try {
234
+ return JSON.parse(content) as T;
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Calculate depth of target directory relative to current working directory.
242
+ * Depth is the number of directory levels from cwd to target.
243
+ * - Positive depth: target is above cwd (parent/ancestor)
244
+ * - Zero depth: target is cwd
245
+ * - This uses path splitting to count directory levels
246
+ */
247
+ export function calculateDepth(cwd: string, targetDir: string, separator: string): number {
248
+ return cwd.split(separator).length - targetDir.split(separator).length;
249
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Discovery Module
3
+ *
4
+ * Auto-registers all providers by importing them.
5
+ * Import this module to ensure all providers are registered with the capability registry.
6
+ */
7
+
8
+ // Import capability definitions (ensures capabilities are defined before providers register)
9
+ import "../capability/context-file";
10
+ import "../capability/extension";
11
+ import "../capability/hook";
12
+ import "../capability/instruction";
13
+ import "../capability/mcp";
14
+ import "../capability/prompt";
15
+ import "../capability/rule";
16
+ import "../capability/settings";
17
+ import "../capability/skill";
18
+ import "../capability/slash-command";
19
+ import "../capability/system-prompt";
20
+ import "../capability/tool";
21
+
22
+ // Import providers (each registers itself on import)
23
+ import "./builtin";
24
+ import "./claude";
25
+ import "./codex";
26
+ import "./gemini";
27
+ import "./cursor";
28
+ import "./windsurf";
29
+ import "./cline";
30
+ import "./github";
31
+ import "./vscode";
32
+ import "./agents-md";
33
+ import "./mcp-json";
34
+
35
+ export type { ContextFile } from "../capability/context-file";
36
+ export type { Extension, ExtensionManifest } from "../capability/extension";
37
+ export type { Hook } from "../capability/hook";
38
+ // Re-export the main API from capability registry
39
+ export {
40
+ cacheStats,
41
+ // Provider management
42
+ disableProvider,
43
+ enableProvider,
44
+ getAllCapabilitiesInfo,
45
+ getAllProvidersInfo,
46
+ // Introspection
47
+ getCapability,
48
+ getCapabilityInfo,
49
+ getDisabledProviders,
50
+ getProviderInfo,
51
+ // Initialization
52
+ initializeWithSettings,
53
+ invalidate,
54
+ isProviderEnabled,
55
+ listCapabilities,
56
+ // Loading API
57
+ load,
58
+ loadSync,
59
+ // Cache management
60
+ reset,
61
+ setDisabledProviders,
62
+ } from "../capability/index";
63
+ export type { Instruction } from "../capability/instruction";
64
+ // Re-export capability item types
65
+ export type { MCPServer } from "../capability/mcp";
66
+ export type { Prompt } from "../capability/prompt";
67
+ export type { Rule, RuleFrontmatter } from "../capability/rule";
68
+ export type { Settings } from "../capability/settings";
69
+ export type { Skill, SkillFrontmatter } from "../capability/skill";
70
+ export type { SlashCommand } from "../capability/slash-command";
71
+ export type { SystemPrompt } from "../capability/system-prompt";
72
+ export type { CustomTool } from "../capability/tool";
73
+ // Re-export types
74
+ export type {
75
+ Capability,
76
+ CapabilityInfo,
77
+ CapabilityResult,
78
+ LoadContext,
79
+ LoadOptions,
80
+ LoadResult,
81
+ Provider,
82
+ ProviderInfo,
83
+ SourceMeta,
84
+ } from "../capability/types";
@@ -0,0 +1,127 @@
1
+ /**
2
+ * MCP JSON Provider
3
+ *
4
+ * Discovers standalone mcp.json / .mcp.json files in the project root.
5
+ * This is a fallback for projects that have a standalone mcp.json without any config directory.
6
+ *
7
+ * Priority: 5 (low, as this is a fallback after tool-specific providers)
8
+ */
9
+
10
+ import { join } from "node:path";
11
+ import { registerProvider } from "../capability/index";
12
+ import { type MCPServer, mcpCapability } from "../capability/mcp";
13
+ import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
14
+ import { createSourceMeta, expandEnvVarsDeep, parseJSON } from "./helpers";
15
+
16
+ const PROVIDER_ID = "mcp-json";
17
+ const DISPLAY_NAME = "MCP Config";
18
+
19
+ /**
20
+ * Raw MCP JSON format (matches Claude Desktop's format).
21
+ */
22
+ interface MCPConfigFile {
23
+ mcpServers?: Record<
24
+ string,
25
+ {
26
+ command?: string;
27
+ args?: string[];
28
+ env?: Record<string, string>;
29
+ url?: string;
30
+ headers?: Record<string, string>;
31
+ type?: "stdio" | "sse" | "http";
32
+ }
33
+ >;
34
+ }
35
+
36
+ /**
37
+ * Transform raw MCP config to canonical MCPServer format.
38
+ */
39
+ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServer[] {
40
+ const servers: MCPServer[] = [];
41
+
42
+ if (config.mcpServers) {
43
+ for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
44
+ const server: MCPServer = {
45
+ name,
46
+ command: serverConfig.command,
47
+ args: serverConfig.args,
48
+ env: serverConfig.env,
49
+ url: serverConfig.url,
50
+ headers: serverConfig.headers,
51
+ transport: serverConfig.type,
52
+ _source: source,
53
+ };
54
+
55
+ // Expand environment variables
56
+ if (server.command) server.command = expandEnvVarsDeep(server.command);
57
+ if (server.args) server.args = expandEnvVarsDeep(server.args);
58
+ if (server.env) server.env = expandEnvVarsDeep(server.env);
59
+ if (server.url) server.url = expandEnvVarsDeep(server.url);
60
+ if (server.headers) server.headers = expandEnvVarsDeep(server.headers);
61
+
62
+ servers.push(server);
63
+ }
64
+ }
65
+
66
+ return servers;
67
+ }
68
+
69
+ /**
70
+ * Load MCP servers from a JSON file.
71
+ */
72
+ function loadMCPJsonFile(ctx: LoadContext, path: string, level: "user" | "project"): LoadResult<MCPServer> {
73
+ const warnings: string[] = [];
74
+ const items: MCPServer[] = [];
75
+
76
+ if (!ctx.fs.isFile(path)) {
77
+ return { items, warnings };
78
+ }
79
+
80
+ const content = ctx.fs.readFile(path);
81
+ if (content === null) {
82
+ warnings.push(`Failed to read ${path}`);
83
+ return { items, warnings };
84
+ }
85
+
86
+ const config = parseJSON<MCPConfigFile>(content);
87
+ if (!config) {
88
+ warnings.push(`Failed to parse JSON in ${path}`);
89
+ return { items, warnings };
90
+ }
91
+
92
+ const source = createSourceMeta(PROVIDER_ID, path, level);
93
+ const servers = transformMCPConfig(config, source);
94
+ items.push(...servers);
95
+
96
+ return { items, warnings };
97
+ }
98
+
99
+ /**
100
+ * MCP JSON Provider loader.
101
+ */
102
+ function load(ctx: LoadContext): LoadResult<MCPServer> {
103
+ const allItems: MCPServer[] = [];
104
+ const allWarnings: string[] = [];
105
+
106
+ // Check for mcp.json or .mcp.json in project root (cwd)
107
+ for (const filename of ["mcp.json", ".mcp.json"]) {
108
+ const path = join(ctx.cwd, filename);
109
+ const result = loadMCPJsonFile(ctx, path, "project");
110
+ allItems.push(...result.items);
111
+ if (result.warnings) allWarnings.push(...result.warnings);
112
+ }
113
+
114
+ return {
115
+ items: allItems,
116
+ warnings: allWarnings.length > 0 ? allWarnings : undefined,
117
+ };
118
+ }
119
+
120
+ // Register provider
121
+ registerProvider(mcpCapability.id, {
122
+ id: PROVIDER_ID,
123
+ displayName: DISPLAY_NAME,
124
+ description: "Load MCP servers from standalone mcp.json or .mcp.json in project root",
125
+ priority: 5,
126
+ load,
127
+ });