@oh-my-pi/pi-coding-agent 2.2.1337 → 3.0.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 (116) hide show
  1. package/CHANGELOG.md +64 -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 +16 -12
  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/export-html/index.ts +9 -9
  55. package/src/core/export-html/template.generated.ts +2 -0
  56. package/src/core/hooks/loader.ts +13 -42
  57. package/src/core/index.ts +0 -1
  58. package/src/core/logger.ts +7 -7
  59. package/src/core/mcp/client.ts +1 -1
  60. package/src/core/mcp/config.ts +94 -146
  61. package/src/core/mcp/index.ts +0 -4
  62. package/src/core/mcp/loader.ts +26 -22
  63. package/src/core/mcp/manager.ts +18 -23
  64. package/src/core/mcp/tool-bridge.ts +9 -1
  65. package/src/core/mcp/types.ts +2 -0
  66. package/src/core/model-registry.ts +25 -8
  67. package/src/core/plugins/installer.ts +1 -1
  68. package/src/core/plugins/loader.ts +17 -11
  69. package/src/core/plugins/manager.ts +2 -2
  70. package/src/core/plugins/paths.ts +12 -7
  71. package/src/core/plugins/types.ts +3 -3
  72. package/src/core/sdk.ts +48 -27
  73. package/src/core/session-manager.ts +4 -4
  74. package/src/core/settings-manager.ts +45 -21
  75. package/src/core/skills.ts +222 -293
  76. package/src/core/slash-commands.ts +34 -165
  77. package/src/core/system-prompt.ts +58 -65
  78. package/src/core/timings.ts +2 -2
  79. package/src/core/tools/lsp/config.ts +38 -17
  80. package/src/core/tools/task/artifacts.ts +1 -1
  81. package/src/core/tools/task/commands.ts +30 -107
  82. package/src/core/tools/task/discovery.ts +54 -66
  83. package/src/core/tools/task/executor.ts +9 -9
  84. package/src/core/tools/task/index.ts +10 -10
  85. package/src/core/tools/task/model-resolver.ts +27 -25
  86. package/src/core/tools/task/types.ts +2 -2
  87. package/src/core/tools/web-fetch.ts +3 -3
  88. package/src/core/tools/web-search/auth.ts +40 -34
  89. package/src/core/tools/web-search/index.ts +1 -1
  90. package/src/core/tools/web-search/providers/anthropic.ts +1 -1
  91. package/src/discovery/agents-md.ts +75 -0
  92. package/src/discovery/builtin.ts +646 -0
  93. package/src/discovery/claude.ts +623 -0
  94. package/src/discovery/cline.ts +102 -0
  95. package/src/discovery/codex.ts +571 -0
  96. package/src/discovery/cursor.ts +264 -0
  97. package/src/discovery/gemini.ts +368 -0
  98. package/src/discovery/github.ts +120 -0
  99. package/src/discovery/helpers.test.ts +127 -0
  100. package/src/discovery/helpers.ts +249 -0
  101. package/src/discovery/index.ts +84 -0
  102. package/src/discovery/mcp-json.ts +127 -0
  103. package/src/discovery/vscode.ts +99 -0
  104. package/src/discovery/windsurf.ts +216 -0
  105. package/src/main.ts +14 -13
  106. package/src/migrations.ts +24 -3
  107. package/src/modes/interactive/components/hook-editor.ts +1 -1
  108. package/src/modes/interactive/components/plugin-settings.ts +1 -1
  109. package/src/modes/interactive/components/settings-defs.ts +38 -2
  110. package/src/modes/interactive/components/settings-selector.ts +1 -0
  111. package/src/modes/interactive/components/welcome.ts +2 -2
  112. package/src/modes/interactive/interactive-mode.ts +211 -16
  113. package/src/modes/interactive/theme/theme-schema.json +1 -1
  114. package/src/utils/clipboard.ts +1 -1
  115. package/src/utils/shell-snapshot.ts +2 -2
  116. package/src/utils/shell.ts +7 -7
package/src/config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
 
@@ -9,12 +9,20 @@ import packageJson from "../package.json" with { type: "json" };
9
9
  // App Config (from embedded package.json)
10
10
  // =============================================================================
11
11
 
