@oh-my-pi/pi-coding-agent 1.341.0 → 2.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 (151) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +1 -1
  3. package/examples/custom-tools/subagent/index.ts +1 -1
  4. package/package.json +5 -3
  5. package/src/cli/args.ts +5 -6
  6. package/src/cli/file-processor.ts +3 -3
  7. package/src/cli/list-models.ts +2 -2
  8. package/src/cli/plugin-cli.ts +1 -1
  9. package/src/cli/session-picker.ts +2 -2
  10. package/src/cli.ts +1 -1
  11. package/src/config.ts +3 -3
  12. package/src/core/agent-session.ts +157 -15
  13. package/src/core/bash-executor.ts +50 -10
  14. package/src/core/compaction/branch-summarization.ts +5 -5
  15. package/src/core/compaction/compaction.ts +3 -3
  16. package/src/core/compaction/index.ts +3 -3
  17. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  18. package/src/core/custom-commands/index.ts +15 -0
  19. package/src/core/custom-commands/loader.ts +232 -0
  20. package/src/core/custom-commands/types.ts +112 -0
  21. package/src/core/custom-tools/index.ts +3 -3
  22. package/src/core/custom-tools/loader.ts +10 -8
  23. package/src/core/custom-tools/types.ts +11 -6
  24. package/src/core/custom-tools/wrapper.ts +2 -1
  25. package/src/core/exec.ts +22 -12
  26. package/src/core/export-html/index.ts +5 -5
  27. package/src/core/file-mentions.ts +54 -0
  28. package/src/core/hooks/index.ts +5 -5
  29. package/src/core/hooks/loader.ts +21 -16
  30. package/src/core/hooks/runner.ts +6 -6
  31. package/src/core/hooks/tool-wrapper.ts +2 -2
  32. package/src/core/hooks/types.ts +12 -15
  33. package/src/core/index.ts +6 -6
  34. package/src/core/logger.ts +112 -0
  35. package/src/core/mcp/client.ts +3 -3
  36. package/src/core/mcp/config.ts +1 -1
  37. package/src/core/mcp/index.ts +12 -12
  38. package/src/core/mcp/loader.ts +2 -2
  39. package/src/core/mcp/manager.ts +6 -6
  40. package/src/core/mcp/tool-bridge.ts +3 -3
  41. package/src/core/mcp/transports/http.ts +1 -1
  42. package/src/core/mcp/transports/index.ts +2 -2
  43. package/src/core/mcp/transports/stdio.ts +1 -1
  44. package/src/core/messages.ts +22 -0
  45. package/src/core/model-registry.ts +2 -2
  46. package/src/core/model-resolver.ts +2 -2
  47. package/src/core/plugins/doctor.ts +1 -1
  48. package/src/core/plugins/index.ts +6 -6
  49. package/src/core/plugins/installer.ts +4 -4
  50. package/src/core/plugins/loader.ts +4 -9
  51. package/src/core/plugins/manager.ts +5 -5
  52. package/src/core/plugins/paths.ts +3 -3
  53. package/src/core/sdk.ts +77 -35
  54. package/src/core/session-manager.ts +6 -6
  55. package/src/core/settings-manager.ts +16 -3
  56. package/src/core/skills.ts +5 -5
  57. package/src/core/slash-commands.ts +60 -45
  58. package/src/core/system-prompt.ts +6 -6
  59. package/src/core/title-generator.ts +2 -2
  60. package/src/core/tools/bash.ts +32 -155
  61. package/src/core/tools/context.ts +2 -2
  62. package/src/core/tools/edit-diff.ts +3 -3
  63. package/src/core/tools/edit.ts +18 -5
  64. package/src/core/tools/exa/company.ts +3 -3
  65. package/src/core/tools/exa/index.ts +16 -17
  66. package/src/core/tools/exa/linkedin.ts +3 -3
  67. package/src/core/tools/exa/mcp-client.ts +9 -9
  68. package/src/core/tools/exa/render.ts +5 -5
  69. package/src/core/tools/exa/researcher.ts +3 -3
  70. package/src/core/tools/exa/search.ts +6 -5
  71. package/src/core/tools/exa/types.ts +5 -6
  72. package/src/core/tools/exa/websets.ts +3 -3
  73. package/src/core/tools/find.ts +3 -3
  74. package/src/core/tools/grep.ts +3 -3
  75. package/src/core/tools/index.ts +48 -34
  76. package/src/core/tools/ls.ts +4 -4
  77. package/src/core/tools/lsp/client.ts +161 -90
  78. package/src/core/tools/lsp/config.ts +1 -1
  79. package/src/core/tools/lsp/edits.ts +2 -2
  80. package/src/core/tools/lsp/index.ts +15 -13
  81. package/src/core/tools/lsp/render.ts +2 -2
  82. package/src/core/tools/lsp/rust-analyzer.ts +3 -3
  83. package/src/core/tools/lsp/utils.ts +1 -1
  84. package/src/core/tools/notebook.ts +1 -1
  85. package/src/core/tools/output.ts +175 -0
  86. package/src/core/tools/read.ts +7 -7
  87. package/src/core/tools/renderers.ts +92 -13
  88. package/src/core/tools/review.ts +268 -0
  89. package/src/core/tools/task/agents.ts +1 -1
  90. package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
  91. package/src/core/tools/task/discovery.ts +2 -2
  92. package/src/core/tools/task/executor.ts +145 -28
  93. package/src/core/tools/task/index.ts +78 -30
  94. package/src/core/tools/task/model-resolver.ts +30 -20
  95. package/src/core/tools/task/parallel.ts +1 -1
  96. package/src/core/tools/task/render.ts +219 -30
  97. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  98. package/src/core/tools/task/types.ts +36 -2
  99. package/src/core/tools/web-fetch.ts +5 -3
  100. package/src/core/tools/web-search/auth.ts +1 -1
  101. package/src/core/tools/web-search/index.ts +17 -15
  102. package/src/core/tools/web-search/providers/anthropic.ts +2 -2
  103. package/src/core/tools/web-search/providers/exa.ts +3 -5
  104. package/src/core/tools/web-search/providers/perplexity.ts +1 -1
  105. package/src/core/tools/web-search/render.ts +3 -3
  106. package/src/core/tools/write.ts +4 -4
  107. package/src/index.ts +29 -18
  108. package/src/main.ts +37 -32
  109. package/src/migrations.ts +3 -3
  110. package/src/modes/index.ts +5 -5
  111. package/src/modes/interactive/components/armin.ts +1 -1
  112. package/src/modes/interactive/components/assistant-message.ts +1 -1
  113. package/src/modes/interactive/components/bash-execution.ts +4 -4
  114. package/src/modes/interactive/components/bordered-loader.ts +2 -2
  115. package/src/modes/interactive/components/branch-summary-message.ts +2 -2
  116. package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
  117. package/src/modes/interactive/components/diff.ts +1 -1
  118. package/src/modes/interactive/components/dynamic-border.ts +1 -1
  119. package/src/modes/interactive/components/footer.ts +5 -5
  120. package/src/modes/interactive/components/hook-editor.ts +2 -2
  121. package/src/modes/interactive/components/hook-input.ts +2 -2
  122. package/src/modes/interactive/components/hook-message.ts +3 -3
  123. package/src/modes/interactive/components/hook-selector.ts +2 -2
  124. package/src/modes/interactive/components/model-selector.ts +281 -59
  125. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  126. package/src/modes/interactive/components/plugin-settings.ts +4 -4
  127. package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
  128. package/src/modes/interactive/components/session-selector.ts +4 -4
  129. package/src/modes/interactive/components/settings-defs.ts +1 -1
  130. package/src/modes/interactive/components/settings-selector.ts +5 -5
  131. package/src/modes/interactive/components/show-images-selector.ts +2 -2
  132. package/src/modes/interactive/components/theme-selector.ts +2 -2
  133. package/src/modes/interactive/components/thinking-selector.ts +2 -2
  134. package/src/modes/interactive/components/tool-execution.ts +26 -8
  135. package/src/modes/interactive/components/tree-selector.ts +3 -3
  136. package/src/modes/interactive/components/user-message-selector.ts +2 -2
  137. package/src/modes/interactive/components/user-message.ts +1 -1
  138. package/src/modes/interactive/components/welcome.ts +2 -2
  139. package/src/modes/interactive/interactive-mode.ts +85 -41
  140. package/src/modes/interactive/theme/theme.ts +8 -7
  141. package/src/modes/print-mode.ts +4 -3
  142. package/src/modes/rpc/rpc-client.ts +4 -4
  143. package/src/modes/rpc/rpc-mode.ts +21 -11
  144. package/src/modes/rpc/rpc-types.ts +3 -3
  145. package/src/utils/changelog.ts +2 -2
  146. package/src/utils/clipboard.ts +1 -1
  147. package/src/utils/shell-snapshot.ts +218 -0
  148. package/src/utils/shell.ts +93 -13
  149. package/src/utils/tools-manager.ts +1 -1
  150. package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
  151. package/src/core/tools/exa/logger.ts +0 -56
