@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
@@ -7,11 +7,12 @@
7
7
  * User directory: ~/.codex
8
8
  */
9
9
 
10
- import { join } from "path";
10
+ import { join } from "node:path";
11
11
  import { parse as parseToml } from "smol-toml";
12
12
  import type { ContextFile } from "../capability/context-file";
13
13
  import { contextFileCapability } from "../capability/context-file";
14
14
  import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
15
+ import { readFile } from "../capability/fs";
15
16
  import type { Hook } from "../capability/hook";
16
17
  import { hookCapability } from "../capability/hook";
17
18
  import { registerProvider } from "../capability/index";
@@ -33,6 +34,7 @@ import {
33
34
  discoverExtensionModulePaths,
34
35
  getExtensionNameFromPath,
35
36
  loadFilesFromDir,
37
+ loadSkillsFromDir,
36
38
  parseFrontmatter,
37
39
  SOURCE_PATHS,
38
40
  } from "./helpers";
@@ -41,27 +43,28 @@ const PROVIDER_ID = "codex";
41
43
  const DISPLAY_NAME = "OpenAI Codex";
42
44
  const PRIORITY = 70;
43
45
 
46
+ function getProjectCodexDir(ctx: LoadContext): string {
47
+ return join(ctx.cwd, ".codex");
48
+ }
49
+
44
50
  // =============================================================================
45
51
  // Context Files (AGENTS.md)
46
52
  // =============================================================================
47
53
 
48
- function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
54
+ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
49
55
  const items: ContextFile[] = [];
50
56
  const warnings: string[] = [];
51
57
 
52
58
  // User level only: ~/.codex/AGENTS.md
53
- const userBase = join(ctx.home, SOURCE_PATHS.codex.userBase);
54
- if (ctx.fs.isDir(userBase)) {
55
- const agentsMd = join(userBase, "AGENTS.md");
56
- const agentsContent = ctx.fs.readFile(agentsMd);
57
- if (agentsContent) {
58
- items.push({
59
- path: agentsMd,
60
- content: agentsContent,
61
- level: "user",
62
- _source: createSourceMeta(PROVIDER_ID, agentsMd, "user"),
63
- });
64
- }
59
+ const agentsMd = join(ctx.home, SOURCE_PATHS.codex.userBase, "AGENTS.md");
60
+ const agentsContent = await readFile(agentsMd);
61
+ if (agentsContent) {
62
+ items.push({
63
+ path: agentsMd,
64
+ content: agentsContent,
65
+ level: "user",
66
+ _source: createSourceMeta(PROVIDER_ID, agentsMd, "user"),
67
+ });
65
68
  }
66
69
 
67
70
  return { items, warnings };
@@ -71,13 +74,19 @@ function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
71
74
  // MCP Servers (config.toml)
72
75
  // =============================================================================
73
76
 
74
- function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
75
- const items: MCPServer[] = [];
77
+ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
76
78
  const warnings: string[] = [];
77
79
 
78
- // User level: ~/.codex/config.toml
79
80
  const userConfigPath = join(ctx.home, SOURCE_PATHS.codex.userBase, "config.toml");
80
- const userConfig = loadTomlConfig(ctx, userConfigPath);
81
+ const codexDir = getProjectCodexDir(ctx);
82
+ const projectConfigPath = join(codexDir, "config.toml");
83
+
84
+ const [userConfig, projectConfig] = await Promise.all([
85
+ loadTomlConfig(ctx, userConfigPath),
86
+ loadTomlConfig(ctx, projectConfigPath),
87
+ ]);
88
+
89
+ const items: MCPServer[] = [];
81
90
  if (userConfig) {
82
91
  const servers = extractMCPServersFromToml(userConfig);
83
92
  for (const [name, config] of Object.entries(servers)) {
@@ -88,29 +97,22 @@ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
88
97
  });
89
98
  }
90
99
  }
