@oh-my-pi/pi-coding-agent 4.2.0 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/docs/sdk.md +5 -5
  3. package/examples/sdk/10-settings.ts +2 -2
  4. package/package.json +5 -5
  5. package/src/capability/fs.ts +90 -0
  6. package/src/capability/index.ts +41 -227
  7. package/src/capability/types.ts +1 -11
  8. package/src/cli/args.ts +4 -0
  9. package/src/core/agent-session.ts +7 -7
  10. package/src/core/agent-storage.ts +50 -0
  11. package/src/core/auth-storage.ts +102 -3
  12. package/src/core/bash-executor.ts +1 -1
  13. package/src/core/custom-tools/loader.ts +2 -2
  14. package/src/core/export-html/index.ts +1 -33
  15. package/src/core/extensions/loader.ts +2 -2
  16. package/src/core/extensions/types.ts +1 -1
  17. package/src/core/hooks/loader.ts +2 -2
  18. package/src/core/mcp/config.ts +2 -2
  19. package/src/core/model-registry.ts +46 -0
  20. package/src/core/sdk.ts +37 -29
  21. package/src/core/settings-manager.ts +152 -135
  22. package/src/core/skills.ts +72 -51
  23. package/src/core/slash-commands.ts +3 -3
  24. package/src/core/system-prompt.ts +52 -10
  25. package/src/core/tools/complete.ts +5 -2
  26. package/src/core/tools/edit.ts +7 -4
  27. package/src/core/tools/index.test.ts +16 -0
  28. package/src/core/tools/index.ts +21 -8
  29. package/src/core/tools/lsp/index.ts +4 -1
  30. package/src/core/tools/ssh.ts +6 -6
  31. package/src/core/tools/task/commands.ts +3 -9
  32. package/src/core/tools/task/executor.ts +88 -3
  33. package/src/core/tools/task/index.ts +4 -0
  34. package/src/core/tools/task/model-resolver.ts +10 -7
  35. package/src/core/tools/task/worker-protocol.ts +48 -2
  36. package/src/core/tools/task/worker.ts +152 -7
  37. package/src/core/tools/write.ts +7 -4
  38. package/src/discovery/agents-md.ts +13 -19
  39. package/src/discovery/builtin.ts +368 -293
  40. package/src/discovery/claude.ts +183 -345
  41. package/src/discovery/cline.ts +30 -10
  42. package/src/discovery/codex.ts +188 -272
  43. package/src/discovery/cursor.ts +106 -121
  44. package/src/discovery/gemini.ts +72 -97
  45. package/src/discovery/github.ts +7 -10
  46. package/src/discovery/helpers.ts +114 -57
  47. package/src/discovery/index.ts +1 -2
  48. package/src/discovery/mcp-json.ts +15 -18
  49. package/src/discovery/ssh.ts +9 -17
  50. package/src/discovery/vscode.ts +10 -5
  51. package/src/discovery/windsurf.ts +52 -86
  52. package/src/main.ts +5 -1
  53. package/src/modes/interactive/components/extensions/extension-dashboard.ts +24 -11
  54. package/src/modes/interactive/components/extensions/state-manager.ts +19 -15
  55. package/src/modes/interactive/controllers/selector-controller.ts +9 -5
  56. package/src/modes/interactive/interactive-mode.ts +22 -15
  57. package/src/prompts/agents/plan.md +107 -30
  58. package/src/prompts/agents/task.md +5 -4
  59. package/src/prompts/system/system-prompt.md +5 -0
  60. package/src/prompts/tools/task.md +25 -19
  61. package/src/utils/shell.ts +2 -2
  62. package/src/prompts/agents/architect-plan.md +0 -10
  63. package/src/prompts/agents/implement-with-critic.md +0 -11
  64. package/src/prompts/agents/implement.md +0 -11
@@ -6,15 +6,15 @@
6
6
  *
7
7
  * Sources:
8
8
  * - User: ~/.cursor
9
- * - Project: .cursor/ (walks up from cwd)
9
+ * - Project: .cursor/ (cwd only)
10
10
  *
11
11
  * Capabilities:
12
12
  * - mcps: From mcp.json with mcpServers key
13
13
  * - rules: From rules/*.mdc files with MDC frontmatter (description, globs, alwaysApply)
14
14
  * - settings: From settings.json if present
15
- * - Legacy: .cursorrules file in project root as a single rule
16
15
  */
17
16
 
17
+ import { readFile } from "../capability/fs";
18
18
  import { registerProvider } from "../capability/index";
19
19
  import { type MCPServer, mcpCapability } from "../capability/mcp";
20
20
  import type { Rule } from "../capability/rule";
@@ -40,66 +40,62 @@ const PRIORITY = 50;
40
40
  // MCP Servers
41
41
  // =============================================================================
42
42
 
43
- function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
43
+ function parseMCPServers(
44
+ content: string,
45
+ path: string,
46
+ level: "user" | "project",
47
+ ): { items: MCPServer[]; warning?: string } {
48
+ const items: MCPServer[] = [];
49
+
50
+ const parsed = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
51
+ if (!parsed?.mcpServers) {
52
+ return { items, warning: `${path}: missing or invalid 'mcpServers' key` };
53
+ }
54
+
55
+ const servers = expandEnvVarsDeep(parsed.mcpServers);
56
+ for (const [name, config] of Object.entries(servers)) {
57
+ const serverConfig = config as Record<string, unknown>;
58
+ items.push({
59
+ name,
60
+ command: serverConfig.command as string | undefined,
61
+ args: serverConfig.args as string[] | undefined,
62
+ env: serverConfig.env as Record<string, string> | undefined,
63
+ url: serverConfig.url as string | undefined,
64
+ headers: serverConfig.headers as Record<string, string> | undefined,
65
+ transport: ["stdio", "sse", "http"].includes(serverConfig.type as string)
66
+ ? (serverConfig.type as "stdio" | "sse" | "http")
67
+ : undefined,
68
+ _source: createSourceMeta(PROVIDER_ID, path, level),
69
+ });
70
+ }
71
+
72
+ return { items };
73
+ }
74
+
75
+ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
44
76
  const items: MCPServer[] = [];
45
77
  const warnings: string[] = [];
46
78
 
47
- // User-level: ~/.cursor/mcp.json
48
79
  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
- }
80
+
81
+ const [userContent, projectPath] = await Promise.all([
82
+ userPath ? readFile(userPath) : Promise.resolve(null),
83
+ getProjectPath(ctx, "cursor", "mcp.json"),
84
+ ]);
85
+
86
+ const projectContentPromise = projectPath ? readFile(projectPath) : Promise.resolve(null);
87
+
88
+ if (userContent && userPath) {
89
+ const result = parseMCPServers(userContent, userPath, "user");
90
+ items.push(...result.items);
91
+ if (result.warning) warnings.push(result.warning);
74
92
  }
75
93
 
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
- }
94
+ const projectContent = await projectContentPromise;
95
+ if (projectContent && projectPath) {
96
+ const result = parseMCPServers(projectContent, projectPath, "project");
97
+ items.push(...result.items);
98
+ if (result.warning) warnings.push(result.warning);
103
99
  }
104
100
 
105
101
  return { items, warnings };
@@ -109,45 +105,34 @@ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
109
105
  // Rules
110
106
  // =============================================================================
111
107
 
112
- function loadRules(ctx: LoadContext): LoadResult<Rule> {
108
+ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
113
109
  const items: Rule[] = [];
114
110
  const warnings: string[] = [];
115
111
 
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
112
  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
113
 
141
- // Project-level: .cursor/rules/*.mdc
142
114
  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
- }
115
+
116
+ const [userResult, projectResult] = await Promise.all([
117
+ userRulesPath
118
+ ? loadFilesFromDir<Rule>(ctx, userRulesPath, PROVIDER_ID, "user", {
119
+ extensions: ["mdc", "md"],
120
+ transform: transformMDCRule,
121
+ })
122
+ : Promise.resolve({ items: [] as Rule[], warnings: undefined }),
123
+ projectRulesPath
124
+ ? loadFilesFromDir<Rule>(ctx, projectRulesPath, PROVIDER_ID, "project", {
125
+ extensions: ["mdc", "md"],
126
+ transform: transformMDCRule,
127
+ })
128
+ : Promise.resolve({ items: [] as Rule[], warnings: undefined }),
129
+ ]);
130
+
131
+ items.push(...userResult.items);
132
+ if (userResult.warnings) warnings.push(...userResult.warnings);
133
+
134
+ items.push(...projectResult.items);
135
+ if (projectResult.warnings) warnings.push(...projectResult.warnings);
151
136
 
