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