91
-
92
- // Project level: .codex/config.toml
93
- const codexDir = ctx.fs.walkUp(".codex", { dir: true });
94
- if (codexDir) {
95
- const projectConfigPath = join(codexDir, "config.toml");
96
- const projectConfig = loadTomlConfig(ctx, projectConfigPath);
97
- if (projectConfig) {
98
- const servers = extractMCPServersFromToml(projectConfig);
99
- for (const [name, config] of Object.entries(servers)) {
100
- items.push({
101
- name,
102
- ...config,
103
- _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"),
104
- });
105
- }
100
+ if (projectConfig) {
101
+ const servers = extractMCPServersFromToml(projectConfig);
102
+ for (const [name, config] of Object.entries(servers)) {
103
+ items.push({
104
+ name,
105
+ ...config,
106
+ _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"),
107
+ });
106
108
  }
107
109
  }
108
110
 
109
111
  return { items, warnings };
110
112
  }
111
113
 
112
- function loadTomlConfig(ctx: LoadContext, path: string): Record<string, unknown> | null {
113
- const content = ctx.fs.readFile(path);
114
+ async function loadTomlConfig(_ctx: LoadContext, path: string): Promise<Record<string, unknown> | null> {
115
+ const content = await readFile(path);
114
116
  if (!content) return null;
115
117
 
116
118
  try {
@@ -205,56 +207,26 @@ function extractMCPServersFromToml(toml: Record<string, unknown>): Record<string
205
207
  // Skills (skills/)
206
208
  // =============================================================================
207
209
 
208
- function loadSkills(ctx: LoadContext): LoadResult<Skill> {
209
- const items: Skill[] = [];
210
- const warnings: string[] = [];
211
-
212
- // User level: ~/.codex/skills/
210
+ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
213
211
  const userSkillsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "skills");
214
- const userResult = loadFilesFromDir(ctx, userSkillsDir, PROVIDER_ID, "user", {
215
- extensions: ["md"],
216
- recursive: true,
217
- transform: (name, content, path, source) => {
218
- const { frontmatter, body } = parseFrontmatter(content);
219
- const skillName = frontmatter.name || name.replace(/\.md$/, "");
212
+ const codexDir = getProjectCodexDir(ctx);
213
+ const projectSkillsDir = join(codexDir, "skills");
220
214
 
221
- return {
222
- name: String(skillName),
223
- path,
224
- content: body,
225
- frontmatter,
226
- level: "user" as const,
227
- _source: source,
228
- };
229
- },
230
- });
231
- items.push(...userResult.items);
232
- warnings.push(...(userResult.warnings || []));
233
-
234
- // Project level: .codex/skills/
235
- const codexDir = ctx.fs.walkUp(".codex", { dir: true });
236
- if (codexDir) {
237
- const projectSkillsDir = join(codexDir, "skills");
238
- const projectResult = loadFilesFromDir(ctx, projectSkillsDir, PROVIDER_ID, "project", {
239
- extensions: ["md"],
240
- recursive: true,
241
- transform: (name, content, path, source) => {
242
- const { frontmatter, body } = parseFrontmatter(content);
243
- const skillName = frontmatter.name || name.replace(/\.md$/, "");
244
-
245
- return {
246
- name: String(skillName),
247
- path,
248
- content: body,
249
- frontmatter,
250
- level: "project" as const,
251
- _source: source,
252
- };
253
- },
254
- });
255
- items.push(...projectResult.items);
256
- warnings.push(...(projectResult.warnings || []));
257
- }
215
+ const results = await Promise.all([
216
+ loadSkillsFromDir(ctx, {
217
+ dir: userSkillsDir,
218
+ providerId: PROVIDER_ID,
219
+ level: "user",
220
+ }),
221
+ loadSkillsFromDir(ctx, {
222
+ dir: projectSkillsDir,
223
+ providerId: PROVIDER_ID,
224
+ level: "project",
225
+ }),
226
+ ]);
227
+
228
+ const items = results.flatMap((r) => r.items);
229
+ const warnings = results.flatMap((r) => r.warnings || []);
258
230
 
259
231
  return { items, warnings };
260
232
  }
@@ -263,34 +235,32 @@ function loadSkills(ctx: LoadContext): LoadResult<Skill> {
263
235
  // Extension Modules (extensions/)
264
236
  // =============================================================================
265
237
 
266
- function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
267
- const items: ExtensionModule[] = [];
238
+ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<ExtensionModule>> {
268
239
  const warnings: string[] = [];
269
240
 
270
- // User level: ~/.codex/extensions/
271
241
  const userExtensionsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "extensions");
272
- for (const extPath of discoverExtensionModulePaths(ctx, userExtensionsDir)) {
273
- items.push({
242
+ const codexDir = getProjectCodexDir(ctx);
243
+ const projectExtensionsDir = join(codexDir, "extensions");
244
+
245
+ const [userPaths, projectPaths] = await Promise.all([
246
+ discoverExtensionModulePaths(ctx, userExtensionsDir),
247
+ discoverExtensionModulePaths(ctx, projectExtensionsDir),
248
+ ]);
249
+
250
+ const items: ExtensionModule[] = [
251
+ ...userPaths.map((extPath) => ({
274
252
  name: getExtensionNameFromPath(extPath),
275
253
  path: extPath,
276
- level: "user",
254
+ level: "user" as const,
277
255
  _source: createSourceMeta(PROVIDER_ID, extPath, "user"),
278
- });
279
- }
280
-
281
- // Project level: .codex/extensions/
282
- const codexDir = ctx.fs.walkUp(".codex", { dir: true });
283
- if (codexDir) {
284
- const projectExtensionsDir = join(codexDir, "extensions");
285
- for (const extPath of discoverExtensionModulePaths(ctx, projectExtensionsDir)) {
286
- items.push({
287
- name: getExtensionNameFromPath(extPath),
288
- path: extPath,
289
- level: "project",
290
- _source: createSourceMeta(PROVIDER_ID, extPath, "project"),
291
- });
292
- }
293
- }
256
+ })),
257
+ ...projectPaths.map((extPath) => ({
258
+ name: getExtensionNameFromPath(extPath),
259
+ path: extPath,
260
+ level: "project" as const,
261
+ _source: createSourceMeta(PROVIDER_ID, extPath, "project"),
262
+ })),
263
+ ];
294
264
 
295
265
  return { items, warnings };
296
266
  }
@@ -299,52 +269,38 @@ function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
299
269
  // Slash Commands (commands/)
300
270
  // =============================================================================
301
271
 
302
- function loadSlashCommands(ctx: LoadContext): LoadResult<SlashCommand> {
303
- const items: SlashCommand[] = [];
304
- const warnings: string[] = [];
305
-
306
- // User level: ~/.codex/commands/
272
+ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
307
273
  const userCommandsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "commands");
308
- const userResult = loadFilesFromDir(ctx, userCommandsDir, PROVIDER_ID, "user", {
309
- extensions: ["md"],
310
- transform: (name, content, path, source) => {
274
+ const codexDir = getProjectCodexDir(ctx);
275
+ const projectCommandsDir = join(codexDir, "commands");
276
+
277
+ const transformCommand =
278
+ (level: "user" | "project") =>
279
+ (name: string, content: string, path: string, source: ReturnType<typeof createSourceMeta>) => {
311
280
  const { frontmatter, body } = parseFrontmatter(content);
312
281
  const commandName = frontmatter.name || name.replace(/\.md$/, "");
313
-
314
282
  return {
315
283
  name: String(commandName),
316
284
  path,
317
285
  content: body,
318
- level: "user" as const,
286
+ level,
319
287
  _source: source,
320
288
  };
321
- },
322
- });
323
- items.push(...userResult.items);
324
- warnings.push(...(userResult.warnings || []));
325
-
326
- // Project level: .codex/commands/
327
- const codexDir = ctx.fs.walkUp(".codex", { dir: true });
328
- if (codexDir) {
329
- const projectCommandsDir = join(codexDir, "commands");
330
- const projectResult = loadFilesFromDir(ctx, projectCommandsDir, PROVIDER_ID, "project", {
289
+ };
290
+
291
+ const results = await Promise.all([
292
+ loadFilesFromDir(ctx, userCommandsDir, PROVIDER_ID, "user", {
331
293
  extensions: ["md"],
332
- transform: (name, content, path, source) => {
333
- const { frontmatter, body } = parseFrontmatter(content);
334
- const commandName = frontmatter.name || name.replace(/\.md$/, "");
335
-
336
- return {
337
- name: String(commandName),
338
- path,
339
- content: body,
340
- level: "project" as const,
341
- _source: source,
342
- };
343
- },
344
- });
345
- items.push(...projectResult.items);
346
- warnings.push(...(projectResult.warnings || []));
347
- }
294
+ transform: transformCommand("user"),
295
+ }),
296
+ loadFilesFromDir(ctx, projectCommandsDir, PROVIDER_ID, "project", {
297
+ extensions: ["md"],
298
+ transform: transformCommand("project"),
299
+ }),
300
+ ]);
301
+
302
+ const items = results.flatMap((r) => r.items);
303
+ const warnings = results.flatMap((r) => r.warnings || []);
348
304
 
349
305
  return { items, warnings };
350
306
  }
@@ -353,52 +309,41 @@ function loadSlashCommands(ctx: LoadContext): LoadResult<SlashCommand> {
353
309
  // Prompts (prompts/*.md)
354
310
  // =============================================================================
355
311
 
356
- function loadPrompts(ctx: LoadContext): LoadResult<Prompt> {
357
- const items: Prompt[] = [];
358
- const warnings: string[] = [];
359
-
360
- // User level: ~/.codex/prompts/
312
+ async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
361
313
  const userPromptsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "prompts");
362
- const userResult = loadFilesFromDir(ctx, userPromptsDir, PROVIDER_ID, "user", {
363
- extensions: ["md"],
364
- transform: (name, content, path, source) => {
365
- const { frontmatter, body } = parseFrontmatter(content);
366
- const promptName = frontmatter.name || name.replace(/\.md$/, "");
314
+ const codexDir = getProjectCodexDir(ctx);
315
+ const projectPromptsDir = join(codexDir, "prompts");
316
+
317
+ const transformPrompt = (
318
+ name: string,
319
+ content: string,
320
+ path: string,
321
+ source: ReturnType<typeof createSourceMeta>,
322
+ ) => {
323
+ const { frontmatter, body } = parseFrontmatter(content);
324
+ const promptName = frontmatter.name || name.replace(/\.md$/, "");
325
+ return {
326
+ name: String(promptName),
327
+ path,
328
+ content: body,
329
+ description: frontmatter.description ? String(frontmatter.description) : undefined,
330
+ _source: source,
331
+ };
332
+ };
367
333
 
368
- return {
369
- name: String(promptName),
370
- path,
371
- content: body,
372
- description: frontmatter.description ? String(frontmatter.description) : undefined,
373
- _source: source,
374
- };
375
- },
376
- });
377
- items.push(...userResult.items);
378
- warnings.push(...(userResult.warnings || []));
379
-
380
- // Project level: .codex/prompts/
381
- const codexDir = ctx.fs.walkUp(".codex", { dir: true });
382
- if (codexDir) {
383
- const projectPromptsDir = join(codexDir, "prompts");
384
- const projectResult = loadFilesFromDir(ctx, projectPromptsDir, PROVIDER_ID, "project", {
334
+ const results = await Promise.all([
335
+ loadFilesFromDir(ctx, userPromptsDir, PROVIDER_ID, "user", {
385
336
  extensions: ["md"],
386
- transform: (name, content, path, source) => {
387
- const { frontmatter, body } = parseFrontmatter(content);
388
- const promptName = frontmatter.name || name.replace(/\.md$/, "");
389
-
390
- return {
391
- name: String(promptName),
392
- path,
393
- content: body,
394
- description: frontmatter.description ? String(frontmatter.description) : undefined,
395
- _source: source,
396
- };
397
- },
398
- });
399
- items.push(...projectResult.items);
400
- warnings.push(...(projectResult.warnings || []));
401
- }
337
+ transform: transformPrompt,
338
+ }),
339
+ loadFilesFromDir(ctx, projectPromptsDir, PROVIDER_ID, "project", {
340
+ extensions: ["md"],
341
+ transform: transformPrompt,
342
+ }),
343
+ ]);
344
+
345
+ const items = results.flatMap((r) => r.items);
346
+ const warnings = results.flatMap((r) => r.warnings || []);
402
347
 
403
348
  return { items, warnings };
404
349
  }
@@ -407,59 +352,41 @@ function loadPrompts(ctx: LoadContext): LoadResult<Prompt> {
407
352
  // Hooks (hooks/)
408
353
  // =============================================================================
409
354
 
410
- function loadHooks(ctx: LoadContext): LoadResult<Hook> {
411
- const items: Hook[] = [];
412
- const warnings: string[] = [];
413
-
414
- // User level: ~/.codex/hooks/
355
+ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
415
356
  const userHooksDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "hooks");
416
- const userResult = loadFilesFromDir(ctx, userHooksDir, PROVIDER_ID, "user", {
417
- extensions: ["ts", "js"],
418
- transform: (name, _content, path, source) => {
419
- // Extract hook type and tool from filename (e.g., pre-bash.ts -> type: pre, tool: bash)
357
+ const codexDir = getProjectCodexDir(ctx);
358
+ const projectHooksDir = join(codexDir, "hooks");
359
+
360
+ const transformHook =
361
+ (level: "user" | "project") =>
362
+ (name: string, _content: string, path: string, source: ReturnType<typeof createSourceMeta>) => {
420
363
  const baseName = name.replace(/\.(ts|js)$/, "");
421
364
  const match = baseName.match(/^(pre|post)-(.+)$/);
422
365
  const hookType = (match?.[1] as "pre" | "post") || "pre";
423
366
  const toolName = match?.[2] || baseName;
424
-
425
367
  return {
426
368
  name,
427
369
  path,
428
370
  type: hookType,
429
371
  tool: toolName,
430
- level: "user" as const,
372
+ level,
431
373
  _source: source,
432
374
  };
433
- },
434
- });
435
- items.push(...userResult.items);
436
- warnings.push(...(userResult.warnings || []));
437
-
438
- // Project level: .codex/hooks/
439
- const codexDir = ctx.fs.walkUp(".codex", { dir: true });
440
- if (codexDir) {
441
- const projectHooksDir = join(codexDir, "hooks");
442
- const projectResult = loadFilesFromDir(ctx, projectHooksDir, PROVIDER_ID, "project", {
375
+ };
376
+
377
+ const results = await Promise.all([
378
+ loadFilesFromDir(ctx, userHooksDir, PROVIDER_ID, "user", {
443
379
  extensions: ["ts", "js"],
444
- transform: (name, _content, path, source) => {
445
- const baseName = name.replace(/\.(ts|js)$/, "");
446
- const match = baseName.match(/^(pre|post)-(.+)$/);
447
- const hookType = (match?.[1] as "pre" | "post") || "pre";
448
- const toolName = match?.[2] || baseName;
449
-
450
- return {
451
- name,
452
- path,
453
- type: hookType,
454
- tool: toolName,
455
- level: "project" as const,
456
- _source: source,
457
- };
458
- },
459
- });
460
- items.push(...projectResult.items);
461
- warnings.push(...(projectResult.warnings || []));
462
- }
380
+ transform: transformHook("user"),
381
+ }),
382
+ loadFilesFromDir(ctx, projectHooksDir, PROVIDER_ID, "project", {
383
+ extensions: ["ts", "js"],
384
+ transform: transformHook("project"),
385
+ }),
386
+ ]);
387
+
388
+ const items = results.flatMap((r) => r.items);
389
+ const warnings = results.flatMap((r) => r.warnings || []);
463
390
 
464
391
  return { items, warnings };
465
392
  }
@@ -468,46 +395,36 @@ function loadHooks(ctx: LoadContext): LoadResult<Hook> {
468
395
  // Tools (tools/)
469
396
  // =============================================================================
470
397
 
471
- function loadTools(ctx: LoadContext): LoadResult<CustomTool> {
472
- const items: CustomTool[] = [];
473
- const warnings: string[] = [];
474
-
475
- // User level: ~/.codex/tools/
398
+ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
476
399
  const userToolsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "tools");
477
- const userResult = loadFilesFromDir(ctx, userToolsDir, PROVIDER_ID, "user", {
478
- extensions: ["ts", "js"],
479
- transform: (name, _content, path, source) => {
400
+ const codexDir = getProjectCodexDir(ctx);
401
+ const projectToolsDir = join(codexDir, "tools");
402
+
403
+ const transformTool =
404
+ (level: "user" | "project") =>
405
+ (name: string, _content: string, path: string, source: ReturnType<typeof createSourceMeta>) => {
480
406
  const toolName = name.replace(/\.(ts|js)$/, "");
481
407
  return {
482
408
  name: toolName,
483
409
  path,
484
- level: "user" as const,
410
+ level,
485
411
  _source: source,
486
412
  } as CustomTool;
487
- },
488
- });
489
- items.push(...userResult.items);
490
- warnings.push(...(userResult.warnings || []));
491
-
492
- // Project level: .codex/tools/
493
- const codexDir = ctx.fs.walkUp(".codex", { dir: true });
494
- if (codexDir) {
495
- const projectToolsDir = join(codexDir, "tools");
496
- const projectResult = loadFilesFromDir(ctx, projectToolsDir, PROVIDER_ID, "project", {
413
+ };
414
+
415
+ const results = await Promise.all([
416
+ loadFilesFromDir(ctx, userToolsDir, PROVIDER_ID, "user", {
497
417
  extensions: ["ts", "js"],
498
- transform: (name, _content, path, source) => {
499
- const toolName = name.replace(/\.(ts|js)$/, "");
500
- return {
501
- name: toolName,
502
- path,
503
- level: "project" as const,
504
- _source: source,
505
- } as CustomTool;
506
- },
507
- });
508
- items.push(...projectResult.items);
509
- warnings.push(...(projectResult.warnings || []));
510
- }
418
+ transform: transformTool("user"),
419
+ }),
420
+ loadFilesFromDir(ctx, projectToolsDir, PROVIDER_ID, "project", {
421
+ extensions: ["ts", "js"],
422
+ transform: transformTool("project"),
423
+ }),
424
+ ]);
425
+
426
+ const items = results.flatMap((r) => r.items);
427
+ const warnings = results.flatMap((r) => r.warnings || []);
511
428
 
512
429
  return { items, warnings };
513
430
  }
@@ -516,31 +433,30 @@ function loadTools(ctx: LoadContext): LoadResult<CustomTool> {
516
433
  // Settings (config.toml)
517
434
  // =============================================================================
518
435
 
519
- function loadSettings(ctx: LoadContext): LoadResult<Settings> {
520
- const items: Settings[] = [];
436
+ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
521
437
  const warnings: string[] = [];
522
438
 
523
- // User level: ~/.codex/config.toml
524
439
  const userConfigPath = join(ctx.home, SOURCE_PATHS.codex.userBase, "config.toml");
525
- const userConfig = loadTomlConfig(ctx, userConfigPath);
440
+ const codexDir = getProjectCodexDir(ctx);
441
+ const projectConfigPath = join(codexDir, "config.toml");
442
+
443
+ const [userConfig, projectConfig] = await Promise.all([
444
+ loadTomlConfig(ctx, userConfigPath),
445
+ loadTomlConfig(ctx, projectConfigPath),
446
+ ]);
447
+
448
+ const items: Settings[] = [];
526
449
  if (userConfig) {
527
450
  items.push({
528
451
  ...userConfig,
529
452
  _source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"),
530
453
  } as Settings);
531
454
  }
532
-
533
- // Project level: .codex/config.toml
534
- const codexDir = ctx.fs.walkUp(".codex", { dir: true });
535
- if (codexDir) {
536
- const projectConfigPath = join(codexDir, "config.toml");
537
- const projectConfig = loadTomlConfig(ctx, projectConfigPath);
538
- if (projectConfig) {
539
- items.push({
540
- ...projectConfig,
541
- _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"),
542
- } as Settings);
543
- }
455
+ if (projectConfig) {
456
+ items.push({
457
+ ...projectConfig,
458
+ _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"),
459
+ } as Settings);
544
460
  }
545
461
 
546
462
  return { items, warnings };