@@ -64,6 +64,19 @@ export interface CompactionSummaryMessage {
64
64
  timestamp: number;
65
65
  }
66
66
 
67
+ /**
68
+ * Message type for auto-read file mentions via @filepath syntax.
69
+ */
70
+ export interface FileMentionMessage {
71
+ role: "fileMention";
72
+ files: Array<{
73
+ path: string;
74
+ content: string;
75
+ lineCount: number;
76
+ }>;
77
+ timestamp: number;
78
+ }
79
+
67
80
  // Extend CustomAgentMessages via declaration merging
68
81
  declare module "@oh-my-pi/pi-agent-core" {
69
82
  interface CustomAgentMessages {
@@ -71,6 +84,7 @@ declare module "@oh-my-pi/pi-agent-core" {
71
84
  hookMessage: HookMessage;
72
85
  branchSummary: BranchSummaryMessage;
73
86
  compactionSummary: CompactionSummaryMessage;
87
+ fileMention: FileMentionMessage;
74
88
  }
75
89
  }
76
90
 
@@ -175,6 +189,14 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
175
189
  ],
176
190
  timestamp: m.timestamp,
177
191
  };
192
+ case "fileMention": {
193
+ const fileContents = m.files.map((f) => `<file path="${f.path}">\n${f.content}\n</file>`).join("\n\n");
194
+ return {
195
+ role: "user",
196
+ content: [{ type: "text" as const, text: `<system-reminder>\n${fileContents}\n</system-reminder>` }],
197
+ timestamp: m.timestamp,
198
+ };
199
+ }
178
200
  case "user":
