@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,9 +5,10 @@
5
5
  * Priority: 80 (tool-specific, below builtin but above shared standards)
6
6
  */
7
7
 
8
- import { dirname, join, sep } from "path";
8
+ import { join, sep } from "node:path";
9
9
  import { type ContextFile, contextFileCapability } from "../capability/context-file";
10
10
  import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
11
+ import { readFile } from "../capability/fs";
11
12
  import { type Hook, hookCapability } from "../capability/hook";
12
13
  import { registerProvider } from "../capability/index";
13
14
  import { type MCPServer, mcpCapability } from "../capability/mcp";
@@ -24,7 +25,7 @@ import {
24
25
  expandEnvVarsDeep,
25
26
  getExtensionNameFromPath,
26
27
  loadFilesFromDir,
27
- parseFrontmatter,
28
+ loadSkillsFromDir,
28
29
  parseJSON,
29
30
  } from "./helpers";
30
31
 
@@ -41,45 +42,49 @@ function getUserClaude(ctx: LoadContext): string {
41
42
  }
42
43
 
43
44
  /**
44
- * Get project-level .claude path (walks up from cwd).
45
+ * Get project-level .claude path (cwd only).
45
46
  */
46
- function getProjectClaude(ctx: LoadContext): string | null {
47
- return ctx.fs.walkUp(CONFIG_DIR, { dir: true });
47
+ function getProjectClaude(ctx: LoadContext): string {
48
+ return join(ctx.cwd, CONFIG_DIR);
48
49
  }
49
50
 
50
51
  // =============================================================================
51
52
  // MCP Servers
52
53
  // =============================================================================
53
54
 
54
- function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
55
+ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
55
56
  const items: MCPServer[] = [];
56
57
  const warnings: string[] = [];
57
58
 
58
- // User-level: ~/.claude.json or ~/.claude/mcp.json
59
59
  const userBase = getUserClaude(ctx);
60
60
  const userClaudeJson = join(ctx.home, ".claude.json");
61
61
  const userMcpJson = join(userBase, "mcp.json");
62
62
 
63
- for (const [path, level] of [
64
- [userClaudeJson, "user"],
65
- [userMcpJson, "user"],
66
- ] as const) {
67
- if (!ctx.fs.isFile(path)) continue;
63
+ const projectBase = join(ctx.cwd, CONFIG_DIR);
64
+ const projectMcpJson = join(projectBase, ".mcp.json");
65
+ const projectMcpJsonAlt = join(projectBase, "mcp.json");
68
66
 
69
- const content = ctx.fs.readFile(path);
70
- if (!content) {
71
- warnings.push(`Failed to read ${path}`);
72
- continue;
73
- }
67
+ const userPaths = [
68
+ { path: userClaudeJson, level: "user" as const },
69
+ { path: userMcpJson, level: "user" as const },
70
+ ];
71
+ const projectPaths = [
72
+ { path: projectMcpJson, level: "project" as const },
73
+ { path: projectMcpJsonAlt, level: "project" as const },
74
+ ];
75
+
76
+ const allPaths = [...userPaths, ...projectPaths];
77
+ const contents = await Promise.all(allPaths.map(({ path }) => readFile(path)));
74
78
 
79
+ const parseMcpServers = (content: string | null, path: string, level: "user" | "project"): MCPServer[] => {
80
+ if (!content) return [];
75
81
  const json = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
76
- if (!json?.mcpServers) continue;
82
+ if (!json?.mcpServers) return [];
77
83
 
78
84
  const mcpServers = expandEnvVarsDeep(json.mcpServers);
79
-
80
- for (const [name, config] of Object.entries(mcpServers)) {
85
+ return Object.entries(mcpServers).map(([name, config]) => {
81
86
  const serverConfig = config as Record<string, unknown>;
82
- items.push({
87
+ return {
83
88
  name,
84
89
  command: serverConfig.command as string | undefined,
85
90
  args: serverConfig.args as string[] | undefined,
@@ -88,45 +93,24 @@ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
88
93
  headers: serverConfig.headers as Record<string, string> | undefined,
89
94
  transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
90
95
  _source: createSourceMeta(PROVIDER_ID, path, level),
91
- });
96
+ };
97
+ });
98
+ };
99
+
100
+ for (let i = 0; i < userPaths.length; i++) {
101
+ const servers = parseMcpServers(contents[i], userPaths[i].path, userPaths[i].level);
102
+ if (servers.length > 0) {
103
+ items.push(...servers);
104
+ break;
92
105
  }
93
- break; // First existing file wins
94
106
  }
95
107
 
96
- // Project-level: <project>/.mcp.json or <project>/mcp.json
97
- const projectBase = getProjectClaude(ctx);
98
- if (projectBase) {
99
- const projectMcpJson = join(projectBase, ".mcp.json");
100
- const projectMcpJsonAlt = join(projectBase, "mcp.json");
101
-
102
- for (const path of [projectMcpJson, projectMcpJsonAlt]) {
103
- if (!ctx.fs.isFile(path)) continue;
104
-
105
- const content = ctx.fs.readFile(path);
106
- if (!content) {
107
- warnings.push(`Failed to read ${path}`);
108
- continue;
109
- }
110
-
111
- const json = parseJSON<{ mcpServers?: Record<string, unknown> }>(content);
112
- if (!json?.mcpServers) continue;
113
-
114
- const mcpServers = expandEnvVarsDeep(json.mcpServers);
115
-
116
- for (const [name, config] of Object.entries(mcpServers)) {
117
- const serverConfig = config as Record<string, unknown>;
118
- items.push({
119
- name,
120
- command: serverConfig.command as string | undefined,
121
- args: serverConfig.args as string[] | undefined,
122
- env: serverConfig.env as Record<string, string> | undefined,
123
- url: serverConfig.url as string | undefined,
124
- headers: serverConfig.headers as Record<string, string> | undefined,
125
- transport: serverConfig.type as "stdio" | "sse" | "http" | undefined,
126
- _source: createSourceMeta(PROVIDER_ID, path, "project"),
127
- });
128
- }
129
- break; // First existing file wins
108
+ const projectOffset = userPaths.length;
109
+ for (let i = 0; i < projectPaths.length; i++) {
110
+ const servers = parseMcpServers(contents[projectOffset + i], projectPaths[i].path, projectPaths[i].level);
111
+ if (servers.length > 0) {
112
+ items.push(...servers);
113
+ break;
130
114
  }
131
115
  }
132
116
 
@@ -137,74 +121,35 @@ function loadMCPServers(ctx: LoadContext): LoadResult<MCPServer> {
137
121
  // Context Files (CLAUDE.md)
138
122
  // =============================================================================
139
123
 
140
- function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
124
+ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
141
125
  const items: ContextFile[] = [];
142
126
  const warnings: string[] = [];
143
127
 
144
- // User-level: ~/.claude/CLAUDE.md
145
128
  const userBase = getUserClaude(ctx);
146
129
  const userClaudeMd = join(userBase, "CLAUDE.md");
147
130
 
148
- if (ctx.fs.isFile(userClaudeMd)) {
149
- const content = ctx.fs.readFile(userClaudeMd);
150
- if (content !== null) {
151
- items.push({
152
- path: userClaudeMd,
153
- content,
154
- level: "user",
155
- _source: createSourceMeta(PROVIDER_ID, userClaudeMd, "user"),
156
- });
157
- } else {
158
- warnings.push(`Failed to read ${userClaudeMd}`);
159
- }
131
+ const userContent = await readFile(userClaudeMd);
132
+ if (userContent !== null) {
133
+ items.push({
134
+ path: userClaudeMd,
135
+ content: userContent,
136
+ level: "user",
137
+ _source: createSourceMeta(PROVIDER_ID, userClaudeMd, "user"),
138
+ });
160
139
  }
161
140
 
162
- // Project-level: walk up looking for .claude/CLAUDE.md
163
141
  const projectBase = getProjectClaude(ctx);
164
- if (projectBase) {
165
- const projectClaudeMd = join(projectBase, "CLAUDE.md");
166
-
167
- if (ctx.fs.isFile(projectClaudeMd)) {
168
- const content = ctx.fs.readFile(projectClaudeMd);
169
- if (content !== null) {
170
- // Calculate depth (distance from cwd)
171
- const depth = calculateDepth(ctx.cwd, projectBase, sep);
172
-
173
- items.push({
174
- path: projectClaudeMd,
175
- content,
176
- level: "project",
177
- depth,
178
- _source: createSourceMeta(PROVIDER_ID, projectClaudeMd, "project"),
179
- });
180
- } else {
181
- warnings.push(`Failed to read ${projectClaudeMd}`);
182
- }
183
- }
184
- }
185
-
186
- // Also check for CLAUDE.md in project root (without .claude directory)
187
- const rootClaudeMd = ctx.fs.walkUp("CLAUDE.md", { file: true });
188
- if (rootClaudeMd) {
189
- const content = ctx.fs.readFile(rootClaudeMd);
190
- if (content !== null) {
191
- // Only add if not already added from .claude/CLAUDE.md
192
- const alreadyAdded = items.some((item) => item.path === rootClaudeMd);
193
- if (!alreadyAdded) {
194
- const fileDir = dirname(rootClaudeMd);
195
- const depth = calculateDepth(ctx.cwd, fileDir, sep);
196
-
197
- items.push({
198
- path: rootClaudeMd,
199
- content,
200
- level: "project",
201
- depth,
202
- _source: createSourceMeta(PROVIDER_ID, rootClaudeMd, "project"),
203
- });
204
- }
205
- } else {
206
- warnings.push(`Failed to read ${rootClaudeMd}`);
207
- }
142
+ const projectClaudeMd = join(projectBase, "CLAUDE.md");
143
+ const projectContent = await readFile(projectClaudeMd);
144
+ if (projectContent !== null) {
145
+ const depth = calculateDepth(ctx.cwd, projectBase, sep);
146
+ items.push({
147
+ path: projectClaudeMd,
148
+ content: projectContent,
149
+ level: "project",
150
+ depth,
151
+ _source: createSourceMeta(PROVIDER_ID, projectClaudeMd, "project"),
152
+ });
208
153
  }
209
154
 
210
155
  return { items, warnings };
@@ -214,115 +159,52 @@ function loadContextFiles(ctx: LoadContext): LoadResult<ContextFile> {
214
159
  // Skills
215
160
  // =============================================================================
216
161
 
217
- function loadSkills(ctx: LoadContext): LoadResult<Skill> {
218
- const items: Skill[] = [];
219
- const warnings: string[] = [];
220
-
221
- // User-level: ~/.claude/skills/*/SKILL.md
222
- const userBase = getUserClaude(ctx);
223
- const userSkillsDir = join(userBase, "skills");
224
-
225
- if (ctx.fs.isDir(userSkillsDir)) {
226
- const skillDirs = ctx.fs.readDir(userSkillsDir);
227
-
228
- for (const dirName of skillDirs) {
229
- if (dirName.startsWith(".")) continue;
230
-
231
- const skillDir = join(userSkillsDir, dirName);
232
- if (!ctx.fs.isDir(skillDir)) continue;
233
-
234
- const skillFile = join(skillDir, "SKILL.md");
235
- if (!ctx.fs.isFile(skillFile)) continue;
236
-
237
- const content = ctx.fs.readFile(skillFile);
238
- if (!content) {
239
- warnings.push(`Failed to read ${skillFile}`);
240
- continue;
241
- }
242
-
243
- const { frontmatter, body } = parseFrontmatter(content);
244
- const name = (frontmatter.name as string) || dirName;
245
-
246
- items.push({
247
- name,
248
- path: skillFile,
249
- content: body,
250
- frontmatter,
251
- level: "user",
252
- _source: createSourceMeta(PROVIDER_ID, skillFile, "user"),
253
- });
254
- }
255
- }
162
+ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
163
+ const userSkillsDir = join(getUserClaude(ctx), "skills");
164
+ const projectSkillsDir = join(getProjectClaude(ctx), "skills");
256
165
 
257
- // Project-level: <project>/.claude/skills/*/SKILL.md
258
- const projectBase = getProjectClaude(ctx);
259
- if (projectBase) {
260
- const projectSkillsDir = join(projectBase, "skills");
261
-
262
- if (ctx.fs.isDir(projectSkillsDir)) {
263
- const skillDirs = ctx.fs.readDir(projectSkillsDir);
264
-
265
- for (const dirName of skillDirs) {
266
- if (dirName.startsWith(".")) continue;
267
-
268
- const skillDir = join(projectSkillsDir, dirName);
269
- if (!ctx.fs.isDir(skillDir)) continue;
270
-
271
- const skillFile = join(skillDir, "SKILL.md");
272
- if (!ctx.fs.isFile(skillFile)) continue;
273
-
274
- const content = ctx.fs.readFile(skillFile);
275
- if (!content) {
276
- warnings.push(`Failed to read ${skillFile}`);
277
- continue;
278
- }
279
-
280
- const { frontmatter, body } = parseFrontmatter(content);
281
- const name = (frontmatter.name as string) || dirName;
282
-
283
- items.push({
284
- name,
285
- path: skillFile,
286
- content: body,
287
- frontmatter,
288
- level: "project",
289
- _source: createSourceMeta(PROVIDER_ID, skillFile, "project"),
290
- });
291
- }
292
- }
293
- }
166
+ const results = await Promise.all([
167
+ loadSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user" }),
168
+ loadSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project" }),
169
+ ]);
294
170
 
295
- return { items, warnings };
171
+ return {
172
+ items: results.flatMap((r) => r.items),
173
+ warnings: results.flatMap((r) => r.warnings ?? []),
174
+ };
296
175
  }
297
176
 
298
177
  // =============================================================================
299
178
  // Extension Modules
300
179
  // =============================================================================
301
180
 
302
- function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
181
+ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<ExtensionModule>> {
303
182
  const items: ExtensionModule[] = [];
304
183
  const warnings: string[] = [];
305
184
 
306
185
  const userBase = getUserClaude(ctx);
307
186
  const userExtensionsDir = join(userBase, "extensions");
308
- for (const extPath of discoverExtensionModulePaths(ctx, userExtensionsDir)) {
309
- items.push({
310
- name: getExtensionNameFromPath(extPath),
311
- path: extPath,
312
- level: "user",
313
- _source: createSourceMeta(PROVIDER_ID, extPath, "user"),
314
- });
315
- }
316
-
317
- const projectBase = getProjectClaude(ctx);
318
- if (projectBase) {
319
- const projectExtensionsDir = join(projectBase, "extensions");
320
- for (const extPath of discoverExtensionModulePaths(ctx, projectExtensionsDir)) {
187
+ const projectExtensionsDir = join(ctx.cwd, CONFIG_DIR, "extensions");
188
+
189
+ const dirsToDiscover: { dir: string; level: "user" | "project" }[] = [
190
+ { dir: userExtensionsDir, level: "user" },
191
+ { dir: projectExtensionsDir, level: "project" },
192
+ ];
193
+
194
+ const pathsByLevel = await Promise.all(
195
+ dirsToDiscover.map(async ({ dir, level }) => {
196
+ const paths = await discoverExtensionModulePaths(ctx, dir);
197
+ return paths.map((extPath) => ({ extPath, level }));
198
+ }),
199
+ );
200
+
201
+ for (const extensions of pathsByLevel) {
202
+ for (const { extPath, level } of extensions) {
321
203
  items.push({
322
204
  name: getExtensionNameFromPath(extPath),
323
205
  path: extPath,
324
- level: "project",
325
- _source: createSourceMeta(PROVIDER_ID, extPath, "project"),
206
+ level,
207
+ _source: createSourceMeta(PROVIDER_ID, extPath, level),
326
208
  });
327
209
  }
328
210
  }
@@ -334,15 +216,14 @@ function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
334
216
  // Slash Commands
335
217
  // =============================================================================
336
218
 
337
- function loadSlashCommands(ctx: LoadContext): LoadResult<SlashCommand> {
219
+ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
338
220
  const items: SlashCommand[] = [];
339
221
  const warnings: string[] = [];
340
222
 
341
- // User-level: ~/.claude/commands/*.md
342
223
  const userBase = getUserClaude(ctx);
343
224
  const userCommandsDir = join(userBase, "commands");
344
225
 
345
- const userResult = loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
226
+ const userResult = await loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
346
227
  extensions: ["md"],
347
228
  transform: (name, content, path, source) => {
348
229
  const cmdName = name.replace(/\.md$/, "");
@@ -359,28 +240,24 @@ function loadSlashCommands(ctx: LoadContext): LoadResult<SlashCommand> {
359
240
  items.push(...userResult.items);
360
241
  if (userResult.warnings) warnings.push(...userResult.warnings);
361
242
 
362
- // Project-level: <project>/.claude/commands/*.md
363
- const projectBase = getProjectClaude(ctx);
364
- if (projectBase) {
365
- const projectCommandsDir = join(projectBase, "commands");
366
-
367
- const projectResult = loadFilesFromDir<SlashCommand>(ctx, projectCommandsDir, PROVIDER_ID, "project", {
368
- extensions: ["md"],
369
- transform: (name, content, path, source) => {
370
- const cmdName = name.replace(/\.md$/, "");
371
- return {
372
- name: cmdName,
373
- path,
374
- content,
375
- level: "project",
376
- _source: source,
377
- };
378
- },
379
- });
243
+ const projectCommandsDir = join(ctx.cwd, CONFIG_DIR, "commands");
380
244
 
381
- items.push(...projectResult.items);
382
- if (projectResult.warnings) warnings.push(...projectResult.warnings);
383
- }
245
+ const projectResult = await loadFilesFromDir<SlashCommand>(ctx, projectCommandsDir, PROVIDER_ID, "project", {
246
+ extensions: ["md"],
247
+ transform: (name, content, path, source) => {
248
+ const cmdName = name.replace(/\.md$/, "");
249
+ return {
250
+ name: cmdName,
251
+ path,
252
+ content,
253
+ level: "project",
254
+ _source: source,
255
+ };
256
+ },
257
+ });
258
+
259
+ items.push(...projectResult.items);
260
+ if (projectResult.warnings) warnings.push(...projectResult.warnings);
384
261
 
385
262
  return { items, warnings };
386
263
  }
@@ -389,63 +266,46 @@ function loadSlashCommands(ctx: LoadContext): LoadResult<SlashCommand> {
389
266
  // Hooks
390
267
  // =============================================================================
391
268
 
392
- function loadHooks(ctx: LoadContext): LoadResult<Hook> {
269
+ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
393
270
  const items: Hook[] = [];
394
271
  const warnings: string[] = [];
395
272
 
396
- // User-level: ~/.claude/hooks/pre/* and ~/.claude/hooks/post/*
397
273
  const userBase = getUserClaude(ctx);
398
274
  const userHooksDir = join(userBase, "hooks");
275
+ const projectBase = getProjectClaude(ctx);
276
+ const projectHooksDir = join(projectBase, "hooks");
399
277
 
400
- for (const hookType of ["pre", "post"] as const) {
401
- const hooksTypeDir = join(userHooksDir, hookType);
402
-
403
- const result = loadFilesFromDir<Hook>(ctx, hooksTypeDir, PROVIDER_ID, "user", {
404
- transform: (name, _content, path, source) => {
405
- // Extract tool name from filename (e.g., "bash.sh" -> "bash", "*.sh" -> "*")
406
- const toolName = name.replace(/\.(sh|bash|zsh|fish)$/, "");
407
-
408
- return {
409
- name,
410
- path,
411
- type: hookType,
412
- tool: toolName,
413
- level: "user",
414
- _source: source,
415
- };
416
- },
417
- });
278
+ const hookTypes = ["pre", "post"] as const;
418
279
 
419
- items.push(...result.items);
420
- if (result.warnings) warnings.push(...result.warnings);
280
+ const loadTasks: { dir: string; hookType: "pre" | "post"; level: "user" | "project" }[] = [];
281
+ for (const hookType of hookTypes) {
282
+ loadTasks.push({ dir: join(userHooksDir, hookType), hookType, level: "user" });
283
+ }
284
+ for (const hookType of hookTypes) {
285
+ loadTasks.push({ dir: join(projectHooksDir, hookType), hookType, level: "project" });
421
286
  }
422
287
 
423
- // Project-level: <project>/.claude/hooks/pre/* and <project>/.claude/hooks/post/*
424
- const projectBase = getProjectClaude(ctx);
425
- if (projectBase) {
426
- const projectHooksDir = join(projectBase, "hooks");
427
-
428
- for (const hookType of ["pre", "post"] as const) {
429
- const hooksTypeDir = join(projectHooksDir, hookType);
430
-
431
- const result = loadFilesFromDir<Hook>(ctx, hooksTypeDir, PROVIDER_ID, "project", {
288
+ const results = await Promise.all(
289
+ loadTasks.map(({ dir, hookType, level }) =>
290
+ loadFilesFromDir<Hook>(ctx, dir, PROVIDER_ID, level, {
432
291
  transform: (name, _content, path, source) => {
433
292
  const toolName = name.replace(/\.(sh|bash|zsh|fish)$/, "");
434
-
435
293
  return {
436
294
  name,
437
295
  path,
438
296
  type: hookType,
439
297
  tool: toolName,
440
- level: "project",
298
+ level,
441
299
  _source: source,
442
300
  };
443
301
  },
444
- });
302
+ }),
303
+ ),
304
+ );
445
305
 
446
- items.push(...result.items);
447
- if (result.warnings) warnings.push(...result.warnings);
448
- }
306
+ for (const result of results) {
307
+ items.push(...result.items);
308
+ if (result.warnings) warnings.push(...result.warnings);
449
309
  }
450
310
 
451
311
  return { items, warnings };
@@ -455,15 +315,14 @@ function loadHooks(ctx: LoadContext): LoadResult<Hook> {
455
315
  // Custom Tools
456
316
  // =============================================================================
457
317
 
458
- function loadTools(ctx: LoadContext): LoadResult<CustomTool> {
318
+ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
459
319
  const items: CustomTool[] = [];
460
320
  const warnings: string[] = [];
461
321
 
462
- // User-level: ~/.claude/tools/*
463
322
  const userBase = getUserClaude(ctx);
464
323
  const userToolsDir = join(userBase, "tools");
465
324
 
466
- const userResult = loadFilesFromDir<CustomTool>(ctx, userToolsDir, PROVIDER_ID, "user", {
325
+ const userResult = await loadFilesFromDir<CustomTool>(ctx, userToolsDir, PROVIDER_ID, "user", {
467
326
  transform: (name, _content, path, source) => {
468
327
  const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, "");
469
328
 
@@ -479,27 +338,24 @@ function loadTools(ctx: LoadContext): LoadResult<CustomTool> {
479
338
  items.push(...userResult.items);
480
339
  if (userResult.warnings) warnings.push(...userResult.warnings);
481
340
 
482
- // Project-level: <project>/.claude/tools/*
483
341
  const projectBase = getProjectClaude(ctx);
484
- if (projectBase) {
485
- const projectToolsDir = join(projectBase, "tools");
486
-
487
- const projectResult = loadFilesFromDir<CustomTool>(ctx, projectToolsDir, PROVIDER_ID, "project", {
488
- transform: (name, _content, path, source) => {
489
- const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, "");
490
-
491
- return {
492
- name: toolName,
493
- path,
494
- level: "project",
495
- _source: source,
496
- };
497
- },
498
- });
342
+ const projectToolsDir = join(projectBase, "tools");
499
343
 
500
- items.push(...projectResult.items);
501
- if (projectResult.warnings) warnings.push(...projectResult.warnings);
502
- }
344
+ const projectResult = await loadFilesFromDir<CustomTool>(ctx, projectToolsDir, PROVIDER_ID, "project", {
345
+ transform: (name, _content, path, source) => {
346
+ const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, "");
347
+
348
+ return {
349
+ name: toolName,
350
+ path,
351
+ level: "project",
352
+ _source: source,
353
+ };
354
+ },
355
+ });
356
+
357
+ items.push(...projectResult.items);
358
+ if (projectResult.warnings) warnings.push(...projectResult.warnings);
503
359
 
504
360
  return { items, warnings };
505
361
  }
@@ -508,26 +364,21 @@ function loadTools(ctx: LoadContext): LoadResult<CustomTool> {
508
364
  // System Prompts
509
365
  // =============================================================================
510
366
 
511
- function loadSystemPrompts(ctx: LoadContext): LoadResult<SystemPrompt> {
367
+ async function loadSystemPrompts(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> {
512
368
  const items: SystemPrompt[] = [];
513
369
  const warnings: string[] = [];
514
370
 
515
- // User-level: ~/.claude/SYSTEM.md
516
371
  const userBase = getUserClaude(ctx);
517
372
  const userSystemMd = join(userBase, "SYSTEM.md");
518
373
 
519
- if (ctx.fs.isFile(userSystemMd)) {
520
- const content = ctx.fs.readFile(userSystemMd);
521
- if (content !== null) {
522
- items.push({
523
- path: userSystemMd,
524
- content,
525
- level: "user",
526
- _source: createSourceMeta(PROVIDER_ID, userSystemMd, "user"),
527
- });
528
- } else {
529
- warnings.push(`Failed to read ${userSystemMd}`);
530
- }
374
+ const content = await readFile(userSystemMd);
375
+ if (content !== null) {
376
+ items.push({
377
+ path: userSystemMd,
378
+ content,
379
+ level: "user",
380
+ _source: createSourceMeta(PROVIDER_ID, userSystemMd, "user"),
381
+ });
531
382
  }
532
383
 
533
384
  return { items, warnings };
@@ -537,55 +388,42 @@ function loadSystemPrompts(ctx: LoadContext): LoadResult<SystemPrompt> {
537
388
  // Settings
538
389
  // =============================================================================
539
390
 
540
- function loadSettings(ctx: LoadContext): LoadResult<Settings> {
391
+ async function loadSettings(ctx: LoadContext): Promise<LoadResult<Settings>> {
541
392
  const items: Settings[] = [];
542
393
  const warnings: string[] = [];
543
394
 
544
- // User-level: ~/.claude/settings.json
545
395
  const userBase = getUserClaude(ctx);
546
396
  const userSettingsJson = join(userBase, "settings.json");
547
397
 
548
- if (ctx.fs.isFile(userSettingsJson)) {
549
- const content = ctx.fs.readFile(userSettingsJson);
550
- if (content) {
551
- const data = parseJSON<Record<string, unknown>>(content);
552
- if (data) {
553
- items.push({
554
- path: userSettingsJson,
555
- data,
556
- level: "user",
557
- _source: createSourceMeta(PROVIDER_ID, userSettingsJson, "user"),
558
- });
559
- } else {
560
- warnings.push(`Failed to parse JSON in ${userSettingsJson}`);
561
- }
398
+ const userContent = await readFile(userSettingsJson);
399
+ if (userContent) {
400
+ const data = parseJSON<Record<string, unknown>>(userContent);
401
+ if (data) {
402
+ items.push({
403
+ path: userSettingsJson,
404
+ data,
405
+ level: "user",
406
+ _source: createSourceMeta(PROVIDER_ID, userSettingsJson, "user"),
407
+ });
562
408
  } else {
563
- warnings.push(`Failed to read ${userSettingsJson}`);
409
+ warnings.push(`Failed to parse JSON in ${userSettingsJson}`);
564
410
  }
565
411
  }
566
412
 
567
- // Project-level: <project>/.claude/settings.json
568
413
  const projectBase = getProjectClaude(ctx);
569
- if (projectBase) {
570
- const projectSettingsJson = join(projectBase, "settings.json");
571
-
572
- if (ctx.fs.isFile(projectSettingsJson)) {
573
- const content = ctx.fs.readFile(projectSettingsJson);
574
- if (content) {
575
- const data = parseJSON<Record<string, unknown>>(content);
576
- if (data) {
577
- items.push({
578
- path: projectSettingsJson,
579
- data,
580
- level: "project",
581
- _source: createSourceMeta(PROVIDER_ID, projectSettingsJson, "project"),
582
- });
583
- } else {
584
- warnings.push(`Failed to parse JSON in ${projectSettingsJson}`);
585
- }
586
- } else {
587
- warnings.push(`Failed to read ${projectSettingsJson}`);
588
- }
414
+ const projectSettingsJson = join(projectBase, "settings.json");
415
+ const projectContent = await readFile(projectSettingsJson);
416
+ if (projectContent) {
417
+ const data = parseJSON<Record<string, unknown>>(projectContent);
418
+ if (data) {
419
+ items.push({
420
+ path: projectSettingsJson,
421
+ data,
422
+ level: "project",
423
+ _source: createSourceMeta(PROVIDER_ID, projectSettingsJson, "project"),
424
+ });
425
+ } else {
426
+ warnings.push(`Failed to parse JSON in ${projectSettingsJson}`);
589
427
  }
590
428
  }
591
429
 
@@ -607,7 +445,7 @@ registerProvider<MCPServer>(mcpCapability.id, {
607
445
  registerProvider<ContextFile>(contextFileCapability.id, {
608
446
  id: PROVIDER_ID,
609
447
  displayName: DISPLAY_NAME,
610
- description: "Load CLAUDE.md files from .claude/ directories and project root",
448
+ description: "Load CLAUDE.md files from .claude/ directories",
611
449
  priority: PRIORITY,
612
450
  load: loadContextFiles,
613
451
  });