@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.
- package/CHANGELOG.md +73 -0
- package/README.md +1 -1
- package/examples/custom-tools/subagent/index.ts +1 -1
- package/package.json +5 -3
- package/src/cli/args.ts +5 -6
- package/src/cli/file-processor.ts +3 -3
- package/src/cli/list-models.ts +2 -2
- package/src/cli/plugin-cli.ts +1 -1
- package/src/cli/session-picker.ts +2 -2
- package/src/cli.ts +1 -1
- package/src/config.ts +3 -3
- package/src/core/agent-session.ts +157 -15
- package/src/core/bash-executor.ts +50 -10
- package/src/core/compaction/branch-summarization.ts +5 -5
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/compaction/index.ts +3 -3
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +232 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +3 -3
- package/src/core/custom-tools/loader.ts +10 -8
- package/src/core/custom-tools/types.ts +11 -6
- package/src/core/custom-tools/wrapper.ts +2 -1
- package/src/core/exec.ts +22 -12
- package/src/core/export-html/index.ts +5 -5
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +5 -5
- package/src/core/hooks/loader.ts +21 -16
- package/src/core/hooks/runner.ts +6 -6
- package/src/core/hooks/tool-wrapper.ts +2 -2
- package/src/core/hooks/types.ts +12 -15
- package/src/core/index.ts +6 -6
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +3 -3
- package/src/core/mcp/config.ts +1 -1
- package/src/core/mcp/index.ts +12 -12
- package/src/core/mcp/loader.ts +2 -2
- package/src/core/mcp/manager.ts +6 -6
- package/src/core/mcp/tool-bridge.ts +3 -3
- package/src/core/mcp/transports/http.ts +1 -1
- package/src/core/mcp/transports/index.ts +2 -2
- package/src/core/mcp/transports/stdio.ts +1 -1
- package/src/core/messages.ts +22 -0
- package/src/core/model-registry.ts +2 -2
- package/src/core/model-resolver.ts +2 -2
- package/src/core/plugins/doctor.ts +1 -1
- package/src/core/plugins/index.ts +6 -6
- package/src/core/plugins/installer.ts +4 -4
- package/src/core/plugins/loader.ts +4 -9
- package/src/core/plugins/manager.ts +5 -5
- package/src/core/plugins/paths.ts +3 -3
- package/src/core/sdk.ts +77 -35
- package/src/core/session-manager.ts +6 -6
- package/src/core/settings-manager.ts +16 -3
- package/src/core/skills.ts +5 -5
- package/src/core/slash-commands.ts +60 -45
- package/src/core/system-prompt.ts +6 -6
- package/src/core/title-generator.ts +2 -2
- package/src/core/tools/bash.ts +32 -155
- package/src/core/tools/context.ts +2 -2
- package/src/core/tools/edit-diff.ts +3 -3
- package/src/core/tools/edit.ts +18 -5
- package/src/core/tools/exa/company.ts +3 -3
- package/src/core/tools/exa/index.ts +16 -17
- package/src/core/tools/exa/linkedin.ts +3 -3
- package/src/core/tools/exa/mcp-client.ts +9 -9
- package/src/core/tools/exa/render.ts +5 -5
- package/src/core/tools/exa/researcher.ts +3 -3
- package/src/core/tools/exa/search.ts +6 -5
- package/src/core/tools/exa/types.ts +5 -6
- package/src/core/tools/exa/websets.ts +3 -3
- package/src/core/tools/find.ts +3 -3
- package/src/core/tools/grep.ts +3 -3
- package/src/core/tools/index.ts +48 -34
- package/src/core/tools/ls.ts +4 -4
- package/src/core/tools/lsp/client.ts +161 -90
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/edits.ts +2 -2
- package/src/core/tools/lsp/index.ts +15 -13
- package/src/core/tools/lsp/render.ts +2 -2
- package/src/core/tools/lsp/rust-analyzer.ts +3 -3
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +175 -0
- package/src/core/tools/read.ts +7 -7
- package/src/core/tools/renderers.ts +92 -13
- package/src/core/tools/review.ts +268 -0
- package/src/core/tools/task/agents.ts +1 -1
- package/src/core/tools/task/bundled-agents/reviewer.md +52 -37
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +145 -28
- package/src/core/tools/task/index.ts +78 -30
- package/src/core/tools/task/model-resolver.ts +30 -20
- package/src/core/tools/task/parallel.ts +1 -1
- package/src/core/tools/task/render.ts +219 -30
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +36 -2
- package/src/core/tools/web-fetch.ts +5 -3
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/web-search/index.ts +17 -15
- package/src/core/tools/web-search/providers/anthropic.ts +2 -2
- package/src/core/tools/web-search/providers/exa.ts +3 -5
- package/src/core/tools/web-search/providers/perplexity.ts +1 -1
- package/src/core/tools/web-search/render.ts +3 -3
- package/src/core/tools/write.ts +4 -4
- package/src/index.ts +29 -18
- package/src/main.ts +37 -32
- package/src/migrations.ts +3 -3
- package/src/modes/index.ts +5 -5
- package/src/modes/interactive/components/armin.ts +1 -1
- package/src/modes/interactive/components/assistant-message.ts +1 -1
- package/src/modes/interactive/components/bash-execution.ts +4 -4
- package/src/modes/interactive/components/bordered-loader.ts +2 -2
- package/src/modes/interactive/components/branch-summary-message.ts +2 -2
- package/src/modes/interactive/components/compaction-summary-message.ts +2 -2
- package/src/modes/interactive/components/diff.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/footer.ts +5 -5
- package/src/modes/interactive/components/hook-editor.ts +2 -2
- package/src/modes/interactive/components/hook-input.ts +2 -2
- package/src/modes/interactive/components/hook-message.ts +3 -3
- package/src/modes/interactive/components/hook-selector.ts +2 -2
- package/src/modes/interactive/components/model-selector.ts +281 -59
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/plugin-settings.ts +4 -4
- package/src/modes/interactive/components/queue-mode-selector.ts +2 -2
- package/src/modes/interactive/components/session-selector.ts +4 -4
- package/src/modes/interactive/components/settings-defs.ts +1 -1
- package/src/modes/interactive/components/settings-selector.ts +5 -5
- package/src/modes/interactive/components/show-images-selector.ts +2 -2
- package/src/modes/interactive/components/theme-selector.ts +2 -2
- package/src/modes/interactive/components/thinking-selector.ts +2 -2
- package/src/modes/interactive/components/tool-execution.ts +26 -8
- package/src/modes/interactive/components/tree-selector.ts +3 -3
- package/src/modes/interactive/components/user-message-selector.ts +2 -2
- package/src/modes/interactive/components/user-message.ts +1 -1
- package/src/modes/interactive/components/welcome.ts +2 -2
- package/src/modes/interactive/interactive-mode.ts +85 -41
- package/src/modes/interactive/theme/theme.ts +8 -7
- package/src/modes/print-mode.ts +4 -3
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/modes/rpc/rpc-mode.ts +21 -11
- package/src/modes/rpc/rpc-types.ts +3 -3
- package/src/utils/changelog.ts +2 -2
- package/src/utils/clipboard.ts +1 -1
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +93 -13
- package/src/utils/tools-manager.ts +1 -1
- package/examples/custom-tools/subagent/agents/reviewer.md +0 -35
- 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
|
|
17
|
-
import type { ReadonlySessionManager, SessionEntry } from "../session-manager
|
|
18
|
-
import { estimateTokens } from "./compaction
|
|
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
|
|
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
|
|
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
|
|
12
|
-
import type { CompactionEntry, SessionEntry } from "../session-manager
|
|
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
|
|
21
|
+
} from "./utils";
|
|
22
22
|
|
|
23
23
|
// ============================================================================
|
|
24
24
|
// File Operation Tracking
|
|
@@ -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
|
|
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
|
|
21
|
-
export { wrapCustomTool, wrapCustomTools } from "./wrapper
|
|
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
|
|
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 {
|
|
19
|
-
import
|
|
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
|
|
13
|
-
import type { ExecOptions, ExecResult } from "../exec
|
|
14
|
-
import type { HookUIContext } from "../hooks/types
|
|
15
|
-
import type {
|
|
16
|
-
import type {
|
|
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
|
|
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
|
|
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
|
};
|