@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,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Provider
|
|
3
|
+
*
|
|
4
|
+
* Loads configuration from Cursor's config directories.
|
|
5
|
+
* Priority: 50 (tool-specific provider)
|
|
6
|
+
*
|
|
7
|
+
* Sources:
|
|
8
|
+
* - User: ~/.cursor
|
|
9
|
+
* - Project: .cursor/ (walks up from cwd)
|
|
10
|
+
*
|
|
11
|
+
* Capabilities:
|
|
12
|
+
* - mcps: From mcp.json with mcpServers key
|
|
13
|
+
* - rules: From rules/*.mdc files with MDC frontmatter (description, globs, alwaysApply)
|
|
14
|
+
* - settings: From settings.json if present
|
|
15
|
+
* - Legacy: .cursorrules file in project root as a single rule
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { registerProvider } from "../capability/index";
|
|
19
|
+
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
20
|
+
import type { Rule } from "../capability/rule";
|
|
21
|
+
import { ruleCapability } from "../capability/rule";
|
|
22
|
+
import type { Settings } from "../capability/settings";
|
|
23
|
+
import { settingsCapability } from "../capability/settings";
|
|
24
|
+
import type { LoadContext, LoadResult } from "../capability/types";
|
|
25
|
+
import {
|
|
26
|
+
createSourceMeta,
|
|
27
|
+
expandEnvVarsDeep,
|
|
28
|
+
getProjectPath,
|
|
29
|
+
getUserPath,
|
|
30
|
+
loadFilesFromDir,
|
|
31
|
+
parseFrontmatter,
|
|
32
|
+
parseJSON,
|
|
33
|
+
} from "./helpers";
|
|
34
|
+
|
|
35
|
+
const PROVIDER_ID = "cursor";
|
|
36
|
+
const DISPLAY_NAME = "Cursor";
|
|
37
|
+
const PRIORITY = 50;
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// MCP Servers
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
|
|
44
|
+
const items: MCPServer[] = [];
|
|
45
|
+
const warnings: string[] = [];
|
|
46
|
+
|
|
47
|
+
// User-level: ~/.cursor/mcp.json
|
|
48
|
+
const userPath = getUserPath(ctx, "cursor", "mcp.json");
|
|
49
|
+
if (userPath && ctx.fs.isFile(userPath)) {
|
|
50
|
+
const content = ctx.fs.readFile(userPath);
|
|
51
|
+
if (content) {
|
|
52
|
+
const parsed = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
|
53
|
+
if (parsed?.mcpServers) {
|
|
54
|
+
const servers = expandEnvVarsDeep(parsed.mcpServers);
|
|
55
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
56
|
+
const serverConfig = config as Record<string, unknown>;
|
|
57
|
+
items.push({
|
|
58
|
+
name,
|
|
59
|
+
command: serverConfig.command as string | undefined,
|
|
60
|
+
args: serverConfig.args as string[] | undefined,
|
|
61
|
+
env: serverConfig.env as Record<string, string> | undefined,
|
|
62
|
+
url: serverConfig.url as string | undefined,
|
|
63
|
+
headers: serverConfig.headers as Record<string, string> | undefined,
|
|
64
|
+
transport: ["stdio", "sse", "http"].includes(serverConfig.type as string)
|
|
65
|
+
? (serverConfig.type as "stdio" | "sse" | "http")
|
|
66
|
+
: undefined,
|
|
67
|
+
_source: createSourceMeta(PROVIDER_ID, userPath, "user"),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
warnings.push(`${userPath}: missing or invalid 'mcpServers' key`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Project-level: .cursor/mcp.json
|
|
77
|
+
const projectPath = getProjectPath(ctx, "cursor", "mcp.json");
|
|
78
|
+
if (projectPath && ctx.fs.isFile(projectPath)) {
|
|
79
|
+
const content = ctx.fs.readFile(projectPath);
|
|
80
|
+
if (content) {
|
|
81
|
+
const parsed = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
|
82
|
+
if (parsed?.mcpServers) {
|
|
83
|
+
const servers = expandEnvVarsDeep(parsed.mcpServers);
|
|
84
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
85
|
+
const serverConfig = config as Record<string, unknown>;
|
|
86
|
+
items.push({
|
|
87
|
+
name,
|
|
88
|
+
command: serverConfig.command as string | undefined,
|
|
89
|
+
args: serverConfig.args as string[] | undefined,
|
|
90
|
+
env: serverConfig.env as Record<string, string> | undefined,
|
|
91
|
+
url: serverConfig.url as string | undefined,
|
|
92
|
+
headers: serverConfig.headers as Record<string, string> | undefined,
|
|
93
|
+
transport: ["stdio", "sse", "http"].includes(serverConfig.type as string)
|
|
94
|
+
? (serverConfig.type as "stdio" | "sse" | "http")
|
|
95
|
+
: undefined,
|
|
96
|
+
_source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
warnings.push(`${projectPath}: missing or invalid 'mcpServers' key`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { items, warnings };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// =============================================================================
|
|
109
|
+
// Rules
|
|
110
|
+
// =============================================================================
|
|
111
|
+
|
|
112
|
+
function loadRules(ctx: LoadContext): LoadResult<Rule> {
|
|
113
|
+
const items: Rule[] = [];
|
|
114
|
+
const warnings: string[] = [];
|
|
115
|
+
|
|
116
|
+
// Legacy: .cursorrules file in project root
|
|
117
|
+
const legacyPath = ctx.fs.walkUp(".cursorrules", { file: true });
|
|
118
|
+
if (legacyPath) {
|
|
119
|
+
const content = ctx.fs.readFile(legacyPath);
|
|
120
|
+
if (content) {
|
|
121
|
+
items.push({
|
|
122
|
+
name: "cursorrules",
|
|
123
|
+
path: legacyPath,
|
|
124
|
+
content,
|
|
125
|
+
_source: createSourceMeta(PROVIDER_ID, legacyPath, "project"),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// User-level: ~/.cursor/rules/*.mdc
|
|
131
|
+
const userRulesPath = getUserPath(ctx, "cursor", "rules");
|
|
132
|
+
if (userRulesPath && ctx.fs.isDir(userRulesPath)) {
|
|
133
|
+
const result = loadFilesFromDir<Rule>(ctx, userRulesPath, PROVIDER_ID, "user", {
|
|
134
|
+
extensions: ["mdc", "md"],
|
|
135
|
+
transform: transformMDCRule,
|
|
136
|
+
});
|
|
137
|
+
items.push(...result.items);
|
|
138
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Project-level: .cursor/rules/*.mdc
|
|
142
|
+
const projectRulesPath = getProjectPath(ctx, "cursor", "rules");
|
|
143
|
+
if (projectRulesPath && ctx.fs.isDir(projectRulesPath)) {
|
|
144
|
+
const result = loadFilesFromDir<Rule>(ctx, projectRulesPath, PROVIDER_ID, "project", {
|
|
145
|
+
extensions: ["mdc", "md"],
|
|
146
|
+
transform: transformMDCRule,
|
|
147
|
+
});
|
|
148
|
+
items.push(...result.items);
|
|
149
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { items, warnings };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function transformMDCRule(
|
|
156
|
+
name: string,
|
|
157
|
+
content: string,
|
|
158
|
+
path: string,
|
|
159
|
+
source: ReturnType<typeof createSourceMeta>,
|
|
160
|
+
): Rule {
|
|
161
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
162
|
+
|
|
163
|
+
// Extract frontmatter fields
|
|
164
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description : undefined;
|
|
165
|
+
const alwaysApply = frontmatter.alwaysApply === true;
|
|
166
|
+
|
|
167
|
+
// Parse globs (can be array or single string)
|
|
168
|
+
let globs: string[] | undefined;
|
|
169
|
+
if (Array.isArray(frontmatter.globs)) {
|
|
170
|
+
globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
|
|
171
|
+
} else if (typeof frontmatter.globs === "string") {
|
|
172
|
+
globs = [frontmatter.globs];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Derive name from filename (strip extension)
|
|
176
|
+
const ruleName = name.replace(/\.(mdc|md)$/, "");
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
name: ruleName,
|
|
180
|
+
path,
|
|
181
|
+
content: body,
|
|
182
|
+
description,
|
|
183
|
+
alwaysApply,
|
|
184
|
+
globs,
|
|
185
|
+
_source: source,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// Settings
|
|
191
|
+
// =============================================================================
|
|
192
|
+
|
|
193
|
+
function loadSettings(ctx: LoadContext): LoadResult<Settings> {
|
|
194
|
+
const items: Settings[] = [];
|
|
195
|
+
const warnings: string[] = [];
|
|
196
|
+
|
|
197
|
+
// User-level: ~/.cursor/settings.json
|
|
198
|
+
const userPath = getUserPath(ctx, "cursor", "settings.json");
|
|
199
|
+
if (userPath && ctx.fs.isFile(userPath)) {
|
|
200
|
+
const content = ctx.fs.readFile(userPath);
|
|
201
|
+
if (content) {
|
|
202
|
+
const parsed = parseJSON<Record<string, unknown>>(content);
|
|
203
|
+
if (parsed) {
|
|
204
|
+
items.push({
|
|
205
|
+
path: userPath,
|
|
206
|
+
data: parsed,
|
|
207
|
+
level: "user",
|
|
208
|
+
_source: createSourceMeta(PROVIDER_ID, userPath, "user"),
|
|
209
|
+
});
|
|
210
|
+
} else {
|
|
211
|
+
warnings.push(`${userPath}: invalid JSON`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Project-level: .cursor/settings.json
|
|
217
|
+
const projectPath = getProjectPath(ctx, "cursor", "settings.json");
|
|
218
|
+
if (projectPath && ctx.fs.isFile(projectPath)) {
|
|
219
|
+
const content = ctx.fs.readFile(projectPath);
|
|
220
|
+
if (content) {
|
|
221
|
+
const parsed = parseJSON<Record<string, unknown>>(content);
|
|
222
|
+
if (parsed) {
|
|
223
|
+
items.push({
|
|
224
|
+
path: projectPath,
|
|
225
|
+
data: parsed,
|
|
226
|
+
level: "project",
|
|
227
|
+
_source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
warnings.push(`${projectPath}: invalid JSON`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { items, warnings };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// =============================================================================
|
|
239
|
+
// Provider Registration
|
|
240
|
+
// =============================================================================
|
|
241
|
+
|
|
242
|
+
registerProvider(mcpCapability.id, {
|
|
243
|
+
id: PROVIDER_ID,
|
|
244
|
+
displayName: DISPLAY_NAME,
|
|
245
|
+
description: "Load MCP servers from ~/.cursor/mcp.json and .cursor/mcp.json",
|
|
246
|
+
priority: PRIORITY,
|
|
247
|
+
load: loadMCPServers,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
registerProvider(ruleCapability.id, {
|
|
251
|
+
id: PROVIDER_ID,
|
|
252
|
+
displayName: DISPLAY_NAME,
|
|
253
|
+
description: "Load rules from .cursor/rules/*.mdc and legacy .cursorrules",
|
|
254
|
+
priority: PRIORITY,
|
|
255
|
+
load: loadRules,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
registerProvider(settingsCapability.id, {
|
|
259
|
+
id: PROVIDER_ID,
|
|
260
|
+
displayName: DISPLAY_NAME,
|
|
261
|
+
description: "Load settings from ~/.cursor/settings.json and .cursor/settings.json",
|
|
262
|
+
priority: PRIORITY,
|
|
263
|
+
load: loadSettings,
|
|
264
|
+
});
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Provider
|
|
3
|
+
*
|
|
4
|
+
* Loads configuration from Gemini CLI's config directories.
|
|
5
|
+
* Priority: 60 (tool-specific provider)
|
|
6
|
+
*
|
|
7
|
+
* Sources:
|
|
8
|
+
* - User: ~/.gemini
|
|
9
|
+
* - Project: .gemini/ (walks up from cwd) or GEMINI.md in ancestors
|
|
10
|
+
*
|
|
11
|
+
* Capabilities:
|
|
12
|
+
* - mcps: From settings.json with mcpServers key
|
|
13
|
+
* - context-files: GEMINI.md files
|
|
14
|
+
* - system-prompt: system.md files for custom system prompt
|
|
15
|
+
* - extensions: From extensions/STAR/gemini-extension.json manifests (STAR = wildcard)
|
|
16
|
+
* - settings: From settings.json
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { dirname, join, sep } from "node:path";
|
|
20
|
+
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
21
|
+
import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
|
|
22
|
+
import { registerProvider } from "../capability/index";
|
|
23
|
+
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
24
|
+
import { type Settings, settingsCapability } from "../capability/settings";
|
|
25
|
+
import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
|
|
26
|
+
import type { LoadContext, LoadResult } from "../capability/types";
|
|
27
|
+
import { calculateDepth, createSourceMeta, expandEnvVarsDeep, getProjectPath, getUserPath, parseJSON } from "./helpers";
|
|
28
|
+
|
|
29
|
+
const PROVIDER_ID = "gemini";
|
|
30
|
+
const DISPLAY_NAME = "Gemini CLI";
|
|
31
|
+
const PRIORITY = 60;
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// MCP Servers
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
|
|
38
|
+
const items: MCPServer[] = [];
|
|
39
|
+
const warnings: string[] = [];
|
|
40
|
+
|
|
41
|
+
// User-level: ~/.gemini/settings.json → mcpServers
|
|
42
|
+
const userPath = getUserPath(ctx, "gemini", "settings.json");
|
|
43
|
+
if (userPath && ctx.fs.isFile(userPath)) {
|
|
44
|
+
const result = loadMCPFromSettings(ctx, userPath, "user");
|
|
45
|
+
items.push(...result.items);
|
|
46
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Project-level: .gemini/settings.json → mcpServers
|
|
50
|
+
const projectPath = getProjectPath(ctx, "gemini", "settings.json");
|
|
51
|
+
if (projectPath && ctx.fs.isFile(projectPath)) {
|
|
52
|
+
const result = loadMCPFromSettings(ctx, projectPath, "project");
|
|
53
|
+
items.push(...result.items);
|
|
54
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { items, warnings };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadMCPFromSettings(ctx: LoadContext, path: string, level: "user" | "project"): LoadResult<MCPServer> {
|
|
61
|
+
const items: MCPServer[] = [];
|
|
62
|
+
const warnings: string[] = [];
|
|
63
|
+
|
|
64
|
+
const content = ctx.fs.readFile(path);
|
|
65
|
+
if (!content) {
|
|
66
|
+
warnings.push(`Failed to read ${path}`);
|
|
67
|
+
return { items, warnings };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const parsed = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
|
|
71
|
+
if (!parsed) {
|
|
72
|
+
warnings.push(`Invalid JSON in ${path}`);
|
|
73
|
+
return { items, warnings };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!parsed.mcpServers || typeof parsed.mcpServers !== "object") {
|
|
77
|
+
return { items, warnings };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const servers = expandEnvVarsDeep(parsed.mcpServers);
|
|
81
|
+
|
|
82
|
+
for (const [name, config] of Object.entries(servers)) {
|
|
83
|
+
if (!config || typeof config !== "object") {
|
|
84
|
+
warnings.push(`Invalid config for server "${name}" in ${path}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const raw = config as Record<string, unknown>;
|
|
89
|
+
|
|
90
|
+
items.push({
|
|
91
|
+
name,
|
|
92
|
+
command: typeof raw.command === "string" ? raw.command : undefined,
|
|
93
|
+
args: Array.isArray(raw.args) ? (raw.args as string[]) : undefined,
|
|
94
|
+
env: raw.env && typeof raw.env === "object" ? (raw.env as Record<string, string>) : undefined,
|
|
95
|
+
url: typeof raw.url === "string" ? raw.url : undefined,
|
|
96
|
+
headers: raw.headers && typeof raw.headers === "object" ? (raw.headers as Record<string, string>) : undefined,
|
|
97
|
+
transport: ["stdio", "sse", "http"].includes(raw.type as string)
|
|
98
|
+
? (raw.type as "stdio" | "sse" | "http")
|
|
99
|
+
: undefined,
|
|
100
|
+
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
101
|
+
} as MCPServer);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { items, warnings };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Context Files
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
|
|
112
|
+
const items: ContextFile[] = [];
|
|
113
|
+
const warnings: string[] = [];
|
|
114
|
+
|
|
115
|
+
// User-level: ~/.gemini/GEMINI.md
|
|
116
|
+
const userGeminiMd = getUserPath(ctx, "gemini", "GEMINI.md");
|
|
117
|
+
if (userGeminiMd && ctx.fs.isFile(userGeminiMd)) {
|
|
118
|
+
const content = ctx.fs.readFile(userGeminiMd);
|
|
119
|
+
if (content) {
|
|
120
|
+
items.push({
|
|
121
|
+
path: userGeminiMd,
|
|
122
|
+
content,
|
|
123
|
+
level: "user",
|
|
124
|
+
_source: createSourceMeta(PROVIDER_ID, userGeminiMd, "user"),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Project-level: .gemini/GEMINI.md
|
|
130
|
+
const projectGeminiMd = getProjectPath(ctx, "gemini", "GEMINI.md");
|
|
131
|
+
if (projectGeminiMd && ctx.fs.isFile(projectGeminiMd)) {
|
|
132
|
+
const content = ctx.fs.readFile(projectGeminiMd);
|
|
133
|
+
if (content) {
|
|
134
|
+
const projectBase = getProjectPath(ctx, "gemini", "");
|
|
135
|
+
const depth = projectBase ? calculateDepth(ctx.cwd, projectBase, sep) : 0;
|
|
136
|
+
|
|
137
|
+
items.push({
|
|
138
|
+
path: projectGeminiMd,
|
|
139
|
+
content,
|
|
140
|
+
level: "project",
|
|
141
|
+
depth,
|
|
142
|
+
_source: createSourceMeta(PROVIDER_ID, projectGeminiMd, "project"),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Also check for GEMINI.md in project root (without .gemini directory)
|
|
148
|
+
const rootGeminiMd = ctx.fs.walkUp("GEMINI.md", { file: true });
|
|
149
|
+
if (rootGeminiMd) {
|
|
150
|
+
const content = ctx.fs.readFile(rootGeminiMd);
|
|
151
|
+
if (content) {
|
|
152
|
+
// Only add if not already added from .gemini/GEMINI.md
|
|
153
|
+
const alreadyAdded = items.some((item) => item.path === rootGeminiMd);
|
|
154
|
+
if (!alreadyAdded) {
|
|
155
|
+
const fileDir = dirname(rootGeminiMd);
|
|
156
|
+
const depth = calculateDepth(ctx.cwd, fileDir, sep);
|
|
157
|
+
|
|
158
|
+
items.push({
|
|
159
|
+
path: rootGeminiMd,
|
|
160
|
+
content,
|
|
161
|
+
level: "project",
|
|
162
|
+
depth,
|
|
163
|
+
_source: createSourceMeta(PROVIDER_ID, rootGeminiMd, "project"),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { items, warnings };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// =============================================================================
|
|
173
|
+
// Extensions
|
|
174
|
+
// =============================================================================
|
|
175
|
+
|
|
176
|
+
function loadExtensions(ctx: LoadContext): LoadResult<Extension> {
|
|
177
|
+
const items: Extension[] = [];
|
|
178
|
+
const warnings: string[] = [];
|
|
179
|
+
|
|
180
|
+
// User-level: ~/.gemini/extensions/*/gemini-extension.json
|
|
181
|
+
const userExtPath = getUserPath(ctx, "gemini", "extensions");
|
|
182
|
+
if (userExtPath && ctx.fs.isDir(userExtPath)) {
|
|
183
|
+
const result = loadExtensionsFromDir(ctx, userExtPath, "user");
|
|
184
|
+
items.push(...result.items);
|
|
185
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Project-level: .gemini/extensions/*/gemini-extension.json
|
|
189
|
+
const projectExtPath = getProjectPath(ctx, "gemini", "extensions");
|
|
190
|
+
if (projectExtPath && ctx.fs.isDir(projectExtPath)) {
|
|
191
|
+
const result = loadExtensionsFromDir(ctx, projectExtPath, "project");
|
|
192
|
+
items.push(...result.items);
|
|
193
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { items, warnings };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function loadExtensionsFromDir(
|
|
200
|
+
ctx: LoadContext,
|
|
201
|
+
extensionsDir: string,
|
|
202
|
+
level: "user" | "project",
|
|
203
|
+
): LoadResult<Extension> {
|
|
204
|
+
const items: Extension[] = [];
|
|
205
|
+
const warnings: string[] = [];
|
|
206
|
+
|
|
207
|
+
const dirs = ctx.fs.readDir(extensionsDir);
|
|
208
|
+
for (const dirName of dirs) {
|
|
209
|
+
const extPath = join(extensionsDir, dirName);
|
|
210
|
+
if (!ctx.fs.isDir(extPath)) continue;
|
|
211
|
+
|
|
212
|
+
const manifestPath = join(extPath, "gemini-extension.json");
|
|
213
|
+
if (!ctx.fs.isFile(manifestPath)) continue;
|
|
214
|
+
|
|
215
|
+
const content = ctx.fs.readFile(manifestPath);
|
|
216
|
+
if (!content) {
|
|
217
|
+
warnings.push(`Failed to read ${manifestPath}`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const manifest = parseJSON<ExtensionManifest>(content);
|
|
222
|
+
if (!manifest) {
|
|
223
|
+
warnings.push(`Invalid JSON in ${manifestPath}`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
items.push({
|
|
228
|
+
name: manifest.name ?? dirName,
|
|
229
|
+
path: extPath,
|
|
230
|
+
manifest,
|
|
231
|
+
level,
|
|
232
|
+
_source: createSourceMeta(PROVIDER_ID, manifestPath, level),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { items, warnings };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// =============================================================================
|
|
240
|
+
// Settings
|
|
241
|
+
// =============================================================================
|
|
242
|
+
|
|
243
|
+
function loadSettings(ctx: LoadContext): LoadResult<Settings> {
|
|
244
|
+
const items: Settings[] = [];
|
|
245
|
+
const warnings: string[] = [];
|
|
246
|
+
|
|
247
|
+
// User-level: ~/.gemini/settings.json
|
|
248
|
+
const userPath = getUserPath(ctx, "gemini", "settings.json");
|
|
249
|
+
if (userPath && ctx.fs.isFile(userPath)) {
|
|
250
|
+
const content = ctx.fs.readFile(userPath);
|
|
251
|
+
if (content) {
|
|
252
|
+
const parsed = parseJSON<Record<string, unknown>>(content);
|
|
253
|
+
if (parsed) {
|
|
254
|
+
items.push({
|
|
255
|
+
path: userPath,
|
|
256
|
+
data: parsed,
|
|
257
|
+
level: "user",
|
|
258
|
+
_source: createSourceMeta(PROVIDER_ID, userPath, "user"),
|
|
259
|
+
});
|
|
260
|
+
} else {
|
|
261
|
+
warnings.push(`Invalid JSON in ${userPath}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Project-level: .gemini/settings.json
|
|
267
|
+
const projectPath = getProjectPath(ctx, "gemini", "settings.json");
|
|
268
|
+
if (projectPath && ctx.fs.isFile(projectPath)) {
|
|
269
|
+
const content = ctx.fs.readFile(projectPath);
|
|
270
|
+
if (content) {
|
|
271
|
+
const parsed = parseJSON<Record<string, unknown>>(content);
|
|
272
|
+
if (parsed) {
|
|
273
|
+
items.push({
|
|
274
|
+
path: projectPath,
|
|
275
|
+
data: parsed,
|
|
276
|
+
level: "project",
|
|
277
|
+
_source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
280
|
+
warnings.push(`Invalid JSON in ${projectPath}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { items, warnings };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// =============================================================================
|
|
289
|
+
// Provider Registration
|
|
290
|
+
// =============================================================================
|
|
291
|
+
|
|
292
|
+
registerProvider(mcpCapability.id, {
|
|
293
|
+
id: PROVIDER_ID,
|
|
294
|
+
displayName: DISPLAY_NAME,
|
|
295
|
+
description: "Load MCP servers from ~/.gemini/settings.json and .gemini/settings.json",
|
|
296
|
+
priority: PRIORITY,
|
|
297
|
+
load: loadMCPServers,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
registerProvider(contextFileCapability.id, {
|
|
301
|
+
id: PROVIDER_ID,
|
|
302
|
+
displayName: DISPLAY_NAME,
|
|
303
|
+
description: "Load GEMINI.md context files",
|
|
304
|
+
priority: PRIORITY,
|
|
305
|
+
load: loadContextFiles,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// =============================================================================
|
|
309
|
+
// System Prompt
|
|
310
|
+
// =============================================================================
|
|
311
|
+
|
|
312
|
+
function loadSystemPrompt(ctx: LoadContext): LoadResult<SystemPrompt> {
|
|
313
|
+
const items: SystemPrompt[] = [];
|
|
314
|
+
|
|
315
|
+
// User-level: ~/.gemini/system.md
|
|
316
|
+
const userSystemMd = getUserPath(ctx, "gemini", "system.md");
|
|
317
|
+
if (userSystemMd && ctx.fs.isFile(userSystemMd)) {
|
|
318
|
+
const content = ctx.fs.readFile(userSystemMd);
|
|
319
|
+
if (content) {
|
|
320
|
+
items.push({
|
|
321
|
+
path: userSystemMd,
|
|
322
|
+
content,
|
|
323
|
+
level: "user",
|
|
324
|
+
_source: createSourceMeta(PROVIDER_ID, userSystemMd, "user"),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Project-level: .gemini/system.md
|
|
330
|
+
const projectSystemMd = getProjectPath(ctx, "gemini", "system.md");
|
|
331
|
+
if (projectSystemMd && ctx.fs.isFile(projectSystemMd)) {
|
|
332
|
+
const content = ctx.fs.readFile(projectSystemMd);
|
|
333
|
+
if (content) {
|
|
334
|
+
items.push({
|
|
335
|
+
path: projectSystemMd,
|
|
336
|
+
content,
|
|
337
|
+
level: "project",
|
|
338
|
+
_source: createSourceMeta(PROVIDER_ID, projectSystemMd, "project"),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { items, warnings: [] };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
registerProvider<SystemPrompt>(systemPromptCapability.id, {
|
|
347
|
+
id: PROVIDER_ID,
|
|
348
|
+
displayName: DISPLAY_NAME,
|
|
349
|
+
description: "Load system.md custom system prompt files",
|
|
350
|
+
priority: PRIORITY,
|
|
351
|
+
load: loadSystemPrompt,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
registerProvider(extensionCapability.id, {
|
|
355
|
+
id: PROVIDER_ID,
|
|
356
|
+
displayName: DISPLAY_NAME,
|
|
357
|
+
description: "Load extensions from ~/.gemini/extensions/ and .gemini/extensions/",
|
|
358
|
+
priority: PRIORITY,
|
|
359
|
+
load: loadExtensions,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
registerProvider(settingsCapability.id, {
|
|
363
|
+
id: PROVIDER_ID,
|
|
364
|
+
displayName: DISPLAY_NAME,
|
|
365
|
+
description: "Load settings from ~/.gemini/settings.json and .gemini/settings.json",
|
|
366
|
+
priority: PRIORITY,
|
|
367
|
+
load: loadSettings,
|
|
368
|
+
});
|