@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
@@ -5,10 +5,11 @@
5
5
  * .pi is an alias for backwards compatibility.
6
6
  */
7
7
 
8
- import { basename, dirname, isAbsolute, join, resolve } from "path";
8
+ 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";
@@ -16,7 +17,7 @@ import { type MCPServer, mcpCapability } from "../capability/mcp";
16
17
  import { type Prompt, promptCapability } from "../capability/prompt";
17
18
  import { type Rule, ruleCapability } from "../capability/rule";
18
19
  import { type Settings, settingsCapability } from "../capability/settings";
19
- import { type Skill, type SkillFrontmatter, skillCapability } from "../capability/skill";
20
+ import { type Skill, skillCapability } from "../capability/skill";
20
21
  import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
21
22
  import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
22
23
  import { type CustomTool, toolCapability } from "../capability/tool";
@@ -27,6 +28,7 @@ import {
27
28
  expandEnvVarsDeep,
28
29
  getExtensionNameFromPath,
29
30
  loadFilesFromDir,
31
+ loadSkillsFromDir,
30
32
  parseFrontmatter,
31
33
  parseJSON,
32
34
  SOURCE_PATHS,
@@ -41,12 +43,13 @@ const PATHS = SOURCE_PATHS.native;
41
43
  const PROJECT_DIRS = [PATHS.projectDir, ...PATHS.aliases];
42
44
  const USER_DIRS = [PATHS.userBase, ...PATHS.aliases];
43
45
 
44
- function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "project" }> {
46
+ async function getConfigDirs(ctx: LoadContext): Promise<Array<{ dir: string; level: "user" | "project" }>> {
45
47
  const result: Array<{ dir: string; level: "user" | "project" }> = [];
46
48
 
47
49
  for (const name of PROJECT_DIRS) {
48
- const projectDir = ctx.fs.walkUp(name, { dir: true });
49
- if (projectDir) {
50
+ const projectDir = join(ctx.cwd, name);
51
+ const entries = await readDirEntries(projectDir);
52
+ if (entries.length > 0) {
50
53
  result.push({ dir: projectDir, level: "project" });
51
54
  break;
52
55
  }
@@ -54,7 +57,8 @@ function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "
54
57
 
55
58
  for (const name of USER_DIRS) {
56
59
  const userDir = join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""));
57
- if (ctx.fs.isDir(userDir)) {
60
+ const entries = await readDirEntries(userDir);
61
+ if (entries.length > 0) {
58
62
  result.push({ dir: userDir, level: "user" });
59
63
  break;
60
64
  }
@@ -64,53 +68,19 @@ function getConfigDirs(ctx: LoadContext): Array<{ dir: string; level: "user" | "
64
68
  }
65
69
 
66
70
  // MCP
67
- function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
71
+ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
68
72
  const items: MCPServer[] = [];
69
73
  const warnings: string[] = [];
70
74
 
71
- for (const name of PROJECT_DIRS) {
72
- const projectDir = ctx.fs.walkUp(name, { dir: true });
73
- if (!projectDir) continue;
74
-
75
- for (const filename of ["mcp.json", ".mcp.json"]) {
76
- const path = join(projectDir, filename);
77
- const content = ctx.fs.readFile(path);
78
- if (!content) continue;
79
-
80
- const data = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
81
- if (!data?.mcpServers) continue;
82
-
83
- const expanded = expandEnvVarsDeep(data.mcpServers);
84
- for (const [serverName, config] of Object.entries(expanded)) {
85
- const serverConfig = config as Record<string, unknown>;
86
- items.push({
87
- name: serverName,
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: serverConfig.type as "stdio" | "sse" | "http" | undefined,
94
- _source: createSourceMeta(PROVIDER_ID, path, "project"),
95
- });
96
- }
97
- break;
98
- }
99
- break;
100
- }
101
-
102
- for (const name of USER_DIRS) {
103
- const userPath = join(ctx.home, name, "mcp.json");
104
- const content = ctx.fs.readFile(userPath);
105
- if (!content) continue;
106
-
75
+ const parseMcpServers = (content: string, path: string, level: "user" | "project"): MCPServer[] => {
76
+ const result: MCPServer[] = [];
107
77
  const data = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
108
- if (!data?.mcpServers) continue;
78
+ if (!data?.mcpServers) return result;
109
79
 
110
80
  const expanded = expandEnvVarsDeep(data.mcpServers);
111
81
  for (const [serverName, config] of Object.entries(expanded)) {
112
82
  const serverConfig = config as Record<string, unknown>;
113
- items.push({
83
+ result.push({
114
84
  name: serverName,
115
85
  command: serverConfig.command as string | undefined,
116
86
  args: serverConfig.args as string[] | undefined,
@@ -118,10 +88,41 @@ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
118
88
  url: serverConfig.url as string | undefined,
119
89
  headers: serverConfig.headers as Record<string, string> | undefined,
120
90
  transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
121
- _source: createSourceMeta(PROVIDER_ID, userPath, "user"),
91
+ _source: createSourceMeta(PROVIDER_ID, path, level),
122
92
  });
123
93
  }
124
- 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
+ }
125
126
  }
126
127
 
127
128
  return { items, warnings };
@@ -136,48 +137,54 @@ registerProvider<MCPServer>(mcpCapability.id, {
136
137
  });
137
138
 
138
139
  // System Prompt (SYSTEM.md)
139
- function loadSystemPrompt(ctx: LoadContext): LoadResult<SystemPrompt> {
140
+ async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> {
140
141
  const items: SystemPrompt[] = [];
141
142
 
142
- // User level: ~/.omp/agent/SYSTEM.md or ~/.pi/agent/SYSTEM.md
143
- for (const name of USER_DIRS) {
144
- const userPath = join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""), "SYSTEM.md");
145
- const userContent = ctx.fs.readFile(userPath);
146
- 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) {
147
150
  items.push({
148
- path: userPath,
149
- content: userContent,
151
+ path: userPaths[i],
152
+ content,
150
153
  level: "user",
151
- _source: createSourceMeta(PROVIDER_ID, userPath, "user"),
154
+ _source: createSourceMeta(PROVIDER_ID, userPaths[i], "user"),
152
155
  });
153
- break; // First match wins
156
+ break;
154
157
  }
155
158
  }
156
159
 
157
- // Project level: walk up looking for .omp/SYSTEM.md or .pi/SYSTEM.md
160
+ const ancestors: string[] = [];
158
161
  let current = ctx.cwd;
159
162
  while (true) {
160
- for (const name of PROJECT_DIRS) {
161
- const configDir = join(current, name);
162
- if (ctx.fs.isDir(configDir)) {
163
- const projectPath = join(configDir, "SYSTEM.md");
164
- const content = ctx.fs.readFile(projectPath);
165
- if (content) {
166
- items.push({
167
- path: projectPath,
168
- content,
169
- level: "project",
170
- _source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
171
- });
172
- break; // First config dir in this directory wins
173
- }
174
- }
175
- }
163
+ ancestors.push(current);
176
164
  const parent = dirname(current);
177
165
  if (parent === current) break;
178
166
  current = parent;
179
167
  }
180
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
+
181
188
  return { items, warnings: [] };
182
189
  }
183
190
 
@@ -190,71 +197,25 @@ registerProvider<SystemPrompt>(systemPromptCapability.id, {
190
197
  });
191
198
 
192
199
  // Skills
193
- function loadSkillFromFile(ctx: LoadContext, path: string, level: "user" | "project"): Skill | null {
194
- const content = ctx.fs.readFile(path);
195
- if (!content) return null;
196
-
197
- const { frontmatter, body } = parseFrontmatter(content);
198
- const skillDir = dirname(path);
199
- const parentDirName = basename(skillDir);
200
- const name = (frontmatter.name as string) || parentDirName;
201
-
202
- if (!frontmatter.description) return null;
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
+ );
203
212
 
204
213
  return {
205
- name,
206
- path,
207
- content: body,
208
- frontmatter: frontmatter as SkillFrontmatter,
209
- level,
210
- _source: createSourceMeta(PROVIDER_ID, path, level),
214
+ items: results.flatMap((r) => r.items),
215
+ warnings: results.flatMap((r) => r.warnings ?? []),
211
216
  };
212
217
  }
213
218
 
214
- function loadSkillsRecursive(ctx: LoadContext, dir: string, level: "user" | "project"): LoadResult<Skill> {
215
- const items: Skill[] = [];
216
- const warnings: string[] = [];
217
-
218
- if (!ctx.fs.isDir(dir)) return { items, warnings };
219
-
220
- for (const name of ctx.fs.readDir(dir)) {
221
- if (name.startsWith(".") || name === "node_modules") continue;
222
-
223
- const path = join(dir, name);
224
-
225
- if (ctx.fs.isDir(path)) {
226
- const skillFile = join(path, "SKILL.md");
227
- if (ctx.fs.isFile(skillFile)) {
228
- const skill = loadSkillFromFile(ctx, skillFile, level);
229
- if (skill) items.push(skill);
230
- }
231
-
232
- const sub = loadSkillsRecursive(ctx, path, level);
233
- items.push(...sub.items);
234
- if (sub.warnings) warnings.push(...sub.warnings);
235
- } else if (name === "SKILL.md") {
236
- const skill = loadSkillFromFile(ctx, path, level);
237
- if (skill) items.push(skill);
238
- }
239
- }
240
-
241
- return { items, warnings };
242
- }
243
-
244
- function loadSkills(ctx: LoadContext): LoadResult<Skill> {
245
- const items: Skill[] = [];
246
- const warnings: string[] = [];
247
-
248
- for (const { dir, level } of getConfigDirs(ctx)) {
249
- const skillsDir = join(dir, "skills");
250
- const result = loadSkillsRecursive(ctx, skillsDir, level);
251
- items.push(...result.items);
252
- if (result.warnings) warnings.push(...result.warnings);
253
- }
254
-
255
- return { items, warnings };
256
- }
257
-
258
219
  registerProvider<Skill>(skillCapability.id, {
259
220
  id: PROVIDER_ID,
260
221
  displayName: DISPLAY_NAME,
@@ -264,13 +225,13 @@ registerProvider<Skill>(skillCapability.id, {
264
225
  });
265
226
 
266
227
  // Slash Commands
267
- function loadSlashCommands(ctx: LoadContext): LoadResult<SlashCommand> {
228
+ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
268
229
  const items: SlashCommand[] = [];
269
230
  const warnings: string[] = [];
270
231
 
271
- for (const { dir, level } of getConfigDirs(ctx)) {
232
+ for (const { dir, level } of await getConfigDirs(ctx)) {
272
233
  const commandsDir = join(dir, "commands");
273
- const result = loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, level, {
234
+ const result = await loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, level, {
274
235
  extensions: ["md"],
275
236
  transform: (name, content, path, source) => ({
276
237
  name: name.replace(/\.md$/, ""),
@@ -296,13 +257,13 @@ registerProvider<SlashCommand>(slashCommandCapability.id, {
296
257
  });
297
258
 
298
259
  // Rules
299
- function loadRules(ctx: LoadContext): LoadResult<Rule> {
260
+ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
300
261
  const items: Rule[] = [];
301
262
  const warnings: string[] = [];
302
263
 
303
- for (const { dir, level } of getConfigDirs(ctx)) {
264
+ for (const { dir, level } of await getConfigDirs(ctx)) {
304
265
  const rulesDir = join(dir, "rules");
305
- const result = loadFilesFromDir<Rule>(ctx, rulesDir, PROVIDER_ID, level, {
266
+ const result = await loadFilesFromDir<Rule>(ctx, rulesDir, PROVIDER_ID, level, {
306
267
  extensions: ["md", "mdc"],
307
268
  transform: (name, content, path, source) => {
308
269
  const { frontmatter, body } = parseFrontmatter(content);
@@ -334,13 +295,13 @@ registerProvider<Rule>(ruleCapability.id, {
334
295
  });
335
296
 
336
297
  // Prompts
337
- function loadPrompts(ctx: LoadContext): LoadResult<Prompt> {
298
+ async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
338
299
  const items: Prompt[] = [];
339
300
  const warnings: string[] = [];
340
301
 
341
- for (const { dir, level } of getConfigDirs(ctx)) {
302
+ for (const { dir, level } of await getConfigDirs(ctx)) {
342
303
  const promptsDir = join(dir, "prompts");
343
- const result = loadFilesFromDir<Prompt>(ctx, promptsDir, PROVIDER_ID, level, {
304
+ const result = await loadFilesFromDir<Prompt>(ctx, promptsDir, PROVIDER_ID, level, {
344
305
  extensions: ["md"],
345
306
  transform: (name, content, path, source) => ({
346
307
  name: name.replace(/\.md$/, ""),
@@ -365,7 +326,7 @@ registerProvider<Prompt>(promptCapability.id, {
365
326
  });
366
327
 
367
328
  // Extension Modules
368
- function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
329
+ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<ExtensionModule>> {
369
330
  const items: ExtensionModule[] = [];
370
331
  const warnings: string[] = [];
371
332
 
@@ -382,45 +343,88 @@ function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
382
343
  return resolve(ctx.cwd, rawPath);
383
344
  };
384
345
 
385
- const addExtensionPath = (extPath: string, level: "user" | "project"): void => {
386
- items.push({
387
- name: getExtensionNameFromPath(extPath),
388
- path: extPath,
389
- level,
390
- _source: createSourceMeta(PROVIDER_ID, extPath, level),
391
- });
392
- };
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
+ });
393
352
 
394
- for (const { dir, level } of getConfigDirs(ctx)) {
395
- const extensionsDir = join(dir, "extensions");
396
- const discovered = discoverExtensionModulePaths(ctx, extensionsDir);
397
- for (const extPath of discovered) {
398
- 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));
399
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;
400
377
 
401
378
  const settingsPath = join(dir, "settings.json");
402
- const settingsContent = ctx.fs.readFile(settingsPath);
403
- if (settingsContent) {
404
- const settingsData = parseJSON<{ extensions?: unknown }>(settingsContent);
405
- const extensions = settingsData?.extensions;
406
- if (Array.isArray(extensions)) {
407
- for (const entry of extensions) {
408
- if (typeof entry !== "string") {
409
- warnings.push(`Invalid extension path in ${settingsPath}: ${String(entry)}`);
410
- continue;
411
- }
412
- const resolvedPath = resolveExtensionPath(entry);
413
- if (ctx.fs.isDir(resolvedPath)) {
414
- for (const extPath of discoverExtensionModulePaths(ctx, resolvedPath)) {
415
- addExtensionPath(extPath, level);
416
- }
417
- } else if (ctx.fs.isFile(resolvedPath)) {
418
- addExtensionPath(resolvedPath, level);
419
- } else {
420
- warnings.push(`Extension path not found: ${resolvedPath}`);
421
- }
422
- }
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;
423
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));
424
428
  }
425
429
  }
426
430
 
@@ -436,40 +440,61 @@ registerProvider<ExtensionModule>(extensionModuleCapability.id, {
436
440
  });
437
441
 
438
442
  // Extensions
439
- function loadExtensions(ctx: LoadContext): LoadResult<Extension> {
443
+ async function loadExtensions(ctx: LoadContext): Promise<LoadResult<Extension>> {
440
444
  const items: Extension[] = [];
441
445
  const warnings: string[] = [];
442
446
 
443
- for (const { dir, level } of getConfigDirs(ctx)) {
444
- const extensionsDir = join(dir, "extensions");
445
- if (!ctx.fs.isDir(extensionsDir)) continue;
446
-
447
- for (const name of ctx.fs.readDir(extensionsDir)) {
448
- if (name.startsWith(".")) continue;
447
+ const configDirs = await getConfigDirs(ctx);
448
+ const entriesResults = await Promise.all(configDirs.map(({ dir }) => readDirEntries(join(dir, "extensions"))));
449
449
 
450
- const extDir = join(extensionsDir, name);
451
- if (!ctx.fs.isDir(extDir)) continue;
450
+ const manifestCandidates: Array<{
451
+ extDir: string;
452
+ manifestPath: string;
453
+ entryName: string;
454
+ level: "user" | "project";
455
+ }> = [];
452
456
 
453
- const manifestPath = join(extDir, "gemini-extension.json");
454
- const content = ctx.fs.readFile(manifestPath);
455
- 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");
456
461
 
457
- const manifest = parseJSON<ExtensionManifest>(content);
458
- if (!manifest) {
459
- warnings.push(`Failed to parse ${manifestPath}`);
460
- continue;
461
- }
462
+ for (const entry of entries) {
463
+ if (entry.name.startsWith(".")) continue;
464
+ if (!entry.isDirectory()) continue;
462
465
 
463
- items.push({
464
- name: manifest.name || name,
465
- path: extDir,
466
- manifest,
466
+ const extDir = join(extensionsDir, entry.name);
467
+ manifestCandidates.push({
468
+ extDir,
469
+ manifestPath: join(extDir, "gemini-extension.json"),
470
+ entryName: entry.name,
467
471
  level,
468
- _source: createSourceMeta(PROVIDER_ID, manifestPath, level),
469
472
  });
470
473
  }
471
474
  }
472
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
+
473
498
  return { items, warnings };
474
499
  }
475
500
 
@@ -482,13 +507,13 @@ registerProvider<Extension>(extensionCapability.id, {
482
507
  });
483
508
 
484
509
  // Instructions
485
- function loadInstructions(ctx: LoadContext): LoadResult<Instruction> {
510
+ async function loadInstructions(ctx: LoadContext): Promise<LoadResult<Instruction>> {
486
511
  const items: Instruction[] = [];
487
512
  const warnings: string[] = [];
488
513
 
489
- for (const { dir, level } of getConfigDirs(ctx)) {
514
+ for (const { dir, level } of await getConfigDirs(ctx)) {
490
515
  const instructionsDir = join(dir, "instructions");
491
- const result = loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, level, {
516
+ const result = await loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, level, {
492
517
  extensions: ["md"],
493
518
  transform: (name, content, path, source) => {
494
519
  const { frontmatter, body } = parseFrontmatter(content);
@@ -517,35 +542,50 @@ registerProvider<Instruction>(instructionCapability.id, {
517
542
  });
518
543
 
519
544
  // Hooks
520
- function loadHooks(ctx: LoadContext): LoadResult<Hook> {
545
+ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
521
546
  const items: Hook[] = [];
522
547
 
523
- for (const { dir, level } of getConfigDirs(ctx)) {
524
- const hooksDir = join(dir, "hooks");
525
- if (!ctx.fs.isDir(hooksDir)) continue;
548
+ const configDirs = await getConfigDirs(ctx);
549
+ const hookTypes = ["pre", "post"] as const;
550
+
551
+ const typeDirRequests: Array<{
552
+ typeDir: string;
553
+ hookType: (typeof hookTypes)[number];
554
+ level: "user" | "project";
555
+ }> = [];
556
+
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
+ }
526
566
 
527
- for (const hookType of ["pre", "post"] as const) {
528
- const typeDir = join(hooksDir, hookType);
529
- if (!ctx.fs.isDir(typeDir)) continue;
567
+ const typeEntriesResults = await Promise.all(typeDirRequests.map(({ typeDir }) => readDirEntries(typeDir)));
530
568
 
531
- for (const name of ctx.fs.readDir(typeDir)) {
532
- if (name.startsWith(".")) continue;
569
+ for (let i = 0; i < typeDirRequests.length; i++) {
570
+ const { typeDir, hookType, level } = typeDirRequests[i];
571
+ const typeEntries = typeEntriesResults[i];
533
572
 
534
- const path = join(typeDir, name);
535
- if (!ctx.fs.isFile(path)) continue;
573
+ for (const entry of typeEntries) {
574
+ if (entry.name.startsWith(".")) continue;
575
+ if (!entry.isFile()) continue;
536
576
 
537
- const baseName = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : name;
538
- const tool = baseName === "*" ? "*" : baseName;
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;
539
580
 
540
- items.push({
541
- name,
542
- path,
543
- type: hookType,
544
- tool,
545
- level,
546
- _source: createSourceMeta(PROVIDER_ID, path, level),
547
- });
548
- }
581
+ items.push({
582
+ name: entry.name,
583
+ path,
584
+ type: hookType,
585
+ tool,
586
+ level,
587
+ _source: createSourceMeta(PROVIDER_ID, path, level),
588
+ });
549
589
  }
550
590
  }
551
591
 
@@ -561,58 +601,86 @@ registerProvider<Hook>(hookCapability.id, {
561
601
  });
562
602
 
563
603
  // Custom Tools
564
- function loadTools(ctx: LoadContext): LoadResult<CustomTool> {
604
+ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
565
605
  const items: CustomTool[] = [];
566
606
  const warnings: string[] = [];
567
607
 
568
- 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
+
569
623
  const toolsDir = join(dir, "tools");
570
- if (!ctx.fs.isDir(toolsDir)) continue;
571
624
 
572
- // Load tool files (JSON and Markdown declarative tools)
573
- const result = loadFilesFromDir<CustomTool>(ctx, toolsDir, PROVIDER_ID, level, {
574
- extensions: ["json", "md"],
575
- transform: (name, content, path, source) => {
576
- if (name.endsWith(".json")) {
577
- 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);
578
640
  return {
579
- name: data?.name || name.replace(/\.json$/, ""),
641
+ name: (frontmatter.name as string) || name.replace(/\.md$/, ""),
580
642
  path,
581
- description: data?.description,
643
+ description: frontmatter.description as string | undefined,
582
644
  level,
583
645
  _source: source,
584
646
  };
585
- }
586
- const { frontmatter } = parseFrontmatter(content);
587
- return {
588
- name: (frontmatter.name as string) || name.replace(/\.md$/, ""),
589
- path,
590
- description: frontmatter.description as string | undefined,
591
- level,
592
- _source: source,
593
- };
594
- },
595
- });
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) {
596
669
  items.push(...result.items);
597
670
  if (result.warnings) warnings.push(...result.warnings);
671
+ }
598
672
 
599
- // Load TypeScript tools from subdirectories (tools/mytool/index.ts pattern)
600
- for (const name of ctx.fs.readDir(toolsDir)) {
601
- if (name.startsWith(".")) continue;
602
-
603
- const subDir = join(toolsDir, name);
604
- if (!ctx.fs.isDir(subDir)) continue;
605
-
606
- const indexPath = join(subDir, "index.ts");
607
- if (ctx.fs.isFile(indexPath)) {
608
- items.push({
609
- name,
610
- path: indexPath,
611
- description: undefined,
612
- level,
613
- _source: createSourceMeta(PROVIDER_ID, indexPath, level),
614
- });
615
- }
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
+ });
616
684
  }
617
685
  }
618
686
 
@@ -628,13 +696,13 @@ registerProvider<CustomTool>(toolCapability.id, {
628
696
  });
629
697
 
630
698
  // Settings
631
- function loadSettings(ctx: LoadContext): LoadResult<Settings> {
699
+ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
632
700
  const items: Settings[] = [];
633
701
  const warnings: string[] = [];
634
702
 
635
- for (const { dir, level } of getConfigDirs(ctx)) {
703
+ for (const { dir, level } of await getConfigDirs(ctx)) {
636
704
  const settingsPath = join(dir, "settings.json");
637
- const content = ctx.fs.readFile(settingsPath);
705
+ const content = await readFile(settingsPath);
638
706
  if (!content) continue;
639
707
 
640
708
  const data = parseJSON<Record<string, unknown>>(content);
@@ -663,52 +731,59 @@ registerProvider<Settings>(settingsCapability.id, {
663
731
  });
664
732
 
665
733
  // Context Files (AGENTS.md)
666
- function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
734
+ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
667
735
  const items: ContextFile[] = [];
668
736
  const warnings: string[] = [];
669
737
 
670
- // User level: ~/.omp/agent/AGENTS.md or ~/.pi/agent/AGENTS.md
671
- for (const name of USER_DIRS) {
672
- const userPath = join(ctx.home, name, PATHS.userAgent.replace(`${PATHS.userBase}/`, ""), "AGENTS.md");
673
- 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];
674
744
  if (content) {
675
745
  items.push({
676
- path: userPath,
746
+ path: userPaths[i],
677
747
  content,
678
748
  level: "user",
679
- _source: createSourceMeta(PROVIDER_ID, userPath, "user"),
749
+ _source: createSourceMeta(PROVIDER_ID, userPaths[i], "user"),
680
750
  });
681
- break; // First match wins
751
+ break;
682
752
  }
683
753
  }
684
754
 
685
- // Project level: walk up looking for .omp/AGENTS.md or .pi/AGENTS.md
755
+ const ancestors: Array<{ dir: string; depth: number }> = [];
686
756
  let current = ctx.cwd;
687
757
  let depth = 0;
688
758
  while (true) {
689
- for (const name of PROJECT_DIRS) {
690
- const configDir = join(current, name);
691
- if (ctx.fs.isDir(configDir)) {
692
- const projectPath = join(configDir, "AGENTS.md");
693
- const content = ctx.fs.readFile(projectPath);
694
- if (content) {
695
- items.push({
696
- path: projectPath,
697
- content,
698
- level: "project",
699
- depth,
700
- _source: createSourceMeta(PROVIDER_ID, projectPath, "project"),
701
- });
702
- return { items, warnings }; // First config dir wins
703
- }
704
- }
705
- }
759
+ ancestors.push({ dir: current, depth });
706
760
  const parent = dirname(current);
707
761
  if (parent === current) break;
708
762
  current = parent;
709
763
  depth++;
710
764
  }
711
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
+
712
787
  return { items, warnings };
713
788
  }
714
789