@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.
- package/CHANGELOG.md +72 -34
- package/README.md +100 -100
- package/docs/compaction.md +8 -8
- package/docs/config-usage.md +113 -0
- package/docs/custom-tools.md +8 -8
- package/docs/extension-loading.md +58 -58
- package/docs/hooks.md +11 -11
- package/docs/rpc.md +4 -4
- package/docs/sdk.md +14 -14
- package/docs/session-tree-plan.md +1 -1
- package/docs/session.md +2 -2
- package/docs/skills.md +16 -16
- package/docs/theme.md +9 -9
- package/docs/tui.md +1 -1
- package/examples/README.md +1 -1
- package/examples/custom-tools/README.md +4 -4
- package/examples/custom-tools/subagent/README.md +13 -13
- package/examples/custom-tools/subagent/agents.ts +2 -2
- package/examples/custom-tools/subagent/index.ts +5 -5
- package/examples/hooks/README.md +3 -3
- package/examples/hooks/auto-commit-on-exit.ts +1 -1
- package/examples/hooks/custom-compaction.ts +1 -1
- package/examples/sdk/01-minimal.ts +1 -1
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/05-tools.ts +1 -1
- package/examples/sdk/08-slash-commands.ts +1 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/README.md +2 -2
- package/package.json +13 -11
- package/src/capability/context-file.ts +40 -0
- package/src/capability/extension.ts +48 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +616 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +52 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +52 -0
- package/src/capability/settings.ts +35 -0
- package/src/capability/skill.ts +49 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/system-prompt.ts +35 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +166 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/plugin-cli.ts +24 -19
- package/src/cli/update-cli.ts +10 -10
- package/src/config.ts +290 -6
- package/src/core/auth-storage.ts +32 -9
- package/src/core/bash-executor.ts +1 -1
- package/src/core/custom-commands/loader.ts +44 -50
- package/src/core/custom-tools/index.ts +1 -0
- package/src/core/custom-tools/loader.ts +67 -69
- package/src/core/custom-tools/types.ts +10 -1
- package/src/core/hooks/loader.ts +13 -42
- package/src/core/index.ts +0 -1
- package/src/core/logger.ts +7 -7
- package/src/core/mcp/client.ts +1 -1
- package/src/core/mcp/config.ts +94 -146
- package/src/core/mcp/index.ts +0 -4
- package/src/core/mcp/loader.ts +26 -22
- package/src/core/mcp/manager.ts +18 -23
- package/src/core/mcp/tool-bridge.ts +9 -1
- package/src/core/mcp/types.ts +2 -0
- package/src/core/model-registry.ts +25 -8
- package/src/core/plugins/installer.ts +1 -1
- package/src/core/plugins/loader.ts +17 -11
- package/src/core/plugins/manager.ts +2 -2
- package/src/core/plugins/paths.ts +12 -7
- package/src/core/plugins/types.ts +3 -3
- package/src/core/sdk.ts +48 -27
- package/src/core/session-manager.ts +4 -4
- package/src/core/settings-manager.ts +45 -21
- package/src/core/skills.ts +208 -293
- package/src/core/slash-commands.ts +34 -165
- package/src/core/system-prompt.ts +58 -65
- package/src/core/timings.ts +2 -2
- package/src/core/tools/lsp/config.ts +38 -17
- package/src/core/tools/task/agents.ts +21 -0
- package/src/core/tools/task/artifacts.ts +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
- package/src/core/tools/task/bundled-agents/task.md +1 -0
- package/src/core/tools/task/commands.ts +30 -107
- package/src/core/tools/task/discovery.ts +75 -66
- package/src/core/tools/task/executor.ts +25 -10
- package/src/core/tools/task/index.ts +35 -10
- package/src/core/tools/task/model-resolver.ts +27 -25
- package/src/core/tools/task/types.ts +6 -2
- package/src/core/tools/web-fetch.ts +3 -3
- package/src/core/tools/web-search/auth.ts +40 -34
- package/src/core/tools/web-search/index.ts +1 -1
- package/src/core/tools/web-search/providers/anthropic.ts +1 -1
- package/src/discovery/agents-md.ts +75 -0
- package/src/discovery/builtin.ts +646 -0
- package/src/discovery/claude.ts +623 -0
- package/src/discovery/cline.ts +102 -0
- package/src/discovery/codex.ts +571 -0
- package/src/discovery/cursor.ts +264 -0
- package/src/discovery/gemini.ts +368 -0
- package/src/discovery/github.ts +120 -0
- package/src/discovery/helpers.test.ts +127 -0
- package/src/discovery/helpers.ts +249 -0
- package/src/discovery/index.ts +84 -0
- package/src/discovery/mcp-json.ts +127 -0
- package/src/discovery/vscode.ts +99 -0
- package/src/discovery/windsurf.ts +216 -0
- package/src/main.ts +14 -13
- package/src/migrations.ts +24 -3
- package/src/modes/interactive/components/hook-editor.ts +1 -1
- package/src/modes/interactive/components/plugin-settings.ts +1 -1
- package/src/modes/interactive/components/settings-defs.ts +38 -2
- package/src/modes/interactive/components/settings-selector.ts +1 -0
- package/src/modes/interactive/components/welcome.ts +2 -2
- package/src/modes/interactive/interactive-mode.ts +233 -16
- package/src/modes/interactive/theme/theme-schema.json +1 -1
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +2 -2
- 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
|
+
});
|