@oh-my-pi/pi-coding-agent 4.2.1 → 4.2.3

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 (58) hide show
  1. package/CHANGELOG.md +30 -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 +4 -4
  10. package/src/core/agent-storage.ts +50 -0
  11. package/src/core/auth-storage.ts +112 -4
  12. package/src/core/bash-executor.ts +1 -1
  13. package/src/core/custom-tools/loader.ts +2 -2
  14. package/src/core/extensions/loader.ts +2 -2
  15. package/src/core/extensions/types.ts +1 -1
  16. package/src/core/hooks/loader.ts +2 -2
  17. package/src/core/mcp/config.ts +2 -2
  18. package/src/core/model-registry.ts +46 -0
  19. package/src/core/sdk.ts +37 -29
  20. package/src/core/settings-manager.ts +152 -135
  21. package/src/core/skills.ts +72 -51
  22. package/src/core/slash-commands.ts +3 -3
  23. package/src/core/system-prompt.ts +10 -10
  24. package/src/core/tools/edit.ts +7 -4
  25. package/src/core/tools/find.ts +2 -2
  26. package/src/core/tools/index.test.ts +16 -0
  27. package/src/core/tools/index.ts +21 -8
  28. package/src/core/tools/lsp/index.ts +4 -1
  29. package/src/core/tools/ssh.ts +6 -6
  30. package/src/core/tools/task/commands.ts +3 -5
  31. package/src/core/tools/task/executor.ts +88 -3
  32. package/src/core/tools/task/index.ts +4 -0
  33. package/src/core/tools/task/model-resolver.ts +10 -7
  34. package/src/core/tools/task/worker-protocol.ts +48 -2
  35. package/src/core/tools/task/worker.ts +152 -7
  36. package/src/core/tools/write.ts +7 -4
  37. package/src/discovery/agents-md.ts +13 -19
  38. package/src/discovery/builtin.ts +367 -247
  39. package/src/discovery/claude.ts +181 -290
  40. package/src/discovery/cline.ts +30 -10
  41. package/src/discovery/codex.ts +185 -244
  42. package/src/discovery/cursor.ts +106 -121
  43. package/src/discovery/gemini.ts +72 -97
  44. package/src/discovery/github.ts +7 -10
  45. package/src/discovery/helpers.ts +94 -88
  46. package/src/discovery/index.ts +1 -2
  47. package/src/discovery/mcp-json.ts +15 -18
  48. package/src/discovery/ssh.ts +9 -17
  49. package/src/discovery/vscode.ts +10 -5
  50. package/src/discovery/windsurf.ts +52 -86
  51. package/src/main.ts +5 -1
  52. package/src/modes/interactive/components/extensions/extension-dashboard.ts +24 -11
  53. package/src/modes/interactive/components/extensions/state-manager.ts +19 -15
  54. package/src/modes/interactive/controllers/selector-controller.ts +6 -2
  55. package/src/modes/interactive/interactive-mode.ts +19 -15
  56. package/src/prompts/agents/plan.md +107 -30
  57. package/src/utils/shell.ts +2 -2
  58. package/src/prompts/agents/planner.md +0 -112
@@ -9,6 +9,7 @@ import { dirname, isAbsolute, join, resolve } from "path";
9
9
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
10
10
  import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
11
11
  import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
12
+ import { readDirEntries, readFile } from "../capability/fs";
12
13
  import { type Hook, hookCapability } from "../capability/hook";
13
14
  import { registerProvider } from "../capability/index";
14
15
  import { type Instruction, instructionCapability } from "../capability/instruction";
@@ -42,12 +43,13 @@ const PATHS = SOURCE_PATHS.native;
42
43
  const PROJECT_DIRS = [PATHS.projectDir, ...PATHS.aliases];
43
44
  const USER_DIRS = [PATHS.userBase, ...PATHS.aliases];
44
45
 