12
- export const APP_NAME: string = (packageJson as { piConfig?: { name?: string } }).piConfig?.name || "pi";
12
+ export const APP_NAME: string = (packageJson as { ompConfig?: { name?: string } }).ompConfig?.name || "omp";
13
13
  export const CONFIG_DIR_NAME: string =
14
- (packageJson as { piConfig?: { configDir?: string } }).piConfig?.configDir || ".pi";
14
+ (packageJson as { ompConfig?: { configDir?: string } }).ompConfig?.configDir || ".omp";
15
15
  export const VERSION: string = (packageJson as { version: string }).version;
16
16
 
17
- // e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
17
+ const priorityList = [
18
+ { dir: ".omp", globalAgentDir: ".omp/agent" },
19
+ { dir: ".pi", globalAgentDir: ".pi/agent" },
20
+ { dir: ".claude" },
21
+ { dir: ".codex" },
22
+ { dir: ".gemini" },
23
+ ];
24
+
25
+ // e.g., OMP_CODING_AGENT_DIR
18
26
  export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
19
27
 
20
28
  // =============================================================================
@@ -58,10 +66,10 @@ export function getChangelogPath(): string {
58
66
  }
59
67
 
60
68
  // =============================================================================
61
- // User Config Paths (~/.pi/agent/*)
69
+ // User Config Paths (~/.omp/agent/*)
62
70
  // =============================================================================
63
71
 
64
- /** Get the agent config directory (e.g., ~/.pi/agent/) */
72
+ /** Get the agent config directory (e.g., ~/.omp/agent/) */
65
73
  export function getAgentDir(): string {
66
74
  return process.env[ENV_AGENT_DIR] || join(homedir(), CONFIG_DIR_NAME, "agent");
67
75
  }
