@oh-my-pi/pi-coding-agent 8.0.16 → 8.1.0
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 +105 -0
- package/package.json +14 -11
- package/scripts/generate-wasm-b64.ts +24 -0
- package/src/capability/context-file.ts +1 -1
- package/src/capability/extension-module.ts +1 -1
- package/src/capability/extension.ts +1 -1
- package/src/capability/hook.ts +1 -1
- package/src/capability/instruction.ts +1 -1
- package/src/capability/mcp.ts +1 -1
- package/src/capability/prompt.ts +1 -1
- package/src/capability/rule.ts +1 -1
- package/src/capability/settings.ts +1 -1
- package/src/capability/skill.ts +1 -1
- package/src/capability/slash-command.ts +1 -1
- package/src/capability/ssh.ts +1 -1
- package/src/capability/system-prompt.ts +1 -1
- package/src/capability/tool.ts +1 -1
- package/src/cli/args.ts +1 -1
- package/src/cli/plugin-cli.ts +1 -5
- package/src/commit/agentic/agent.ts +309 -0
- package/src/commit/agentic/fallback.ts +96 -0
- package/src/commit/agentic/index.ts +359 -0
- package/src/commit/agentic/prompts/analyze-file.md +22 -0
- package/src/commit/agentic/prompts/session-user.md +26 -0
- package/src/commit/agentic/prompts/split-confirm.md +1 -0
- package/src/commit/agentic/prompts/system.md +40 -0
- package/src/commit/agentic/state.ts +74 -0
- package/src/commit/agentic/tools/analyze-file.ts +131 -0
- package/src/commit/agentic/tools/git-file-diff.ts +194 -0
- package/src/commit/agentic/tools/git-hunk.ts +50 -0
- package/src/commit/agentic/tools/git-overview.ts +84 -0
- package/src/commit/agentic/tools/index.ts +56 -0
- package/src/commit/agentic/tools/propose-changelog.ts +128 -0
- package/src/commit/agentic/tools/propose-commit.ts +154 -0
- package/src/commit/agentic/tools/recent-commits.ts +81 -0
- package/src/commit/agentic/tools/split-commit.ts +284 -0
- package/src/commit/agentic/topo-sort.ts +44 -0
- package/src/commit/agentic/trivial.ts +51 -0
- package/src/commit/agentic/validation.ts +200 -0
- package/src/commit/analysis/conventional.ts +169 -0
- package/src/commit/analysis/index.ts +4 -0
- package/src/commit/analysis/scope.ts +242 -0
- package/src/commit/analysis/summary.ts +114 -0
- package/src/commit/analysis/validation.ts +66 -0
- package/src/commit/changelog/detect.ts +36 -0
- package/src/commit/changelog/generate.ts +112 -0
- package/src/commit/changelog/index.ts +233 -0
- package/src/commit/changelog/parse.ts +44 -0
- package/src/commit/cli.ts +93 -0
- package/src/commit/git/diff.ts +148 -0
- package/src/commit/git/errors.ts +11 -0
- package/src/commit/git/index.ts +217 -0
- package/src/commit/git/operations.ts +53 -0
- package/src/commit/index.ts +5 -0
- package/src/commit/map-reduce/.map-phase.ts.kate-swp +0 -0
- package/src/commit/map-reduce/index.ts +63 -0
- package/src/commit/map-reduce/map-phase.ts +193 -0
- package/src/commit/map-reduce/reduce-phase.ts +147 -0
- package/src/commit/map-reduce/utils.ts +9 -0
- package/src/commit/message.ts +11 -0
- package/src/commit/model-selection.ts +84 -0
- package/src/commit/pipeline.ts +242 -0
- package/src/commit/prompts/analysis-system.md +155 -0
- package/src/commit/prompts/analysis-user.md +41 -0
- package/src/commit/prompts/changelog-system.md +56 -0
- package/src/commit/prompts/changelog-user.md +19 -0
- package/src/commit/prompts/file-observer-system.md +26 -0
- package/src/commit/prompts/file-observer-user.md +9 -0
- package/src/commit/prompts/reduce-system.md +60 -0
- package/src/commit/prompts/reduce-user.md +17 -0
- package/src/commit/prompts/summary-retry.md +4 -0
- package/src/commit/prompts/summary-system.md +52 -0
- package/src/commit/prompts/summary-user.md +13 -0
- package/src/commit/prompts/types-description.md +2 -0
- package/src/commit/types.ts +109 -0
- package/src/commit/utils/exclusions.ts +42 -0
- package/src/config/file-lock.ts +111 -0
- package/src/config/model-registry.ts +16 -7
- package/src/config/settings-manager.ts +115 -40
- package/src/config.ts +5 -5
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/builtin.ts +1 -1
- package/src/discovery/claude.ts +1 -1
- package/src/discovery/cline.ts +1 -1
- package/src/discovery/codex.ts +1 -1
- package/src/discovery/cursor.ts +1 -1
- package/src/discovery/gemini.ts +1 -1
- package/src/discovery/github.ts +1 -1
- package/src/discovery/index.ts +11 -11
- package/src/discovery/mcp-json.ts +1 -1
- package/src/discovery/ssh.ts +1 -1
- package/src/discovery/vscode.ts +1 -1
- package/src/discovery/windsurf.ts +1 -1
- package/src/extensibility/custom-commands/loader.ts +1 -1
- package/src/extensibility/custom-commands/types.ts +1 -1
- package/src/extensibility/custom-tools/loader.ts +1 -1
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/loader.ts +1 -1
- package/src/extensibility/extensions/types.ts +1 -1
- package/src/extensibility/hooks/loader.ts +1 -1
- package/src/extensibility/hooks/types.ts +3 -3
- package/src/index.ts +10 -10
- package/src/ipy/executor.ts +97 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/render.ts +90 -46
- package/src/main.ts +16 -3
- package/src/mcp/loader.ts +3 -3
- package/src/migrations.ts +3 -3
- package/src/modes/components/assistant-message.ts +29 -1
- package/src/modes/components/tool-execution.ts +5 -3
- package/src/modes/components/tree-selector.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -3
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-mode.ts +1 -4
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/theme/mermaid-cache.ts +89 -0
- package/src/modes/theme/theme.ts +2 -0
- package/src/modes/types.ts +2 -2
- package/src/patch/index.ts +3 -9
- package/src/patch/shared.ts +33 -5
- package/src/prompts/tools/task.md +2 -0
- package/src/sdk.ts +60 -22
- package/src/session/agent-session.ts +3 -3
- package/src/session/agent-storage.ts +32 -28
- package/src/session/artifacts.ts +24 -1
- package/src/session/auth-storage.ts +25 -10
- package/src/session/storage-migration.ts +12 -53
- package/src/system-prompt.ts +2 -2
- package/src/task/.executor.ts.kate-swp +0 -0
- package/src/task/executor.ts +1 -1
- package/src/task/index.ts +10 -1
- package/src/task/output-manager.ts +94 -0
- package/src/task/render.ts +7 -12
- package/src/task/worker.ts +1 -1
- package/src/tools/ask.ts +35 -13
- package/src/tools/bash.ts +80 -87
- package/src/tools/calculator.ts +42 -40
- package/src/tools/complete.ts +1 -1
- package/src/tools/fetch.ts +67 -104
- package/src/tools/find.ts +83 -86
- package/src/tools/grep.ts +80 -96
- package/src/tools/index.ts +10 -7
- package/src/tools/ls.ts +39 -65
- package/src/tools/notebook.ts +48 -64
- package/src/tools/output-utils.ts +1 -1
- package/src/tools/python.ts +71 -183
- package/src/tools/read.ts +74 -15
- package/src/tools/render-utils.ts +1 -15
- package/src/tools/ssh.ts +43 -24
- package/src/tools/todo-write.ts +27 -15
- package/src/tools/write.ts +93 -64
- package/src/tui/code-cell.ts +115 -0
- package/src/tui/file-list.ts +48 -0
- package/src/tui/index.ts +11 -0
- package/src/tui/output-block.ts +73 -0
- package/src/tui/status-line.ts +40 -0
- package/src/tui/tree-list.ts +56 -0
- package/src/tui/types.ts +17 -0
- package/src/tui/utils.ts +49 -0
- package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
- package/src/web/search/auth.ts +1 -1
- package/src/web/search/index.ts +1 -1
- package/src/web/search/render.ts +119 -163
- package/tsconfig.json +0 -42
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import analyzeFilePrompt from "@oh-my-pi/pi-coding-agent/commit/agentic/prompts/analyze-file.md" with { type: "text" };
|
|
2
|
+
import type { CommitAgentState } from "@oh-my-pi/pi-coding-agent/commit/agentic/state";
|
|
3
|
+
import { getFilePriority } from "@oh-my-pi/pi-coding-agent/commit/agentic/tools/git-file-diff";
|
|
4
|
+
import type { NumstatEntry } from "@oh-my-pi/pi-coding-agent/commit/types";
|
|
5
|
+
import type { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
|
6
|
+
import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
|
|
7
|
+
import type { SettingsManager } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
|
|
8
|
+
import type { CustomTool, CustomToolContext } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
|
|
9
|
+
import type { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
|
10
|
+
import { TaskTool } from "@oh-my-pi/pi-coding-agent/task";
|
|
11
|
+
import type { TaskParams } from "@oh-my-pi/pi-coding-agent/task/types";
|
|
12
|
+
import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
|
|
13
|
+
import { Type } from "@sinclair/typebox";
|
|
14
|
+
|
|
15
|
+
const analyzeFileSchema = Type.Object({
|
|
16
|
+
files: Type.Array(Type.String({ description: "File path" }), { minItems: 1 }),
|
|
17
|
+
goal: Type.Optional(Type.String({ description: "Optional analysis focus" })),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const analyzeFileOutputSchema = {
|
|
21
|
+
properties: {
|
|
22
|
+
summary: { type: "string" },
|
|
23
|
+
highlights: { elements: { type: "string" } },
|
|
24
|
+
risks: { elements: { type: "string" } },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function buildToolSession(
|
|
29
|
+
ctx: CustomToolContext,
|
|
30
|
+
options: {
|
|
31
|
+
cwd: string;
|
|
32
|
+
authStorage: AuthStorage;
|
|
33
|
+
modelRegistry: ModelRegistry;
|
|
34
|
+
settingsManager: SettingsManager;
|
|
35
|
+
spawns: string;
|
|
36
|
+
},
|
|
37
|
+
): ToolSession {
|
|
38
|
+
const sessionFile = () => ctx.sessionManager.getSessionFile() ?? null;
|
|
39
|
+
return {
|
|
40
|
+
cwd: options.cwd,
|
|
41
|
+
hasUI: false,
|
|
42
|
+
getSessionFile: sessionFile,
|
|
43
|
+
getSessionSpawns: () => options.spawns,
|
|
44
|
+
settings: options.settingsManager,
|
|
45
|
+
settingsManager: options.settingsManager,
|
|
46
|
+
authStorage: options.authStorage,
|
|
47
|
+
modelRegistry: options.modelRegistry,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createAnalyzeFileTool(options: {
|
|
52
|
+
cwd: string;
|
|
53
|
+
authStorage: AuthStorage;
|
|
54
|
+
modelRegistry: ModelRegistry;
|
|
55
|
+
settingsManager: SettingsManager;
|
|
56
|
+
spawns: string;
|
|
57
|
+
state: CommitAgentState;
|
|
58
|
+
}): CustomTool<typeof analyzeFileSchema> {
|
|
59
|
+
return {
|
|
60
|
+
name: "analyze_files",
|
|
61
|
+
label: "Analyze Files",
|
|
62
|
+
description: "Spawn quick_task agents to analyze files.",
|
|
63
|
+
parameters: analyzeFileSchema,
|
|
64
|
+
async execute(toolCallId, params, onUpdate, ctx, signal) {
|
|
65
|
+
const toolSession = buildToolSession(ctx, options);
|
|
66
|
+
const taskTool = await TaskTool.create(toolSession);
|
|
67
|
+
const context = "{{prompt}}";
|
|
68
|
+
const numstat = options.state.overview?.numstat ?? [];
|
|
69
|
+
const tasks = params.files.map((file, index) => {
|
|
70
|
+
const relatedFiles = formatRelatedFiles(params.files, file, numstat);
|
|
71
|
+
const prompt = renderPromptTemplate(analyzeFilePrompt, {
|
|
72
|
+
file,
|
|
73
|
+
goal: params.goal,
|
|
74
|
+
related_files: relatedFiles,
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
id: `AnalyzeFile${index + 1}`,
|
|
78
|
+
description: `Analyze ${file}`,
|
|
79
|
+
args: { prompt },
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
const taskParams: TaskParams = {
|
|
83
|
+
agent: "quick_task",
|
|
84
|
+
context,
|
|
85
|
+
output: analyzeFileOutputSchema,
|
|
86
|
+
tasks,
|
|
87
|
+
};
|
|
88
|
+
return taskTool.execute(toolCallId, taskParams, signal, onUpdate);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function inferFileType(path: string): string {
|
|
94
|
+
const priority = getFilePriority(path);
|
|
95
|
+
const lowerPath = path.toLowerCase();
|
|
96
|
+
|
|
97
|
+
if (priority === -100) return "binary file";
|
|
98
|
+
if (priority === 10) return "test file";
|
|
99
|
+
if (lowerPath.endsWith(".md") || lowerPath.endsWith(".txt")) return "documentation";
|
|
100
|
+
if (
|
|
101
|
+
lowerPath.endsWith(".json") ||
|
|
102
|
+
lowerPath.endsWith(".yaml") ||
|
|
103
|
+
lowerPath.endsWith(".yml") ||
|
|
104
|
+
lowerPath.endsWith(".toml")
|
|
105
|
+
)
|
|
106
|
+
return "configuration";
|
|
107
|
+
if (priority === 70) return "dependency manifest";
|
|
108
|
+
if (priority === 80) return "script";
|
|
109
|
+
if (priority === 100) return "implementation";
|
|
110
|
+
|
|
111
|
+
return "source file";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatRelatedFiles(files: string[], currentFile: string, numstat: NumstatEntry[]): string | undefined {
|
|
115
|
+
const others = files.filter((file) => file !== currentFile);
|
|
116
|
+
if (others.length === 0) return undefined;
|
|
117
|
+
|
|
118
|
+
const numstatMap = new Map(numstat.map((entry) => [entry.path, entry]));
|
|
119
|
+
|
|
120
|
+
const lines = others.map((file) => {
|
|
121
|
+
const entry = numstatMap.get(file);
|
|
122
|
+
const fileType = inferFileType(file);
|
|
123
|
+
if (entry) {
|
|
124
|
+
const lineCount = entry.additions + entry.deletions;
|
|
125
|
+
return `- ${file} (${lineCount} lines): ${fileType}`;
|
|
126
|
+
}
|
|
127
|
+
return `- ${file}: ${fileType}`;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return `OTHER FILES IN THIS CHANGE:\n${lines.join("\n")}`;
|
|
131
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { CommitAgentState } from "@oh-my-pi/pi-coding-agent/commit/agentic/state";
|
|
2
|
+
import type { ControlledGit } from "@oh-my-pi/pi-coding-agent/commit/git";
|
|
3
|
+
import type { CustomTool } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
|
|
6
|
+
const TARGET_TOKENS = 30000;
|
|
7
|
+
const CHARS_PER_TOKEN = 4;
|
|
8
|
+
const MAX_CHARS = TARGET_TOKENS * CHARS_PER_TOKEN;
|
|
9
|
+
const TRUNCATE_THRESHOLD_LINES = 30;
|
|
10
|
+
const KEEP_HEAD_LINES = 15;
|
|
11
|
+
const KEEP_TAIL_LINES = 10;
|
|
12
|
+
|
|
13
|
+
const HIGH_PRIORITY_EXTENSIONS = new Set([
|
|
14
|
+
".rs",
|
|
15
|
+
".go",
|
|
16
|
+
".py",
|
|
17
|
+
".js",
|
|
18
|
+
".ts",
|
|
19
|
+
".tsx",
|
|
20
|
+
".jsx",
|
|
21
|
+
".java",
|
|
22
|
+
".c",
|
|
23
|
+
".cpp",
|
|
24
|
+
".h",
|
|
25
|
+
".hpp",
|
|
26
|
+
]);
|
|
27
|
+
const SHELL_SQL_EXTENSIONS = new Set([".sh", ".bash", ".zsh", ".sql"]);
|
|
28
|
+
const MANIFEST_FILES = new Set([
|
|
29
|
+
"Cargo.toml",
|
|
30
|
+
"package.json",
|
|
31
|
+
"go.mod",
|
|
32
|
+
"pyproject.toml",
|
|
33
|
+
"requirements.txt",
|
|
34
|
+
"Gemfile",
|
|
35
|
+
"build.gradle",
|
|
36
|
+
"pom.xml",
|
|
37
|
+
]);
|
|
38
|
+
const LOW_PRIORITY_EXTENSIONS = new Set([".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".xml", ".csv"]);
|
|
39
|
+
const BINARY_EXTENSIONS = new Set([
|
|
40
|
+
".png",
|
|
41
|
+
".jpg",
|
|
42
|
+
".jpeg",
|
|
43
|
+
".gif",
|
|
44
|
+
".ico",
|
|
45
|
+
".woff",
|
|
46
|
+
".woff2",
|
|
47
|
+
".ttf",
|
|
48
|
+
".eot",
|
|
49
|
+
".pdf",
|
|
50
|
+
".zip",
|
|
51
|
+
".tar",
|
|
52
|
+
".gz",
|
|
53
|
+
".exe",
|
|
54
|
+
".dll",
|
|
55
|
+
".so",
|
|
56
|
+
".dylib",
|
|
57
|
+
]);
|
|
58
|
+
const TEST_PATTERNS = ["/test/", "/tests/", "/__tests__/", "_test.", ".test.", ".spec.", "_spec."];
|
|
59
|
+
|
|
60
|
+
export function getFilePriority(filename: string): number {
|
|
61
|
+
const basename = filename.split("/").pop() ?? filename;
|
|
62
|
+
const ext = basename.includes(".") ? `.${basename.split(".").pop()}` : "";
|
|
63
|
+
|
|
64
|
+
if (BINARY_EXTENSIONS.has(ext)) return -100;
|
|
65
|
+
|
|
66
|
+
const lowerPath = filename.toLowerCase();
|
|
67
|
+
for (const pattern of TEST_PATTERNS) {
|
|
68
|
+
if (lowerPath.includes(pattern)) return 10;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (LOW_PRIORITY_EXTENSIONS.has(ext) && !MANIFEST_FILES.has(basename)) return 20;
|
|
72
|
+
if (MANIFEST_FILES.has(basename)) return 70;
|
|
73
|
+
if (SHELL_SQL_EXTENSIONS.has(ext)) return 80;
|
|
74
|
+
if (HIGH_PRIORITY_EXTENSIONS.has(ext)) return 100;
|
|
75
|
+
|
|
76
|
+
return 50;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function truncateDiffContent(diff: string): { content: string; truncated: boolean } {
|
|
80
|
+
const lines = diff.split("\n");
|
|
81
|
+
if (lines.length <= TRUNCATE_THRESHOLD_LINES) {
|
|
82
|
+
return { content: diff, truncated: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const head = lines.slice(0, KEEP_HEAD_LINES);
|
|
86
|
+
const tail = lines.slice(-KEEP_TAIL_LINES);
|
|
87
|
+
const truncatedCount = lines.length - KEEP_HEAD_LINES - KEEP_TAIL_LINES;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
content: [...head, `\n... (truncated ${truncatedCount} lines) ...\n`, ...tail].join("\n"),
|
|
91
|
+
truncated: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function processDiffs(files: string[], diffs: Map<string, string>): { result: string; truncatedFiles: string[] } {
|
|
96
|
+
const sortedFiles = [...files].sort((a, b) => getFilePriority(b) - getFilePriority(a));
|
|
97
|
+
|
|
98
|
+
const truncatedFiles: string[] = [];
|
|
99
|
+
const parts: string[] = [];
|
|
100
|
+
let totalChars = 0;
|
|
101
|
+
|
|
102
|
+
for (const file of sortedFiles) {
|
|
103
|
+
const diff = diffs.get(file);
|
|
104
|
+
if (!diff) continue;
|
|
105
|
+
|
|
106
|
+
const remaining = MAX_CHARS - totalChars;
|
|
107
|
+
if (remaining <= 0) {
|
|
108
|
+
truncatedFiles.push(file);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let content = diff;
|
|
113
|
+
if (content.length > remaining || content.split("\n").length > TRUNCATE_THRESHOLD_LINES) {
|
|
114
|
+
const { content: truncated, truncated: wasTruncated } = truncateDiffContent(content);
|
|
115
|
+
if (wasTruncated) {
|
|
116
|
+
truncatedFiles.push(file);
|
|
117
|
+
}
|
|
118
|
+
content = truncated;
|
|
119
|
+
if (content.length > remaining) {
|
|
120
|
+
content = `${content.slice(0, remaining)}\n... (diff truncated due to size) ...`;
|
|
121
|
+
if (!truncatedFiles.includes(file)) {
|
|
122
|
+
truncatedFiles.push(file);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
parts.push(`=== ${file} ===\n${content}`);
|
|
128
|
+
totalChars += content.length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { result: parts.join("\n\n"), truncatedFiles };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const gitFileDiffSchema = Type.Object({
|
|
135
|
+
files: Type.Array(Type.String({ description: "Files to diff" }), { minItems: 1, maxItems: 10 }),
|
|
136
|
+
staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export function createGitFileDiffTool(
|
|
140
|
+
git: ControlledGit,
|
|
141
|
+
state: CommitAgentState,
|
|
142
|
+
): CustomTool<typeof gitFileDiffSchema> {
|
|
143
|
+
return {
|
|
144
|
+
name: "git_file_diff",
|
|
145
|
+
label: "Git File Diff",
|
|
146
|
+
description: "Return the diff for specific files.",
|
|
147
|
+
parameters: gitFileDiffSchema,
|
|
148
|
+
async execute(_toolCallId, params) {
|
|
149
|
+
const staged = params.staged ?? true;
|
|
150
|
+
const cacheKey = (file: string) => `${file}:${staged}`;
|
|
151
|
+
|
|
152
|
+
if (!state.diffCache) {
|
|
153
|
+
state.diffCache = new Map();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const diffs = new Map<string, string>();
|
|
157
|
+
const uncachedFiles: string[] = [];
|
|
158
|
+
|
|
159
|
+
for (const file of params.files) {
|
|
160
|
+
const cached = state.diffCache.get(cacheKey(file));
|
|
161
|
+
if (cached !== undefined) {
|
|
162
|
+
diffs.set(file, cached);
|
|
163
|
+
} else {
|
|
164
|
+
uncachedFiles.push(file);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (uncachedFiles.length > 0) {
|
|
169
|
+
for (const file of uncachedFiles) {
|
|
170
|
+
const diff = await git.getDiffForFiles([file], staged);
|
|
171
|
+
if (diff) {
|
|
172
|
+
diffs.set(file, diff);
|
|
173
|
+
state.diffCache.set(cacheKey(file), diff);
|
|
174
|
+
} else {
|
|
175
|
+
state.diffCache.set(cacheKey(file), "");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const { result, truncatedFiles } = processDiffs(params.files, diffs);
|
|
181
|
+
const output = result || "(no diff)";
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: output }],
|
|
185
|
+
details: {
|
|
186
|
+
files: params.files,
|
|
187
|
+
staged,
|
|
188
|
+
truncatedFiles: truncatedFiles.length > 0 ? truncatedFiles : undefined,
|
|
189
|
+
cacheHits: params.files.length - uncachedFiles.length,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ControlledGit } from "@oh-my-pi/pi-coding-agent/commit/git";
|
|
2
|
+
import type { DiffHunk, FileHunks } from "@oh-my-pi/pi-coding-agent/commit/types";
|
|
3
|
+
import type { CustomTool } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
|
|
6
|
+
const gitHunkSchema = Type.Object({
|
|
7
|
+
file: Type.String({ description: "File path" }),
|
|
8
|
+
hunks: Type.Optional(Type.Array(Type.Number({ description: "1-based hunk indices" }), { minItems: 1 })),
|
|
9
|
+
staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function selectHunks(fileHunks: FileHunks, requested?: number[]): DiffHunk[] {
|
|
13
|
+
if (!requested || requested.length === 0) return fileHunks.hunks;
|
|
14
|
+
const wanted = new Set(requested.map((value) => Math.max(1, Math.floor(value))));
|
|
15
|
+
return fileHunks.hunks.filter((hunk) => wanted.has(hunk.index + 1));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createGitHunkTool(git: ControlledGit): CustomTool<typeof gitHunkSchema> {
|
|
19
|
+
return {
|
|
20
|
+
name: "git_hunk",
|
|
21
|
+
label: "Git Hunk",
|
|
22
|
+
description: "Return specific hunks from a file diff.",
|
|
23
|
+
parameters: gitHunkSchema,
|
|
24
|
+
async execute(_toolCallId, params) {
|
|
25
|
+
const staged = params.staged ?? true;
|
|
26
|
+
const hunks = await git.getHunks([params.file], staged);
|
|
27
|
+
const fileHunks = hunks.find((entry) => entry.filename === params.file) ?? {
|
|
28
|
+
filename: params.file,
|
|
29
|
+
isBinary: false,
|
|
30
|
+
hunks: [],
|
|
31
|
+
};
|
|
32
|
+
if (fileHunks.isBinary) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: "Binary file diff; no hunks available." }],
|
|
35
|
+
details: { file: params.file, staged, hunks: [] },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const selected = selectHunks(fileHunks, params.hunks);
|
|
39
|
+
const text = selected.length ? selected.map((hunk) => hunk.content).join("\n\n") : "(no matching hunks)";
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: "text", text }],
|
|
42
|
+
details: {
|
|
43
|
+
file: params.file,
|
|
44
|
+
staged,
|
|
45
|
+
hunks: selected,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { CommitAgentState, GitOverviewSnapshot } from "@oh-my-pi/pi-coding-agent/commit/agentic/state";
|
|
2
|
+
import { extractScopeCandidates } from "@oh-my-pi/pi-coding-agent/commit/analysis/scope";
|
|
3
|
+
import type { ControlledGit } from "@oh-my-pi/pi-coding-agent/commit/git";
|
|
4
|
+
import type { CustomTool } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
|
|
5
|
+
import { Type } from "@sinclair/typebox";
|
|
6
|
+
|
|
7
|
+
const EXCLUDED_LOCK_FILES = new Set([
|
|
8
|
+
"Cargo.lock",
|
|
9
|
+
"package-lock.json",
|
|
10
|
+
"yarn.lock",
|
|
11
|
+
"pnpm-lock.yaml",
|
|
12
|
+
"bun.lock",
|
|
13
|
+
"bun.lockb",
|
|
14
|
+
"go.sum",
|
|
15
|
+
"poetry.lock",
|
|
16
|
+
"Pipfile.lock",
|
|
17
|
+
"uv.lock",
|
|
18
|
+
"composer.lock",
|
|
19
|
+
"Gemfile.lock",
|
|
20
|
+
"flake.lock",
|
|
21
|
+
"pubspec.lock",
|
|
22
|
+
"Podfile.lock",
|
|
23
|
+
"mix.lock",
|
|
24
|
+
"gradle.lockfile",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function isExcludedFile(path: string): boolean {
|
|
28
|
+
const basename = path.split("/").pop() ?? path;
|
|
29
|
+
return EXCLUDED_LOCK_FILES.has(basename);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function filterExcludedFiles(files: string[]): { filtered: string[]; excluded: string[] } {
|
|
33
|
+
const filtered: string[] = [];
|
|
34
|
+
const excluded: string[] = [];
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
if (isExcludedFile(file)) {
|
|
37
|
+
excluded.push(file);
|
|
38
|
+
} else {
|
|
39
|
+
filtered.push(file);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { filtered, excluded };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const gitOverviewSchema = Type.Object({
|
|
46
|
+
staged: Type.Optional(Type.Boolean({ description: "Use staged changes (default: true)" })),
|
|
47
|
+
include_untracked: Type.Optional(Type.Boolean({ description: "Include untracked files when staged=false" })),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export function createGitOverviewTool(
|
|
51
|
+
git: ControlledGit,
|
|
52
|
+
state: CommitAgentState,
|
|
53
|
+
): CustomTool<typeof gitOverviewSchema> {
|
|
54
|
+
return {
|
|
55
|
+
name: "git_overview",
|
|
56
|
+
label: "Git Overview",
|
|
57
|
+
description: "Return staged files, diff stat summary, and numstat entries.",
|
|
58
|
+
parameters: gitOverviewSchema,
|
|
59
|
+
async execute(_toolCallId, params) {
|
|
60
|
+
const staged = params.staged ?? true;
|
|
61
|
+
const allFiles = staged ? await git.getStagedFiles() : await git.getChangedFiles(false);
|
|
62
|
+
const { filtered: files, excluded } = filterExcludedFiles(allFiles);
|
|
63
|
+
const stat = await git.getStat(staged);
|
|
64
|
+
const allNumstat = await git.getNumstat(staged);
|
|
65
|
+
const numstat = allNumstat.filter((entry) => !isExcludedFile(entry.path));
|
|
66
|
+
const scopeResult = extractScopeCandidates(numstat);
|
|
67
|
+
const untrackedFiles = !staged && params.include_untracked ? await git.getUntrackedFiles() : undefined;
|
|
68
|
+
const snapshot: GitOverviewSnapshot = {
|
|
69
|
+
files,
|
|
70
|
+
stat,
|
|
71
|
+
numstat,
|
|
72
|
+
scopeCandidates: scopeResult.scopeCandidates,
|
|
73
|
+
isWideScope: scopeResult.isWide,
|
|
74
|
+
untrackedFiles,
|
|
75
|
+
excludedFiles: excluded.length > 0 ? excluded : undefined,
|
|
76
|
+
};
|
|
77
|
+
state.overview = snapshot;
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: JSON.stringify(snapshot, null, 2) }],
|
|
80
|
+
details: snapshot,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CommitAgentState } from "@oh-my-pi/pi-coding-agent/commit/agentic/state";
|
|
2
|
+
import { createAnalyzeFileTool } from "@oh-my-pi/pi-coding-agent/commit/agentic/tools/analyze-file";
|
|
3
|
+
import { createGitFileDiffTool } from "@oh-my-pi/pi-coding-agent/commit/agentic/tools/git-file-diff";
|
|
4
|
+
import { createGitHunkTool } from "@oh-my-pi/pi-coding-agent/commit/agentic/tools/git-hunk";
|
|
5
|
+
import { createGitOverviewTool } from "@oh-my-pi/pi-coding-agent/commit/agentic/tools/git-overview";
|
|
6
|
+
import { createProposeChangelogTool } from "@oh-my-pi/pi-coding-agent/commit/agentic/tools/propose-changelog";
|
|
7
|
+
import { createProposeCommitTool } from "@oh-my-pi/pi-coding-agent/commit/agentic/tools/propose-commit";
|
|
8
|
+
import { createRecentCommitsTool } from "@oh-my-pi/pi-coding-agent/commit/agentic/tools/recent-commits";
|
|
9
|
+
import { createSplitCommitTool } from "@oh-my-pi/pi-coding-agent/commit/agentic/tools/split-commit";
|
|
10
|
+
import type { ControlledGit } from "@oh-my-pi/pi-coding-agent/commit/git";
|
|
11
|
+
import type { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
|
12
|
+
import type { SettingsManager } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
|
|
13
|
+
import type { CustomTool } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
|
|
14
|
+
import type { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
|
|
15
|
+
|
|
16
|
+
export interface CommitToolOptions {
|
|
17
|
+
cwd: string;
|
|
18
|
+
git: ControlledGit;
|
|
19
|
+
authStorage: AuthStorage;
|
|
20
|
+
modelRegistry: ModelRegistry;
|
|
21
|
+
settingsManager: SettingsManager;
|
|
22
|
+
spawns: string;
|
|
23
|
+
state: CommitAgentState;
|
|
24
|
+
changelogTargets: string[];
|
|
25
|
+
enableAnalyzeFiles?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createCommitTools(options: CommitToolOptions): Array<CustomTool<any, any>> {
|
|
29
|
+
const tools: Array<CustomTool<any, any>> = [
|
|
30
|
+
createGitOverviewTool(options.git, options.state),
|
|
31
|
+
createGitFileDiffTool(options.git, options.state),
|
|
32
|
+
createGitHunkTool(options.git),
|
|
33
|
+
createRecentCommitsTool(options.git),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
if (options.enableAnalyzeFiles ?? true) {
|
|
37
|
+
tools.push(
|
|
38
|
+
createAnalyzeFileTool({
|
|
39
|
+
cwd: options.cwd,
|
|
40
|
+
authStorage: options.authStorage,
|
|
41
|
+
modelRegistry: options.modelRegistry,
|
|
42
|
+
settingsManager: options.settingsManager,
|
|
43
|
+
spawns: options.spawns,
|
|
44
|
+
state: options.state,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
tools.push(
|
|
50
|
+
createProposeChangelogTool(options.state, options.changelogTargets),
|
|
51
|
+
createProposeCommitTool(options.git, options.state),
|
|
52
|
+
createSplitCommitTool(options.git, options.state, options.changelogTargets),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return tools;
|
|
56
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { CommitAgentState } from "@oh-my-pi/pi-coding-agent/commit/agentic/state";
|
|
2
|
+
import type { ChangelogCategory } from "@oh-my-pi/pi-coding-agent/commit/types";
|
|
3
|
+
import type { CustomTool } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
|
|
6
|
+
const changelogEntrySchema = Type.Object({
|
|
7
|
+
path: Type.String(),
|
|
8
|
+
entries: Type.Record(Type.String(), Type.Array(Type.String())),
|
|
9
|
+
deletions: Type.Optional(
|
|
10
|
+
Type.Record(Type.String(), Type.Array(Type.String()), {
|
|
11
|
+
description: "Entries to remove from existing changelog sections (case-insensitive match)",
|
|
12
|
+
}),
|
|
13
|
+
),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const proposeChangelogSchema = Type.Object({
|
|
17
|
+
entries: Type.Array(changelogEntrySchema),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
interface ChangelogResponse {
|
|
21
|
+
valid: boolean;
|
|
22
|
+
errors: string[];
|
|
23
|
+
warnings: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const allowedCategories = new Set<ChangelogCategory>([
|
|
27
|
+
"Breaking Changes",
|
|
28
|
+
"Added",
|
|
29
|
+
"Changed",
|
|
30
|
+
"Deprecated",
|
|
31
|
+
"Removed",
|
|
32
|
+
"Fixed",
|
|
33
|
+
"Security",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export function createProposeChangelogTool(
|
|
37
|
+
state: CommitAgentState,
|
|
38
|
+
changelogTargets: string[],
|
|
39
|
+
): CustomTool<typeof proposeChangelogSchema> {
|
|
40
|
+
return {
|
|
41
|
+
name: "propose_changelog",
|
|
42
|
+
label: "Propose Changelog",
|
|
43
|
+
description: "Provide changelog entries for targeted CHANGELOG.md files.",
|
|
44
|
+
parameters: proposeChangelogSchema,
|
|
45
|
+
async execute(_toolCallId, params) {
|
|
46
|
+
const errors: string[] = [];
|
|
47
|
+
const warnings: string[] = [];
|
|
48
|
+
const targets = new Set(changelogTargets);
|
|
49
|
+
const seen = new Set<string>();
|
|
50
|
+
|
|
51
|
+
const normalized = params.entries.map((entry) => {
|
|
52
|
+
const cleaned: Record<string, string[]> = {};
|
|
53
|
+
for (const [category, values] of Object.entries(entry.entries ?? {})) {
|
|
54
|
+
if (!allowedCategories.has(category as ChangelogCategory)) {
|
|
55
|
+
errors.push(`Unknown changelog category for ${entry.path}: ${category}`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const items = values.map((value) => value.trim().replace(/\.$/, "")).filter((value) => value.length > 0);
|
|
59
|
+
if (items.length > 0) {
|
|
60
|
+
cleaned[category] = Array.from(new Set(items));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let cleanedDeletions: Record<string, string[]> | undefined;
|
|
65
|
+
if (entry.deletions) {
|
|
66
|
+
cleanedDeletions = {};
|
|
67
|
+
for (const [category, values] of Object.entries(entry.deletions)) {
|
|
68
|
+
if (!allowedCategories.has(category as ChangelogCategory)) {
|
|
69
|
+
errors.push(`Unknown deletion category for ${entry.path}: ${category}`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const items = values.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
73
|
+
if (items.length > 0) {
|
|
74
|
+
cleanedDeletions[category] = Array.from(new Set(items));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (Object.keys(cleanedDeletions).length === 0) {
|
|
78
|
+
cleanedDeletions = undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Object.keys(cleaned).length === 0 && !cleanedDeletions) {
|
|
83
|
+
warnings.push(`No changelog entries provided for ${entry.path}.`);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
path: entry.path,
|
|
87
|
+
entries: cleaned,
|
|
88
|
+
deletions: cleanedDeletions,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
for (const entry of normalized) {
|
|
93
|
+
if (targets.size > 0 && !targets.has(entry.path)) {
|
|
94
|
+
errors.push(`Changelog not expected: ${entry.path}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (seen.has(entry.path)) {
|
|
98
|
+
errors.push(`Duplicate changelog entry for ${entry.path}`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
seen.add(entry.path);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (targets.size > 0) {
|
|
105
|
+
for (const target of targets) {
|
|
106
|
+
if (!seen.has(target)) {
|
|
107
|
+
errors.push(`Missing changelog entries for ${target}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const response: ChangelogResponse = {
|
|
113
|
+
valid: errors.length === 0,
|
|
114
|
+
errors,
|
|
115
|
+
warnings,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (response.valid) {
|
|
119
|
+
state.changelogProposal = { entries: normalized };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
124
|
+
details: response,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|