179
201
  case "assistant":
180
202
  case "toolResult":
@@ -2,6 +2,7 @@
2
2
  * Model registry - manages built-in and custom models, provides API key resolution.
3
3
  */
4
4
 
5
+ import { existsSync, readFileSync } from "node:fs";
5
6
  import {
6
7
  type Api,
7
8
  getGitHubCopilotBaseUrl,
@@ -13,8 +14,7 @@ import {
13
14
  } from "@oh-my-pi/pi-ai";
14
15
  import { type Static, Type } from "@sinclair/typebox";
15
16
  import AjvModule from "ajv";
16
- import { existsSync, readFileSync } from "fs";
17
- import type { AuthStorage } from "./auth-storage.js";
17
+ import type { AuthStorage } from "./auth-storage";
18
18
 
19
19
  const Ajv = (AjvModule as any).default || AjvModule;
20
20
 
@@ -6,8 +6,8 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
6
6
  import { type Api, type KnownProvider, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
7
7
  import chalk from "chalk";
8
8
  import { minimatch } from "minimatch";
9
- import { isValidThinkingLevel } from "../cli/args.js";
10
- import type { ModelRegistry } from "./model-registry.js";
9
+ import { isValidThinkingLevel } from "../cli/args";
10
+ import type { ModelRegistry } from "./model-registry";
11
11
 
12
12
  /** Default model IDs for each known provider */
13
13
  export const defaultModelPerProvider: Record<KnownProvider, string> = {
@@ -1,4 +1,4 @@
1
- import type { DoctorCheck } from "./types.js";
1
+ import type { DoctorCheck } from "./types";
2
2
 
3
3
  export async function runDoctorChecks(): Promise<DoctorCheck[]> {
4
4
  const checks: DoctorCheck[] = [];
@@ -1,5 +1,5 @@
1
1
  // Plugin system exports
2
- export { formatDoctorResults, runDoctorChecks } from "./doctor.js";
2
+ export { formatDoctorResults, runDoctorChecks } from "./doctor";
3
3
  export {
4
4
  getAllPluginCommandPaths,
5
5
  getAllPluginHookPaths,
@@ -9,16 +9,16 @@ export {
9
9
  resolvePluginCommandPaths,
10
10
  resolvePluginHookPaths,
11
11
  resolvePluginToolPaths,
12
- } from "./loader.js";
13
- export { PluginManager, parseSettingValue, validateSetting } from "./manager.js";
14
- export { extractPackageName, formatPluginSpec, parsePluginSpec } from "./parser.js";
12
+ } from "./loader";
13
+ export { PluginManager, parseSettingValue, validateSetting } from "./manager";
14
+ export { extractPackageName, formatPluginSpec, parsePluginSpec } from "./parser";
15
15
  export {
16
16
  getPluginsDir,
17
17
  getPluginsLockfile,
18
18
  getPluginsNodeModules,
19
19
  getPluginsPackageJson,
20
20
  getProjectPluginOverrides,
21
- } from "./paths.js";
21
+ } from "./paths";
22
22
  export type {
23
23
  BooleanSetting,
24
24
  DoctorCheck,
@@ -35,4 +35,4 @@ export type {
35
35
  PluginSettingType,
36
36
  ProjectPluginOverrides,
37
37
  StringSetting,
38
- } from "./types.js";
38
+ } from "./types";
@@ -1,7 +1,7 @@
1
- import { mkdir } from "fs/promises";
2
- import { join, resolve } from "path";
3
- import { getAgentDir } from "../../config.js";
4
- import type { InstalledPlugin } from "./types.js";
1
+ import { mkdir } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { getAgentDir } from "../../config";
4
+ import type { InstalledPlugin } from "./types";
5
5
 
6
6
  const PLUGINS_DIR = join(getAgentDir(), "plugins");
7
7
 
@@ -5,15 +5,10 @@
5
5
  * based on manifest entries and enabled features.
6
6
  */
7
7
 
8
- import { existsSync, readFileSync } from "fs";
9
- import { join } from "path";
10
- import {
11
- getPluginsLockfile,
12
- getPluginsNodeModules,
13
- getPluginsPackageJson,
14
- getProjectPluginOverrides,
15
- } from "./paths.js";
16
- import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPluginOverrides } from "./types.js";
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { getPluginsLockfile, getPluginsNodeModules, getPluginsPackageJson, getProjectPluginOverrides } from "./paths";
11
+ import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPluginOverrides } from "./types";
17
12
 
18
13
  // =============================================================================
19
14
  // Runtime Config Loading
@@ -1,13 +1,13 @@
1
- import { existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, unlinkSync, writeFileSync } from "fs";
2
- import { join, resolve } from "path";
3
- import { extractPackageName, parsePluginSpec } from "./parser.js";
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { extractPackageName, parsePluginSpec } from "./parser";
4
4
  import {
5
5
  getPluginsDir,
6
6
  getPluginsLockfile,
7
7
  getPluginsNodeModules,
8
8
  getPluginsPackageJson,
9
9
  getProjectPluginOverrides,
10
- } from "./paths.js";
10
+ } from "./paths";
11
11
  import type {
12
12
  DoctorCheck,
13
13
  DoctorOptions,
@@ -17,7 +17,7 @@ import type {
17
17
  PluginRuntimeConfig,
18
18
  PluginSettingSchema,
19
19
  ProjectPluginOverrides,
20
- } from "./types.js";
20
+ } from "./types";
21
21
 
