@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,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
+ });