@oh-my-pi/pi-coding-agent 2.3.1337 → 3.1.1337

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 (117) hide show
  1. package/CHANGELOG.md +72 -34
  2. package/README.md +100 -100
  3. package/docs/compaction.md +8 -8
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +8 -8
  6. package/docs/extension-loading.md +58 -58
  7. package/docs/hooks.md +11 -11
  8. package/docs/rpc.md +4 -4
  9. package/docs/sdk.md +14 -14
  10. package/docs/session-tree-plan.md +1 -1
  11. package/docs/session.md +2 -2
  12. package/docs/skills.md +16 -16
  13. package/docs/theme.md +9 -9
  14. package/docs/tui.md +1 -1
  15. package/examples/README.md +1 -1
  16. package/examples/custom-tools/README.md +4 -4
  17. package/examples/custom-tools/subagent/README.md +13 -13
  18. package/examples/custom-tools/subagent/agents.ts +2 -2
  19. package/examples/custom-tools/subagent/index.ts +5 -5
  20. package/examples/hooks/README.md +3 -3
  21. package/examples/hooks/auto-commit-on-exit.ts +1 -1
  22. package/examples/hooks/custom-compaction.ts +1 -1
  23. package/examples/sdk/01-minimal.ts +1 -1
  24. package/examples/sdk/04-skills.ts +1 -1
  25. package/examples/sdk/05-tools.ts +1 -1
  26. package/examples/sdk/08-slash-commands.ts +1 -1
  27. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  28. package/examples/sdk/README.md +2 -2
  29. package/package.json +13 -11
  30. package/src/capability/context-file.ts +40 -0
  31. package/src/capability/extension.ts +48 -0
  32. package/src/capability/hook.ts +40 -0
  33. package/src/capability/index.ts +616 -0
  34. package/src/capability/instruction.ts +37 -0
  35. package/src/capability/mcp.ts +52 -0
  36. package/src/capability/prompt.ts +35 -0
  37. package/src/capability/rule.ts +52 -0
  38. package/src/capability/settings.ts +35 -0
  39. package/src/capability/skill.ts +49 -0
  40. package/src/capability/slash-command.ts +40 -0
  41. package/src/capability/system-prompt.ts +35 -0
  42. package/src/capability/tool.ts +38 -0
  43. package/src/capability/types.ts +166 -0
  44. package/src/cli/args.ts +2 -2
  45. package/src/cli/plugin-cli.ts +24 -19
  46. package/src/cli/update-cli.ts +10 -10
  47. package/src/config.ts +290 -6
  48. package/src/core/auth-storage.ts +32 -9
  49. package/src/core/bash-executor.ts +1 -1
  50. package/src/core/custom-commands/loader.ts +44 -50
  51. package/src/core/custom-tools/index.ts +1 -0
  52. package/src/core/custom-tools/loader.ts +67 -69
  53. package/src/core/custom-tools/types.ts +10 -1
  54. package/src/core/hooks/loader.ts +13 -42
  55. package/src/core/index.ts +0 -1
  56. package/src/core/logger.ts +7 -7
  57. package/src/core/mcp/client.ts +1 -1
  58. package/src/core/mcp/config.ts +94 -146
  59. package/src/core/mcp/index.ts +0 -4
  60. package/src/core/mcp/loader.ts +26 -22
  61. package/src/core/mcp/manager.ts +18 -23
  62. package/src/core/mcp/tool-bridge.ts +9 -1
  63. package/src/core/mcp/types.ts +2 -0
  64. package/src/core/model-registry.ts +25 -8
  65. package/src/core/plugins/installer.ts +1 -1
  66. package/src/core/plugins/loader.ts +17 -11
  67. package/src/core/plugins/manager.ts +2 -2
  68. package/src/core/plugins/paths.ts +12 -7
  69. package/src/core/plugins/types.ts +3 -3
  70. package/src/core/sdk.ts +48 -27
  71. package/src/core/session-manager.ts +4 -4
  72. package/src/core/settings-manager.ts +45 -21
  73. package/src/core/skills.ts +208 -293
  74. package/src/core/slash-commands.ts +34 -165
  75. package/src/core/system-prompt.ts +58 -65
  76. package/src/core/timings.ts +2 -2
  77. package/src/core/tools/lsp/config.ts +38 -17
  78. package/src/core/tools/task/agents.ts +21 -0
  79. package/src/core/tools/task/artifacts.ts +1 -1
  80. package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
  81. package/src/core/tools/task/bundled-agents/task.md +1 -0
  82. package/src/core/tools/task/commands.ts +30 -107
  83. package/src/core/tools/task/discovery.ts +75 -66
  84. package/src/core/tools/task/executor.ts +25 -10
  85. package/src/core/tools/task/index.ts +35 -10
  86. package/src/core/tools/task/model-resolver.ts +27 -25
  87. package/src/core/tools/task/types.ts +6 -2
  88. package/src/core/tools/web-fetch.ts +3 -3
  89. package/src/core/tools/web-search/auth.ts +40 -34
  90. package/src/core/tools/web-search/index.ts +1 -1
  91. package/src/core/tools/web-search/providers/anthropic.ts +1 -1
  92. package/src/discovery/agents-md.ts +75 -0
  93. package/src/discovery/builtin.ts +646 -0
  94. package/src/discovery/claude.ts +623 -0
  95. package/src/discovery/cline.ts +102 -0
  96. package/src/discovery/codex.ts +571 -0
  97. package/src/discovery/cursor.ts +264 -0
  98. package/src/discovery/gemini.ts +368 -0
  99. package/src/discovery/github.ts +120 -0
  100. package/src/discovery/helpers.test.ts +127 -0
  101. package/src/discovery/helpers.ts +249 -0
  102. package/src/discovery/index.ts +84 -0
  103. package/src/discovery/mcp-json.ts +127 -0
  104. package/src/discovery/vscode.ts +99 -0
  105. package/src/discovery/windsurf.ts +216 -0
  106. package/src/main.ts +14 -13
  107. package/src/migrations.ts +24 -3
  108. package/src/modes/interactive/components/hook-editor.ts +1 -1
  109. package/src/modes/interactive/components/plugin-settings.ts +1 -1
  110. package/src/modes/interactive/components/settings-defs.ts +38 -2
  111. package/src/modes/interactive/components/settings-selector.ts +1 -0
  112. package/src/modes/interactive/components/welcome.ts +2 -2
  113. package/src/modes/interactive/interactive-mode.ts +233 -16
  114. package/src/modes/interactive/theme/theme-schema.json +1 -1
  115. package/src/utils/clipboard.ts +1 -1
  116. package/src/utils/shell-snapshot.ts +2 -2
  117. package/src/utils/shell.ts +7 -7
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Cline Provider
3
+ *
4
+ * Loads rules from .clinerules (can be single file or directory with *.md files).
5
+ * Project-only (no user-level config).
6
+ */
7
+
8
+ import { registerProvider } from "../capability/index";
9
+ import type { Rule } from "../capability/rule";
10
+ import { ruleCapability } from "../capability/rule";
11
+ import type { LoadContext, LoadResult } from "../capability/types";
12
+ import { createSourceMeta, loadFilesFromDir, parseFrontmatter } from "./helpers";
13
+
14
+ const PROVIDER_ID = "cline";
15
+ const DISPLAY_NAME = "Cline";
16
+ const PRIORITY = 40;
17
+
18
+ /**
19
+ * Load rules from .clinerules
20
+ */
21
+ function loadRules(ctx: LoadContext): LoadResult<Rule> {
22
+ const items: Rule[] = [];
23
+ const warnings: string[] = [];
24
+
25
+ // Project-level only (Cline uses root-level .clinerules)
26
+ const projectPath = ctx.fs.walkUp(".clinerules");
27
+ if (!projectPath) {
28
+ return { items, warnings };
29
+ }
30
+
31
+ // Check if .clinerules is a directory or file
32
+ if (ctx.fs.isDir(projectPath)) {
33
+ // Directory format: load all *.md files
34
+ const result = loadFilesFromDir(ctx, projectPath, PROVIDER_ID, "project", {
35
+ extensions: ["md"],
36
+ transform: (name, content, path, source) => {
37
+ const { frontmatter, body } = parseFrontmatter(content);
38
+ const ruleName = name.replace(/\.md$/, "");
39
+
40
+ // Parse globs (can be array or single string)
41
+ let globs: string[] | undefined;
42
+ if (Array.isArray(frontmatter.globs)) {
43
+ globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
44
+ } else if (typeof frontmatter.globs === "string") {
45
+ globs = [frontmatter.globs];
46
+ }
47
+
48
+ return {
49
+ name: ruleName,
50
+ path,
51
+ content: body,
52
+ globs,
53
+ alwaysApply: typeof frontmatter.alwaysApply === "boolean" ? frontmatter.alwaysApply : undefined,
54
+ description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
55
+ _source: source,
56
+ };
57
+ },
58
+ });
59
+
60
+ items.push(...result.items);
61
+ if (result.warnings) warnings.push(...result.warnings);
62
+ } else if (ctx.fs.isFile(projectPath)) {
63
+ // Single file format
64
+ const content = ctx.fs.readFile(projectPath);
65
+ if (content === null) {
66
+ warnings.push(`Failed to read .clinerules at ${projectPath}`);
67
+ return { items, warnings };
68
+ }
69
+
70
+ const { frontmatter, body } = parseFrontmatter(content);
71
+ const source = createSourceMeta(PROVIDER_ID, projectPath, "project");
72
+
73
+ // Parse globs (can be array or single string)
74
+ let globs: string[] | undefined;
75
+ if (Array.isArray(frontmatter.globs)) {
76
+ globs = frontmatter.globs.filter((g): g is string => typeof g === "string");
77
+ } else if (typeof frontmatter.globs === "string") {
78
+ globs = [frontmatter.globs];
79
+ }
80
+
81
+ items.push({
82
+ name: "clinerules",
83
+ path: projectPath,
84
+ content: body,
85
+ globs,
86
+ alwaysApply: typeof frontmatter.alwaysApply === "boolean" ? frontmatter.alwaysApply : undefined,
87
+ description: typeof frontmatter.description === "string" ? frontmatter.description : undefined,
88
+ _source: source,
89
+ });
90
+ }
91
+
92
+ return { items, warnings };
93
+ }
94
+
95
+ // Register provider
96
+ registerProvider<Rule>(ruleCapability.id, {
97
+ id: PROVIDER_ID,
98
+ displayName: DISPLAY_NAME,
99
+ description: "Load rules from .clinerules (single file or directory)",
100
+ priority: PRIORITY,
101
+ load: loadRules,
102
+ });
@@ -0,0 +1,571 @@
1
+ /**
2
+ * Codex Discovery Provider
3
+ *
4
+ * Loads configuration from OpenAI Codex format:
5
+ * - System Instructions: AGENTS.md (user-level only at ~/.codex/AGENTS.md)
6
+ *
7
+ * User directory: ~/.codex
8
+ */
9
+
10
+ import { join } from "node:path";
11
+ import { parse as parseToml } from "smol-toml";
12
+ import type { ContextFile } from "../capability/context-file";
13
+ import { contextFileCapability } from "../capability/context-file";
14
+ import type { Hook } from "../capability/hook";
15
+ import { hookCapability } from "../capability/hook";
16
+ import { registerProvider } from "../capability/index";
17
+ import type { MCPServer } from "../capability/mcp";
18
+ import { mcpCapability } from "../capability/mcp";
19
+ import type { Prompt } from "../capability/prompt";
20
+ import { promptCapability } from "../capability/prompt";
21
+ import type { Settings } from "../capability/settings";
22
+ import { settingsCapability } from "../capability/settings";
23
+ import type { Skill } from "../capability/skill";
24
+ import { skillCapability } from "../capability/skill";
25
+ import type { SlashCommand } from "../capability/slash-command";
26
+ import { slashCommandCapability } from "../capability/slash-command";
27
+ import type { CustomTool } from "../capability/tool";
28
+ import { toolCapability } from "../capability/tool";
29
+ import type { LoadContext, LoadResult } from "../capability/types";
30
+ import { createSourceMeta, loadFilesFromDir, parseFrontmatter, SOURCE_PATHS } from "./helpers";
31
+
32
+ const PROVIDER_ID = "codex";
33
+ const DISPLAY_NAME = "OpenAI Codex";
34
+ const PRIORITY = 70;
35
+
36
+ // =============================================================================
37
+ // Context Files (AGENTS.md)
38
+ // =============================================================================
39
+
40
+ function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
41
+ const items: ContextFile[] = [];
42
+ const warnings: string[] = [];
43
+
44
+ // User level only: ~/.codex/AGENTS.md
45
+ const userBase = join(ctx.home, SOURCE_PATHS.codex.userBase);
46
+ if (ctx.fs.isDir(userBase)) {
47
+ const agentsMd = join(userBase, "AGENTS.md");
48
+ const agentsContent = ctx.fs.readFile(agentsMd);
49
+ if (agentsContent) {
50
+ items.push({
51
+ path: agentsMd,
52
+ content: agentsContent,
53
+ level: "user",
54
+ _source: createSourceMeta(PROVIDER_ID, agentsMd, "user"),
55
+ });
56
+ }
57
+ }
58
+
59
+ return { items, warnings };
60
+ }
61
+
62
+ // =============================================================================
63
+ // MCP Servers (config.toml)
64
+ // =============================================================================
65
+
66
+ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
67
+ const items: MCPServer[] = [];
68
+ const warnings: string[] = [];
69
+
70
+ // User level: ~/.codex/config.toml
71
+ const userConfigPath = join(ctx.home, SOURCE_PATHS.codex.userBase, "config.toml");
72
+ const userConfig = loadTomlConfig(ctx, userConfigPath);
73
+ if (userConfig) {
74
+ const servers = extractMCPServersFromToml(userConfig);
75
+ for (const [name, config] of Object.entries(servers)) {
76
+ items.push({
77
+ name,
78
+ ...config,
79
+ _source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"),
80
+ });
81
+ }
82
+ }
83
+
84
+ // Project level: .codex/config.toml
85
+ const codexDir = ctx.fs.walkUp(".codex", { dir: true });
86
+ if (codexDir) {
87
+ const projectConfigPath = join(codexDir, "config.toml");
88
+ const projectConfig = loadTomlConfig(ctx, projectConfigPath);
89
+ if (projectConfig) {
90
+ const servers = extractMCPServersFromToml(projectConfig);
91
+ for (const [name, config] of Object.entries(servers)) {
92
+ items.push({
93
+ name,
94
+ ...config,
95
+ _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"),
96
+ });
97
+ }
98
+ }
99
+ }
100
+
101
+ return { items, warnings };
102
+ }
103
+
104
+ function loadTomlConfig(ctx: LoadContext, path: string): Record<string, unknown> | null {
105
+ const content = ctx.fs.readFile(path);
106
+ if (!content) return null;
107
+
108
+ try {
109
+ return parseToml(content) as Record<string, unknown>;
110
+ } catch (_err) {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /** Codex MCP server config format (from config.toml) */
116
+ interface CodexMCPConfig {
117
+ command?: string;
118
+ args?: string[];
119
+ env?: Record<string, string>;
120
+ env_vars?: string[]; // Environment variable names to forward from parent
121
+ url?: string;
122
+ http_headers?: Record<string, string>;
123
+ env_http_headers?: Record<string, string>; // Header name -> env var name
124
+ bearer_token_env_var?: string;
125
+ cwd?: string;
126
+ startup_timeout_sec?: number;
127
+ tool_timeout_sec?: number;
128
+ enabled_tools?: string[];
129
+ disabled_tools?: string[];
130
+ }
131
+
132
+ function extractMCPServersFromToml(toml: Record<string, unknown>): Record<string, Partial<MCPServer>> {
133
+ // Check for [mcp_servers.*] sections (Codex format)
134
+ if (!toml.mcp_servers || typeof toml.mcp_servers !== "object") {
135
+ return {};
136
+ }
137
+
138
+ const codexServers = toml.mcp_servers as Record<string, CodexMCPConfig>;
139
+ const result: Record<string, Partial<MCPServer>> = {};
140
+
141
+ for (const [name, config] of Object.entries(codexServers)) {
142
+ const server: Partial<MCPServer> = {
143
+ command: config.command,
144
+ args: config.args,
145
+ url: config.url,
146
+ };
147
+
148
+ // Build env by merging explicit env and forwarded env_vars
149
+ const env: Record<string, string> = { ...config.env };
150
+ if (config.env_vars) {
151
+ for (const varName of config.env_vars) {
152
+ const value = process.env[varName];
153
+ if (value !== undefined) {
154
+ env[varName] = value;
155
+ }
156
+ }
157
+ }
158
+ if (Object.keys(env).length > 0) {
159
+ server.env = env;
160
+ }
161
+
162
+ // Build headers from http_headers, env_http_headers, and bearer_token_env_var
163
+ const headers: Record<string, string> = { ...config.http_headers };
164
+ if (config.env_http_headers) {
165
+ for (const [headerName, envVarName] of Object.entries(config.env_http_headers)) {
166
+ const value = process.env[envVarName];
167
+ if (value !== undefined) {
168
+ headers[headerName] = value;
169
+ }
170
+ }
171
+ }
172
+ if (config.bearer_token_env_var) {
173
+ const token = process.env[config.bearer_token_env_var];
174
+ if (token) {
175
+ headers.Authorization = `Bearer ${token}`;
176
+ }
177
+ }
178
+ if (Object.keys(headers).length > 0) {
179
+ server.headers = headers;
180
+ }
181
+
182
+ // Determine transport type (infer from config if not explicit)
183
+ if (config.url) {
184
+ server.transport = "http";
185
+ } else if (config.command) {
186
+ server.transport = "stdio";
187
+ }
188
+ // Note: validation of transport vs endpoint is handled by mcpCapability.validate()
189
+
190
+ result[name] = server;
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ // =============================================================================
197
+ // Skills (skills/)
198
+ // =============================================================================
199
+
200
+ function loadSkills(ctx: LoadContext): LoadResult<Skill> {
201
+ const items: Skill[] = [];
202
+ const warnings: string[] = [];
203
+
204
+ // User level: ~/.codex/skills/
205
+ const userSkillsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "skills");
206
+ const userResult = loadFilesFromDir(ctx, userSkillsDir, PROVIDER_ID, "user", {
207
+ extensions: ["md"],
208
+ recursive: true,
209
+ transform: (name, content, path, source) => {
210
+ const { frontmatter, body } = parseFrontmatter(content);
211
+ const skillName = frontmatter.name || name.replace(/\.md$/, "");
212
+
213
+ return {
214
+ name: String(skillName),
215
+ path,
216
+ content: body,
217
+ frontmatter,
218
+ level: "user" as const,
219
+ _source: source,
220
+ };
221
+ },
222
+ });
223
+ items.push(...userResult.items);
224
+ warnings.push(...(userResult.warnings || []));
225
+
226
+ // Project level: .codex/skills/
227
+ const codexDir = ctx.fs.walkUp(".codex", { dir: true });
228
+ if (codexDir) {
229
+ const projectSkillsDir = join(codexDir, "skills");
230
+ const projectResult = loadFilesFromDir(ctx, projectSkillsDir, PROVIDER_ID, "project", {
231
+ extensions: ["md"],
232
+ recursive: true,
233
+ transform: (name, content, path, source) => {
234
+ const { frontmatter, body } = parseFrontmatter(content);
235
+ const skillName = frontmatter.name || name.replace(/\.md$/, "");
236
+
237
+ return {
238
+ name: String(skillName),
239
+ path,
240
+ content: body,
241
+ frontmatter,
242
+ level: "project" as const,
243
+ _source: source,
244
+ };
245
+ },
246
+ });
247
+ items.push(...projectResult.items);
248
+ warnings.push(...(projectResult.warnings || []));
249
+ }
250
+
251
+ return { items, warnings };
252
+ }
253
+
254
+ // =============================================================================
255
+ // Slash Commands (commands/)
256
+ // =============================================================================
257
+
258
+ function loadSlashCommands(ctx: LoadContext): LoadResult<SlashCommand> {
259
+ const items: SlashCommand[] = [];
260
+ const warnings: string[] = [];
261
+
262
+ // User level: ~/.codex/commands/
263
+ const userCommandsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "commands");
264
+ const userResult = loadFilesFromDir(ctx, userCommandsDir, PROVIDER_ID, "user", {
265
+ extensions: ["md"],
266
+ transform: (name, content, path, source) => {
267
+ const { frontmatter, body } = parseFrontmatter(content);
268
+ const commandName = frontmatter.name || name.replace(/\.md$/, "");
269
+
270
+ return {
271
+ name: String(commandName),
272
+ path,
273
+ content: body,
274
+ level: "user" as const,
275
+ _source: source,
276
+ };
277
+ },
278
+ });
279
+ items.push(...userResult.items);
280
+ warnings.push(...(userResult.warnings || []));
281
+
282
+ // Project level: .codex/commands/
283
+ const codexDir = ctx.fs.walkUp(".codex", { dir: true });
284
+ if (codexDir) {
285
+ const projectCommandsDir = join(codexDir, "commands");
286
+ const projectResult = loadFilesFromDir(ctx, projectCommandsDir, PROVIDER_ID, "project", {
287
+ extensions: ["md"],
288
+ transform: (name, content, path, source) => {
289
+ const { frontmatter, body } = parseFrontmatter(content);
290
+ const commandName = frontmatter.name || name.replace(/\.md$/, "");
291
+
292
+ return {
293
+ name: String(commandName),
294
+ path,
295
+ content: body,
296
+ level: "project" as const,
297
+ _source: source,
298
+ };
299
+ },
300
+ });
301
+ items.push(...projectResult.items);
302
+ warnings.push(...(projectResult.warnings || []));
303
+ }
304
+
305
+ return { items, warnings };
306
+ }
307
+
308
+ // =============================================================================
309
+ // Prompts (prompts/*.md)
310
+ // =============================================================================
311
+
312
+ function loadPrompts(ctx: LoadContext): LoadResult<Prompt> {
313
+ const items: Prompt[] = [];
314
+ const warnings: string[] = [];
315
+
316
+ // User level: ~/.codex/prompts/
317
+ const userPromptsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "prompts");
318
+ const userResult = loadFilesFromDir(ctx, userPromptsDir, PROVIDER_ID, "user", {
319
+ extensions: ["md"],
320
+ transform: (name, content, path, source) => {
321
+ const { frontmatter, body } = parseFrontmatter(content);
322
+ const promptName = frontmatter.name || name.replace(/\.md$/, "");
323
+
324
+ return {
325
+ name: String(promptName),
326
+ path,
327
+ content: body,
328
+ description: frontmatter.description ? String(frontmatter.description) : undefined,
329
+ _source: source,
330
+ };
331
+ },
332
+ });
333
+ items.push(...userResult.items);
334
+ warnings.push(...(userResult.warnings || []));
335
+
336
+ // Project level: .codex/prompts/
337
+ const codexDir = ctx.fs.walkUp(".codex", { dir: true });
338
+ if (codexDir) {
339
+ const projectPromptsDir = join(codexDir, "prompts");
340
+ const projectResult = loadFilesFromDir(ctx, projectPromptsDir, PROVIDER_ID, "project", {
341
+ extensions: ["md"],
342
+ transform: (name, content, path, source) => {
343
+ const { frontmatter, body } = parseFrontmatter(content);
344
+ const promptName = frontmatter.name || name.replace(/\.md$/, "");
345
+
346
+ return {
347
+ name: String(promptName),
348
+ path,
349
+ content: body,
350
+ description: frontmatter.description ? String(frontmatter.description) : undefined,
351
+ _source: source,
352
+ };
353
+ },
354
+ });
355
+ items.push(...projectResult.items);
356
+ warnings.push(...(projectResult.warnings || []));
357
+ }
358
+
359
+ return { items, warnings };
360
+ }
361
+
362
+ // =============================================================================
363
+ // Hooks (hooks/)
364
+ // =============================================================================
365
+
366
+ function loadHooks(ctx: LoadContext): LoadResult<Hook> {
367
+ const items: Hook[] = [];
368
+ const warnings: string[] = [];
369
+
370
+ // User level: ~/.codex/hooks/
371
+ const userHooksDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "hooks");
372
+ const userResult = loadFilesFromDir(ctx, userHooksDir, PROVIDER_ID, "user", {
373
+ extensions: ["ts", "js"],
374
+ transform: (name, _content, path, source) => {
375
+ // Extract hook type and tool from filename (e.g., pre-bash.ts -> type: pre, tool: bash)
376
+ const baseName = name.replace(/\.(ts|js)$/, "");
377
+ const match = baseName.match(/^(pre|post)-(.+)$/);
378
+ const hookType = (match?.[1] as "pre" | "post") || "pre";
379
+ const toolName = match?.[2] || baseName;
380
+
381
+ return {
382
+ name,
383
+ path,
384
+ type: hookType,
385
+ tool: toolName,
386
+ level: "user" as const,
387
+ _source: source,
388
+ };
389
+ },
390
+ });
391
+ items.push(...userResult.items);
392
+ warnings.push(...(userResult.warnings || []));
393
+
394
+ // Project level: .codex/hooks/
395
+ const codexDir = ctx.fs.walkUp(".codex", { dir: true });
396
+ if (codexDir) {
397
+ const projectHooksDir = join(codexDir, "hooks");
398
+ const projectResult = loadFilesFromDir(ctx, projectHooksDir, PROVIDER_ID, "project", {
399
+ extensions: ["ts", "js"],
400
+ transform: (name, _content, path, source) => {
401
+ const baseName = name.replace(/\.(ts|js)$/, "");
402
+ const match = baseName.match(/^(pre|post)-(.+)$/);
403
+ const hookType = (match?.[1] as "pre" | "post") || "pre";
404
+ const toolName = match?.[2] || baseName;
405
+
406
+ return {
407
+ name,
408
+ path,
409
+ type: hookType,
410
+ tool: toolName,
411
+ level: "project" as const,
412
+ _source: source,
413
+ };
414
+ },
415
+ });
416
+ items.push(...projectResult.items);
417
+ warnings.push(...(projectResult.warnings || []));
418
+ }
419
+
420
+ return { items, warnings };
421
+ }
422
+
423
+ // =============================================================================
424
+ // Tools (tools/)
425
+ // =============================================================================
426
+
427
+ function loadTools(ctx: LoadContext): LoadResult<CustomTool> {
428
+ const items: CustomTool[] = [];
429
+ const warnings: string[] = [];
430
+
431
+ // User level: ~/.codex/tools/
432
+ const userToolsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "tools");
433
+ const userResult = loadFilesFromDir(ctx, userToolsDir, PROVIDER_ID, "user", {
434
+ extensions: ["ts", "js"],
435
+ transform: (name, _content, path, source) => {
436
+ const toolName = name.replace(/\.(ts|js)$/, "");
437
+ return {
438
+ name: toolName,
439
+ path,
440
+ level: "user" as const,
441
+ _source: source,
442
+ } as CustomTool;
443
+ },
444
+ });
445
+ items.push(...userResult.items);
446
+ warnings.push(...(userResult.warnings || []));
447
+
448
+ // Project level: .codex/tools/
449
+ const codexDir = ctx.fs.walkUp(".codex", { dir: true });
450
+ if (codexDir) {
451
+ const projectToolsDir = join(codexDir, "tools");
452
+ const projectResult = loadFilesFromDir(ctx, projectToolsDir, PROVIDER_ID, "project", {
453
+ extensions: ["ts", "js"],
454
+ transform: (name, _content, path, source) => {
455
+ const toolName = name.replace(/\.(ts|js)$/, "");
456
+ return {
457
+ name: toolName,
458
+ path,
459
+ level: "project" as const,
460
+ _source: source,
461
+ } as CustomTool;
462
+ },
463
+ });
464
+ items.push(...projectResult.items);
465
+ warnings.push(...(projectResult.warnings || []));
466
+ }
467
+
468
+ return { items, warnings };
469
+ }
470
+
471
+ // =============================================================================
472
+ // Settings (config.toml)
473
+ // =============================================================================
474
+
475
+ function loadSettings(ctx: LoadContext): LoadResult<Settings> {
476
+ const items: Settings[] = [];
477
+ const warnings: string[] = [];
478
+
479
+ // User level: ~/.codex/config.toml
480
+ const userConfigPath = join(ctx.home, SOURCE_PATHS.codex.userBase, "config.toml");
481
+ const userConfig = loadTomlConfig(ctx, userConfigPath);
482
+ if (userConfig) {
483
+ items.push({
484
+ ...userConfig,
485
+ _source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"),
486
+ } as Settings);
487
+ }
488
+
489
+ // Project level: .codex/config.toml
490
+ const codexDir = ctx.fs.walkUp(".codex", { dir: true });
491
+ if (codexDir) {
492
+ const projectConfigPath = join(codexDir, "config.toml");
493
+ const projectConfig = loadTomlConfig(ctx, projectConfigPath);
494
+ if (projectConfig) {
495
+ items.push({
496
+ ...projectConfig,
497
+ _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"),
498
+ } as Settings);
499
+ }
500
+ }
501
+
502
+ return { items, warnings };
503
+ }
504
+
505
+ // =============================================================================
506
+ // Provider Registration (executes on module import)
507
+ // =============================================================================
508
+
509
+ registerProvider<ContextFile>(contextFileCapability.id, {
510
+ id: PROVIDER_ID,
511
+ displayName: DISPLAY_NAME,
512
+ description: "Load context files from ~/.codex/AGENTS.md (user-level only)",
513
+ priority: PRIORITY,
514
+ load: loadContextFiles,
515
+ });
516
+
517
+ registerProvider<MCPServer>(mcpCapability.id, {
518
+ id: PROVIDER_ID,
519
+ displayName: DISPLAY_NAME,
520
+ description: "Load MCP servers from config.toml [mcp_servers.*] sections",
521
+ priority: PRIORITY,
522
+ load: loadMCPServers,
523
+ });
524
+
525
+ registerProvider<Skill>(skillCapability.id, {
526
+ id: PROVIDER_ID,
527
+ displayName: DISPLAY_NAME,
528
+ description: "Load skills from ~/.codex/skills and .codex/skills/",
529
+ priority: PRIORITY,
530
+ load: loadSkills,
531
+ });
532
+
533
+ registerProvider<SlashCommand>(slashCommandCapability.id, {
534
+ id: PROVIDER_ID,
535
+ displayName: DISPLAY_NAME,
536
+ description: "Load slash commands from ~/.codex/commands and .codex/commands/",
537
+ priority: PRIORITY,
538
+ load: loadSlashCommands,
539
+ });
540
+
541
+ registerProvider<Prompt>(promptCapability.id, {
542
+ id: PROVIDER_ID,
543
+ displayName: DISPLAY_NAME,
544
+ description: "Load prompts from ~/.codex/prompts and .codex/prompts/",
545
+ priority: PRIORITY,
546
+ load: loadPrompts,
547
+ });
548
+
549
+ registerProvider<Hook>(hookCapability.id, {
550
+ id: PROVIDER_ID,
551
+ displayName: DISPLAY_NAME,
552
+ description: "Load hooks from ~/.codex/hooks and .codex/hooks/",
553
+ priority: PRIORITY,
554
+ load: loadHooks,
555
+ });
556
+
557
+ registerProvider<CustomTool>(toolCapability.id, {
558
+ id: PROVIDER_ID,
559
+ displayName: DISPLAY_NAME,
560
+ description: "Load custom tools from ~/.codex/tools and .codex/tools/",
561
+ priority: PRIORITY,
562
+ load: loadTools,
563
+ });
564
+
565
+ registerProvider<Settings>(settingsCapability.id, {
566
+ id: PROVIDER_ID,
567
+ displayName: DISPLAY_NAME,
568
+ description: "Load settings from config.toml",
569
+ priority: PRIORITY,
570
+ load: loadSettings,
571
+ });