45
- function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "project" }> {
46
+ async function getConfigDirs(ctx: LoadContext): Promise<Array<{ dir: string; level: "user" | "project" }>> {
46
47
  const result: Array<{ dir: string; level: "user" | "project" }> = [];
47
48
 
48
49
  for (const name of PROJECT_DIRS) {
49
- const projectDir = ctx.fs.walkUp(name, { dir: true });
50
- if (projectDir) {
50
+ const projectDir = join(ctx.cwd, name);
51
+ const entries = await readDirEntries(projectDir);
52
+ if (entries.length > 0) {
51
53
  result.push({ dir: projectDir, level: "project" });
52
54
  break;
53
55
  }
@@ -55,7 +57,8 @@ function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "
55
57
 
56
58
  for (const name of USER_DIRS) {
57
59
  const userDir = join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""));
58
- if (ctx.fs.isDir(userDir)) {
60
+ const entries = await readDirEntries(userDir);
61
+ if (entries.length > 0) {
59
62
  result.push({ dir: userDir, level: "user" });
60
63
  break;
61
64
  }
@@ -65,53 +68,19 @@ function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "
65
68
  }
66
69
 
67
70
  // MCP
68
- function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
71
+ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
69
72
  const items: MCPServer[] = [];
70
73
  const warnings: string[] = [];
71
74
 
72
- for (const name of PROJECT_DIRS) {
73
- const projectDir = ctx.fs.walkUp(name, { dir: true });
74
- if (!projectDir) continue;
75
-
76
- for (const filename of ["mcp.json", ".mcp.json"]) {
77
- const path = join(projectDir, filename);
78
- const content = ctx.fs.readFile(path);
79
- if (!content) continue;
80
-
81
- const data = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
82
- if (!data?.mcpServers) continue;
83
-
84
- const expanded = expandEnvVarsDeep(data.mcpServers);
85
- for (const [serverName, config] of Object.entries(expanded)) {
86
- const serverConfig = config as Record<string, unknown>;
87
- items.push({
88
- name: serverName,
89
- command: serverConfig.command as string | undefined,
90
- args: serverConfig.args as string[] | undefined,
91
- env: serverConfig.env as Record<string, string> | undefined,
92
- url: serverConfig.url as string | undefined,
93
- headers: serverConfig.headers as Record<string, string> | undefined,
94
- transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
95
- _source: createSourceMeta(PROVIDER_ID, path, "project"),
96
- });
97
- }
98
- break;
99
- }
100
- break;
101
- }
102
-
103
- for (const name of USER_DIRS) {
104
- const userPath = join(ctx.home, name, "mcp.json");
105
- const content = ctx.fs.readFile(userPath);
106
- if (!content) continue;
107
-
75
+ const parseMcpServers = (content: string, path: string, level: "user" | "project"): MCPServer[] => {
76
+ const result: MCPServer[] = [];
108
77
  const data = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
109
- if (!data?.mcpServers) continue;
78
+ if (!data?.mcpServers) return result;
110
79
 
111
80
  const expanded = expandEnvVarsDeep(data.mcpServers);
112
81
  for (const [serverName, config] of Object.entries(expanded)) {
113
82
  const serverConfig = config as Record<string, unknown>;
114
- items.push({
83
+ result.push({
115
84
  name: serverName,
116
85
  command: serverConfig.command as string | undefined,
117
86
  args: serverConfig.args as string[] | undefined,
@@ -119,10 +88,41 @@ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
119
88
  url: serverConfig.url as string | undefined,
120
89
  headers: serverConfig.headers as Record<string, string> | undefined,
121
90
  transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
122
- _source: createSourceMeta(PROVIDER_ID, userPath, "user"),
91
+ _source: createSourceMeta(PROVIDER_ID, path, level),
123
92
  });
124
93
  }
125
- break;
94
+ return result;
95
+ };
96
+
97
+ const projectDirs = await Promise.all(
98
+ PROJECT_DIRS.map(async (name) => {
99
+ const dir = join(ctx.cwd, name);
100
+ const entries = await readDirEntries(dir);
101
+ return entries.length > 0 ? dir : null;
102
+ }),
103
+ );
104
+ const userPaths = USER_DIRS.map((name) => join(ctx.home, name, "mcp.json"));
105
+
106
+ const projectDir = projectDirs.find((dir) => dir !== null);
107
+ if (projectDir) {
108
+ const projectCandidates = ["mcp.json", ".mcp.json"].map((filename) => join(projectDir, filename));
109
+ const projectContents = await Promise.all(projectCandidates.map((path) => readFile(path)));
110
+ for (let i = 0; i < projectCandidates.length; i++) {
111
+ const content = projectContents[i];
112
+ if (content) {
113
+ items.push(...parseMcpServers(content, projectCandidates[i], "project"));
114
+ break;
115
+ }
116
+ }
117
+ }
118
+
119
+ const userContents = await Promise.all(userPaths.map((path) => readFile(path)));
120
+ for (let i = 0; i < userPaths.length; i++) {
121
+ const content = userContents[i];
122
+ if (content) {
123
+ items.push(...parseMcpServers(content, userPaths[i], "user"));
124
+ break;
125
+ }
126
126
  }
127
127
 
128
128
  return { items, warnings };
@@ -137,48 +137,54 @@ registerProvider<MCPServer>(mcpCapability.id, {
137
137
  });
138
138
 
139
139
  // System Prompt (SYSTEM.md)
140
- function loadSystemPrompt(ctx: LoadContext): LoadResult<SystemPrompt> {
140
+ async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> {
141
141
  const items: SystemPrompt[] = [];
142
142
 
143
- // User level: ~/.omp/agent/SYSTEM.md or ~/.pi/agent/SYSTEM.md
144
- for (const name of USER_DIRS) {
145
- const userPath = join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""), "SYSTEM.md");
146
- const userContent = ctx.fs.readFile(userPath);
147
- if (userContent) {
143
+ const userPaths = USER_DIRS.map((name) =>
144
+ join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""), "SYSTEM.md"),
145
+ );
146
+ const userContents = await Promise.all(userPaths.map((p) => readFile(p)));
147
+ for (let i = 0; i < userPaths.length; i++) {
148
+ const content = userContents[i];
149
+ if (content) {
148
150
  items.push({
149
- path: userPath,
150
- content: userContent,
151
+ path: userPaths[i],
152
+ content,
151
153
  level: "user",
152
- _source: createSourceMeta(PROVIDER_ID, userPath, "user"),
154
+ _source: createSourceMeta(PROVIDER_ID, userPaths[i], "user"),
153
155
  });
154
- break; // First match wins
156
+ break;
155
157
  }
156
158
  }
157
159
 
158
- // Project level: walk up looking for .omp/SYSTEM.md or .pi/SYSTEM.md
160
+ const ancestors: string[] = [];
159
161
  let current = ctx.cwd;
160
162
  while (true) {
161
- for (const name of PROJECT_DIRS) {
162
- const configDir = join(current, name);
163
- if (ctx.fs.isDir(configDir)) {
164
- const projectPath = join(configDir, "SYSTEM.md");
165
- const content = ctx.fs.readFile(projectPath);
166
- if (content) {
167
- items.push({
168
- path: projectPath,
169
- content,
170
- level: "project",
171
- _source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
172
- });
173
- break; // First config dir in this directory wins
174
- }
175
- }
176
- }
163
+ ancestors.push(current);
177
164
  const parent = dirname(current);
178
165
  if (parent === current) break;
179
166
  current = parent;
180
167
  }
181
168
 
169
+ for (const dir of ancestors) {
170
+ const configDirs = PROJECT_DIRS.map((name) => join(dir, name));
171
+ const entriesResults = await Promise.all(configDirs.map((d) => readDirEntries(d)));
172
+ const validConfigDir = configDirs.find((_, i) => entriesResults[i].length > 0);
173
+ if (!validConfigDir) continue;
174
+
175
+ const projectPath = join(validConfigDir, "SYSTEM.md");
176
+ const content = await readFile(projectPath);
177
+ if (content) {
178
+ items.push({
179
+ path: projectPath,
180
+ content,
181
+ level: "project",
182
+ _source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
183
+ });
184
+ }
185
+ break;
186
+ }
187
+
182
188
  return { items, warnings: [] };
183
189
  }
184
190
 
@@ -191,23 +197,23 @@ registerProvider<SystemPrompt>(systemPromptCapability.id, {
191
197
  });
192
198
 
193
199
  // Skills
194
- function loadSkills(ctx: LoadContext): LoadResult<Skill> {
195
- const items: Skill[] = [];
196
- const warnings: string[] = [];
197
-
198
- for (const { dir, level } of getConfigDirs(ctx)) {
199
- const skillsDir = join(dir, "skills");
200
- const result = loadSkillsFromDir(ctx, {
201
- dir: skillsDir,
202
- providerId: PROVIDER_ID,
203
- level,
204
- requireDescription: true,
205
- });
206
- items.push(...result.items);
207
- if (result.warnings) warnings.push(...result.warnings);
208
- }
200
+ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
201
+ const configDirs = await getConfigDirs(ctx);
202
+ const results = await Promise.all(
203
+ configDirs.map(({ dir, level }) =>
204
+ loadSkillsFromDir(ctx, {
205
+ dir: join(dir, "skills"),
206
+ providerId: PROVIDER_ID,
207
+ level,
208
+ requireDescription: true,
209
+ }),
210
+ ),
211
+ );
209
212
 
210
- return { items, warnings };
213
+ return {
214
+ items: results.flatMap((r) => r.items),
215
+ warnings: results.flatMap((r) => r.warnings ?? []),
216
+ };
211
217
  }
212
218
 
213
219
  registerProvider<Skill>(skillCapability.id, {
@@ -219,13 +225,13 @@ registerProvider<Skill>(skillCapability.id, {
219
225
  });
220
226
 
221
227
  // Slash Commands
222
- function loadSlashCommands(ctx: LoadContext): LoadResult<SlashCommand> {
228
+ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
223
229
  const items: SlashCommand[] = [];
224
230
  const warnings: string[] = [];
225
231
 
226
- for (const { dir, level } of getConfigDirs(ctx)) {
232
+ for (const { dir, level } of await getConfigDirs(ctx)) {
227
233
  const commandsDir = join(dir, "commands");
228
- const result = loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, level, {
234
+ const result = await loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, level, {
229
235
  extensions: ["md"],
230
236
  transform: (name, content, path, source) => ({
231
237
  name: name.replace(/\.md$/, ""),
@@ -251,13 +257,13 @@ registerProvider<SlashCommand>(slashCommandCapability.id, {
251
257
  });
252
258
 
253
259
  // Rules
254
- function loadRules(ctx: LoadContext): LoadResult<Rule> {
260
+ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
255
261
  const items: Rule[] = [];
256
262
  const warnings: string[] = [];
257
263
 
258
- for (const { dir, level } of getConfigDirs(ctx)) {
264
+ for (const { dir, level } of await getConfigDirs(ctx)) {
259
265
  const rulesDir = join(dir, "rules");
260
- const result = loadFilesFromDir<Rule>(ctx, rulesDir, PROVIDER_ID, level, {
266
+ const result = await loadFilesFromDir<Rule>(ctx, rulesDir, PROVIDER_ID, level, {
261
267
  extensions: ["md", "mdc"],
262
268
  transform: (name, content, path, source) => {
263
269
  const { frontmatter, body } = parseFrontmatter(content);
@@ -289,13 +295,13 @@ registerProvider<Rule>(ruleCapability.id, {
289
295
  });
290
296
 
291
297
  // Prompts
292
- function loadPrompts(ctx: LoadContext): LoadResult<Prompt> {
298
+ async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
293
299
  const items: Prompt[] = [];
294
300
  const warnings: string[] = [];
295
301
 
296
- for (const { dir, level } of getConfigDirs(ctx)) {
302
+ for (const { dir, level } of await getConfigDirs(ctx)) {
297
303
  const promptsDir = join(dir, "prompts");
298
- const result = loadFilesFromDir<Prompt>(ctx, promptsDir, PROVIDER_ID, level, {
304
+ const result = await loadFilesFromDir<Prompt>(ctx, promptsDir, PROVIDER_ID, level, {
299
305
  extensions: ["md"],
300
306
  transform: (name, content, path, source) => ({
301
307
  name: name.replace(/\.md$/, ""),
@@ -320,7 +326,7 @@ registerProvider<Prompt>(promptCapability.id, {
320
326
  });
321
327
 
322
328
  // Extension Modules
323
- function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
329
+ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<ExtensionModule>> {
324
330
  const items: ExtensionModule[] = [];
325
331
  const warnings: string[] = [];
326
332
 
@@ -337,45 +343,88 @@ function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
337
343
  return resolve(ctx.cwd, rawPath);
338
344
  };
339
345
 
340
- const addExtensionPath = (extPath: string, level: "user" | "project"): void => {
341
- items.push({
342
- name: getExtensionNameFromPath(extPath),
343
- path: extPath,
344
- level,
345
- _source: createSourceMeta(PROVIDER_ID, extPath, level),
346
- });
347
- };
346
+ const createExtensionModule = (extPath: string, level: "user" | "project"): ExtensionModule => ({
347
+ name: getExtensionNameFromPath(extPath),
348
+ path: extPath,
349
+ level,
350
+ _source: createSourceMeta(PROVIDER_ID, extPath, level),
351
+ });
348
352
 
349
- for (const { dir, level } of getConfigDirs(ctx)) {
350
- const extensionsDir = join(dir, "extensions");
351
- const discovered = discoverExtensionModulePaths(ctx, extensionsDir);
352
- for (const extPath of discovered) {
353
- addExtensionPath(extPath, level);
353
+ const configDirs = await getConfigDirs(ctx);
354
+
355
+ const [discoveredResults, settingsResults] = await Promise.all([
356
+ Promise.all(configDirs.map(({ dir }) => discoverExtensionModulePaths(ctx, join(dir, "extensions")))),
357
+ Promise.all(configDirs.map(({ dir }) => readFile(join(dir, "settings.json")))),
358
+ ]);
359
+
360
+ for (let i = 0; i < configDirs.length; i++) {
361
+ const { level } = configDirs[i];
362
+ for (const extPath of discoveredResults[i]) {
363
+ items.push(createExtensionModule(extPath, level));
354
364
  }
365
+ }
366
+
367
+ const settingsExtensions: Array<{
368
+ resolvedPath: string;
369
+ settingsPath: string;
370
+ level: "user" | "project";
371
+ }> = [];
372
+
373
+ for (let i = 0; i < configDirs.length; i++) {
374
+ const { dir, level } = configDirs[i];
375
+ const settingsContent = settingsResults[i];
376
+ if (!settingsContent) continue;
355
377
 
356
378
  const settingsPath = join(dir, "settings.json");
357
- const settingsContent = ctx.fs.readFile(settingsPath);
358
- if (settingsContent) {
359
- const settingsData = parseJSON<{ extensions?: unknown }>(settingsContent);
360
- const extensions = settingsData?.extensions;
361
- if (Array.isArray(extensions)) {
362
- for (const entry of extensions) {
363
- if (typeof entry !== "string") {
364
- warnings.push(`Invalid extension path in ${settingsPath}: ${String(entry)}`);
365
- continue;
366
- }
367
- const resolvedPath = resolveExtensionPath(entry);
368
- if (ctx.fs.isDir(resolvedPath)) {
369
- for (const extPath of discoverExtensionModulePaths(ctx, resolvedPath)) {
370
- addExtensionPath(extPath, level);
371
- }
372
- } else if (ctx.fs.isFile(resolvedPath)) {
373
- addExtensionPath(resolvedPath, level);
374
- } else {
375
- warnings.push(`Extension path not found: ${resolvedPath}`);
376
- }
377
- }
379
+ const settingsData = parseJSON<{ extensions?: unknown }>(settingsContent);
380
+ const extensions = settingsData?.extensions;
381
+ if (!Array.isArray(extensions)) continue;
382
+
383
+ for (const entry of extensions) {
384
+ if (typeof entry !== "string") {
385
+ warnings.push(`Invalid extension path in ${settingsPath}: ${String(entry)}`);
386
+ continue;
378
387
  }
388
+ settingsExtensions.push({
389
+ resolvedPath: resolveExtensionPath(entry),
390
+ settingsPath,
391
+ level,
392
+ });
393
+ }
394
+ }
395
+
396
+ const [entriesResults, fileContents] = await Promise.all([
397
+ Promise.all(settingsExtensions.map(({ resolvedPath }) => readDirEntries(resolvedPath))),
398
+ Promise.all(settingsExtensions.map(({ resolvedPath }) => readFile(resolvedPath))),
399
+ ]);
400
+
401
+ const dirDiscoveryPromises: Array<{
402
+ promise: Promise<string[]>;
403
+ level: "user" | "project";
404
+ }> = [];
405
+
406
+ for (let i = 0; i < settingsExtensions.length; i++) {
407
+ const { resolvedPath, level } = settingsExtensions[i];
408
+ const entries = entriesResults[i];
409
+ const content = fileContents[i];
410
+
411
+ if (entries.length > 0) {
412
+ dirDiscoveryPromises.push({
413
+ promise: discoverExtensionModulePaths(ctx, resolvedPath),
414
+ level,
415
+ });
416
+ } else if (content !== null) {
417
+ items.push(createExtensionModule(resolvedPath, level));
418
+ } else {
419
+ warnings.push(`Extension path not found: ${resolvedPath}`);
420
+ }
421
+ }
422
+
423
+ const dirDiscoveryResults = await Promise.all(dirDiscoveryPromises.map((d) => d.promise));
424
+ for (let i = 0; i < dirDiscoveryPromises.length; i++) {
425
+ const { level } = dirDiscoveryPromises[i];
426
+ for (const extPath of dirDiscoveryResults[i]) {
427
+ items.push(createExtensionModule(extPath, level));
379
428
  }
380
429
  }
381
430
 
@@ -391,40 +440,61 @@ registerProvider<ExtensionModule>(extensionModuleCapability.id, {
391
440
  });
392
441
 
393
442
  // Extensions
394
- function loadExtensions(ctx: LoadContext): LoadResult<Extension> {
443
+ async function loadExtensions(ctx: LoadContext): Promise<LoadResult<Extension>> {
395
444
  const items: Extension[] = [];
396
445
  const warnings: string[] = [];
397
446
 
398
- for (const { dir, level } of getConfigDirs(ctx)) {
399
- const extensionsDir = join(dir, "extensions");
400
- if (!ctx.fs.isDir(extensionsDir)) continue;
401
-
402
- for (const name of ctx.fs.readDir(extensionsDir)) {
403
- if (name.startsWith(".")) continue;
447
+ const configDirs = await getConfigDirs(ctx);
448
+ const entriesResults = await Promise.all(configDirs.map(({ dir }) => readDirEntries(join(dir, "extensions"))));
404
449
 
405
- const extDir = join(extensionsDir, name);
406
- if (!ctx.fs.isDir(extDir)) continue;
450
+ const manifestCandidates: Array<{
451
+ extDir: string;
452
+ manifestPath: string;
453
+ entryName: string;
454
+ level: "user" | "project";
455
+ }> = [];
407
456
 
408
- const manifestPath = join(extDir, "gemini-extension.json");
409
- const content = ctx.fs.readFile(manifestPath);
410
- if (!content) continue;
457
+ for (let i = 0; i < configDirs.length; i++) {
458
+ const { dir, level } = configDirs[i];
459
+ const entries = entriesResults[i];
460
+ const extensionsDir = join(dir, "extensions");
411
461
 
412
- const manifest = parseJSON<ExtensionManifest>(content);
413
- if (!manifest) {
414
- warnings.push(`Failed to parse ${manifestPath}`);
415
- continue;
416
- }
462
+ for (const entry of entries) {
463
+ if (entry.name.startsWith(".")) continue;
464
+ if (!entry.isDirectory()) continue;
417
465
 
418
- items.push({
419
- name: manifest.name || name,
420
- path: extDir,
421
- manifest,
466
+ const extDir = join(extensionsDir, entry.name);
467
+ manifestCandidates.push({
468
+ extDir,
469
+ manifestPath: join(extDir, "gemini-extension.json"),
470
+ entryName: entry.name,
422
471
  level,
423
- _source: createSourceMeta(PROVIDER_ID, manifestPath, level),
424
472
  });
425
473
  }
426
474
  }
427
475
 
476
+ const manifestContents = await Promise.all(manifestCandidates.map(({ manifestPath }) => readFile(manifestPath)));
477
+
478
+ for (let i = 0; i < manifestCandidates.length; i++) {
479
+ const content = manifestContents[i];
480
+ if (!content) continue;
481
+
482
+ const { extDir, manifestPath, entryName, level } = manifestCandidates[i];
483
+ const manifest = parseJSON<ExtensionManifest>(content);
484
+ if (!manifest) {
485
+ warnings.push(`Failed to parse ${manifestPath}`);
486
+ continue;
487
+ }
488
+
489
+ items.push({
490
+ name: manifest.name || entryName,
491
+ path: extDir,
492
+ manifest,
493
+ level,
494
+ _source: createSourceMeta(PROVIDER_ID, manifestPath, level),
495
+ });
496
+ }
497
+
428
498
  return { items, warnings };
429
499
  }
430
500
 
@@ -437,13 +507,13 @@ registerProvider<Extension>(extensionCapability.id, {
437
507
  });
438
508
 
439
509
  // Instructions
440
- function loadInstructions(ctx: LoadContext): LoadResult<Instruction> {
510
+ async function loadInstructions(ctx: LoadContext): Promise<LoadResult<Instruction>> {
441
511
  const items: Instruction[] = [];
442
512
  const warnings: string[] = [];
443
513
 
444
- for (const { dir, level } of getConfigDirs(ctx)) {
514
+ for (const { dir, level } of await getConfigDirs(ctx)) {
445
515
  const instructionsDir = join(dir, "instructions");
446
- const result = loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, level, {
516
+ const result = await loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, level, {
447
517
  extensions: ["md"],
448
518
  transform: (name, content, path, source) => {
449
519
  const { frontmatter, body } = parseFrontmatter(content);
@@ -472,35 +542,50 @@ registerProvider<Instruction>(instructionCapability.id, {
472
542
  });
473
543
 
474
544
  // Hooks
475
- function loadHooks(ctx: LoadContext): LoadResult<Hook> {
545
+ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
476
546
  const items: Hook[] = [];
477
547
 
478
- for (const { dir, level } of getConfigDirs(ctx)) {
479
- const hooksDir = join(dir, "hooks");
480
- if (!ctx.fs.isDir(hooksDir)) continue;
548
+ const configDirs = await getConfigDirs(ctx);
549
+ const hookTypes = ["pre", "post"] as const;
481
550
 
482
- for (const hookType of ["pre", "post"] as const) {
483
- const typeDir = join(hooksDir, hookType);
484
- if (!ctx.fs.isDir(typeDir)) continue;
551
+ const typeDirRequests: Array<{
552
+ typeDir: string;
553
+ hookType: (typeof hookTypes)[number];
554
+ level: "user" | "project";
555
+ }> = [];
485
556
 
486
- for (const name of ctx.fs.readDir(typeDir)) {
487
- if (name.startsWith(".")) continue;
557
+ for (const { dir, level } of configDirs) {
558
+ for (const hookType of hookTypes) {
559
+ typeDirRequests.push({
560
+ typeDir: join(dir, "hooks", hookType),
561
+ hookType,
562
+ level,
563
+ });
564
+ }
565
+ }
488
566
 
489
- const path = join(typeDir, name);
490
- if (!ctx.fs.isFile(path)) continue;
567
+ const typeEntriesResults = await Promise.all(typeDirRequests.map(({ typeDir }) => readDirEntries(typeDir)));
491
568
 
492
- const baseName = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name;
493
- const tool = baseName === "*" ? "*" : baseName;
569
+ for (let i = 0; i < typeDirRequests.length; i++) {
570
+ const { typeDir, hookType, level } = typeDirRequests[i];
571
+ const typeEntries = typeEntriesResults[i];
494
572
 
495
- items.push({
496
- name,
497
- path,
498
- type: hookType,
499
- tool,
500
- level,
501
- _source: createSourceMeta(PROVIDER_ID, path, level),
502
- });
503
- }
573
+ for (const entry of typeEntries) {
574
+ if (entry.name.startsWith(".")) continue;
575
+ if (!entry.isFile()) continue;
576
+
577
+ const path = join(typeDir, entry.name);
578
+ const baseName = entry.name.includes(".") ? entry.name.slice(0, entry.name.lastIndexOf(".")) : entry.name;
579
+ const tool = baseName === "*" ? "*" : baseName;
580
+
581
+ items.push({
582
+ name: entry.name,
583
+ path,
584
+ type: hookType,
585
+ tool,
586
+ level,
587
+ _source: createSourceMeta(PROVIDER_ID, path, level),
588
+ });
504
589
  }
505
590
  }
506
591
 
@@ -516,58 +601,86 @@ registerProvider<Hook>(hookCapability.id, {
516
601
  });
517
602
 
518
603
  // Custom Tools
519
- function loadTools(ctx: LoadContext): LoadResult<CustomTool> {
604
+ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
520
605
  const items: CustomTool[] = [];
521
606
  const warnings: string[] = [];
522
607
 
523
- for (const { dir, level } of getConfigDirs(ctx)) {
608
+ const configDirs = await getConfigDirs(ctx);
609
+ const entriesResults = await Promise.all(configDirs.map(({ dir }) => readDirEntries(join(dir, "tools"))));
610
+
611
+ const fileLoadPromises: Array<Promise<{ items: CustomTool[]; warnings?: string[] }>> = [];
612
+ const subDirCandidates: Array<{
613
+ indexPath: string;
614
+ entryName: string;
615
+ level: "user" | "project";
616
+ }> = [];
617
+
618
+ for (let i = 0; i < configDirs.length; i++) {
619
+ const { dir, level } = configDirs[i];
620
+ const toolEntries = entriesResults[i];
621
+ if (toolEntries.length === 0) continue;
622
+
524
623
  const toolsDir = join(dir, "tools");
525
- if (!ctx.fs.isDir(toolsDir)) continue;
526
624
 
527
- // Load tool files (JSON and Markdown declarative tools)
528
- const result = loadFilesFromDir<CustomTool>(ctx, toolsDir, PROVIDER_ID, level, {
529
- extensions: ["json", "md"],
530
- transform: (name, content, path, source) => {
531
- if (name.endsWith(".json")) {
532
- const data = parseJSON<{ name?: string; description?: string }>(content);
625
+ fileLoadPromises.push(
626
+ loadFilesFromDir<CustomTool>(ctx, toolsDir, PROVIDER_ID, level, {
627
+ extensions: ["json", "md"],
628
+ transform: (name, content, path, source) => {
629
+ if (name.endsWith(".json")) {
630
+ const data = parseJSON<{ name?: string; description?: string }>(content);
631
+ return {
632
+ name: data?.name || name.replace(/\.json$/, ""),
633
+ path,
634
+ description: data?.description,
635
+ level,
636
+ _source: source,
637
+ };
638
+ }
639
+ const { frontmatter } = parseFrontmatter(content);
533
640
  return {
534
- name: data?.name || name.replace(/\.json$/, ""),
641
+ name: (frontmatter.name as string) || name.replace(/\.md$/, ""),
535
642
  path,
536
- description: data?.description,
643
+ description: frontmatter.description as string | undefined,
537
644
  level,
538
645
  _source: source,
539
646
  };
540
- }
541
- const { frontmatter } = parseFrontmatter(content);
542
- return {
543
- name: (frontmatter.name as string) || name.replace(/\.md$/, ""),
544
- path,
545
- description: frontmatter.description as string | undefined,
546
- level,
547
- _source: source,
548
- };
549
- },
550
- });
647
+ },
648
+ }),
649
+ );
650
+
651
+ for (const entry of toolEntries) {
652
+ if (entry.name.startsWith(".")) continue;
653
+ if (!entry.isDirectory()) continue;
654
+
655
+ subDirCandidates.push({
656
+ indexPath: join(toolsDir, entry.name, "index.ts"),
657
+ entryName: entry.name,
658
+ level,
659
+ });
660
+ }
661
+ }
662
+
663
+ const [fileResults, indexContents] = await Promise.all([
664
+ Promise.all(fileLoadPromises),
665
+ Promise.all(subDirCandidates.map(({ indexPath }) => readFile(indexPath))),
666
+ ]);
667
+
668
+ for (const result of fileResults) {
551
669
  items.push(...result.items);
552
670
  if (result.warnings) warnings.push(...result.warnings);
671
+ }
553
672
 
554
- // Load TypeScript tools from subdirectories (tools/mytool/index.ts pattern)
555
- for (const name of ctx.fs.readDir(toolsDir)) {
556
- if (name.startsWith(".")) continue;
557
-
558
- const subDir = join(toolsDir, name);
559
- if (!ctx.fs.isDir(subDir)) continue;
560
-
561
- const indexPath = join(subDir, "index.ts");
562
- if (ctx.fs.isFile(indexPath)) {
563
- items.push({
564
- name,
565
- path: indexPath,
566
- description: undefined,
567
- level,
568
- _source: createSourceMeta(PROVIDER_ID, indexPath, level),
569
- });
570
- }
673
+ for (let i = 0; i < subDirCandidates.length; i++) {
674
+ const indexContent = indexContents[i];
675
+ if (indexContent !== null) {
676
+ const { indexPath, entryName, level } = subDirCandidates[i];
677
+ items.push({
678
+ name: entryName,
679
+ path: indexPath,
680
+ description: undefined,
681
+ level,
682
+ _source: createSourceMeta(PROVIDER_ID, indexPath, level),
683
+ });
571
684
  }
572
685
  }
573
686
 
@@ -583,13 +696,13 @@ registerProvider<CustomTool>(toolCapability.id, {
583
696
  });
584
697
 
585
698
  // Settings
586
- function loadSettings(ctx: LoadContext): LoadResult<Settings> {
699
+ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
587
700
  const items: Settings[] = [];
588
701
  const warnings: string[] = [];
589
702
 
590
- for (const { dir, level } of getConfigDirs(ctx)) {
703
+ for (const { dir, level } of await getConfigDirs(ctx)) {
591
704
  const settingsPath = join(dir, "settings.json");
592
- const content = ctx.fs.readFile(settingsPath);
705
+ const content = await readFile(settingsPath);
593
706
  if (!content) continue;
594
707
 
595
708
  const data = parseJSON<Record<string, unknown>>(content);
@@ -618,52 +731,59 @@ registerProvider<Settings>(settingsCapability.id, {
618
731
  });
619
732
 
620
733
  // Context Files (AGENTS.md)
621
- function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
734
+ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
622
735
  const items: ContextFile[] = [];
623
736
  const warnings: string[] = [];
624
737
 
625
- // User level: ~/.omp/agent/AGENTS.md or ~/.pi/agent/AGENTS.md
626
- for (const name of USER_DIRS) {
627
- const userPath = join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""), "AGENTS.md");
628
- const content = ctx.fs.readFile(userPath);
738
+ const userPaths = USER_DIRS.map((name) =>
739
+ join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""), "AGENTS.md"),
740
+ );
741
+ const userContents = await Promise.all(userPaths.map((p) => readFile(p)));
742
+ for (let i = 0; i < userPaths.length; i++) {
743
+ const content = userContents[i];
629
744
  if (content) {
630
745
  items.push({
631
- path: userPath,
746
+ path: userPaths[i],
632
747
  content,
633
748
  level: "user",
634
- _source: createSourceMeta(PROVIDER_ID, userPath, "user"),
749
+ _source: createSourceMeta(PROVIDER_ID, userPaths[i], "user"),
635
750
  });
636
- break; // First match wins
751
+ break;
637
752
  }
638
753
  }
639
754
 
640
- // Project level: walk up looking for .omp/AGENTS.md or .pi/AGENTS.md
755
+ const ancestors: Array<{ dir: string; depth: number }> = [];
641
756
  let current = ctx.cwd;
642
757
  let depth = 0;
643
758
  while (true) {
644
- for (const name of PROJECT_DIRS) {
645
- const configDir = join(current, name);
646
- if (ctx.fs.isDir(configDir)) {
647
- const projectPath = join(configDir, "AGENTS.md");
648
- const content = ctx.fs.readFile(projectPath);
649
- if (content) {
650
- items.push({
651
- path: projectPath,
652
- content,
653
- level: "project",
654
- depth,
655
- _source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
656
- });
657
- return { items, warnings }; // First config dir wins
658
- }
659
- }
660
- }
759
+ ancestors.push({ dir: current, depth });
661
760
  const parent = dirname(current);
662
761
  if (parent === current) break;
663
762
  current = parent;
664
763
  depth++;
665
764
  }
666
765
 
766
+ for (const { dir, depth: ancestorDepth } of ancestors) {
767
+ const configDirs = PROJECT_DIRS.map((name) => join(dir, name));
768
+ const entriesResults = await Promise.all(configDirs.map((d) => readDirEntries(d)));
769
+ const validConfigDir = configDirs.find((_, i) => entriesResults[i].length > 0);
770
+ if (!validConfigDir) continue;
771
+
772
+ const projectPath = join(validConfigDir, "AGENTS.md");
773
+ const content = await readFile(projectPath);
774
+ if (content) {
775
+ items.push({
776
+ path: projectPath,
777
+ content,
778
+ level: "project",
779
+ depth: ancestorDepth,
780
+ _source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
781
+ });
782
+ return { items, warnings };
783
+ }
784
+ break;
785
+ }
786
+
667
787
  return { items, warnings };
668
788
  }
669
789