22
22
  // =============================================================================
23
23
  // Validation
@@ -1,6 +1,6 @@
1
- import { homedir } from "os";
2
- import { join } from "path";
3
- import { CONFIG_DIR_NAME } from "../../config.js";
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { CONFIG_DIR_NAME } from "../../config";
4
4
 
5
5
  // =============================================================================
6
6
  // Plugin Directory Paths
package/src/core/sdk.ts CHANGED
@@ -29,34 +29,38 @@
29
29
  * ```
30
30
  */
31
31
 
32
+ import { join } from "node:path";
32
33
  import { Agent, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
33
34
  import type { Model } from "@oh-my-pi/pi-ai";
34
- import { join } from "path";
35
- import { getAgentDir } from "../config.js";
36
- import { AgentSession } from "./agent-session.js";
37
- import { AuthStorage } from "./auth-storage.js";
35
+ import { getAgentDir } from "../config";
36
+ import { AgentSession } from "./agent-session";
37
+ import { AuthStorage } from "./auth-storage";
38
+ import {
39
+ type CustomCommandsLoadResult,
40
+ loadCustomCommands as loadCustomCommandsInternal,
41
+ } from "./custom-commands/index";
38
42
  import {
39
43
  type CustomToolsLoadResult,
40
44
  discoverAndLoadCustomTools,
41
45
  type LoadedCustomTool,
42
46
  wrapCustomTools,
43
- } from "./custom-tools/index.js";
44
- import type { CustomTool } from "./custom-tools/types.js";
45
- import { discoverAndLoadHooks, HookRunner, type LoadedHook, wrapToolsWithHooks } from "./hooks/index.js";
46
- import type { HookFactory } from "./hooks/types.js";
47
- import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp/index.js";
48
- import { convertToLlm } from "./messages.js";
49
- import { ModelRegistry } from "./model-registry.js";
50
- import { SessionManager } from "./session-manager.js";
51
- import { type Settings, SettingsManager, type SkillsSettings } from "./settings-manager.js";
52
- import { loadSkills as loadSkillsInternal, type Skill } from "./skills.js";
53
- import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./slash-commands.js";
47
+ } from "./custom-tools/index";
48
+ import type { CustomTool } from "./custom-tools/types";
49
+ import { discoverAndLoadHooks, HookRunner, type LoadedHook, wrapToolsWithHooks } from "./hooks/index";
50
+ import type { HookFactory } from "./hooks/types";
51
+ import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp/index";
52
+ import { convertToLlm } from "./messages";
53
+ import { ModelRegistry } from "./model-registry";
54
+ import { SessionManager } from "./session-manager";
55
+ import { type CommandsSettings, type Settings, SettingsManager, type SkillsSettings } from "./settings-manager";
56
+ import { loadSkills as loadSkillsInternal, type Skill } from "./skills";
57
+ import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./slash-commands";
54
58
  import {
55
59
  buildSystemPrompt as buildSystemPromptInternal,
56
60
  loadProjectContextFiles as loadContextFilesInternal,
57
- } from "./system-prompt.js";
58
- import { time } from "./timings.js";
59
- import { createToolContextStore } from "./tools/context.js";
61
+ } from "./system-prompt";
62
+ import { time } from "./timings";
63
+ import { createToolContextStore } from "./tools/context";
60
64
  import {
61
65
  allTools,
62
66
  applyBashInterception,
@@ -80,7 +84,7 @@ import {
80
84
  type Tool,
81
85
  warmupLspServers,
82
86
  writeTool,
83
- } from "./tools/index.js";
87
+ } from "./tools/index";
84
88
 
85
89
  // Types
86
90
 
@@ -127,6 +131,9 @@ export interface CreateAgentSessionOptions {
127
131
  /** Enable MCP server discovery from .mcp.json files. Default: true */
128
132
  enableMCP?: boolean;
129
133
 
134
+ /** Tool names explicitly requested (enables disabled-by-default tools) */
135
+ explicitTools?: string[];
136
+
130
137
  /** Session manager. Default: SessionManager.create(cwd) */
131
138
  sessionManager?: SessionManager;
132
139
 
@@ -153,13 +160,14 @@ export interface CreateAgentSessionResult {
153
160
 
154
161
  // Re-exports
155
162
 
156
- export type { CustomTool } from "./custom-tools/types.js";
157
- export type { HookAPI, HookCommandContext, HookContext, HookFactory } from "./hooks/types.js";
158
- export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResult } from "./mcp/index.js";
159
- export type { Settings, SkillsSettings } from "./settings-manager.js";
160
- export type { Skill } from "./skills.js";
161
- export type { FileSlashCommand } from "./slash-commands.js";
162
- export type { Tool } from "./tools/index.js";
163
+ export type { CustomCommand, CustomCommandFactory } from "./custom-commands/types";
164
+ export type { CustomTool } from "./custom-tools/types";
165
+ export type { HookAPI, HookCommandContext, HookContext, HookFactory } from "./hooks/types";
166
+ export type { MCPManager, MCPServerConfig, MCPServerConnection, MCPToolsLoadResult } from "./mcp/index";
167
+ export type { Settings, SkillsSettings } from "./settings-manager";
168
+ export type { Skill } from "./skills";
169
+ export type { FileSlashCommand } from "./slash-commands";
170
+ export type { Tool } from "./tools/index";
163
171
 
164
172
  export {
165
173
  // Pre-built tools (use process.cwd())
@@ -278,10 +286,29 @@ export function discoverContextFiles(cwd?: string, agentDir?: string): Array<{ p
278
286
  /**
279
287
  * Discover slash commands from cwd and agentDir.
280
288
  */
281
- export function discoverSlashCommands(cwd?: string, agentDir?: string): FileSlashCommand[] {
289
+ export function discoverSlashCommands(
290
+ cwd?: string,
291
+ agentDir?: string,
292
+ settings?: CommandsSettings,
293
+ ): FileSlashCommand[] {
282
294
  return loadSlashCommandsInternal({
283
295
  cwd: cwd ?? process.cwd(),
284
296
  agentDir: agentDir ?? getDefaultAgentDir(),
297
+ enableClaudeUser: settings?.enableClaudeUser,
298
+ enableClaudeProject: settings?.enableClaudeProject,
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Discover custom commands (TypeScript slash commands) from cwd and agentDir.
304
+ */
305
+ export async function discoverCustomTSCommands(cwd?: string, agentDir?: string): Promise<CustomCommandsLoadResult> {
306
+ const resolvedCwd = cwd ?? process.cwd();
307
+ const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
308
+
309
+ return loadCustomCommandsInternal({
310
+ cwd: resolvedCwd,
311
+ agentDir: resolvedAgentDir,
285
312
  });
286
313
  }
287
314
 
@@ -555,12 +582,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
555
582
  const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
556
583
  time("discoverContextFiles");
557
584
 
558
- // Hook runner - created early for hooks
559
- let hookRunner: HookRunner | undefined;
585
+ // Hook runner - always created (needed for custom command context even without hooks)
586
+ let loadedHooks: LoadedHook[] = [];
560
587
  if (options.hooks !== undefined) {
561
588
  if (options.hooks.length > 0) {
562
- const loadedHooks = createLoadedHooksFromDefinitions(options.hooks);
563
- hookRunner = new HookRunner(loadedHooks, cwd, sessionManager, modelRegistry);
589
+ loadedHooks = createLoadedHooksFromDefinitions(options.hooks);
564
590
  }
565
591
  } else {
566
592
  // Discover hooks, merging with additional paths
@@ -570,10 +596,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
570
596
  for (const { path, error } of errors) {
571
597
  console.error(`Failed to load hook "${path}": ${error}`);
572
598
  }
573
- if (hooks.length > 0) {
574
- hookRunner = new HookRunner(hooks, cwd, sessionManager, modelRegistry);
575
- }
599
+ loadedHooks = hooks;
576
600
  }
601
+ const hookRunner = new HookRunner(loadedHooks, cwd, sessionManager, modelRegistry);
577
602
 
578
603
  const sessionContext = {
579
604
  getSessionFile: () => sessionManager.getSessionFile() ?? null,
@@ -695,6 +720,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
695
720
  };
696
721
 
697
722
  let allToolsArray: Tool[] = [...builtInTools, ...wrappedCustomTools];
723
+
724
+ // Filter out hidden tools unless explicitly requested
725
+ if (options.explicitTools) {
726
+ const explicitSet = new Set(options.explicitTools);
727
+ allToolsArray = allToolsArray.filter((tool) => !tool.hidden || explicitSet.has(tool.name));
728
+ } else {
729
+ allToolsArray = allToolsArray.filter((tool) => !tool.hidden);
730
+ }
698
731
  time("combineTools");
699
732
 
700
733
  // Apply bash interception to redirect common shell patterns to proper tools (if enabled)
@@ -730,9 +763,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
730
763
  systemPrompt = options.systemPrompt(defaultPrompt);
731
764
  }
732
765
 
733
- const slashCommands = options.slashCommands ?? discoverSlashCommands(cwd, agentDir);
766
+ const commandsSettings = settingsManager.getCommandsSettings();
767
+ const slashCommands = options.slashCommands ?? discoverSlashCommands(cwd, agentDir, commandsSettings);
734
768
  time("discoverSlashCommands");
735
769
 
770
+ // Discover custom commands (TypeScript slash commands)
771
+ const customCommandsResult = await loadCustomCommandsInternal({ cwd, agentDir });
772
+ time("discoverCustomCommands");
773
+ for (const { path, error } of customCommandsResult.errors) {
774
+ console.error(`Failed to load custom command "${path}": ${error}`);
775
+ }
776
+
736
777
  agent = new Agent({
737
778
  initialState: {
738
779
  systemPrompt,
@@ -782,6 +823,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
782
823
  fileCommands: slashCommands,
783
824
  hookRunner,
784
825
  customTools: customToolsResult.tools,
826
+ customCommands: customCommandsResult.commands,
785
827
  skillsSettings: settingsManager.getSkillsSettings(),
786
828
  modelRegistry,
787
829
  });
@@ -1,5 +1,3 @@
1
- import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
- import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
3
1
  import {
4
2
  appendFileSync,
5
3
  closeSync,
@@ -11,16 +9,18 @@ import {
11
9
  readSync,
12
10
  statSync,
13
11
  writeFileSync,
14
- } from "fs";
15
- import { join, resolve } from "path";
16
- import { getAgentDir as getDefaultAgentDir } from "../config.js";
12
+ } from "node:fs";
13
+ import { join, resolve } from "node:path";
14
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
15
+ import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
16
+ import { getAgentDir as getDefaultAgentDir } from "../config";
17
17
  import {
18
18
  type BashExecutionMessage,
19
19
  createBranchSummaryMessage,
20
20
  createCompactionSummaryMessage,
21
21
  createHookMessage,
22
22
  type HookMessage,
23
- } from "./messages.js";
23
+ } from "./messages";
24
24
 
25
25
  export const CURRENT_SESSION_VERSION = 2;
26
26
 
@@ -1,6 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
- import { dirname, join } from "path";
3
- import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { CONFIG_DIR_NAME, getAgentDir } from "../config";
4
4
 
5
5
  export interface CompactionSettings {
6
6
  enabled?: boolean; // default: true
@@ -30,6 +30,11 @@ export interface SkillsSettings {
30
30
  includeSkills?: string[]; // default: [] (empty = include all; glob patterns to filter)
31
31
  }
32
32
 
33
+ export interface CommandsSettings {
34
+ enableClaudeUser?: boolean; // default: true (load from ~/.claude/commands/)
35
+ enableClaudeProject?: boolean; // default: true (load from .claude/commands/)
36
+ }
37
+
33
38
  export interface TerminalSettings {
34
39
  showImages?: boolean; // default: true (only relevant if terminal supports images)
35
40
  }
@@ -78,6 +83,7 @@ export interface Settings {
78
83
  hooks?: string[]; // Array of hook file paths
79
84
  customTools?: string[]; // Array of custom tool file paths
80
85
  skills?: SkillsSettings;
86
+ commands?: CommandsSettings;
81
87
  terminal?: TerminalSettings;
82
88
  enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
83
89
  exa?: ExaSettings;
@@ -399,6 +405,13 @@ export class SettingsManager {
399
405
  };
400
406
  }
401
407
 
408
+ getCommandsSettings(): Required<CommandsSettings> {
409
+ return {
410
+ enableClaudeUser: this.settings.commands?.enableClaudeUser ?? true,
411
+ enableClaudeProject: this.settings.commands?.enableClaudeProject ?? true,
412
+ };
413
+ }
414
+
402
415
  getShowImages(): boolean {
403
416
  return this.settings.terminal?.showImages ?? true;
404
417
  }
@@ -1,9 +1,9 @@
1
- import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs";
1
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, dirname, join, resolve } from "node:path";
2
4
  import { minimatch } from "minimatch";
3
- import { homedir } from "os";
4
- import { basename, dirname, join, resolve } from "path";
5
- import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
6
- import type { SkillsSettings } from "./settings-manager.js";
5
+ import { CONFIG_DIR_NAME, getAgentDir } from "../config";
6
+ import type { SkillsSettings } from "./settings-manager";
7
7
 
8
8
  /**
9
9
  * Standard frontmatter fields per Agent Skills spec.
@@ -1,6 +1,8 @@
1
- import { existsSync, readdirSync, readFileSync } from "fs";
2
- import { join, resolve } from "path";
3
- import { CONFIG_DIR_NAME, getCommandsDir } from "../config.js";
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { CONFIG_DIR_NAME, getCommandsDir } from "../config";
5
+ import { logger } from "./logger";
4
6
 
5
7
  /**
6
8
  * Represents a custom slash command loaded from a file
@@ -98,14 +100,12 @@ export function substituteArgs(content: string, args: string[]): string {
98
100
  return result;
99
101
  }
100
102
 
103
+ type CommandSource = "builtin" | "claude-user" | "claude-project" | "user" | "project";
104
+
101
105
  /**
102
106
  * Recursively scan a directory for .md files (and symlinks to .md files) and load them as slash commands
103
107
  */
104
- function loadCommandsFromDir(
105
- dir: string,
106
- source: "builtin" | "user" | "project",
107
- subdir: string = "",
108
- ): FileSlashCommand[] {
108
+ function loadCommandsFromDir(dir: string, source: CommandSource, subdir: string = ""): FileSlashCommand[] {
109
109
  const commands: FileSlashCommand[] = [];
110
110
 
111
111
  if (!existsSync(dir)) {
@@ -129,15 +129,18 @@ function loadCommandsFromDir(
129
129
 
130
130
  const name = entry.name.slice(0, -3); // Remove .md extension
131
131
 
132
- // Build source string
133
- let sourceStr: string;
134
- if (source === "builtin") {
135
- sourceStr = subdir ? `(builtin:${subdir})` : "(builtin)";
136
- } else if (source === "user") {
137
- sourceStr = subdir ? `(user:${subdir})` : "(user)";
138
- } else {
139
- sourceStr = subdir ? `(project:${subdir})` : "(project)";
140
- }
132
+ // Build source string based on source type
133
+ const sourceLabel =
134
+ source === "builtin"
135
+ ? "builtin"
136
+ : source === "claude-user"
137
+ ? "claude-user"
138
+ : source === "claude-project"
139
+ ? "claude-project"
140
+ : source === "user"
141
+ ? "user"
142
+ : "project";
143
+ const sourceStr = subdir ? `(${sourceLabel}:${subdir})` : `(${sourceLabel})`;
141
144
 
142
145
  // Get description from frontmatter or first non-empty line
143
146
  let description = frontmatter.description || "";
@@ -159,13 +162,13 @@ function loadCommandsFromDir(
159
162
  content,
160
163
  source: sourceStr,
161
164
  });
162
- } catch (_error) {
163
- // Silently skip files that can't be read
165
+ } catch (err) {
166
+ logger.debug("Failed to read slash command file", { error: String(err) });
164
167
  }
165
168
  }
166
169
  }
167
- } catch (_error) {
168
- // Silently skip directories that can't be read
170
+ } catch (err) {
171
+ logger.debug("Failed to read slash command directory", { error: String(err) });
169
172
  }
170
173
 
171
174
  return commands;
@@ -176,54 +179,66 @@ export interface LoadSlashCommandsOptions {
176
179
  cwd?: string;
177
180
  /** Agent config directory for global commands. Default: from getCommandsDir() */
178
181
  agentDir?: string;
182
+ /** Enable loading from ~/.claude/commands/. Default: true */
183
+ enableClaudeUser?: boolean;
184
+ /** Enable loading from .claude/commands/. Default: true */
185
+ enableClaudeProject?: boolean;
179
186
  }
180
187
 
181
188
  /**
182
189
  * Load all custom slash commands from:
183
190
  * 1. Builtin: package commands/
184
- * 2. Global: agentDir/commands/
185
- * 3. Project: cwd/{CONFIG_DIR_NAME}/commands/
191
+ * 2. Claude user: ~/.claude/commands/
192
+ * 3. Claude project: .claude/commands/
193
+ * 4. Pi user: agentDir/commands/
194
+ * 5. Pi project: cwd/{CONFIG_DIR_NAME}/commands/
195
+ *
196
+ * First occurrence wins (earlier sources have priority).
186
197
  */
187
198
  export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileSlashCommand[] {
188
199
  const resolvedCwd = options.cwd ?? process.cwd();
189
200
  const resolvedAgentDir = options.agentDir ?? getCommandsDir();
201
+ const enableClaudeUser = options.enableClaudeUser ?? true;
202
+ const enableClaudeProject = options.enableClaudeProject ?? true;
190
203
 
191
204
  const commands: FileSlashCommand[] = [];
192
205
  const seenNames = new Set<string>();
193
206
 
194
- // 1. Builtin commands (from package)
195
- const builtinDir = join(import.meta.dir, "../commands");
196
- if (existsSync(builtinDir)) {
197
- const builtinCommands = loadCommandsFromDir(builtinDir, "builtin");
198
- for (const cmd of builtinCommands) {
207
+ const addCommands = (newCommands: FileSlashCommand[]) => {
208
+ for (const cmd of newCommands) {
199
209
  if (!seenNames.has(cmd.name)) {
200
210
  commands.push(cmd);
201
211
  seenNames.add(cmd.name);
202
212
  }
203
213
  }
214
+ };
215
+
216
+ // 1. Builtin commands (from package)
217
+ const builtinDir = join(import.meta.dir, "../commands");
218
+ if (existsSync(builtinDir)) {
219
+ addCommands(loadCommandsFromDir(builtinDir, "builtin"));
204
220
  }
205
221
 
206
- // 2. Load global commands from agentDir/commands/
207
- // Note: if agentDir is provided, it should be the agent dir, not the commands dir
208
- const globalCommandsDir = options.agentDir ? join(options.agentDir, "commands") : resolvedAgentDir;
209
- const globalCommands = loadCommandsFromDir(globalCommandsDir, "user");
210
- for (const cmd of globalCommands) {
211
- if (!seenNames.has(cmd.name)) {
212
- commands.push(cmd);
213
- seenNames.add(cmd.name);
214
- }
222
+ // 2. Claude user commands (~/.claude/commands/)
223
+ if (enableClaudeUser) {
224
+ const claudeUserDir = join(homedir(), ".claude", "commands");
225
+ addCommands(loadCommandsFromDir(claudeUserDir, "claude-user"));
215
226
  }
216
227
 
217
- // 3. Load project commands from cwd/{CONFIG_DIR_NAME}/commands/
218
- const projectCommandsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "commands");
219
- const projectCommands = loadCommandsFromDir(projectCommandsDir, "project");
220
- for (const cmd of projectCommands) {
221
- if (!seenNames.has(cmd.name)) {
222
- commands.push(cmd);
223
- seenNames.add(cmd.name);
224
- }
228
+ // 3. Claude project commands (.claude/commands/)
229
+ if (enableClaudeProject) {
230
+ const claudeProjectDir = resolve(resolvedCwd, ".claude", "commands");
231
+ addCommands(loadCommandsFromDir(claudeProjectDir, "claude-project"));
225
232
  }
226
233
 
234
+ // 4. Pi user commands (agentDir/commands/)
235
+ const globalCommandsDir = options.agentDir ? join(options.agentDir, "commands") : resolvedAgentDir;
236
+ addCommands(loadCommandsFromDir(globalCommandsDir, "user"));
237
+
238
+ // 5. Pi project commands (cwd/{CONFIG_DIR_NAME}/commands/)
239
+ const projectCommandsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "commands");
240
+ addCommands(loadCommandsFromDir(projectCommandsDir, "project"));
241
+
227
242
  return commands;
228
243
  }
229
244
 
@@ -2,13 +2,13 @@
2
2
  * System prompt construction and project context loading
3
3
  */
4
4
 
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { join, resolve } from "node:path";
5
7
  import chalk from "chalk";
6
- import { existsSync, readFileSync } from "fs";
7
- import { join, resolve } from "path";
8
- import { getAgentDir, getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
9
- import type { SkillsSettings } from "./settings-manager.js";
10
- import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills.js";
11
- import type { ToolName } from "./tools/index.js";
8
+ import { getAgentDir, getDocsPath, getExamplesPath, getReadmePath } from "../config";
9
+ import type { SkillsSettings } from "./settings-manager";
10
+ import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills";
11
+ import type { ToolName } from "./tools/index";
12
12
 
13
13
  /**
14
14
  * Execute a git command synchronously and return stdout or null on failure.