@@ -105,3 +113,279 @@ export function getSessionsDir(): string {
105
113
  export function getDebugLogPath(): string {
106
114
  return join(getAgentDir(), `${APP_NAME}-debug.log`);
107
115
  }
116
+
117
+ // =============================================================================
118
+ // Multi-Config Directory Helpers
119
+ // =============================================================================
120
+
121
+ /**
122
+ * Config directory bases in priority order (highest first).
123
+ * User-level: ~/.omp/agent, ~/.pi/agent, ~/.claude, ~/.codex, ~/.gemini
124
+ * Project-level: .omp, .pi, .claude, .codex, .gemini
125
+ */
126
+ const USER_CONFIG_BASES = priorityList.map(({ dir, globalAgentDir }) => ({
127
+ base: () => join(homedir(), globalAgentDir ?? dir),
128
+ name: dir,
129
+ }));
130
+
131
+ const PROJECT_CONFIG_BASES = priorityList.map(({ dir }) => ({
132
+ base: dir,
133
+ name: dir,
134
+ }));
135
+
136
+ export interface ConfigDirEntry {
137
+ path: string;
138
+ source: string; // e.g., ".omp", ".pi", ".claude"
139
+ level: "user" | "project";
140
+ }
141
+
142
+ export interface GetConfigDirsOptions {
143
+ /** Include user-level directories (~/.omp/agent/...). Default: true */
144
+ user?: boolean;
145
+ /** Include project-level directories (.omp/...). Default: true */
146
+ project?: boolean;
147
+ /** Current working directory for project paths. Default: process.cwd() */
148
+ cwd?: string;
149
+ /** Only return directories that exist. Default: false */
150
+ existingOnly?: boolean;
151
+ }
152
+
153
+ /**
154
+ * Get all config directories for a subpath, ordered by priority (highest first).
155
+ *
156
+ * @param subpath - Subpath within config dirs (e.g., "commands", "hooks", "agents")
157
+ * @param options - Options for filtering
158
+ * @returns Array of directory entries, highest priority first
159
+ *
160
+ * @example
161
+ * // Get all command directories
162
+ * getConfigDirs("commands")
163
+ * // → [{ path: "~/.omp/agent/commands", source: ".omp", level: "user" }, ...]
164
+ *
165
+ * @example
166
+ * // Get only existing project skill directories
167
+ * getConfigDirs("skills", { user: false, existingOnly: true })
168
+ */
169
+ export function getConfigDirs(subpath: string, options: GetConfigDirsOptions = {}): ConfigDirEntry[] {
170
+ const { user = true, project = true, cwd = process.cwd(), existingOnly = false } = options;
171
+ const results: ConfigDirEntry[] = [];
172
+
173
+ // User-level directories (highest priority)
174
+ if (user) {
175
+ for (const { base, name } of USER_CONFIG_BASES) {
176
+ const path = join(base(), subpath);
177
+ if (!existingOnly || existsSync(path)) {
178
+ results.push({ path, source: name, level: "user" });
179
+ }
180
+ }
181
+ }
182
+
183
+ // Project-level directories
184
+ if (project) {
185
+ for (const { base, name } of PROJECT_CONFIG_BASES) {
186
+ const path = resolve(cwd, base, subpath);
187
+ if (!existingOnly || existsSync(path)) {
188
+ results.push({ path, source: name, level: "project" });
189
+ }
190
+ }
191
+ }
192
+
193
+ return results;
194
+ }
195
+
196
+ /**
197
+ * Get all config directory paths for a subpath (convenience wrapper).
198
+ * Returns just the paths, highest priority first.
199
+ */
200
+ export function getConfigDirPaths(subpath: string, options: GetConfigDirsOptions = {}): string[] {
201
+ return getConfigDirs(subpath, options).map((e) => e.path);
202
+ }
203
+
204
+ export interface ConfigFileResult<T> {
205
+ path: string;
206
+ source: string;
207
+ level: "user" | "project";
208
+ content: T;
209
+ }
210
+
211
+ /**
212
+ * Read the first existing config file from priority-ordered locations.
213
+ *
214
+ * @param subpath - Subpath within config dirs (e.g., "settings.json", "models.json")
215
+ * @param options - Options for filtering (same as getConfigDirs)
216
+ * @returns The parsed content and metadata, or undefined if not found
217
+ *
218
+ * @example
219
+ * const result = readConfigFile<Settings>("settings.json", { project: false });
220
+ * if (result) {
221
+ * console.log(`Loaded from ${result.path}`);
222
+ * console.log(result.content);
223
+ * }
224
+ */
225
+ export function readConfigFile<T = unknown>(
226
+ subpath: string,
227
+ options: GetConfigDirsOptions = {},
228
+ ): ConfigFileResult<T> | undefined {
229
+ const dirs = getConfigDirs("", { ...options, existingOnly: false });
230
+
231
+ for (const { path: base, source, level } of dirs) {
232
+ const filePath = join(base, subpath);
233
+ if (existsSync(filePath)) {
234
+ try {
235
+ const content = readFileSync(filePath, "utf-8");
236
+ return {
237
+ path: filePath,
238
+ source,
239
+ level,
240
+ content: JSON.parse(content) as T,
241
+ };
242
+ } catch {
243
+ // Continue to next file on parse error
244
+ }
245
+ }
246
+ }
247
+
248
+ return undefined;
249
+ }
250
+
251
+ /**
252
+ * Get all existing config files for a subpath (for merging scenarios).
253
+ * Returns in priority order (highest first).
254
+ */
255
+ export function readAllConfigFiles<T = unknown>(
256
+ subpath: string,
257
+ options: GetConfigDirsOptions = {},
258
+ ): ConfigFileResult<T>[] {
259
+ const dirs = getConfigDirs("", { ...options, existingOnly: false });
260
+ const results: ConfigFileResult<T>[] = [];
261
+
262
+ for (const { path: base, source, level } of dirs) {
263
+ const filePath = join(base, subpath);
264
+ if (existsSync(filePath)) {
265
+ try {
266
+ const content = readFileSync(filePath, "utf-8");
267
+ results.push({
268
+ path: filePath,
269
+ source,
270
+ level,
271
+ content: JSON.parse(content) as T,
272
+ });
273
+ } catch {
274
+ // Skip files that fail to parse
275
+ }
276
+ }
277
+ }
278
+
279
+ return results;
280
+ }
281
+
282
+ /**
283
+ * Find the first existing config file (for non-JSON files like SYSTEM.md).
284
+ * Returns just the path, or undefined if not found.
285
+ */
286
+ export function findConfigFile(subpath: string, options: GetConfigDirsOptions = {}): string | undefined {
287
+ const dirs = getConfigDirs("", { ...options, existingOnly: false });
288
+
289
+ for (const { path: base } of dirs) {
290
+ const filePath = join(base, subpath);
291
+ if (existsSync(filePath)) {
292
+ return filePath;
293
+ }
294
+ }
295
+
296
+ return undefined;
297
+ }
298
+
299
+ /**
300
+ * Find the first existing config file with metadata.
301
+ */
302
+ export function findConfigFileWithMeta(
303
+ subpath: string,
304
+ options: GetConfigDirsOptions = {},
305
+ ): Omit<ConfigFileResult<never>, "content"> | undefined {
306
+ const dirs = getConfigDirs("", { ...options, existingOnly: false });
307
+
308
+ for (const { path: base, source, level } of dirs) {
309
+ const filePath = join(base, subpath);
310
+ if (existsSync(filePath)) {
311
+ return { path: filePath, source, level };
312
+ }
313
+ }
314
+
315
+ return undefined;
316
+ }
317
+
318
+ // =============================================================================
319
+ // Walk-Up Config Discovery (for monorepo scenarios)
320
+ // =============================================================================
321
+
322
+ function isDirectory(p: string): boolean {
323
+ try {
324
+ return statSync(p).isDirectory();
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Find nearest config directory by walking up from cwd.
332
+ * Checks all config bases (.omp, .pi, .claude) at each level.
333
+ *
334
+ * @param subpath - Subpath within config dirs (e.g., "commands", "agents")
335
+ * @param cwd - Starting directory
336
+ * @returns First existing directory found, or undefined
337
+ */
338
+ export function findNearestProjectConfigDir(subpath: string, cwd: string = process.cwd()): ConfigDirEntry | undefined {
339
+ let currentDir = cwd;
340
+
341
+ while (true) {
342
+ // Check all config bases at this level, in priority order
343
+ for (const { base, name } of PROJECT_CONFIG_BASES) {
344
+ const candidate = join(currentDir, base, subpath);
345
+ if (isDirectory(candidate)) {
346
+ return { path: candidate, source: name, level: "project" };
347
+ }
348
+ }
349
+
350
+ // Move up one directory
351
+ const parentDir = dirname(currentDir);
352
+ if (parentDir === currentDir) break; // Reached root
353
+ currentDir = parentDir;
354
+ }
355
+
356
+ return undefined;
357
+ }
358
+
359
+ /**
360
+ * Find all nearest config directories by walking up from cwd.
361
+ * Returns one entry per config base (.omp, .pi, .claude) - the nearest one found.
362
+ * Results are in priority order (highest first).
363
+ */
364
+ export function findAllNearestProjectConfigDirs(subpath: string, cwd: string = process.cwd()): ConfigDirEntry[] {
365
+ const results: ConfigDirEntry[] = [];
366
+ const foundBases = new Set<string>();
367
+
368
+ let currentDir = cwd;
369
+
370
+ while (foundBases.size < PROJECT_CONFIG_BASES.length) {
371
+ for (const { base, name } of PROJECT_CONFIG_BASES) {
372
+ if (foundBases.has(name)) continue;
373
+
374
+ const candidate = join(currentDir, base, subpath);
375
+ if (isDirectory(candidate)) {
376
+ results.push({ path: candidate, source: name, level: "project" });
377
+ foundBases.add(name);
378
+ }
379
+ }
380
+
381
+ const parentDir = dirname(currentDir);
382
+ if (parentDir === currentDir) break;
383
+ currentDir = parentDir;
384
+ }
385
+
386
+ // Sort by priority order
387
+ const order = PROJECT_CONFIG_BASES.map((b) => b.name);
388
+ results.sort((a, b) => order.indexOf(a.source) - order.indexOf(b.source));
389
+
390
+ return results;
391
+ }
@@ -15,6 +15,7 @@ import {
15
15
  type OAuthCredentials,
16
16
  type OAuthProvider,
17
17
  } from "@oh-my-pi/pi-ai";
18
+ import { logger } from "./logger";
18
19
 
19
20
  export type ApiKeyCredential = {
20
21
  type: "api_key";
@@ -31,13 +32,21 @@ export type AuthStorageData = Record<string, AuthCredential>;
31
32
 
32
33
  /**
33
34
  * Credential storage backed by a JSON file.
35
+ * Reads from multiple fallback paths, writes to primary path.
34
36
  */
35
37
  export class AuthStorage {
36
38
  private data: AuthStorageData = {};
37
39
  private runtimeOverrides: Map<string, string> = new Map();
38
40
  private fallbackResolver?: (provider: string) => string | undefined;
39
41
 
40
- constructor(private authPath: string) {
42
+ /**
43
+ * @param authPath - Primary path for reading/writing auth.json
44
+ * @param fallbackPaths - Additional paths to check when reading (legacy support)
45
+ */
46
+ constructor(
47
+ private authPath: string,
48
+ private fallbackPaths: string[] = [],
49
+ ) {
41
50
  this.reload();
42
51
  }
43
52
 
@@ -66,17 +75,31 @@ export class AuthStorage {
66
75
 
67
76
  /**
68
77
  * Reload credentials from disk.
78
+ * Checks primary path first, then fallback paths.
69
79
  */
70
80
  reload(): void {
71
- if (!existsSync(this.authPath)) {
72
- this.data = {};
73
- return;
74
- }
75
- try {
76
- this.data = JSON.parse(readFileSync(this.authPath, "utf-8"));
77
- } catch {
78
- this.data = {};
81
+ const pathsToCheck = [this.authPath, ...this.fallbackPaths];
82
+
83
+ logger.debug("AuthStorage.reload checking paths", { paths: pathsToCheck });
84
+
85
+ for (const authPath of pathsToCheck) {
86
+ const exists = existsSync(authPath);
87
+ logger.debug("AuthStorage.reload path check", { path: authPath, exists });
88
+
89
+ if (exists) {
90
+ try {
91
+ this.data = JSON.parse(readFileSync(authPath, "utf-8"));
92
+ logger.debug("AuthStorage.reload loaded", { path: authPath, providers: Object.keys(this.data) });
93
+ return;
94
+ } catch (e) {
95
+ logger.error("AuthStorage failed to parse auth file", { path: authPath, error: String(e) });
96
+ // Continue to next path on parse error
97
+ }
98
+ }
79
99
  }
100
+
101
+ logger.warn("AuthStorage no auth file found", { checkedPaths: pathsToCheck });
102
+ this.data = {};
80
103
  }
81
104
 
82
105
  /**
@@ -132,7 +132,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
132
132
  if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
133
133
  const randomId = crypto.getRandomValues(new Uint8Array(8));
134
134
  const id = Array.from(randomId, (b) => b.toString(16).padStart(2, "0")).join("");
135
- tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
135
+ tempFilePath = join(tmpdir(), `omp-bash-${id}.log`);
136
136
  tempFileStream = createWriteStream(tempFilePath);
137
137
  // Write already-buffered chunks to temp file
138
138
  for (const chunk of outputChunks) {
@@ -5,10 +5,10 @@
5
5
  * to avoid import resolution issues with custom commands loaded from user directories.
6
6
  */
7
7
 
8
- import * as fs from "node:fs";
8
+ import { type Dirent, existsSync, readdirSync } from "node:fs";
9
9
  import * as path from "node:path";
10
10
  import * as typebox from "@sinclair/typebox";
11
- import { CONFIG_DIR_NAME, getAgentDir } from "../../config";
11
+ import { getAgentDir, getConfigDirs } from "../../config";
12
12
  import * as piCodingAgent from "../../index";
13
13
  import { execCommand } from "../exec";
14
14
  import { createReviewCommand } from "./bundled/review";
@@ -60,36 +60,6 @@ async function loadCommandModule(
60
60
  }
61
61
  }
62
62
 
63
- /**
64
- * Discover command modules from a directory.
65
- * Loads index.ts files from subdirectories (e.g., commands/deploy/index.ts).
66
- */
67
- function discoverCommandsInDir(dir: string): string[] {
68
- if (!fs.existsSync(dir)) {
69
- return [];
70
- }
71
-
72
- const commands: string[] = [];
73
-
74
- try {
75
- const entries = fs.readdirSync(dir, { withFileTypes: true });
76
-
77
- for (const entry of entries) {
78
- if (entry.isDirectory() || entry.isSymbolicLink()) {
79
- // Check for index.ts in subdirectory
80
- const indexPath = path.join(dir, entry.name, "index.ts");
81
- if (fs.existsSync(indexPath)) {
82
- commands.push(indexPath);
83
- }
84
- }
85
- }
86
- } catch {
87
- return [];
88
- }
89
-
90
- return commands;
91
- }
92
-
93
63
  export interface DiscoverCustomCommandsOptions {
94
64
  /** Current working directory. Default: process.cwd() */
95
65
  cwd?: string;
@@ -103,34 +73,58 @@ export interface DiscoverCustomCommandsResult {
103
73
  }
104
74
 
105
75
  /**
106
- * Discover custom command modules from standard locations:
107
- * - agentDir/commands/[name]/index.ts (user)
108
- * - cwd/.pi/commands/[name]/index.ts (project)
76
+ * Discover custom command modules (TypeScript slash commands).
77
+ * Markdown slash commands are handled by core/slash-commands.ts.
109
78
  */
110
79
  export function discoverCustomCommands(options: DiscoverCustomCommandsOptions = {}): DiscoverCustomCommandsResult {
111
80
  const cwd = options.cwd ?? process.cwd();
112
81
  const agentDir = options.agentDir ?? getAgentDir();
113
-
114
82
  const paths: Array<{ path: string; source: CustomCommandSource }> = [];
115
83
  const seen = new Set<string>();
116
84
 
117
- const addPaths = (modulePaths: string[], source: CustomCommandSource) => {
118
- for (const p of modulePaths) {
119
- const resolved = path.resolve(p);
120
- if (!seen.has(resolved)) {
121
- seen.add(resolved);
122
- paths.push({ path: p, source });
123
- }
124
- }
85
+ const addPath = (commandPath: string, source: CustomCommandSource): void => {
86
+ const resolved = path.resolve(commandPath);
87
+ if (seen.has(resolved)) return;
88
+ seen.add(resolved);
89
+ paths.push({ path: resolved, source });
125
90
  };
126
91
 
127
- // 1. User commands: agentDir/commands/
128
- const userCommandsDir = path.join(agentDir, "commands");
129
- addPaths(discoverCommandsInDir(userCommandsDir), "user");
92
+ const commandDirs: Array<{ path: string; source: CustomCommandSource }> = [];
93
+ if (agentDir) {
94
+ const userCommandsDir = path.join(agentDir, "commands");
95
+ if (existsSync(userCommandsDir)) {
96
+ commandDirs.push({ path: userCommandsDir, source: "user" });
97
+ }
98
+ }
99
+
100
+ for (const entry of getConfigDirs("commands", { cwd, existingOnly: true })) {
101
+ const source = entry.level === "user" ? "user" : "project";
102
+ if (!commandDirs.some((d) => d.path === entry.path)) {
103
+ commandDirs.push({ path: entry.path, source });
104
+ }
105
+ }
130
106
 
131
- // 2. Project commands: cwd/.pi/commands/
132
- const projectCommandsDir = path.join(cwd, CONFIG_DIR_NAME, "commands");
133
- addPaths(discoverCommandsInDir(projectCommandsDir), "project");
107
+ const indexCandidates = ["index.ts", "index.js", "index.mjs", "index.cjs"];
108
+ for (const { path: commandsDir, source } of commandDirs) {
109
+ let entries: Dirent[];
110
+ try {
111
+ entries = readdirSync(commandsDir, { withFileTypes: true });
112
+ } catch {
113
+ continue;
114
+ }
115
+ for (const entry of entries) {
116
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
117
+ const commandDir = path.join(commandsDir, entry.name);
118
+
119
+ for (const filename of indexCandidates) {
120
+ const candidate = path.join(commandDir, filename);
121
+ if (existsSync(candidate)) {
122
+ addPath(candidate, source);
123
+ break;
124
+ }
125
+ }
126
+ }
127
+ }
134
128
 
135
129
  return { paths };
136
130
  }
@@ -17,5 +17,6 @@ export type {
17
17
  ExecResult,
18
18
  LoadedCustomTool,
19
19
  RenderResultOptions,
20
+ ToolLoadError,
20
21
  } from "./types";
21
22
  export { wrapCustomTool, wrapCustomTools } from "./wrapper";