@kkelly-offical/kkcode 0.1.6 → 0.2.1
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/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +19 -2
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2929
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +17 -4
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
package/src/tool/git-auto.mjs
CHANGED
|
@@ -1,526 +1,526 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import { mkdir } from "node:fs/promises"
|
|
3
|
-
import { userRootDir } from "../storage/paths.mjs"
|
|
4
|
-
import { readJson, writeJson } from "../storage/json-store.mjs"
|
|
5
|
-
import {
|
|
6
|
-
isGitRepo,
|
|
7
|
-
createGhostCommit,
|
|
8
|
-
restoreGhostCommit,
|
|
9
|
-
applyPatch,
|
|
10
|
-
preflightPatch,
|
|
11
|
-
getGitInfo,
|
|
12
|
-
getDiff,
|
|
13
|
-
getStagedDiff
|
|
14
|
-
} from "../util/git.mjs"
|
|
15
|
-
import {
|
|
16
|
-
saveGhostCommit,
|
|
17
|
-
loadGhostCommit,
|
|
18
|
-
listGhostCommits,
|
|
19
|
-
deleteGhostCommit,
|
|
20
|
-
getLatestGhostCommit,
|
|
21
|
-
cleanupAllExpired
|
|
22
|
-
} from "../storage/ghost-commit-store.mjs"
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Git 自动化工具模块
|
|
26
|
-
*
|
|
27
|
-
* 为 AI Agent 提供安全的 Git 操作能力:
|
|
28
|
-
* 1. git_snapshot - 创建幽灵提交(临时快照)
|
|
29
|
-
* 2. git_restore - 恢复到指定快照
|
|
30
|
-
* 3. git_apply_patch - 应用 AI 生成的 diff 补丁
|
|
31
|
-
* 4. git_info - 获取仓库信息
|
|
32
|
-
* 5. git_status - 获取当前状态
|
|
33
|
-
*
|
|
34
|
-
* 安全原则:
|
|
35
|
-
* - AI 只能创建快照和应用补丁,不能直接执行 git commit/push
|
|
36
|
-
* - 所有操作都通过临时索引进行,不干扰用户工作区
|
|
37
|
-
* - 快照有过期时间,自动清理旧数据
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
const SNAPSHOT_STATE_FILE = "git-snapshot-state.json"
|
|
41
|
-
|
|
42
|
-
/** 获取快照状态文件路径 */
|
|
43
|
-
function getSnapshotStatePath(sessionId) {
|
|
44
|
-
return path.join(userRootDir(), "sessions", sessionId, SNAPSHOT_STATE_FILE)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 加载会话的快照状态 */
|
|
48
|
-
async function loadSnapshotState(sessionId) {
|
|
49
|
-
const filePath = getSnapshotStatePath(sessionId)
|
|
50
|
-
return readJson(filePath, { snapshots: [], lastSnapshotId: null })
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** 保存会话的快照状态 */
|
|
54
|
-
async function saveSnapshotState(sessionId, state) {
|
|
55
|
-
const filePath = getSnapshotStatePath(sessionId)
|
|
56
|
-
await mkdir(path.dirname(filePath), { recursive: true })
|
|
57
|
-
await writeJson(filePath, state)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ============================================================================
|
|
61
|
-
// Tool: git_snapshot - 创建幽灵提交快照
|
|
62
|
-
// ============================================================================
|
|
63
|
-
|
|
64
|
-
export const gitSnapshotTool = {
|
|
65
|
-
name: "git_snapshot",
|
|
66
|
-
description: "Create a ghost commit snapshot of the current working directory. This captures the current state without creating a regular git commit, allowing you to restore later if needed. Uses temporary git index to avoid interfering with user's staging area.",
|
|
67
|
-
inputSchema: {
|
|
68
|
-
type: "object",
|
|
69
|
-
properties: {
|
|
70
|
-
message: {
|
|
71
|
-
type: "string",
|
|
72
|
-
description: "Optional snapshot message (default: 'kkcode snapshot')"
|
|
73
|
-
},
|
|
74
|
-
paths: {
|
|
75
|
-
type: "array",
|
|
76
|
-
items: { type: "string" },
|
|
77
|
-
description: "Specific file paths to include (default: all changes)"
|
|
78
|
-
},
|
|
79
|
-
auto: {
|
|
80
|
-
type: "boolean",
|
|
81
|
-
description: "Whether this is an automatic snapshot (default: false)"
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
required: []
|
|
85
|
-
},
|
|
86
|
-
async execute(args, ctx) {
|
|
87
|
-
const cwd = ctx.cwd || process.cwd()
|
|
88
|
-
const sessionId = ctx.sessionId || "default"
|
|
89
|
-
|
|
90
|
-
// 检查是否是 Git 仓库
|
|
91
|
-
if (!(await isGitRepo(cwd))) {
|
|
92
|
-
return {
|
|
93
|
-
ok: false,
|
|
94
|
-
error: "not_a_git_repo",
|
|
95
|
-
message: "Current directory is not a git repository"
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const message = args.message || (args.auto ? "kkcode auto snapshot" : "kkcode snapshot")
|
|
100
|
-
const paths = args.paths || []
|
|
101
|
-
|
|
102
|
-
// 创建幽灵提交
|
|
103
|
-
const result = await createGhostCommit(cwd, message, paths)
|
|
104
|
-
if (!result.ok) {
|
|
105
|
-
return {
|
|
106
|
-
ok: false,
|
|
107
|
-
error: "create_failed",
|
|
108
|
-
message: result.error
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 持久化存储
|
|
113
|
-
await saveGhostCommit(result.ghostCommit)
|
|
114
|
-
|
|
115
|
-
// 更新会话状态
|
|
116
|
-
const state = await loadSnapshotState(sessionId)
|
|
117
|
-
state.snapshots.push({
|
|
118
|
-
id: result.ghostCommit.id,
|
|
119
|
-
commitHash: result.ghostCommit.commitHash,
|
|
120
|
-
createdAt: result.ghostCommit.createdAt,
|
|
121
|
-
message: result.ghostCommit.message,
|
|
122
|
-
auto: !!args.auto
|
|
123
|
-
})
|
|
124
|
-
state.lastSnapshotId = result.ghostCommit.id
|
|
125
|
-
await saveSnapshotState(sessionId, state)
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
ok: true,
|
|
129
|
-
snapshot: {
|
|
130
|
-
id: result.ghostCommit.id,
|
|
131
|
-
commitHash: result.ghostCommit.commitHash,
|
|
132
|
-
shortHash: result.ghostCommit.commitHash.slice(0, 8),
|
|
133
|
-
message: result.ghostCommit.message,
|
|
134
|
-
createdAt: result.ghostCommit.createdAt,
|
|
135
|
-
files: result.ghostCommit.files
|
|
136
|
-
},
|
|
137
|
-
message: `Created ghost commit ${result.ghostCommit.commitHash.slice(0, 8)} with ${result.ghostCommit.files.length} file(s)`
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ============================================================================
|
|
143
|
-
// Tool: git_restore - 恢复到指定快照
|
|
144
|
-
// ============================================================================
|
|
145
|
-
|
|
146
|
-
export const gitRestoreTool = {
|
|
147
|
-
name: "git_restore",
|
|
148
|
-
description: "Restore the working directory to a previously created ghost commit snapshot. This will overwrite current changes with the snapshot state.",
|
|
149
|
-
inputSchema: {
|
|
150
|
-
type: "object",
|
|
151
|
-
properties: {
|
|
152
|
-
snapshot_id: {
|
|
153
|
-
type: "string",
|
|
154
|
-
description: "The ghost commit snapshot ID to restore to"
|
|
155
|
-
},
|
|
156
|
-
restore_index: {
|
|
157
|
-
type: "boolean",
|
|
158
|
-
description: "Whether to also restore the staging area (default: false)"
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
required: ["snapshot_id"]
|
|
162
|
-
},
|
|
163
|
-
async execute(args, ctx) {
|
|
164
|
-
const cwd = ctx.cwd || process.cwd()
|
|
165
|
-
const sessionId = ctx.sessionId || "default"
|
|
166
|
-
|
|
167
|
-
if (!(await isGitRepo(cwd))) {
|
|
168
|
-
return {
|
|
169
|
-
ok: false,
|
|
170
|
-
error: "not_a_git_repo",
|
|
171
|
-
message: "Current directory is not a git repository"
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const snapshotId = args.snapshot_id
|
|
176
|
-
const restoreIndex = args.restore_index || false
|
|
177
|
-
|
|
178
|
-
// 加载幽灵提交元数据
|
|
179
|
-
const ghostCommit = await loadGhostCommit(cwd, snapshotId)
|
|
180
|
-
if (!ghostCommit) {
|
|
181
|
-
return {
|
|
182
|
-
ok: false,
|
|
183
|
-
error: "snapshot_not_found",
|
|
184
|
-
message: `Snapshot ${snapshotId} not found. Use git_list_snapshots to see available snapshots.`
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 恢复到幽灵提交
|
|
189
|
-
const result = await restoreGhostCommit(cwd, ghostCommit.commitHash, restoreIndex)
|
|
190
|
-
if (!result.ok) {
|
|
191
|
-
return {
|
|
192
|
-
ok: false,
|
|
193
|
-
error: "restore_failed",
|
|
194
|
-
message: result.error
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
ok: true,
|
|
200
|
-
restored: {
|
|
201
|
-
snapshotId: ghostCommit.id,
|
|
202
|
-
commitHash: ghostCommit.commitHash,
|
|
203
|
-
shortHash: ghostCommit.commitHash.slice(0, 8),
|
|
204
|
-
message: ghostCommit.message,
|
|
205
|
-
createdAt: ghostCommit.createdAt
|
|
206
|
-
},
|
|
207
|
-
message: `Restored to snapshot ${ghostCommit.commitHash.slice(0, 8)}: ${ghostCommit.message}`
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ============================================================================
|
|
213
|
-
// Tool: git_list_snapshots - 列出所有快照
|
|
214
|
-
// ============================================================================
|
|
215
|
-
|
|
216
|
-
export const gitListSnapshotsTool = {
|
|
217
|
-
name: "git_list_snapshots",
|
|
218
|
-
description: "List all ghost commit snapshots for the current repository, ordered by creation time (newest first).",
|
|
219
|
-
inputSchema: {
|
|
220
|
-
type: "object",
|
|
221
|
-
properties: {
|
|
222
|
-
include_expired: {
|
|
223
|
-
type: "boolean",
|
|
224
|
-
description: "Include expired snapshots (default: false)"
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
required: []
|
|
228
|
-
},
|
|
229
|
-
async execute(args, ctx) {
|
|
230
|
-
const cwd = ctx.cwd || process.cwd()
|
|
231
|
-
|
|
232
|
-
if (!(await isGitRepo(cwd))) {
|
|
233
|
-
return {
|
|
234
|
-
ok: false,
|
|
235
|
-
error: "not_a_git_repo",
|
|
236
|
-
message: "Current directory is not a git repository"
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const snapshots = await listGhostCommits(cwd, {
|
|
241
|
-
includeExpired: args.include_expired
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
ok: true,
|
|
246
|
-
count: snapshots.length,
|
|
247
|
-
snapshots: snapshots.map(s => ({
|
|
248
|
-
id: s.id,
|
|
249
|
-
commitHash: s.commitHash,
|
|
250
|
-
shortHash: s.commitHash.slice(0, 8),
|
|
251
|
-
message: s.message,
|
|
252
|
-
createdAt: s.createdAt,
|
|
253
|
-
fileCount: s.files?.length || 0,
|
|
254
|
-
isExpired: s.isExpired || false
|
|
255
|
-
}))
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ============================================================================
|
|
261
|
-
// Tool: git_apply_patch - 应用 AI 生成的 diff 补丁
|
|
262
|
-
// ============================================================================
|
|
263
|
-
|
|
264
|
-
export const gitApplyPatchTool = {
|
|
265
|
-
name: "git_apply_patch",
|
|
266
|
-
description: "Apply a unified diff/patch to the working directory. Supports 3-way merge for conflict resolution. First runs a preflight check to validate the patch can be applied, then applies it if valid.",
|
|
267
|
-
inputSchema: {
|
|
268
|
-
type: "object",
|
|
269
|
-
properties: {
|
|
270
|
-
diff: {
|
|
271
|
-
type: "string",
|
|
272
|
-
description: "The unified diff/patch content to apply"
|
|
273
|
-
},
|
|
274
|
-
preflight_only: {
|
|
275
|
-
type: "boolean",
|
|
276
|
-
description: "Only check if patch can be applied, don't actually apply (default: false)"
|
|
277
|
-
},
|
|
278
|
-
threeway: {
|
|
279
|
-
type: "boolean",
|
|
280
|
-
description: "Use 3-way merge for better conflict resolution (default: true)"
|
|
281
|
-
}
|
|
282
|
-
},
|
|
283
|
-
required: ["diff"]
|
|
284
|
-
},
|
|
285
|
-
async execute(args, ctx) {
|
|
286
|
-
const cwd = ctx.cwd || process.cwd()
|
|
287
|
-
|
|
288
|
-
if (!(await isGitRepo(cwd))) {
|
|
289
|
-
return {
|
|
290
|
-
ok: false,
|
|
291
|
-
error: "not_a_git_repo",
|
|
292
|
-
message: "Current directory is not a git repository"
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const diff = args.diff
|
|
297
|
-
const preflightOnly = args.preflight_only || false
|
|
298
|
-
const threeway = args.threeway !== false // 默认 true
|
|
299
|
-
|
|
300
|
-
if (!diff || !diff.trim()) {
|
|
301
|
-
return {
|
|
302
|
-
ok: false,
|
|
303
|
-
error: "empty_diff",
|
|
304
|
-
message: "Diff content is empty"
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// 预检
|
|
309
|
-
const preflight = await preflightPatch(cwd, diff)
|
|
310
|
-
if (!preflight.applicable) {
|
|
311
|
-
return {
|
|
312
|
-
ok: false,
|
|
313
|
-
error: "preflight_failed",
|
|
314
|
-
message: "Patch cannot be applied",
|
|
315
|
-
conflicts: preflight.conflicts,
|
|
316
|
-
details: preflight.error
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (preflightOnly) {
|
|
321
|
-
return {
|
|
322
|
-
ok: true,
|
|
323
|
-
preflight: true,
|
|
324
|
-
applicable: true,
|
|
325
|
-
message: "Patch can be applied successfully"
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 应用 patch
|
|
330
|
-
const result = await applyPatch(cwd, diff, {
|
|
331
|
-
threeway,
|
|
332
|
-
check: false
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
if (!result.ok) {
|
|
336
|
-
return {
|
|
337
|
-
ok: false,
|
|
338
|
-
error: "apply_failed",
|
|
339
|
-
message: result.error,
|
|
340
|
-
conflicts: result.conflicts
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
ok: true,
|
|
346
|
-
applied: result.applied,
|
|
347
|
-
message: `Successfully applied patch to ${result.applied.length} file(s)`,
|
|
348
|
-
files: result.applied
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// ============================================================================
|
|
354
|
-
// Tool: git_info - 获取仓库信息
|
|
355
|
-
// ============================================================================
|
|
356
|
-
|
|
357
|
-
export const gitInfoTool = {
|
|
358
|
-
name: "git_info",
|
|
359
|
-
description: "Get comprehensive git repository information including current branch, commit hash, remote URLs, and working tree status. Useful for understanding the repository context before making changes.",
|
|
360
|
-
inputSchema: {
|
|
361
|
-
type: "object",
|
|
362
|
-
properties: {},
|
|
363
|
-
required: []
|
|
364
|
-
},
|
|
365
|
-
async execute(args, ctx) {
|
|
366
|
-
const cwd = ctx.cwd || process.cwd()
|
|
367
|
-
|
|
368
|
-
const result = await getGitInfo(cwd)
|
|
369
|
-
if (!result.ok) {
|
|
370
|
-
return {
|
|
371
|
-
ok: false,
|
|
372
|
-
error: "git_info_failed",
|
|
373
|
-
message: result.error,
|
|
374
|
-
isGitRepo: false
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return {
|
|
379
|
-
ok: true,
|
|
380
|
-
info: result.info
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// ============================================================================
|
|
386
|
-
// Tool: git_status - 获取当前状态
|
|
387
|
-
// ============================================================================
|
|
388
|
-
|
|
389
|
-
export const gitStatusTool = {
|
|
390
|
-
name: "git_status",
|
|
391
|
-
description: "Get detailed git status including uncommitted changes, staged changes, and diffs. Shows both working tree and staging area status.",
|
|
392
|
-
inputSchema: {
|
|
393
|
-
type: "object",
|
|
394
|
-
properties: {
|
|
395
|
-
include_diff: {
|
|
396
|
-
type: "boolean",
|
|
397
|
-
description: "Include actual diff content (default: true)"
|
|
398
|
-
}
|
|
399
|
-
},
|
|
400
|
-
required: []
|
|
401
|
-
},
|
|
402
|
-
async execute(args, ctx) {
|
|
403
|
-
const cwd = ctx.cwd || process.cwd()
|
|
404
|
-
|
|
405
|
-
if (!(await isGitRepo(cwd))) {
|
|
406
|
-
return {
|
|
407
|
-
ok: false,
|
|
408
|
-
error: "not_a_git_repo",
|
|
409
|
-
message: "Current directory is not a git repository"
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const includeDiff = args.include_diff !== false // 默认 true
|
|
414
|
-
|
|
415
|
-
const [infoResult, unstagedResult, stagedResult] = await Promise.all([
|
|
416
|
-
getGitInfo(cwd),
|
|
417
|
-
includeDiff ? getDiff(cwd) : { ok: true, diff: "" },
|
|
418
|
-
includeDiff ? getStagedDiff(cwd) : { ok: true, diff: "" }
|
|
419
|
-
])
|
|
420
|
-
|
|
421
|
-
if (!infoResult.ok) {
|
|
422
|
-
return {
|
|
423
|
-
ok: false,
|
|
424
|
-
error: "status_failed",
|
|
425
|
-
message: infoResult.error
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return {
|
|
430
|
-
ok: true,
|
|
431
|
-
status: {
|
|
432
|
-
branch: infoResult.info.currentBranch,
|
|
433
|
-
commit: infoResult.info.currentCommit,
|
|
434
|
-
shortCommit: infoResult.info.currentCommit?.slice(0, 8),
|
|
435
|
-
hasUncommittedChanges: infoResult.info.hasUncommittedChanges,
|
|
436
|
-
changedFiles: infoResult.info.changedFiles,
|
|
437
|
-
unstagedDiff: unstagedResult.diff,
|
|
438
|
-
stagedDiff: stagedResult.diff
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// ============================================================================
|
|
445
|
-
// Tool: git_delete_snapshot - 删除快照
|
|
446
|
-
// ============================================================================
|
|
447
|
-
|
|
448
|
-
export const gitDeleteSnapshotTool = {
|
|
449
|
-
name: "git_delete_snapshot",
|
|
450
|
-
description: "Delete a ghost commit snapshot by ID. This only removes the metadata; the git commit object may still exist until git garbage collection runs.",
|
|
451
|
-
inputSchema: {
|
|
452
|
-
type: "object",
|
|
453
|
-
properties: {
|
|
454
|
-
snapshot_id: {
|
|
455
|
-
type: "string",
|
|
456
|
-
description: "The snapshot ID to delete"
|
|
457
|
-
}
|
|
458
|
-
},
|
|
459
|
-
required: ["snapshot_id"]
|
|
460
|
-
},
|
|
461
|
-
async execute(args, ctx) {
|
|
462
|
-
const cwd = ctx.cwd || process.cwd()
|
|
463
|
-
const snapshotId = args.snapshot_id
|
|
464
|
-
|
|
465
|
-
const deleted = await deleteGhostCommit(cwd, snapshotId)
|
|
466
|
-
if (!deleted) {
|
|
467
|
-
return {
|
|
468
|
-
ok: false,
|
|
469
|
-
error: "not_found",
|
|
470
|
-
message: `Snapshot ${snapshotId} not found`
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return {
|
|
475
|
-
ok: true,
|
|
476
|
-
deleted: snapshotId,
|
|
477
|
-
message: `Deleted snapshot ${snapshotId}`
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// ============================================================================
|
|
483
|
-
// Tool: git_cleanup - 清理过期快照
|
|
484
|
-
// ============================================================================
|
|
485
|
-
|
|
486
|
-
export const gitCleanupTool = {
|
|
487
|
-
name: "git_cleanup",
|
|
488
|
-
description: "Clean up all expired ghost commit snapshots across all repositories. Snapshots older than 7 days are considered expired.",
|
|
489
|
-
inputSchema: {
|
|
490
|
-
type: "object",
|
|
491
|
-
properties: {},
|
|
492
|
-
required: []
|
|
493
|
-
},
|
|
494
|
-
async execute() {
|
|
495
|
-
const result = await cleanupAllExpired()
|
|
496
|
-
return {
|
|
497
|
-
ok: true,
|
|
498
|
-
deleted: result.deleted,
|
|
499
|
-
message: `Cleaned up ${result.deleted} expired snapshot(s)`
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// ============================================================================
|
|
505
|
-
// 导出所有 Git 自动化工具
|
|
506
|
-
// ============================================================================
|
|
507
|
-
|
|
508
|
-
export const gitAutoTools = [
|
|
509
|
-
gitSnapshotTool,
|
|
510
|
-
gitRestoreTool,
|
|
511
|
-
gitListSnapshotsTool,
|
|
512
|
-
gitApplyPatchTool,
|
|
513
|
-
gitInfoTool,
|
|
514
|
-
gitStatusTool,
|
|
515
|
-
gitDeleteSnapshotTool,
|
|
516
|
-
gitCleanupTool
|
|
517
|
-
]
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* 获取最后一次快照 ID(用于自动恢复)
|
|
521
|
-
*/
|
|
522
|
-
export async function getLastSnapshotId(sessionId) {
|
|
523
|
-
const state = await loadSnapshotState(sessionId)
|
|
524
|
-
return state.lastSnapshotId
|
|
525
|
-
}
|
|
526
|
-
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import { userRootDir } from "../storage/paths.mjs"
|
|
4
|
+
import { readJson, writeJson } from "../storage/json-store.mjs"
|
|
5
|
+
import {
|
|
6
|
+
isGitRepo,
|
|
7
|
+
createGhostCommit,
|
|
8
|
+
restoreGhostCommit,
|
|
9
|
+
applyPatch,
|
|
10
|
+
preflightPatch,
|
|
11
|
+
getGitInfo,
|
|
12
|
+
getDiff,
|
|
13
|
+
getStagedDiff
|
|
14
|
+
} from "../util/git.mjs"
|
|
15
|
+
import {
|
|
16
|
+
saveGhostCommit,
|
|
17
|
+
loadGhostCommit,
|
|
18
|
+
listGhostCommits,
|
|
19
|
+
deleteGhostCommit,
|
|
20
|
+
getLatestGhostCommit,
|
|
21
|
+
cleanupAllExpired
|
|
22
|
+
} from "../storage/ghost-commit-store.mjs"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Git 自动化工具模块
|
|
26
|
+
*
|
|
27
|
+
* 为 AI Agent 提供安全的 Git 操作能力:
|
|
28
|
+
* 1. git_snapshot - 创建幽灵提交(临时快照)
|
|
29
|
+
* 2. git_restore - 恢复到指定快照
|
|
30
|
+
* 3. git_apply_patch - 应用 AI 生成的 diff 补丁
|
|
31
|
+
* 4. git_info - 获取仓库信息
|
|
32
|
+
* 5. git_status - 获取当前状态
|
|
33
|
+
*
|
|
34
|
+
* 安全原则:
|
|
35
|
+
* - AI 只能创建快照和应用补丁,不能直接执行 git commit/push
|
|
36
|
+
* - 所有操作都通过临时索引进行,不干扰用户工作区
|
|
37
|
+
* - 快照有过期时间,自动清理旧数据
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const SNAPSHOT_STATE_FILE = "git-snapshot-state.json"
|
|
41
|
+
|
|
42
|
+
/** 获取快照状态文件路径 */
|
|
43
|
+
function getSnapshotStatePath(sessionId) {
|
|
44
|
+
return path.join(userRootDir(), "sessions", sessionId, SNAPSHOT_STATE_FILE)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 加载会话的快照状态 */
|
|
48
|
+
async function loadSnapshotState(sessionId) {
|
|
49
|
+
const filePath = getSnapshotStatePath(sessionId)
|
|
50
|
+
return readJson(filePath, { snapshots: [], lastSnapshotId: null })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 保存会话的快照状态 */
|
|
54
|
+
async function saveSnapshotState(sessionId, state) {
|
|
55
|
+
const filePath = getSnapshotStatePath(sessionId)
|
|
56
|
+
await mkdir(path.dirname(filePath), { recursive: true })
|
|
57
|
+
await writeJson(filePath, state)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Tool: git_snapshot - 创建幽灵提交快照
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
export const gitSnapshotTool = {
|
|
65
|
+
name: "git_snapshot",
|
|
66
|
+
description: "Create a ghost commit snapshot of the current working directory. This captures the current state without creating a regular git commit, allowing you to restore later if needed. Uses temporary git index to avoid interfering with user's staging area.",
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: "object",
|
|
69
|
+
properties: {
|
|
70
|
+
message: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "Optional snapshot message (default: 'kkcode snapshot')"
|
|
73
|
+
},
|
|
74
|
+
paths: {
|
|
75
|
+
type: "array",
|
|
76
|
+
items: { type: "string" },
|
|
77
|
+
description: "Specific file paths to include (default: all changes)"
|
|
78
|
+
},
|
|
79
|
+
auto: {
|
|
80
|
+
type: "boolean",
|
|
81
|
+
description: "Whether this is an automatic snapshot (default: false)"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
required: []
|
|
85
|
+
},
|
|
86
|
+
async execute(args, ctx) {
|
|
87
|
+
const cwd = ctx.cwd || process.cwd()
|
|
88
|
+
const sessionId = ctx.sessionId || "default"
|
|
89
|
+
|
|
90
|
+
// 检查是否是 Git 仓库
|
|
91
|
+
if (!(await isGitRepo(cwd))) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: "not_a_git_repo",
|
|
95
|
+
message: "Current directory is not a git repository"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const message = args.message || (args.auto ? "kkcode auto snapshot" : "kkcode snapshot")
|
|
100
|
+
const paths = args.paths || []
|
|
101
|
+
|
|
102
|
+
// 创建幽灵提交
|
|
103
|
+
const result = await createGhostCommit(cwd, message, paths)
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: "create_failed",
|
|
108
|
+
message: result.error
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 持久化存储
|
|
113
|
+
await saveGhostCommit(result.ghostCommit)
|
|
114
|
+
|
|
115
|
+
// 更新会话状态
|
|
116
|
+
const state = await loadSnapshotState(sessionId)
|
|
117
|
+
state.snapshots.push({
|
|
118
|
+
id: result.ghostCommit.id,
|
|
119
|
+
commitHash: result.ghostCommit.commitHash,
|
|
120
|
+
createdAt: result.ghostCommit.createdAt,
|
|
121
|
+
message: result.ghostCommit.message,
|
|
122
|
+
auto: !!args.auto
|
|
123
|
+
})
|
|
124
|
+
state.lastSnapshotId = result.ghostCommit.id
|
|
125
|
+
await saveSnapshotState(sessionId, state)
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
snapshot: {
|
|
130
|
+
id: result.ghostCommit.id,
|
|
131
|
+
commitHash: result.ghostCommit.commitHash,
|
|
132
|
+
shortHash: result.ghostCommit.commitHash.slice(0, 8),
|
|
133
|
+
message: result.ghostCommit.message,
|
|
134
|
+
createdAt: result.ghostCommit.createdAt,
|
|
135
|
+
files: result.ghostCommit.files
|
|
136
|
+
},
|
|
137
|
+
message: `Created ghost commit ${result.ghostCommit.commitHash.slice(0, 8)} with ${result.ghostCommit.files.length} file(s)`
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ============================================================================
|
|
143
|
+
// Tool: git_restore - 恢复到指定快照
|
|
144
|
+
// ============================================================================
|
|
145
|
+
|
|
146
|
+
export const gitRestoreTool = {
|
|
147
|
+
name: "git_restore",
|
|
148
|
+
description: "Restore the working directory to a previously created ghost commit snapshot. This will overwrite current changes with the snapshot state.",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
snapshot_id: {
|
|
153
|
+
type: "string",
|
|
154
|
+
description: "The ghost commit snapshot ID to restore to"
|
|
155
|
+
},
|
|
156
|
+
restore_index: {
|
|
157
|
+
type: "boolean",
|
|
158
|
+
description: "Whether to also restore the staging area (default: false)"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
required: ["snapshot_id"]
|
|
162
|
+
},
|
|
163
|
+
async execute(args, ctx) {
|
|
164
|
+
const cwd = ctx.cwd || process.cwd()
|
|
165
|
+
const sessionId = ctx.sessionId || "default"
|
|
166
|
+
|
|
167
|
+
if (!(await isGitRepo(cwd))) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
error: "not_a_git_repo",
|
|
171
|
+
message: "Current directory is not a git repository"
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const snapshotId = args.snapshot_id
|
|
176
|
+
const restoreIndex = args.restore_index || false
|
|
177
|
+
|
|
178
|
+
// 加载幽灵提交元数据
|
|
179
|
+
const ghostCommit = await loadGhostCommit(cwd, snapshotId)
|
|
180
|
+
if (!ghostCommit) {
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
error: "snapshot_not_found",
|
|
184
|
+
message: `Snapshot ${snapshotId} not found. Use git_list_snapshots to see available snapshots.`
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 恢复到幽灵提交
|
|
189
|
+
const result = await restoreGhostCommit(cwd, ghostCommit.commitHash, restoreIndex)
|
|
190
|
+
if (!result.ok) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: "restore_failed",
|
|
194
|
+
message: result.error
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
ok: true,
|
|
200
|
+
restored: {
|
|
201
|
+
snapshotId: ghostCommit.id,
|
|
202
|
+
commitHash: ghostCommit.commitHash,
|
|
203
|
+
shortHash: ghostCommit.commitHash.slice(0, 8),
|
|
204
|
+
message: ghostCommit.message,
|
|
205
|
+
createdAt: ghostCommit.createdAt
|
|
206
|
+
},
|
|
207
|
+
message: `Restored to snapshot ${ghostCommit.commitHash.slice(0, 8)}: ${ghostCommit.message}`
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// Tool: git_list_snapshots - 列出所有快照
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
export const gitListSnapshotsTool = {
|
|
217
|
+
name: "git_list_snapshots",
|
|
218
|
+
description: "List all ghost commit snapshots for the current repository, ordered by creation time (newest first).",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
include_expired: {
|
|
223
|
+
type: "boolean",
|
|
224
|
+
description: "Include expired snapshots (default: false)"
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
required: []
|
|
228
|
+
},
|
|
229
|
+
async execute(args, ctx) {
|
|
230
|
+
const cwd = ctx.cwd || process.cwd()
|
|
231
|
+
|
|
232
|
+
if (!(await isGitRepo(cwd))) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
error: "not_a_git_repo",
|
|
236
|
+
message: "Current directory is not a git repository"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const snapshots = await listGhostCommits(cwd, {
|
|
241
|
+
includeExpired: args.include_expired
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
count: snapshots.length,
|
|
247
|
+
snapshots: snapshots.map(s => ({
|
|
248
|
+
id: s.id,
|
|
249
|
+
commitHash: s.commitHash,
|
|
250
|
+
shortHash: s.commitHash.slice(0, 8),
|
|
251
|
+
message: s.message,
|
|
252
|
+
createdAt: s.createdAt,
|
|
253
|
+
fileCount: s.files?.length || 0,
|
|
254
|
+
isExpired: s.isExpired || false
|
|
255
|
+
}))
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// Tool: git_apply_patch - 应用 AI 生成的 diff 补丁
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
export const gitApplyPatchTool = {
|
|
265
|
+
name: "git_apply_patch",
|
|
266
|
+
description: "Apply a unified diff/patch to the working directory. Supports 3-way merge for conflict resolution. First runs a preflight check to validate the patch can be applied, then applies it if valid.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
properties: {
|
|
270
|
+
diff: {
|
|
271
|
+
type: "string",
|
|
272
|
+
description: "The unified diff/patch content to apply"
|
|
273
|
+
},
|
|
274
|
+
preflight_only: {
|
|
275
|
+
type: "boolean",
|
|
276
|
+
description: "Only check if patch can be applied, don't actually apply (default: false)"
|
|
277
|
+
},
|
|
278
|
+
threeway: {
|
|
279
|
+
type: "boolean",
|
|
280
|
+
description: "Use 3-way merge for better conflict resolution (default: true)"
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
required: ["diff"]
|
|
284
|
+
},
|
|
285
|
+
async execute(args, ctx) {
|
|
286
|
+
const cwd = ctx.cwd || process.cwd()
|
|
287
|
+
|
|
288
|
+
if (!(await isGitRepo(cwd))) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
error: "not_a_git_repo",
|
|
292
|
+
message: "Current directory is not a git repository"
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const diff = args.diff
|
|
297
|
+
const preflightOnly = args.preflight_only || false
|
|
298
|
+
const threeway = args.threeway !== false // 默认 true
|
|
299
|
+
|
|
300
|
+
if (!diff || !diff.trim()) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
error: "empty_diff",
|
|
304
|
+
message: "Diff content is empty"
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 预检
|
|
309
|
+
const preflight = await preflightPatch(cwd, diff)
|
|
310
|
+
if (!preflight.applicable) {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
error: "preflight_failed",
|
|
314
|
+
message: "Patch cannot be applied",
|
|
315
|
+
conflicts: preflight.conflicts,
|
|
316
|
+
details: preflight.error
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (preflightOnly) {
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
preflight: true,
|
|
324
|
+
applicable: true,
|
|
325
|
+
message: "Patch can be applied successfully"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 应用 patch
|
|
330
|
+
const result = await applyPatch(cwd, diff, {
|
|
331
|
+
threeway,
|
|
332
|
+
check: false
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
if (!result.ok) {
|
|
336
|
+
return {
|
|
337
|
+
ok: false,
|
|
338
|
+
error: "apply_failed",
|
|
339
|
+
message: result.error,
|
|
340
|
+
conflicts: result.conflicts
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
ok: true,
|
|
346
|
+
applied: result.applied,
|
|
347
|
+
message: `Successfully applied patch to ${result.applied.length} file(s)`,
|
|
348
|
+
files: result.applied
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ============================================================================
|
|
354
|
+
// Tool: git_info - 获取仓库信息
|
|
355
|
+
// ============================================================================
|
|
356
|
+
|
|
357
|
+
export const gitInfoTool = {
|
|
358
|
+
name: "git_info",
|
|
359
|
+
description: "Get comprehensive git repository information including current branch, commit hash, remote URLs, and working tree status. Useful for understanding the repository context before making changes.",
|
|
360
|
+
inputSchema: {
|
|
361
|
+
type: "object",
|
|
362
|
+
properties: {},
|
|
363
|
+
required: []
|
|
364
|
+
},
|
|
365
|
+
async execute(args, ctx) {
|
|
366
|
+
const cwd = ctx.cwd || process.cwd()
|
|
367
|
+
|
|
368
|
+
const result = await getGitInfo(cwd)
|
|
369
|
+
if (!result.ok) {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
error: "git_info_failed",
|
|
373
|
+
message: result.error,
|
|
374
|
+
isGitRepo: false
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
ok: true,
|
|
380
|
+
info: result.info
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ============================================================================
|
|
386
|
+
// Tool: git_status - 获取当前状态
|
|
387
|
+
// ============================================================================
|
|
388
|
+
|
|
389
|
+
export const gitStatusTool = {
|
|
390
|
+
name: "git_status",
|
|
391
|
+
description: "Get detailed git status including uncommitted changes, staged changes, and diffs. Shows both working tree and staging area status.",
|
|
392
|
+
inputSchema: {
|
|
393
|
+
type: "object",
|
|
394
|
+
properties: {
|
|
395
|
+
include_diff: {
|
|
396
|
+
type: "boolean",
|
|
397
|
+
description: "Include actual diff content (default: true)"
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
required: []
|
|
401
|
+
},
|
|
402
|
+
async execute(args, ctx) {
|
|
403
|
+
const cwd = ctx.cwd || process.cwd()
|
|
404
|
+
|
|
405
|
+
if (!(await isGitRepo(cwd))) {
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
error: "not_a_git_repo",
|
|
409
|
+
message: "Current directory is not a git repository"
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const includeDiff = args.include_diff !== false // 默认 true
|
|
414
|
+
|
|
415
|
+
const [infoResult, unstagedResult, stagedResult] = await Promise.all([
|
|
416
|
+
getGitInfo(cwd),
|
|
417
|
+
includeDiff ? getDiff(cwd) : { ok: true, diff: "" },
|
|
418
|
+
includeDiff ? getStagedDiff(cwd) : { ok: true, diff: "" }
|
|
419
|
+
])
|
|
420
|
+
|
|
421
|
+
if (!infoResult.ok) {
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
error: "status_failed",
|
|
425
|
+
message: infoResult.error
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
ok: true,
|
|
431
|
+
status: {
|
|
432
|
+
branch: infoResult.info.currentBranch,
|
|
433
|
+
commit: infoResult.info.currentCommit,
|
|
434
|
+
shortCommit: infoResult.info.currentCommit?.slice(0, 8),
|
|
435
|
+
hasUncommittedChanges: infoResult.info.hasUncommittedChanges,
|
|
436
|
+
changedFiles: infoResult.info.changedFiles,
|
|
437
|
+
unstagedDiff: unstagedResult.diff,
|
|
438
|
+
stagedDiff: stagedResult.diff
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ============================================================================
|
|
445
|
+
// Tool: git_delete_snapshot - 删除快照
|
|
446
|
+
// ============================================================================
|
|
447
|
+
|
|
448
|
+
export const gitDeleteSnapshotTool = {
|
|
449
|
+
name: "git_delete_snapshot",
|
|
450
|
+
description: "Delete a ghost commit snapshot by ID. This only removes the metadata; the git commit object may still exist until git garbage collection runs.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {
|
|
454
|
+
snapshot_id: {
|
|
455
|
+
type: "string",
|
|
456
|
+
description: "The snapshot ID to delete"
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
required: ["snapshot_id"]
|
|
460
|
+
},
|
|
461
|
+
async execute(args, ctx) {
|
|
462
|
+
const cwd = ctx.cwd || process.cwd()
|
|
463
|
+
const snapshotId = args.snapshot_id
|
|
464
|
+
|
|
465
|
+
const deleted = await deleteGhostCommit(cwd, snapshotId)
|
|
466
|
+
if (!deleted) {
|
|
467
|
+
return {
|
|
468
|
+
ok: false,
|
|
469
|
+
error: "not_found",
|
|
470
|
+
message: `Snapshot ${snapshotId} not found`
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
ok: true,
|
|
476
|
+
deleted: snapshotId,
|
|
477
|
+
message: `Deleted snapshot ${snapshotId}`
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ============================================================================
|
|
483
|
+
// Tool: git_cleanup - 清理过期快照
|
|
484
|
+
// ============================================================================
|
|
485
|
+
|
|
486
|
+
export const gitCleanupTool = {
|
|
487
|
+
name: "git_cleanup",
|
|
488
|
+
description: "Clean up all expired ghost commit snapshots across all repositories. Snapshots older than 7 days are considered expired.",
|
|
489
|
+
inputSchema: {
|
|
490
|
+
type: "object",
|
|
491
|
+
properties: {},
|
|
492
|
+
required: []
|
|
493
|
+
},
|
|
494
|
+
async execute() {
|
|
495
|
+
const result = await cleanupAllExpired()
|
|
496
|
+
return {
|
|
497
|
+
ok: true,
|
|
498
|
+
deleted: result.deleted,
|
|
499
|
+
message: `Cleaned up ${result.deleted} expired snapshot(s)`
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ============================================================================
|
|
505
|
+
// 导出所有 Git 自动化工具
|
|
506
|
+
// ============================================================================
|
|
507
|
+
|
|
508
|
+
export const gitAutoTools = [
|
|
509
|
+
gitSnapshotTool,
|
|
510
|
+
gitRestoreTool,
|
|
511
|
+
gitListSnapshotsTool,
|
|
512
|
+
gitApplyPatchTool,
|
|
513
|
+
gitInfoTool,
|
|
514
|
+
gitStatusTool,
|
|
515
|
+
gitDeleteSnapshotTool,
|
|
516
|
+
gitCleanupTool
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 获取最后一次快照 ID(用于自动恢复)
|
|
521
|
+
*/
|
|
522
|
+
export async function getLastSnapshotId(sessionId) {
|
|
523
|
+
const state = await loadSnapshotState(sessionId)
|
|
524
|
+
return state.lastSnapshotId
|
|
525
|
+
}
|
|
526
|
+
|