152
137
  return { items, warnings };
153
138
  }
@@ -192,45 +177,45 @@ function transformMDCRule(
192
177
  // Settings
193
178
  // =============================================================================
194
179
 
195
- function loadSettings(ctx: LoadContext): LoadResult<Settings> {
180
+ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
196
181
  const items: Settings[] = [];
197
182
  const warnings: string[] = [];
198
183
 
199
- // User-level: ~/.cursor/settings.json
200
184
  const userPath = getUserPath(ctx, "cursor", "settings.json");
201
- if (userPath && ctx.fs.isFile(userPath)) {
202
- const content = ctx.fs.readFile(userPath);
203
- if (content) {
204
- const parsed = parseJSON<Record<string, unknown>>(content);
205
- if (parsed) {
206
- items.push({
207
- path: userPath,
208
- data: parsed,
209
- level: "user",
210
- _source: createSourceMeta(PROVIDER_ID, userPath, "user"),
211
- });
212
- } else {
213
- warnings.push(`${userPath}: invalid JSON`);
214
- }
185
+
186
+ const [userContent, projectPath] = await Promise.all([
187
+ userPath ? readFile(userPath) : Promise.resolve(null),
188
+ getProjectPath(ctx, "cursor", "settings.json"),
189
+ ]);
190
+
191
+ const projectContentPromise = projectPath ? readFile(projectPath) : Promise.resolve(null);
192
+
193
+ if (userContent && userPath) {
194
+ const parsed = parseJSON<Record<string, unknown>>(userContent);
195
+ if (parsed) {
196
+ items.push({
197
+ path: userPath,
198
+ data: parsed,
199
+ level: "user",
200
+ _source: createSourceMeta(PROVIDER_ID, userPath, "user"),
201
+ });
202
+ } else {
203
+ warnings.push(`${userPath}: invalid JSON`);
215
204
  }
216
205
  }
217
206
 
218
- // Project-level: .cursor/settings.json
219
- const projectPath = getProjectPath(ctx, "cursor", "settings.json");
220
- if (projectPath && ctx.fs.isFile(projectPath)) {
221
- const content = ctx.fs.readFile(projectPath);
222
- if (content) {
223
- const parsed = parseJSON<Record<string, unknown>>(content);
224
- if (parsed) {
225
- items.push({
226
- path: projectPath,
227
- data: parsed,
228
- level: "project",
229
- _source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
230
- });
231
- } else {
232
- warnings.push(`${projectPath}: invalid JSON`);
233
- }
207
+ const projectContent = await projectContentPromise;
208
+ if (projectContent && projectPath) {
209
+ const parsed = parseJSON<Record<string, unknown>>(projectContent);
210
+ if (parsed) {
211
+ items.push({
212
+ path: projectPath,
213
+ data: parsed,
214
+ level: "project",
215
+ _source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
216
+ });
217
+ } else {
218
+ warnings.push(`${projectPath}: invalid JSON`);
234
219
  }
235
220
  }
236
221
 
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Sources:
8
8
  * - User: ~/.gemini
9
- * - Project: .gemini/ (walks up from cwd) or GEMINI.md in ancestors
9
+ * - Project: .gemini/ (cwd only)
10
10
  *
11
11
  * Capabilities:
12
12
  * - mcps: From settings.json with mcpServers key
@@ -16,10 +16,11 @@
16
16
  * - settings: From settings.json
17
17
  */
18
18
 
19
- import { dirname, join, sep } from "path";
19
+ import { join, sep } from "node:path";
20
20
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
21
21
  import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
22
22
  import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
23
+ import { readDirEntries, readFile } from "../capability/fs";
23
24
  import { registerProvider } from "../capability/index";
24
25
  import { type MCPServer, mcpCapability } from "../capability/mcp";
25
26
  import { type Settings, settingsCapability } from "../capability/settings";
@@ -44,22 +45,22 @@ const PRIORITY = 60;
44
45
  // MCP Servers
45
46
  // =============================================================================
46
47
 
47
- function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
48
+ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
48
49
  const items: MCPServer[] = [];
49
50
  const warnings: string[] = [];
50
51
 
51
52
  // User-level: ~/.gemini/settings.json → mcpServers
52
53
  const userPath = getUserPath(ctx, "gemini", "settings.json");
53
- if (userPath && ctx.fs.isFile(userPath)) {
54
- const result = loadMCPFromSettings(ctx, userPath, "user");
54
+ if (userPath) {
55
+ const result = await loadMCPFromSettings(ctx, userPath, "user");
55
56
  items.push(...result.items);
56
57
  if (result.warnings) warnings.push(...result.warnings);
57
58
  }
58
59
 
59
60
  // Project-level: .gemini/settings.json → mcpServers
60
61
  const projectPath = getProjectPath(ctx, "gemini", "settings.json");
61
- if (projectPath && ctx.fs.isFile(projectPath)) {
62
- const result = loadMCPFromSettings(ctx, projectPath, "project");
62
+ if (projectPath) {
63
+ const result = await loadMCPFromSettings(ctx, projectPath, "project");
63
64
  items.push(...result.items);
64
65
  if (result.warnings) warnings.push(...result.warnings);
65
66
  }
@@ -67,13 +68,16 @@ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
67
68
  return { items, warnings };
68
69
  }
69
70
 
70
- function loadMCPFromSettings(ctx: LoadContext, path: string, level: "user" | "project"): LoadResult<MCPServer> {
71
+ async function loadMCPFromSettings(
72
+ _ctx: LoadContext,
73
+ path: string,
74
+ level: "user" | "project",
75
+ ): Promise<LoadResult<MCPServer>> {
71
76
  const items: MCPServer[] = [];
72
77
  const warnings: string[] = [];
73
78
 
74
- const content = ctx.fs.readFile(path);
79
+ const content = await readFile(path);
75
80
  if (!content) {
76
- warnings.push(`Failed to read ${path}`);
77
81
  return { items, warnings };
78
82
  }
79
83
 
@@ -118,14 +122,14 @@ function loadMCPFromSettings(ctx: LoadContext, path: string, level: "user" | "pr
118
122
  // Context Files
119
123
  // =============================================================================
120
124
 
121
- function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
125
+ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
122
126
  const items: ContextFile[] = [];
123
127
  const warnings: string[] = [];
124
128
 
125
129
  // User-level: ~/.gemini/GEMINI.md
126
130
  const userGeminiMd = getUserPath(ctx, "gemini", "GEMINI.md");
127
- if (userGeminiMd && ctx.fs.isFile(userGeminiMd)) {
128
- const content = ctx.fs.readFile(userGeminiMd);
131
+ if (userGeminiMd) {
132
+ const content = await readFile(userGeminiMd);
129
133
  if (content) {
130
134
  items.push({
131
135
  path: userGeminiMd,
@@ -138,8 +142,8 @@ function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
138
142
 
139
143
  // Project-level: .gemini/GEMINI.md
140
144
  const projectGeminiMd = getProjectPath(ctx, "gemini", "GEMINI.md");
141
- if (projectGeminiMd && ctx.fs.isFile(projectGeminiMd)) {
142
- const content = ctx.fs.readFile(projectGeminiMd);
145
+ if (projectGeminiMd) {
146
+ const content = await readFile(projectGeminiMd);
143
147
  if (content) {
144
148
  const projectBase = getProjectPath(ctx, "gemini", "");
145
149
  const depth = projectBase ? calculateDepth(ctx.cwd, projectBase, sep) : 0;
@@ -154,28 +158,6 @@ function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
154
158
  }
155
159
  }
156
160
 
157
- // Also check for GEMINI.md in project root (without .gemini directory)
158
- const rootGeminiMd = ctx.fs.walkUp("GEMINI.md", { file: true });
159
- if (rootGeminiMd) {
160
- const content = ctx.fs.readFile(rootGeminiMd);
161
- if (content) {
162
- // Only add if not already added from .gemini/GEMINI.md
163
- const alreadyAdded = items.some((item) => item.path === rootGeminiMd);
164
- if (!alreadyAdded) {
165
- const fileDir = dirname(rootGeminiMd);
166
- const depth = calculateDepth(ctx.cwd, fileDir, sep);
167
-
168
- items.push({
169
- path: rootGeminiMd,
170
- content,
171
- level: "project",
172
- depth,
173
- _source: createSourceMeta(PROVIDER_ID, rootGeminiMd, "project"),
174
- });
175
- }
176
- }
177
- }
178
-
179
161
  return { items, warnings };
180
162
  }
181
163
 
@@ -183,22 +165,22 @@ function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
183
165
  // Extensions
184
166
  // =============================================================================
185
167
 
186
- function loadExtensions(ctx: LoadContext): LoadResult<Extension> {
168
+ async function loadExtensions(ctx: LoadContext): Promise<LoadResult<Extension>> {
187
169
  const items: Extension[] = [];
188
170
  const warnings: string[] = [];
189
171
 
190
172
  // User-level: ~/.gemini/extensions/*/gemini-extension.json
191
173
  const userExtPath = getUserPath(ctx, "gemini", "extensions");
192
- if (userExtPath && ctx.fs.isDir(userExtPath)) {
193
- const result = loadExtensionsFromDir(ctx, userExtPath, "user");
174
+ if (userExtPath) {
175
+ const result = await loadExtensionsFromDir(userExtPath, "user");
194
176
  items.push(...result.items);
195
177
  if (result.warnings) warnings.push(...result.warnings);
196
178
  }
197
179
 
198
180
  // Project-level: .gemini/extensions/*/gemini-extension.json
199
181
  const projectExtPath = getProjectPath(ctx, "gemini", "extensions");
200
- if (projectExtPath && ctx.fs.isDir(projectExtPath)) {
201
- const result = loadExtensionsFromDir(ctx, projectExtPath, "project");
182
+ if (projectExtPath) {
183
+ const result = await loadExtensionsFromDir(projectExtPath, "project");
202
184
  items.push(...result.items);
203
185
  if (result.warnings) warnings.push(...result.warnings);
204
186
  }
@@ -206,27 +188,24 @@ function loadExtensions(ctx: LoadContext): LoadResult<Extension> {
206
188
  return { items, warnings };
207
189
  }
208
190
 
209
- function loadExtensionsFromDir(
210
- ctx: LoadContext,
211
- extensionsDir: string,
212
- level: "user" | "project",
213
- ): LoadResult<Extension> {
214
- const items: Extension[] = [];
215
- const warnings: string[] = [];
191
+ async function loadExtensionsFromDir(extensionsDir: string, level: "user" | "project"): Promise<LoadResult<Extension>> {
192
+ const entries = await readDirEntries(extensionsDir);
193
+ const dirEntries = entries.filter((entry) => entry.isDirectory());
216
194
 
217
- const dirs = ctx.fs.readDir(extensionsDir);
218
- for (const dirName of dirs) {
219
- const extPath = join(extensionsDir, dirName);
220
- if (!ctx.fs.isDir(extPath)) continue;
195
+ const results = await Promise.all(
196
+ dirEntries.map(async (entry) => {
197
+ const extPath = join(extensionsDir, entry.name);
198
+ const manifestPath = join(extPath, "gemini-extension.json");
199
+ const content = await readFile(manifestPath);
200
+ return { entry, extPath, manifestPath, content };
201
+ }),
202
+ );
221
203
 
222
- const manifestPath = join(extPath, "gemini-extension.json");
223
- if (!ctx.fs.isFile(manifestPath)) continue;
204
+ const items: Extension[] = [];
205
+ const warnings: string[] = [];
224
206
 
225
- const content = ctx.fs.readFile(manifestPath);
226
- if (!content) {
227
- warnings.push(`Failed to read ${manifestPath}`);
228
- continue;
229
- }
207
+ for (const { entry, extPath, manifestPath, content } of results) {
208
+ if (!content) continue;
230
209
 
231
210
  const manifest = parseJSON<ExtensionManifest>(content);
232
211
  if (!manifest) {
@@ -235,7 +214,7 @@ function loadExtensionsFromDir(
235
214
  }
236
215
 
237
216
  items.push({
238
- name: manifest.name ?? dirName,
217
+ name: manifest.name ?? entry.name,
239
218
  path: extPath,
240
219
  manifest,
241
220
  level,
@@ -250,49 +229,45 @@ function loadExtensionsFromDir(
250
229
  // Extension Modules
251
230
  // =============================================================================
252
231
 
253
- function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
254
- const items: ExtensionModule[] = [];
255
- const warnings: string[] = [];
256
-
232
+ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<ExtensionModule>> {
257
233
  const userExtensionsDir = getUserPath(ctx, "gemini", "extensions");
258
- if (userExtensionsDir) {
259
- for (const extPath of discoverExtensionModulePaths(ctx, userExtensionsDir)) {
260
- items.push({
261
- name: getExtensionNameFromPath(extPath),
262
- path: extPath,
263
- level: "user",
264
- _source: createSourceMeta(PROVIDER_ID, extPath, "user"),
265
- });
266
- }
267
- }
268
-
269
234
  const projectExtensionsDir = getProjectPath(ctx, "gemini", "extensions");
270
- if (projectExtensionsDir) {
271
- for (const extPath of discoverExtensionModulePaths(ctx, projectExtensionsDir)) {
272
- items.push({
273
- name: getExtensionNameFromPath(extPath),
274
- path: extPath,
275
- level: "project",
276
- _source: createSourceMeta(PROVIDER_ID, extPath, "project"),
277
- });
278
- }
279
- }
280
235
 
281
- return { items, warnings };
236
+ const [userPaths, projectPaths] = await Promise.all([
237
+ userExtensionsDir ? discoverExtensionModulePaths(ctx, userExtensionsDir) : Promise.resolve([]),
238
+ projectExtensionsDir ? discoverExtensionModulePaths(ctx, projectExtensionsDir) : Promise.resolve([]),
239
+ ]);
240
+
241
+ const items: ExtensionModule[] = [
242
+ ...userPaths.map((extPath) => ({
243
+ name: getExtensionNameFromPath(extPath),
244
+ path: extPath,
245
+ level: "user" as const,
246
+ _source: createSourceMeta(PROVIDER_ID, extPath, "user"),
247
+ })),
248
+ ...projectPaths.map((extPath) => ({
249
+ name: getExtensionNameFromPath(extPath),
250
+ path: extPath,
251
+ level: "project" as const,
252
+ _source: createSourceMeta(PROVIDER_ID, extPath, "project"),
253
+ })),
254
+ ];
255
+
256
+ return { items, warnings: [] };
282
257
  }
283
258
 
284
259
  // =============================================================================
285
260
  // Settings
286
261
  // =============================================================================
287
262
 
288
- function loadSettings(ctx: LoadContext): LoadResult<Settings> {
263
+ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
289
264
  const items: Settings[] = [];
290
265
  const warnings: string[] = [];
291
266
 
292
267
  // User-level: ~/.gemini/settings.json
293
268
  const userPath = getUserPath(ctx, "gemini", "settings.json");
294
- if (userPath && ctx.fs.isFile(userPath)) {
295
- const content = ctx.fs.readFile(userPath);
269
+ if (userPath) {
270
+ const content = await readFile(userPath);
296
271
  if (content) {
297
272
  const parsed = parseJSON<Record<string, unknown>>(content);
298
273
  if (parsed) {
@@ -310,8 +285,8 @@ function loadSettings(ctx: LoadContext): LoadResult<Settings> {
310
285
 
311
286
  // Project-level: .gemini/settings.json
312
287
  const projectPath = getProjectPath(ctx, "gemini", "settings.json");
313
- if (projectPath && ctx.fs.isFile(projectPath)) {
314
- const content = ctx.fs.readFile(projectPath);
288
+ if (projectPath) {
289
+ const content = await readFile(projectPath);
315
290
  if (content) {
316
291
  const parsed = parseJSON<Record<string, unknown>>(content);
317
292
  if (parsed) {
@@ -354,13 +329,13 @@ registerProvider(contextFileCapability.id, {
354
329
  // System Prompt
355
330
  // =============================================================================
356
331
 
357
- function loadSystemPrompt(ctx: LoadContext): LoadResult<SystemPrompt> {
332
+ async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> {
358
333
  const items: SystemPrompt[] = [];
359
334
 
360
335
  // User-level: ~/.gemini/system.md
361
336
  const userSystemMd = getUserPath(ctx, "gemini", "system.md");
362
- if (userSystemMd && ctx.fs.isFile(userSystemMd)) {
363
- const content = ctx.fs.readFile(userSystemMd);
337
+ if (userSystemMd) {
338
+ const content = await readFile(userSystemMd);
364
339
  if (content) {
365
340
  items.push({
366
341
  path: userSystemMd,
@@ -373,8 +348,8 @@ function loadSystemPrompt(ctx: LoadContext): LoadResult<SystemPrompt> {
373
348
 
374
349
  // Project-level: .gemini/system.md
375
350
  const projectSystemMd = getProjectPath(ctx, "gemini", "system.md");
376
- if (projectSystemMd && ctx.fs.isFile(projectSystemMd)) {
377
- const content = ctx.fs.readFile(projectSystemMd);
351
+ if (projectSystemMd) {
352
+ const content = await readFile(projectSystemMd);
378
353
  if (content) {
379
354
  items.push({
380
355
  path: projectSystemMd,
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { basename, dirname, sep } from "node:path";
16
16
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
17
+ import { readFile } from "../capability/fs";
17
18
  import { registerProvider } from "../capability/index";
18
19
  import { type Instruction, instructionCapability } from "../capability/instruction";
19
20
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
@@ -27,14 +28,13 @@ const PRIORITY = 30;
27
28
  // Context Files
28
29
  // =============================================================================
29
30
 
30
- function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
31
+ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
31
32
  const items: ContextFile[] = [];
32
33
  const warnings: string[] = [];
33
34
 
34
- // Project-level: .github/copilot-instructions.md
35
35
  const copilotInstructionsPath = getProjectPath(ctx, "github", "copilot-instructions.md");
36
- if (copilotInstructionsPath && ctx.fs.isFile(copilotInstructionsPath)) {
37
- const content = ctx.fs.readFile(copilotInstructionsPath);
36
+ if (copilotInstructionsPath) {
37
+ const content = await readFile(copilotInstructionsPath);
38
38
  if (content) {
39
39
  const fileDir = dirname(copilotInstructionsPath);
40
40
  const depth = calculateDepth(ctx.cwd, fileDir, sep);
@@ -46,8 +46,6 @@ function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
46
46
  depth,
47
47
  _source: createSourceMeta(PROVIDER_ID, copilotInstructionsPath, "project"),
48
48
  });
49
- } else {
50
- warnings.push(`Failed to read ${copilotInstructionsPath}`);
51
49
  }
52
50
  }
53
51
 
@@ -58,14 +56,13 @@ function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
58
56
  // Instructions
59
57
  // =============================================================================
60
58
 
61
- function loadInstructions(ctx: LoadContext): LoadResult<Instruction> {
59
+ async function loadInstructions(ctx: LoadContext): Promise<LoadResult<Instruction>> {
62
60
  const items: Instruction[] = [];
63
61
  const warnings: string[] = [];
64
62
 
65
- // Project-level: .github/instructions/*.instructions.md
66
63
  const instructionsDir = getProjectPath(ctx, "github", "instructions");
67
- if (instructionsDir && ctx.fs.isDir(instructionsDir)) {
68
- const result = loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, "project", {
64
+ if (instructionsDir) {
65
+ const result = await loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, "project", {
69
66
  extensions: ["md"],
70
67
  transform: transformInstruction,
71
68
  });