@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
@@ -13,9 +13,9 @@ import {
13
13
  createBranchSummaryMessage,
14
14
  createCompactionSummaryMessage,
15
15
  createHookMessage,
16
- } from "../messages.js";
17
- import type { ReadonlySessionManager, SessionEntry } from "../session-manager.js";
18
- import { estimateTokens } from "./compaction.js";
16
+ } from "../messages";
17
+ import type { ReadonlySessionManager, SessionEntry } from "../session-manager";
18
+ import { estimateTokens } from "./compaction";
19
19
  import {
20
20
  computeFileLists,
21
21
  createFileOps,
@@ -24,7 +24,7 @@ import {
24
24
  formatFileOperations,
25
25
  SUMMARIZATION_SYSTEM_PROMPT,
26
26
  serializeConversation,
27
- } from "./utils.js";
27
+ } from "./utils";
28
28
 
29
29
  // ============================================================================
30
30
  // Types
@@ -44,7 +44,7 @@ export interface BranchSummaryDetails {
44
44
  modifiedFiles: string[];
45
45
  }
46
46
 
47
- export type { FileOperations } from "./utils.js";
47
+ export type { FileOperations } from "./utils";
48
48
 
49
49
  export interface BranchPreparation {
50
50
  /** Messages extracted for summarization, in chronological order */
@@ -8,8 +8,8 @@
8
8
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
9
9
  import type { AssistantMessage, Model, Usage } from "@oh-my-pi/pi-ai";
10
10
  import { complete, completeSimple } from "@oh-my-pi/pi-ai";
11
- import { convertToLlm, createBranchSummaryMessage, createHookMessage } from "../messages.js";
12
- import type { CompactionEntry, SessionEntry } from "../session-manager.js";
11
+ import { convertToLlm, createBranchSummaryMessage, createHookMessage } from "../messages";
12
+ import type { CompactionEntry, SessionEntry } from "../session-manager";
13
13
  import {
14
14
  computeFileLists,
15
15
  createFileOps,
@@ -18,7 +18,7 @@ import {
18
18
  formatFileOperations,
19
19
  SUMMARIZATION_SYSTEM_PROMPT,
20
20
  serializeConversation,
21
- } from "./utils.js";
21
+ } from "./utils";
22
22
 
23
23
  // ============================================================================
24
24
  // File Operation Tracking
@@ -2,6 +2,6 @@
2
2
  * Compaction and summarization utilities.
3
3
  */
4
4
 
5
- export * from "./branch-summarization.js";
6
- export * from "./compaction.js";
7
- export * from "./utils.js";
5
+ export * from "./branch-summarization";
6
+ export * from "./compaction";
7
+ export * from "./utils";
@@ -0,0 +1,156 @@
1
+ /**
2
+ * /review command - Interactive code review launcher
3
+ *
4
+ * Provides a menu to select review mode:
5
+ * 1. Review against a base branch (PR style)
6
+ * 2. Review uncommitted changes
7
+ * 3. Review a specific commit
8
+ * 4. Custom review instructions
9
+ */
10
+
11
+ import type { HookCommandContext } from "../../../hooks/types";
12
+ import type { CustomCommand, CustomCommandAPI } from "../../types";
13
+
14
+ export function createReviewCommand(api: CustomCommandAPI): CustomCommand {
15
+ return {
16
+ name: "review",
17
+ description: "Launch interactive code review",
18
+
19
+ async execute(_args: string[], ctx: HookCommandContext): Promise<string | undefined> {
20
+ if (!ctx.hasUI) {
21
+ return "Use the Task tool to run the 'reviewer' agent to review recent code changes.";
22
+ }
23
+
24
+ // Main menu
25
+ const mode = await ctx.ui.select("Review Mode", [
26
+ "1. Review against a base branch (PR Style)",
27
+ "2. Review uncommitted changes",
28
+ "3. Review a specific commit",
29
+ "4. Custom review instructions",
30
+ ]);
31
+
32
+ if (!mode) return undefined;
33
+
34
+ const modeNum = parseInt(mode[0], 10);
35
+
36
+ switch (modeNum) {
37
+ case 1: {
38
+ // PR-style review against base branch
39
+ const branches = await getGitBranches(api);
40
+ if (branches.length === 0) {
41
+ ctx.ui.notify("No git branches found", "error");
42
+ return undefined;
43
+ }
44
+
45
+ const baseBranch = await ctx.ui.select("Select base branch to compare against", branches);
46
+ if (!baseBranch) return undefined;
47
+
48
+ const currentBranch = await getCurrentBranch(api);
49
+ return `Use the Task tool to run the "reviewer" agent with this task:
50
+
51
+ Review the changes between "${baseBranch}" and "${currentBranch}".
52
+
53
+ Run \`git diff ${baseBranch}...${currentBranch}\` to see the changes, then analyze the modified files.`;
54
+ }
55
+
56
+ case 2: {
57
+ // Uncommitted changes
58
+ const status = await getGitStatus(api);
59
+ if (!status.trim()) {
60
+ ctx.ui.notify("No uncommitted changes found", "warning");
61
+ return undefined;
62
+ }
63
+
64
+ return `Use the Task tool to run the "reviewer" agent with this task:
65
+
66
+ Review all uncommitted changes in the working directory.
67
+
68
+ Run \`git diff\` for unstaged changes and \`git diff --cached\` for staged changes.`;
69
+ }
70
+
71
+ case 3: {
72
+ // Specific commit
73
+ const commits = await getRecentCommits(api, 20);
74
+ if (commits.length === 0) {
75
+ ctx.ui.notify("No commits found", "error");
76
+ return undefined;
77
+ }
78
+
79
+ const selected = await ctx.ui.select("Select commit to review", commits);
80
+ if (!selected) return undefined;
81
+
82
+ // Extract commit hash from selection (format: "abc1234 message")
83
+ const hash = selected.split(" ")[0];
84
+
85
+ return `Use the Task tool to run the "reviewer" agent with this task:
86
+
87
+ Review commit ${hash}.
88
+
89
+ Run \`git show ${hash}\` to see the changes introduced by this commit.`;
90
+ }
91
+
92
+ case 4: {
93
+ // Custom instructions
94
+ const instructions = await ctx.ui.editor(
95
+ "Enter custom review instructions",
96
+ "Review the following:\n\n",
97
+ );
98
+ if (!instructions?.trim()) return undefined;
99
+
100
+ return `Use the Task tool to run the "reviewer" agent with this task:
101
+
102
+ ${instructions}`;
103
+ }
104
+
105
+ default:
106
+ return undefined;
107
+ }
108
+ },
109
+ };
110
+ }
111
+
112
+ async function getGitBranches(api: CustomCommandAPI): Promise<string[]> {
113
+ try {
114
+ const result = await api.exec("git", ["branch", "-a", "--format=%(refname:short)"]);
115
+ if (result.code !== 0) return [];
116
+ return result.stdout
117
+ .split("\n")
118
+ .map((b) => b.trim())
119
+ .filter(Boolean);
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+
125
+ async function getCurrentBranch(api: CustomCommandAPI): Promise<string> {
126
+ try {
127
+ const result = await api.exec("git", ["branch", "--show-current"]);
128
+ return result.stdout.trim() || "HEAD";
129
+ } catch {
130
+ return "HEAD";
131
+ }
132
+ }
133
+
134
+ async function getGitStatus(api: CustomCommandAPI): Promise<string> {
135
+ try {
136
+ const result = await api.exec("git", ["status", "--porcelain"]);
137
+ return result.stdout;
138
+ } catch {
139
+ return "";
140
+ }
141
+ }
142
+
143
+ async function getRecentCommits(api: CustomCommandAPI, count: number): Promise<string[]> {
144
+ try {
145
+ const result = await api.exec("git", ["log", `-${count}`, "--oneline", "--no-decorate"]);
146
+ if (result.code !== 0) return [];
147
+ return result.stdout
148
+ .split("\n")
149
+ .map((c) => c.trim())
150
+ .filter(Boolean);
151
+ } catch {
152
+ return [];
153
+ }
154
+ }
155
+
156
+ export default createReviewCommand;
@@ -0,0 +1,15 @@
1
+ export {
2
+ type DiscoverCustomCommandsOptions,
3
+ type DiscoverCustomCommandsResult,
4
+ discoverCustomCommands,
5
+ type LoadCustomCommandsOptions,
6
+ loadCustomCommands,
7
+ } from "./loader";
8
+ export type {
9
+ CustomCommand,
10
+ CustomCommandAPI,
11
+ CustomCommandFactory,
12
+ CustomCommandSource,
13
+ CustomCommandsLoadResult,
14
+ LoadedCustomCommand,
15
+ } from "./types";
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Custom command loader - loads TypeScript command modules using native Bun import.
3
+ *
4
+ * Dependencies (@sinclair/typebox and pi-coding-agent) are injected via the CustomCommandAPI
5
+ * to avoid import resolution issues with custom commands loaded from user directories.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import * as typebox from "@sinclair/typebox";
11
+ import { CONFIG_DIR_NAME, getAgentDir } from "../../config";
12
+ import * as piCodingAgent from "../../index";
13
+ import { execCommand } from "../exec";
14
+ import { createReviewCommand } from "./bundled/review";
15
+ import type {
16
+ CustomCommand,
17
+ CustomCommandAPI,
18
+ CustomCommandFactory,
19
+ CustomCommandSource,
20
+ CustomCommandsLoadResult,
21
+ LoadedCustomCommand,
22
+ } from "./types";
23
+
24
+ /**
25
+ * Load a single command module using native Bun import.
26
+ */
27
+ async function loadCommandModule(
28
+ commandPath: string,
29
+ _cwd: string,
30
+ sharedApi: CustomCommandAPI,
31
+ ): Promise<{ commands: CustomCommand[] | null; error: string | null }> {
32
+ try {
33
+ const module = await import(commandPath);
34
+ const factory = (module.default ?? module) as CustomCommandFactory;
35
+
36
+ if (typeof factory !== "function") {
37
+ return { commands: null, error: "Command must export a default function" };
38
+ }
39
+
40
+ const result = await factory(sharedApi);
41
+ const commands = Array.isArray(result) ? result : [result];
42
+
43
+ // Validate commands
44
+ for (const cmd of commands) {
45
+ if (!cmd.name || typeof cmd.name !== "string") {
46
+ return { commands: null, error: "Command must have a name" };
47
+ }
48
+ if (!cmd.description || typeof cmd.description !== "string") {
49
+ return { commands: null, error: `Command "${cmd.name}" must have a description` };
50
+ }
51
+ if (typeof cmd.execute !== "function") {
52
+ return { commands: null, error: `Command "${cmd.name}" must have an execute function` };
53
+ }
54
+ }
55
+
56
+ return { commands, error: null };
57
+ } catch (err) {
58
+ const message = err instanceof Error ? err.message : String(err);
59
+ return { commands: null, error: `Failed to load command: ${message}` };
60
+ }
61
+ }
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
+ export interface DiscoverCustomCommandsOptions {
94
+ /** Current working directory. Default: process.cwd() */
95
+ cwd?: string;
96
+ /** Agent config directory. Default: from getAgentDir() */
97
+ agentDir?: string;
98
+ }
99
+
100
+ export interface DiscoverCustomCommandsResult {
101
+ /** Paths to command modules */
102
+ paths: Array<{ path: string; source: CustomCommandSource }>;
103
+ }
104
+
105
+ /**
106
+ * Discover custom command modules from standard locations:
107
+ * - agentDir/commands/[name]/index.ts (user)
108
+ * - cwd/.pi/commands/[name]/index.ts (project)
109
+ */
110
+ export function discoverCustomCommands(options: DiscoverCustomCommandsOptions = {}): DiscoverCustomCommandsResult {
111
+ const cwd = options.cwd ?? process.cwd();
112
+ const agentDir = options.agentDir ?? getAgentDir();
113
+
114
+ const paths: Array<{ path: string; source: CustomCommandSource }> = [];
115
+ const seen = new Set<string>();
116
+
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
+ }
125
+ };
126
+
127
+ // 1. User commands: agentDir/commands/
128
+ const userCommandsDir = path.join(agentDir, "commands");
129
+ addPaths(discoverCommandsInDir(userCommandsDir), "user");
130
+
131
+ // 2. Project commands: cwd/.pi/commands/
132
+ const projectCommandsDir = path.join(cwd, CONFIG_DIR_NAME, "commands");
133
+ addPaths(discoverCommandsInDir(projectCommandsDir), "project");
134
+
135
+ return { paths };
136
+ }
137
+
138
+ export interface LoadCustomCommandsOptions {
139
+ /** Current working directory. Default: process.cwd() */
140
+ cwd?: string;
141
+ /** Agent config directory. Default: from getAgentDir() */
142
+ agentDir?: string;
143
+ }
144
+
145
+ /**
146
+ * Load bundled commands (shipped with pi-coding-agent).
147
+ */
148
+ function loadBundledCommands(sharedApi: CustomCommandAPI): LoadedCustomCommand[] {
149
+ const bundled: LoadedCustomCommand[] = [];
150
+
151
+ // Add bundled commands here
152
+ const reviewCommand = createReviewCommand(sharedApi);
153
+ bundled.push({
154
+ path: "bundled:review",
155
+ resolvedPath: "bundled:review",
156
+ command: reviewCommand,
157
+ source: "bundled",
158
+ });
159
+
160
+ return bundled;
161
+ }
162
+
163
+ /**
164
+ * Discover and load custom commands from standard locations.
165
+ */
166
+ export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}): Promise<CustomCommandsLoadResult> {
167
+ const cwd = options.cwd ?? process.cwd();
168
+ const agentDir = options.agentDir ?? getAgentDir();
169
+
170
+ const { paths } = discoverCustomCommands({ cwd, agentDir });
171
+
172
+ const commands: LoadedCustomCommand[] = [];
173
+ const errors: Array<{ path: string; error: string }> = [];
174
+ const seenNames = new Set<string>();
175
+
176
+ // Shared API object - all commands get the same instance
177
+ const sharedApi: CustomCommandAPI = {
178
+ cwd,
179
+ exec: (command: string, args: string[], execOptions) =>
180
+ execCommand(command, args, execOptions?.cwd ?? cwd, execOptions),
181
+ typebox,
182
+ pi: piCodingAgent,
183
+ };
184
+
185
+ // 1. Load bundled commands first (lowest priority - can be overridden)
186
+ for (const loaded of loadBundledCommands(sharedApi)) {
187
+ seenNames.add(loaded.command.name);
188
+ commands.push(loaded);
189
+ }
190
+
191
+ // 2. Load user/project commands (can override bundled)
192
+ for (const { path: commandPath, source } of paths) {
193
+ const { commands: loadedCommands, error } = await loadCommandModule(commandPath, cwd, sharedApi);
194
+
195
+ if (error) {
196
+ errors.push({ path: commandPath, error });
197
+ continue;
198
+ }
199
+
200
+ if (loadedCommands) {
201
+ for (const command of loadedCommands) {
202
+ // Allow overriding bundled commands, but not user/project conflicts
203
+ const existingIdx = commands.findIndex((c) => c.command.name === command.name);
204
+ if (existingIdx !== -1) {
205
+ const existing = commands[existingIdx];
206
+ if (existing.source === "bundled") {
207
+ // Override bundled command
208
+ commands.splice(existingIdx, 1);
209
+ seenNames.delete(command.name);
210
+ } else {
211
+ // Conflict between user/project commands
212
+ errors.push({
213
+ path: commandPath,
214
+ error: `Command name "${command.name}" conflicts with existing command`,
215
+ });
216
+ continue;
217
+ }
218
+ }
219
+
220
+ seenNames.add(command.name);
221
+ commands.push({
222
+ path: commandPath,
223
+ resolvedPath: path.resolve(commandPath),
224
+ command,
225
+ source,
226
+ });
227
+ }
228
+ }
229
+ }
230
+
231
+ return { commands, errors };
232
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Custom command types.
3
+ *
4
+ * Custom commands are TypeScript modules that define executable slash commands.
5
+ * Unlike markdown commands which expand to prompts, custom commands can execute
6
+ * arbitrary logic with full access to the hook context.
7
+ */
8
+
9
+ import type { ExecOptions, ExecResult, HookCommandContext } from "../hooks/types";
10
+
11
+ // Re-export for custom commands to use
12
+ export type { ExecOptions, ExecResult, HookCommandContext };
13
+
14
+ /**
15
+ * API passed to custom command factory.
16
+ * Similar to HookAPI but focused on command needs.
17
+ */
18
+ export interface CustomCommandAPI {
19
+ /** Current working directory */
20
+ cwd: string;
21
+ /** Execute a shell command */
22
+ exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
23
+ /** Injected @sinclair/typebox module */
24
+ typebox: typeof import("@sinclair/typebox");
25
+ /** Injected pi-coding-agent exports */
26
+ pi: typeof import("../../index.js");
27
+ }
28
+
29
+ /**
30
+ * Custom command definition.
31
+ *
32
+ * Commands can either:
33
+ * - Return a string to be sent to the LLM as a prompt
34
+ * - Return void/undefined to do nothing (fire-and-forget)
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const factory: CustomCommandFactory = (pi) => ({
39
+ * name: "deploy",
40
+ * description: "Deploy current branch to staging",
41
+ * async execute(args, ctx) {
42
+ * const env = args[0] || "staging";
43
+ * const confirmed = await ctx.ui.confirm("Deploy", `Deploy to ${env}?`);
44
+ * if (!confirmed) return;
45
+ *
46
+ * const result = await pi.exec("./deploy.sh", [env]);
47
+ * if (result.exitCode !== 0) {
48
+ * ctx.ui.notify(`Deploy failed: ${result.stderr}`, "error");
49
+ * return;
50
+ * }
51
+ *
52
+ * ctx.ui.notify("Deploy successful!", "info");
53
+ * // No return = no prompt sent to LLM
54
+ * }
55
+ * });
56
+ * ```
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // Return a prompt to send to the LLM
61
+ * const factory: CustomCommandFactory = (pi) => ({
62
+ * name: "git:status",
63
+ * description: "Show git status and suggest actions",
64
+ * async execute(args, ctx) {
65
+ * const result = await pi.exec("git", ["status", "--porcelain"]);
66
+ * return `Here's the git status:\n\`\`\`\n${result.stdout}\`\`\`\nSuggest what to do next.`;
67
+ * }
68
+ * });
69
+ * ```
70
+ */
71
+ export interface CustomCommand {
72
+ /** Command name (can include namespace like "git:commit") */
73
+ name: string;
74
+ /** Description shown in command autocomplete */
75
+ description: string;
76
+ /**
77
+ * Execute the command.
78
+ * @param args - Parsed command arguments
79
+ * @param ctx - Command context with UI and session control
80
+ * @returns String to send as prompt, or void for fire-and-forget
81
+ */
82
+ execute(args: string[], ctx: HookCommandContext): Promise<string | undefined> | string | undefined;
83
+ }
84
+
85
+ /**
86
+ * Factory function that creates custom command(s).
87
+ * Can return a single command or an array of commands.
88
+ */
89
+ export type CustomCommandFactory = (
90
+ api: CustomCommandAPI,
91
+ ) => CustomCommand | CustomCommand[] | Promise<CustomCommand | CustomCommand[]>;
92
+
93
+ /** Source of a loaded custom command */
94
+ export type CustomCommandSource = "bundled" | "user" | "project";
95
+
96
+ /** Loaded custom command with metadata */
97
+ export interface LoadedCustomCommand {
98
+ /** Original path to the command module */
99
+ path: string;
100
+ /** Resolved absolute path */
101
+ resolvedPath: string;
102
+ /** The command definition */
103
+ command: CustomCommand;
104
+ /** Where the command was loaded from */
105
+ source: CustomCommandSource;
106
+ }
107
+
108
+ /** Result from loading custom commands */
109
+ export interface CustomCommandsLoadResult {
110
+ commands: LoadedCustomCommand[];
111
+ errors: Array<{ path: string; error: string }>;
112
+ }
@@ -2,7 +2,7 @@
2
2
  * Custom tools module.
3
3
  */
4
4
 
5
- export { discoverAndLoadCustomTools, loadCustomTools } from "./loader.js";
5
+ export { discoverAndLoadCustomTools, loadCustomTools } from "./loader";
6
6
  export type {
7
7
  AgentToolResult,
8
8
  AgentToolUpdateCallback,
@@ -17,5 +17,5 @@ export type {
17
17
  ExecResult,
18
18
  LoadedCustomTool,
19
19
  RenderResultOptions,
20
- } from "./types.js";
21
- export { wrapCustomTool, wrapCustomTools } from "./wrapper.js";
20
+ } from "./types";
21
+ export { wrapCustomTool, wrapCustomTools } from "./wrapper";
@@ -9,14 +9,15 @@ import * as fs from "node:fs";
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
  import * as typebox from "@sinclair/typebox";
12
- import { getAgentDir } from "../../config.js";
13
- import * as piCodingAgent from "../../index.js";
14
- import { theme } from "../../modes/interactive/theme/theme.js";
15
- import type { ExecOptions } from "../exec.js";
16
- import { execCommand } from "../exec.js";
17
- import type { HookUIContext } from "../hooks/types.js";
18
- import { getAllPluginToolPaths } from "../plugins/loader.js";
19
- import type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from "./types.js";
12
+ import { getAgentDir } from "../../config";
13
+ import * as piCodingAgent from "../../index";
14
+ import { theme } from "../../modes/interactive/theme/theme";
15
+ import type { ExecOptions } from "../exec";
16
+ import { execCommand } from "../exec";
17
+ import type { HookUIContext } from "../hooks/types";
18
+ import { logger } from "../logger";
19
+ import { getAllPluginToolPaths } from "../plugins/loader";
20
+ import type { CustomToolAPI, CustomToolFactory, CustomToolsLoadResult, LoadedCustomTool } from "./types";
20
21
 
21
22
  const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
22
23
 
@@ -128,6 +129,7 @@ export async function loadCustomTools(
128
129
  execCommand(command, args, options?.cwd ?? cwd, options),
129
130
  ui: createNoOpUIContext(),
130
131
  hasUI: false,
132
+ logger,
131
133
  typebox,
132
134
  pi: piCodingAgent,
133
135
  };
@@ -9,11 +9,12 @@ import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agen
9
9
  import type { Model } from "@oh-my-pi/pi-ai";
10
10
  import type { Component } from "@oh-my-pi/pi-tui";
11
11
  import type { Static, TSchema } from "@sinclair/typebox";
12
- import type { Theme } from "../../modes/interactive/theme/theme.js";
13
- import type { ExecOptions, ExecResult } from "../exec.js";
14
- import type { HookUIContext } from "../hooks/types.js";
15
- import type { ModelRegistry } from "../model-registry.js";
16
- import type { ReadonlySessionManager } from "../session-manager.js";
12
+ import type { Theme } from "../../modes/interactive/theme/theme";
13
+ import type { ExecOptions, ExecResult } from "../exec";
14
+ import type { HookUIContext } from "../hooks/types";
15
+ import type { Logger } from "../logger";
16
+ import type { ModelRegistry } from "../model-registry";
17
+ import type { ReadonlySessionManager } from "../session-manager";
17
18
 
18
19
  /** Alias for clarity */
19
20
  export type CustomToolUIContext = HookUIContext;
@@ -22,7 +23,7 @@ export type CustomToolUIContext = HookUIContext;
22
23
  export type { AgentToolResult, AgentToolUpdateCallback };
23
24
 
24
25
  // Re-export for backward compatibility
25
- export type { ExecOptions, ExecResult } from "../exec.js";
26
+ export type { ExecOptions, ExecResult } from "../exec";
26
27
 
27
28
  /** API passed to custom tool factory (stable across session changes) */
28
29
  export interface CustomToolAPI {
@@ -34,6 +35,8 @@ export interface CustomToolAPI {
34
35
  ui: CustomToolUIContext;
35
36
  /** Whether UI is available (false in print/RPC mode) */
36
37
  hasUI: boolean;
38
+ /** File logger for error/warning/debug messages */
39
+ logger: Logger;
37
40
  /** Injected @sinclair/typebox module */
38
41
  typebox: typeof import("@sinclair/typebox");
39
42
  /** Injected pi-coding-agent exports */
@@ -119,6 +122,8 @@ export interface CustomTool<TParams extends TSchema = TSchema, TDetails = any> {
119
122
  description: string;
120
123
  /** Parameter schema (TypeBox) */
121
124
  parameters: TParams;
125
+ /** If true, tool is excluded unless explicitly listed in --tools or agent's tools field */
126
+ hidden?: boolean;
122
127
 
123
128
  /**
124
129
  * Execute the tool.
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
6
- import type { CustomTool, CustomToolContext, LoadedCustomTool } from "./types.js";
6
+ import type { CustomTool, CustomToolContext, LoadedCustomTool } from "./types";
7
7
 
8
8
  /**
9
9
  * Wrap a CustomTool into an AgentTool.
@@ -15,6 +15,7 @@ export function wrapCustomTool(tool: CustomTool, getContext: () => CustomToolCon
15
15
  label: tool.label,
16
16
  description: tool.description,
17
17
  parameters: tool.parameters,
18
+ hidden: tool.hidden,
18
19
  execute: (toolCallId, params, signal, onUpdate, context) =>
19
20
  tool.execute(toolCallId, params, onUpdate, context ?? getContext(), signal),
20
21
  };