@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.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.
Files changed (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -1,478 +1,487 @@
1
- import path from "node:path"
2
- import { exec } from "node:child_process"
3
- import { promisify } from "node:util"
4
- import {
5
- isGitRepo,
6
- currentBranch,
7
- commitAll as gitCommitAll
8
- } from "../util/git.mjs"
9
- import { gitSnapshotTool } from "./git-auto.mjs"
10
- import { isFullAutoMode, getPolicyMode } from "../permission/exec-policy.mjs"
11
-
12
- const execAsync = promisify(exec)
13
-
14
- /**
15
- * 全自动化 Git 操作工具
16
- *
17
- * 当启用 full_auto 模式时,AI 可以:
18
- * 1. 自动 stage 更改 (git add)
19
- * 2. 自动创建提交 (git commit)
20
- * 3. 自动推送到远程 (git push)
21
- * 4. 执行其他 Git 操作
22
- *
23
- * 警告:此模式会赋予 AI 更大的权限,可能导致不可逆的操作。
24
- * 建议仅在受控环境或 CI/CD 场景中使用。
25
- */
26
-
27
- /**
28
- * 执行 Git 命令
29
- */
30
- async function runGit(args, cwd, timeoutMs = 30000) {
31
- try {
32
- const { stdout, stderr } = await execAsync(
33
- `git ${args.join(" ")}`,
34
- { cwd, timeout: timeoutMs, encoding: "utf8" }
35
- )
36
- return {
37
- ok: true,
38
- stdout: stdout?.trim() || "",
39
- stderr: stderr?.trim() || ""
40
- }
41
- } catch (error) {
42
- return {
43
- ok: false,
44
- stdout: error.stdout?.trim() || "",
45
- stderr: error.stderr?.trim() || "",
46
- error: error.message
47
- }
48
- }
49
- }
50
-
51
- /**
52
- * 生成提交信息
53
- * 基于更改内容自动生成符合 Conventional Commits 格式的消息
54
- */
55
- async function generateCommitMessage(cwd, customMessage = null) {
56
- if (customMessage) return customMessage
57
-
58
- // 获取变更的概要
59
- const result = await runGit(["status", "--short"], cwd)
60
- if (!result.ok) return "chore: update files"
61
-
62
- const files = result.stdout.split("\n").filter(Boolean)
63
- if (files.length === 0) return "chore: empty commit"
64
-
65
- // 分析文件类型来确定提交类型
66
- const hasTests = files.some(f => f.includes("test") || f.includes("spec"))
67
- const hasDocs = files.some(f => f.endsWith(".md") || f.includes("doc"))
68
- const hasConfig = files.some(f =>
69
- f.includes("config") ||
70
- f.endsWith(".json") ||
71
- f.endsWith(".yaml") ||
72
- f.endsWith(".yml")
73
- )
74
-
75
- let type = "chore"
76
- if (hasTests) type = "test"
77
- else if (hasDocs) type = "docs"
78
- else if (hasConfig) type = "chore"
79
- else if (files.some(f => f.includes("fix") || f.includes("bug"))) type = "fix"
80
- else if (files.some(f => f.includes("feat") || f.includes("feature"))) type = "feat"
81
- else if (files.length > 5) type = "refactor"
82
-
83
- // 生成描述
84
- let description
85
- if (files.length === 1) {
86
- const file = files[0].slice(3) // 移除状态前缀
87
- description = `update ${path.basename(file)}`
88
- } else {
89
- const scope = files.length <= 3
90
- ? files.map(f => path.basename(f.slice(3))).join(", ")
91
- : `${files.length} files`
92
- description = `update ${scope}`
93
- }
94
-
95
- return `${type}: ${description}`
96
- }
97
-
98
- // ============================================================================
99
- // Tool: git_auto_commit - 全自动提交
100
- // ============================================================================
101
-
102
- export const gitAutoCommitTool = {
103
- name: "git_auto_commit",
104
- description: "[FULL-AUTO MODE] Automatically stage all changes and create a git commit. Only available when git_auto.full_auto and git_auto.auto_commit are enabled. This operation cannot be undone without git restore.",
105
- inputSchema: {
106
- type: "object",
107
- properties: {
108
- message: {
109
- type: "string",
110
- description: "Commit message (optional, will be auto-generated if not provided)"
111
- },
112
- stage_all: {
113
- type: "boolean",
114
- description: "Stage all changes including untracked files (default: true)"
115
- },
116
- amend: {
117
- type: "boolean",
118
- description: "Amend the previous commit instead of creating a new one (default: false)"
119
- },
120
- no_verify: {
121
- type: "boolean",
122
- description: "Bypass pre-commit hooks (default: false)"
123
- }
124
- },
125
- required: []
126
- },
127
- async execute(args, ctx) {
128
- const cwd = ctx.cwd || process.cwd()
129
-
130
- // 检查全自动化模式
131
- if (!isFullAutoMode(ctx.config)) {
132
- return {
133
- ok: false,
134
- error: "full_auto_disabled",
135
- message: "Full-auto mode is not enabled. Set git_auto.full_auto: true and git_auto.auto_commit: true in your config."
136
- }
137
- }
138
-
139
- if (ctx.config?.git_auto?.auto_commit !== true) {
140
- return {
141
- ok: false,
142
- error: "auto_commit_disabled",
143
- message: "Auto commit is not enabled. Set git_auto.auto_commit: true in your config."
144
- }
145
- }
146
-
147
- // 检查是否是 Git 仓库
148
- if (!(await isGitRepo(cwd))) {
149
- return {
150
- ok: false,
151
- error: "not_a_git_repo",
152
- message: "Current directory is not a git repository"
153
- }
154
- }
155
-
156
- // 检查是否有更改
157
- const statusResult = await runGit(["status", "--porcelain"], cwd)
158
- if (!statusResult.ok) {
159
- return {
160
- ok: false,
161
- error: "status_check_failed",
162
- message: statusResult.error
163
- }
164
- }
165
-
166
- if (!statusResult.stdout.trim()) {
167
- return {
168
- ok: true,
169
- skipped: true,
170
- message: "No changes to commit"
171
- }
172
- }
173
-
174
- // 1. 创建快照(用于可能的回滚)
175
- const snapshotResult = await gitSnapshotTool.execute(
176
- { auto: true, message: "Pre-auto-commit snapshot" },
177
- { cwd, sessionId: ctx.sessionId, config: ctx.config }
178
- )
179
-
180
- const snapshotId = snapshotResult.ok ? snapshotResult.snapshot?.id : null
181
-
182
- // 2. Stage 更改
183
- const stageAll = args.stage_all !== false // 默认 true
184
- if (stageAll) {
185
- const addResult = await runGit(["add", "-A"], cwd)
186
- if (!addResult.ok) {
187
- return {
188
- ok: false,
189
- error: "stage_failed",
190
- message: `Failed to stage changes: ${addResult.stderr}`,
191
- snapshotId
192
- }
193
- }
194
- }
195
-
196
- // 3. 生成或获取提交信息
197
- const message = await generateCommitMessage(cwd, args.message)
198
-
199
- // 4. 创建提交
200
- const commitArgs = ["commit", "-m", message]
201
- if (args.amend) commitArgs.push("--amend")
202
- if (args.no_verify) commitArgs.push("--no-verify")
203
-
204
- const commitResult = await runGit(commitArgs, cwd)
205
- if (!commitResult.ok) {
206
- return {
207
- ok: false,
208
- error: "commit_failed",
209
- message: `Failed to create commit: ${commitResult.stderr}`,
210
- snapshotId,
211
- staged: stageAll
212
- }
213
- }
214
-
215
- // 5. 获取提交信息
216
- const logResult = await runGit(["log", "-1", "--format=%H|%s"], cwd)
217
- const [hash, subject] = logResult.ok
218
- ? logResult.stdout.split("|")
219
- : ["", message]
220
-
221
- return {
222
- ok: true,
223
- commit: {
224
- hash: hash?.slice(0, 8),
225
- fullHash: hash,
226
- message: subject || message,
227
- branch: await currentBranch(cwd)
228
- },
229
- snapshotId,
230
- staged: stageAll,
231
- message: `Created commit ${hash?.slice(0, 8)}: ${subject || message}`,
232
- warning: "This is an automatic commit. Use git_restore with the snapshot_id to revert if needed."
233
- }
234
- }
235
- }
236
-
237
- // ============================================================================
238
- // Tool: git_auto_push - 全自动推送
239
- // ============================================================================
240
-
241
- export const gitAutoPushTool = {
242
- name: "git_auto_push",
243
- description: "[FULL-AUTO MODE] Automatically push commits to remote. Only available when git_auto.full_auto and git_auto.auto_push are enabled. WARNING: This will upload your changes to remote repository.",
244
- inputSchema: {
245
- type: "object",
246
- properties: {
247
- remote: {
248
- type: "string",
249
- description: "Remote name (default: origin)"
250
- },
251
- branch: {
252
- type: "string",
253
- description: "Branch name (default: current branch)"
254
- },
255
- force: {
256
- type: "boolean",
257
- description: "Force push (DANGEROUS, only works if allow_dangerous_ops is also enabled) (default: false)"
258
- },
259
- set_upstream: {
260
- type: "boolean",
261
- description: "Set upstream for new branch (default: true)"
262
- }
263
- },
264
- required: []
265
- },
266
- async execute(args, ctx) {
267
- const cwd = ctx.cwd || process.cwd()
268
-
269
- // 检查全自动化模式
270
- if (!isFullAutoMode(ctx.config)) {
271
- return {
272
- ok: false,
273
- error: "full_auto_disabled",
274
- message: "Full-auto mode is not enabled. Set git_auto.full_auto: true and git_auto.auto_push: true in your config."
275
- }
276
- }
277
-
278
- if (ctx.config?.git_auto?.auto_push !== true) {
279
- return {
280
- ok: false,
281
- error: "auto_push_disabled",
282
- message: "Auto push is not enabled. Set git_auto.auto_push: true in your config."
283
- }
284
- }
285
-
286
- // 检查是否是 Git 仓库
287
- if (!(await isGitRepo(cwd))) {
288
- return {
289
- ok: false,
290
- error: "not_a_git_repo",
291
- message: "Current directory is not a git repository"
292
- }
293
- }
294
-
295
- const remote = args.remote || "origin"
296
- const branch = args.branch || await currentBranch(cwd)
297
-
298
- if (!branch) {
299
- return {
300
- ok: false,
301
- error: "no_branch",
302
- message: "Could not determine current branch"
303
- }
304
- }
305
-
306
- // 检查是否需要设置 upstream
307
- const setUpstream = args.set_upstream !== false && branch !== "main" && branch !== "master"
308
-
309
- // 构建 push 命令
310
- const pushArgs = ["push"]
311
- if (args.force) {
312
- if (ctx.config?.git_auto?.allow_dangerous_ops !== true) {
313
- return {
314
- ok: false,
315
- error: "force_push_forbidden",
316
- message: "Force push is forbidden. Enable git_auto.allow_dangerous_ops: true to allow force push."
317
- }
318
- }
319
- pushArgs.push("--force")
320
- }
321
- if (setUpstream) pushArgs.push("-u")
322
- pushArgs.push(remote, branch)
323
-
324
- const pushResult = await runGit(pushArgs, cwd)
325
- if (!pushResult.ok) {
326
- return {
327
- ok: false,
328
- error: "push_failed",
329
- message: `Failed to push: ${pushResult.stderr}`
330
- }
331
- }
332
-
333
- return {
334
- ok: true,
335
- pushed: {
336
- remote,
337
- branch,
338
- force: !!args.force
339
- },
340
- output: pushResult.stdout,
341
- message: `Pushed ${branch} to ${remote}${args.force ? " (forced)" : ""}`,
342
- warning: args.force ? "Force push was used. This may have overwritten remote history." : undefined
343
- }
344
- }
345
- }
346
-
347
- // ============================================================================
348
- // Tool: git_auto_stage - 自动暂存
349
- // ============================================================================
350
-
351
- export const gitAutoStageTool = {
352
- name: "git_auto_stage",
353
- description: "[FULL-AUTO MODE] Automatically stage files for commit. Available when git_auto.full_auto is enabled.",
354
- inputSchema: {
355
- type: "object",
356
- properties: {
357
- files: {
358
- type: "array",
359
- items: { type: "string" },
360
- description: "Specific files to stage (default: all changes)"
361
- },
362
- all: {
363
- type: "boolean",
364
- description: "Stage all changes including untracked files (default: true if files not specified)"
365
- }
366
- },
367
- required: []
368
- },
369
- async execute(args, ctx) {
370
- const cwd = ctx.cwd || process.cwd()
371
-
372
- // 检查全自动化模式
373
- if (!isFullAutoMode(ctx.config)) {
374
- return {
375
- ok: false,
376
- error: "full_auto_disabled",
377
- message: "Full-auto mode is not enabled. Set git_auto.full_auto: true in your config."
378
- }
379
- }
380
-
381
- if (!(await isGitRepo(cwd))) {
382
- return {
383
- ok: false,
384
- error: "not_a_git_repo",
385
- message: "Current directory is not a git repository"
386
- }
387
- }
388
-
389
- const hasSpecificFiles = Array.isArray(args.files) && args.files.length > 0
390
- const stageAll = !hasSpecificFiles && (args.all !== false)
391
-
392
- let result
393
- if (hasSpecificFiles) {
394
- result = await runGit(["add", "--", ...args.files], cwd)
395
- } else if (stageAll) {
396
- result = await runGit(["add", "-A"], cwd)
397
- } else {
398
- return {
399
- ok: false,
400
- error: "nothing_to_stage",
401
- message: "No files specified and all: false"
402
- }
403
- }
404
-
405
- if (!result.ok) {
406
- return {
407
- ok: false,
408
- error: "stage_failed",
409
- message: result.stderr
410
- }
411
- }
412
-
413
- // 获取 staged 文件列表
414
- const diffResult = await runGit(["diff", "--staged", "--name-only"], cwd)
415
- const stagedFiles = diffResult.ok
416
- ? diffResult.stdout.split("\n").filter(Boolean)
417
- : []
418
-
419
- return {
420
- ok: true,
421
- staged: stagedFiles,
422
- message: `Staged ${stagedFiles.length} file(s) for commit`
423
- }
424
- }
425
- }
426
-
427
- // ============================================================================
428
- // Tool: git_full_auto_status - 获取全自动化模式状态
429
- // ============================================================================
430
-
431
- export const gitFullAutoStatusTool = {
432
- name: "git_full_auto_status",
433
- description: "Check the full-auto mode status and available operations. Shows current configuration and what operations are permitted.",
434
- inputSchema: {
435
- type: "object",
436
- properties: {},
437
- required: []
438
- },
439
- async execute(args, ctx) {
440
- const cwd = ctx.cwd || process.cwd()
441
- const policyMode = getPolicyMode(ctx.config)
442
- const isGit = await isGitRepo(cwd)
443
-
444
- return {
445
- ok: true,
446
- mode: policyMode.mode,
447
- restrictions: policyMode.restrictions,
448
- isGitRepo: isGit,
449
- config: {
450
- full_auto: ctx.config?.git_auto?.full_auto === true,
451
- auto_commit: ctx.config?.git_auto?.auto_commit === true,
452
- auto_push: ctx.config?.git_auto?.auto_push === true,
453
- auto_stage: ctx.config?.git_auto?.auto_stage !== false,
454
- allow_dangerous_ops: ctx.config?.git_auto?.allow_dangerous_ops === true
455
- },
456
- available_tools: [
457
- ...(ctx.config?.git_auto?.full_auto ? ["git_auto_stage"] : []),
458
- ...(ctx.config?.git_auto?.full_auto && ctx.config?.git_auto?.auto_commit ? ["git_auto_commit"] : []),
459
- ...(ctx.config?.git_auto?.full_auto && ctx.config?.git_auto?.auto_push ? ["git_auto_push"] : []),
460
- "git_snapshot",
461
- "git_restore",
462
- "git_info",
463
- "git_status"
464
- ]
465
- }
466
- }
467
- }
468
-
469
- // ============================================================================
470
- // 导出所有全自动化工具
471
- // ============================================================================
472
-
473
- export const gitFullAutoTools = [
474
- gitAutoCommitTool,
475
- gitAutoPushTool,
476
- gitAutoStageTool,
477
- gitFullAutoStatusTool
478
- ]
1
+ import path from "node:path"
2
+ import { spawn } from "node:child_process"
3
+ import {
4
+ isGitRepo,
5
+ currentBranch,
6
+ commitAll as gitCommitAll
7
+ } from "../util/git.mjs"
8
+ import { gitSnapshotTool } from "./git-auto.mjs"
9
+ import { isFullAutoMode, getPolicyMode } from "../permission/exec-policy.mjs"
10
+
11
+ /**
12
+ * 全自动化 Git 操作工具
13
+ *
14
+ * 当启用 full_auto 模式时,AI 可以:
15
+ * 1. 自动 stage 更改 (git add)
16
+ * 2. 自动创建提交 (git commit)
17
+ * 3. 自动推送到远程 (git push)
18
+ * 4. 执行其他 Git 操作
19
+ *
20
+ * 警告:此模式会赋予 AI 更大的权限,可能导致不可逆的操作。
21
+ * 建议仅在受控环境或 CI/CD 场景中使用。
22
+ */
23
+
24
+ /**
25
+ * 执行 Git 命令
26
+ */
27
+ async function runGit(args, cwd, timeoutMs = 30000) {
28
+ return new Promise((resolve) => {
29
+ let stdout = ""
30
+ let stderr = ""
31
+ let done = false
32
+ const child = spawn("git", args, {
33
+ cwd,
34
+ windowsHide: true,
35
+ stdio: ["ignore", "pipe", "pipe"]
36
+ })
37
+ const timer = setTimeout(() => {
38
+ if (done) return
39
+ done = true
40
+ child.kill()
41
+ resolve({ ok: false, stdout, stderr: "git command timed out", error: "timeout" })
42
+ }, timeoutMs)
43
+ child.stdout.on("data", (buf) => { stdout += String(buf) })
44
+ child.stderr.on("data", (buf) => { stderr += String(buf) })
45
+ child.on("error", (err) => {
46
+ if (done) return
47
+ done = true
48
+ clearTimeout(timer)
49
+ resolve({ ok: false, stdout, stderr: err.message, error: err.message })
50
+ })
51
+ child.on("close", (code) => {
52
+ if (done) return
53
+ done = true
54
+ clearTimeout(timer)
55
+ resolve({ ok: code === 0, stdout: stdout.trim(), stderr: stderr.trim() })
56
+ })
57
+ })
58
+ }
59
+
60
+ /**
61
+ * 生成提交信息
62
+ * 基于更改内容自动生成符合 Conventional Commits 格式的消息
63
+ */
64
+ async function generateCommitMessage(cwd, customMessage = null) {
65
+ if (customMessage) return customMessage
66
+
67
+ // 获取变更的概要
68
+ const result = await runGit(["status", "--short"], cwd)
69
+ if (!result.ok) return "chore: update files"
70
+
71
+ const files = result.stdout.split("\n").filter(Boolean)
72
+ if (files.length === 0) return "chore: empty commit"
73
+
74
+ // 分析文件类型来确定提交类型
75
+ const hasTests = files.some(f => f.includes("test") || f.includes("spec"))
76
+ const hasDocs = files.some(f => f.endsWith(".md") || f.includes("doc"))
77
+ const hasConfig = files.some(f =>
78
+ f.includes("config") ||
79
+ f.endsWith(".json") ||
80
+ f.endsWith(".yaml") ||
81
+ f.endsWith(".yml")
82
+ )
83
+
84
+ let type = "chore"
85
+ if (hasTests) type = "test"
86
+ else if (hasDocs) type = "docs"
87
+ else if (hasConfig) type = "chore"
88
+ else if (files.some(f => f.includes("fix") || f.includes("bug"))) type = "fix"
89
+ else if (files.some(f => f.includes("feat") || f.includes("feature"))) type = "feat"
90
+ else if (files.length > 5) type = "refactor"
91
+
92
+ // 生成描述
93
+ let description
94
+ if (files.length === 1) {
95
+ const file = files[0].slice(3) // 移除状态前缀
96
+ description = `update ${path.basename(file)}`
97
+ } else {
98
+ const scope = files.length <= 3
99
+ ? files.map(f => path.basename(f.slice(3))).join(", ")
100
+ : `${files.length} files`
101
+ description = `update ${scope}`
102
+ }
103
+
104
+ return `${type}: ${description}`
105
+ }
106
+
107
+ // ============================================================================
108
+ // Tool: git_auto_commit - 全自动提交
109
+ // ============================================================================
110
+
111
+ export const gitAutoCommitTool = {
112
+ name: "git_auto_commit",
113
+ description: "[FULL-AUTO MODE] Automatically stage all changes and create a git commit. Only available when git_auto.full_auto and git_auto.auto_commit are enabled. This operation cannot be undone without git restore.",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {
117
+ message: {
118
+ type: "string",
119
+ description: "Commit message (optional, will be auto-generated if not provided)"
120
+ },
121
+ stage_all: {
122
+ type: "boolean",
123
+ description: "Stage all changes including untracked files (default: true)"
124
+ },
125
+ amend: {
126
+ type: "boolean",
127
+ description: "Amend the previous commit instead of creating a new one (default: false)"
128
+ },
129
+ no_verify: {
130
+ type: "boolean",
131
+ description: "Bypass pre-commit hooks (default: false)"
132
+ }
133
+ },
134
+ required: []
135
+ },
136
+ async execute(args, ctx) {
137
+ const cwd = ctx.cwd || process.cwd()
138
+
139
+ // 检查全自动化模式
140
+ if (!isFullAutoMode(ctx.config)) {
141
+ return {
142
+ ok: false,
143
+ error: "full_auto_disabled",
144
+ message: "Full-auto mode is not enabled. Set git_auto.full_auto: true and git_auto.auto_commit: true in your config."
145
+ }
146
+ }
147
+
148
+ if (ctx.config?.git_auto?.auto_commit !== true) {
149
+ return {
150
+ ok: false,
151
+ error: "auto_commit_disabled",
152
+ message: "Auto commit is not enabled. Set git_auto.auto_commit: true in your config."
153
+ }
154
+ }
155
+
156
+ // 检查是否是 Git 仓库
157
+ if (!(await isGitRepo(cwd))) {
158
+ return {
159
+ ok: false,
160
+ error: "not_a_git_repo",
161
+ message: "Current directory is not a git repository"
162
+ }
163
+ }
164
+
165
+ // 检查是否有更改
166
+ const statusResult = await runGit(["status", "--porcelain"], cwd)
167
+ if (!statusResult.ok) {
168
+ return {
169
+ ok: false,
170
+ error: "status_check_failed",
171
+ message: statusResult.error
172
+ }
173
+ }
174
+
175
+ if (!statusResult.stdout.trim()) {
176
+ return {
177
+ ok: true,
178
+ skipped: true,
179
+ message: "No changes to commit"
180
+ }
181
+ }
182
+
183
+ // 1. 创建快照(用于可能的回滚)
184
+ const snapshotResult = await gitSnapshotTool.execute(
185
+ { auto: true, message: "Pre-auto-commit snapshot" },
186
+ { cwd, sessionId: ctx.sessionId, config: ctx.config }
187
+ )
188
+
189
+ const snapshotId = snapshotResult.ok ? snapshotResult.snapshot?.id : null
190
+
191
+ // 2. Stage 更改
192
+ const stageAll = args.stage_all !== false // 默认 true
193
+ if (stageAll) {
194
+ const addResult = await runGit(["add", "-A"], cwd)
195
+ if (!addResult.ok) {
196
+ return {
197
+ ok: false,
198
+ error: "stage_failed",
199
+ message: `Failed to stage changes: ${addResult.stderr}`,
200
+ snapshotId
201
+ }
202
+ }
203
+ }
204
+
205
+ // 3. 生成或获取提交信息
206
+ const message = await generateCommitMessage(cwd, args.message)
207
+
208
+ // 4. 创建提交
209
+ const commitArgs = ["commit", "-m", message]
210
+ if (args.amend) commitArgs.push("--amend")
211
+ if (args.no_verify) commitArgs.push("--no-verify")
212
+
213
+ const commitResult = await runGit(commitArgs, cwd)
214
+ if (!commitResult.ok) {
215
+ return {
216
+ ok: false,
217
+ error: "commit_failed",
218
+ message: `Failed to create commit: ${commitResult.stderr}`,
219
+ snapshotId,
220
+ staged: stageAll
221
+ }
222
+ }
223
+
224
+ // 5. 获取提交信息
225
+ const logResult = await runGit(["log", "-1", "--format=%H|%s"], cwd)
226
+ const [hash, subject] = logResult.ok
227
+ ? logResult.stdout.split("|")
228
+ : ["", message]
229
+
230
+ return {
231
+ ok: true,
232
+ commit: {
233
+ hash: hash?.slice(0, 8),
234
+ fullHash: hash,
235
+ message: subject || message,
236
+ branch: await currentBranch(cwd)
237
+ },
238
+ snapshotId,
239
+ staged: stageAll,
240
+ message: `Created commit ${hash?.slice(0, 8)}: ${subject || message}`,
241
+ warning: "This is an automatic commit. Use git_restore with the snapshot_id to revert if needed."
242
+ }
243
+ }
244
+ }
245
+
246
+ // ============================================================================
247
+ // Tool: git_auto_push - 全自动推送
248
+ // ============================================================================
249
+
250
+ export const gitAutoPushTool = {
251
+ name: "git_auto_push",
252
+ description: "[FULL-AUTO MODE] Automatically push commits to remote. Only available when git_auto.full_auto and git_auto.auto_push are enabled. WARNING: This will upload your changes to remote repository.",
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: {
256
+ remote: {
257
+ type: "string",
258
+ description: "Remote name (default: origin)"
259
+ },
260
+ branch: {
261
+ type: "string",
262
+ description: "Branch name (default: current branch)"
263
+ },
264
+ force: {
265
+ type: "boolean",
266
+ description: "Force push (DANGEROUS, only works if allow_dangerous_ops is also enabled) (default: false)"
267
+ },
268
+ set_upstream: {
269
+ type: "boolean",
270
+ description: "Set upstream for new branch (default: true)"
271
+ }
272
+ },
273
+ required: []
274
+ },
275
+ async execute(args, ctx) {
276
+ const cwd = ctx.cwd || process.cwd()
277
+
278
+ // 检查全自动化模式
279
+ if (!isFullAutoMode(ctx.config)) {
280
+ return {
281
+ ok: false,
282
+ error: "full_auto_disabled",
283
+ message: "Full-auto mode is not enabled. Set git_auto.full_auto: true and git_auto.auto_push: true in your config."
284
+ }
285
+ }
286
+
287
+ if (ctx.config?.git_auto?.auto_push !== true) {
288
+ return {
289
+ ok: false,
290
+ error: "auto_push_disabled",
291
+ message: "Auto push is not enabled. Set git_auto.auto_push: true in your config."
292
+ }
293
+ }
294
+
295
+ // 检查是否是 Git 仓库
296
+ if (!(await isGitRepo(cwd))) {
297
+ return {
298
+ ok: false,
299
+ error: "not_a_git_repo",
300
+ message: "Current directory is not a git repository"
301
+ }
302
+ }
303
+
304
+ const remote = args.remote || "origin"
305
+ const branch = args.branch || await currentBranch(cwd)
306
+
307
+ if (!branch) {
308
+ return {
309
+ ok: false,
310
+ error: "no_branch",
311
+ message: "Could not determine current branch"
312
+ }
313
+ }
314
+
315
+ // 检查是否需要设置 upstream
316
+ const setUpstream = args.set_upstream !== false && branch !== "main" && branch !== "master"
317
+
318
+ // 构建 push 命令
319
+ const pushArgs = ["push"]
320
+ if (args.force) {
321
+ if (ctx.config?.git_auto?.allow_dangerous_ops !== true) {
322
+ return {
323
+ ok: false,
324
+ error: "force_push_forbidden",
325
+ message: "Force push is forbidden. Enable git_auto.allow_dangerous_ops: true to allow force push."
326
+ }
327
+ }
328
+ pushArgs.push("--force")
329
+ }
330
+ if (setUpstream) pushArgs.push("-u")
331
+ pushArgs.push(remote, branch)
332
+
333
+ const pushResult = await runGit(pushArgs, cwd)
334
+ if (!pushResult.ok) {
335
+ return {
336
+ ok: false,
337
+ error: "push_failed",
338
+ message: `Failed to push: ${pushResult.stderr}`
339
+ }
340
+ }
341
+
342
+ return {
343
+ ok: true,
344
+ pushed: {
345
+ remote,
346
+ branch,
347
+ force: !!args.force
348
+ },
349
+ output: pushResult.stdout,
350
+ message: `Pushed ${branch} to ${remote}${args.force ? " (forced)" : ""}`,
351
+ warning: args.force ? "Force push was used. This may have overwritten remote history." : undefined
352
+ }
353
+ }
354
+ }
355
+
356
+ // ============================================================================
357
+ // Tool: git_auto_stage - 自动暂存
358
+ // ============================================================================
359
+
360
+ export const gitAutoStageTool = {
361
+ name: "git_auto_stage",
362
+ description: "[FULL-AUTO MODE] Automatically stage files for commit. Available when git_auto.full_auto is enabled.",
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {
366
+ files: {
367
+ type: "array",
368
+ items: { type: "string" },
369
+ description: "Specific files to stage (default: all changes)"
370
+ },
371
+ all: {
372
+ type: "boolean",
373
+ description: "Stage all changes including untracked files (default: true if files not specified)"
374
+ }
375
+ },
376
+ required: []
377
+ },
378
+ async execute(args, ctx) {
379
+ const cwd = ctx.cwd || process.cwd()
380
+
381
+ // 检查全自动化模式
382
+ if (!isFullAutoMode(ctx.config)) {
383
+ return {
384
+ ok: false,
385
+ error: "full_auto_disabled",
386
+ message: "Full-auto mode is not enabled. Set git_auto.full_auto: true in your config."
387
+ }
388
+ }
389
+
390
+ if (!(await isGitRepo(cwd))) {
391
+ return {
392
+ ok: false,
393
+ error: "not_a_git_repo",
394
+ message: "Current directory is not a git repository"
395
+ }
396
+ }
397
+
398
+ const hasSpecificFiles = Array.isArray(args.files) && args.files.length > 0
399
+ const stageAll = !hasSpecificFiles && (args.all !== false)
400
+
401
+ let result
402
+ if (hasSpecificFiles) {
403
+ result = await runGit(["add", "--", ...args.files], cwd)
404
+ } else if (stageAll) {
405
+ result = await runGit(["add", "-A"], cwd)
406
+ } else {
407
+ return {
408
+ ok: false,
409
+ error: "nothing_to_stage",
410
+ message: "No files specified and all: false"
411
+ }
412
+ }
413
+
414
+ if (!result.ok) {
415
+ return {
416
+ ok: false,
417
+ error: "stage_failed",
418
+ message: result.stderr
419
+ }
420
+ }
421
+
422
+ // 获取 staged 文件列表
423
+ const diffResult = await runGit(["diff", "--staged", "--name-only"], cwd)
424
+ const stagedFiles = diffResult.ok
425
+ ? diffResult.stdout.split("\n").filter(Boolean)
426
+ : []
427
+
428
+ return {
429
+ ok: true,
430
+ staged: stagedFiles,
431
+ message: `Staged ${stagedFiles.length} file(s) for commit`
432
+ }
433
+ }
434
+ }
435
+
436
+ // ============================================================================
437
+ // Tool: git_full_auto_status - 获取全自动化模式状态
438
+ // ============================================================================
439
+
440
+ export const gitFullAutoStatusTool = {
441
+ name: "git_full_auto_status",
442
+ description: "Check the full-auto mode status and available operations. Shows current configuration and what operations are permitted.",
443
+ inputSchema: {
444
+ type: "object",
445
+ properties: {},
446
+ required: []
447
+ },
448
+ async execute(args, ctx) {
449
+ const cwd = ctx.cwd || process.cwd()
450
+ const policyMode = getPolicyMode(ctx.config)
451
+ const isGit = await isGitRepo(cwd)
452
+
453
+ return {
454
+ ok: true,
455
+ mode: policyMode.mode,
456
+ restrictions: policyMode.restrictions,
457
+ isGitRepo: isGit,
458
+ config: {
459
+ full_auto: ctx.config?.git_auto?.full_auto === true,
460
+ auto_commit: ctx.config?.git_auto?.auto_commit === true,
461
+ auto_push: ctx.config?.git_auto?.auto_push === true,
462
+ auto_stage: ctx.config?.git_auto?.auto_stage !== false,
463
+ allow_dangerous_ops: ctx.config?.git_auto?.allow_dangerous_ops === true
464
+ },
465
+ available_tools: [
466
+ ...(ctx.config?.git_auto?.full_auto ? ["git_auto_stage"] : []),
467
+ ...(ctx.config?.git_auto?.full_auto && ctx.config?.git_auto?.auto_commit ? ["git_auto_commit"] : []),
468
+ ...(ctx.config?.git_auto?.full_auto && ctx.config?.git_auto?.auto_push ? ["git_auto_push"] : []),
469
+ "git_snapshot",
470
+ "git_restore",
471
+ "git_info",
472
+ "git_status"
473
+ ]
474
+ }
475
+ }
476
+ }
477
+
478
+ // ============================================================================
479
+ // 导出所有全自动化工具
480
+ // ============================================================================
481
+
482
+ export const gitFullAutoTools = [
483
+ gitAutoCommitTool,
484
+ gitAutoPushTool,
485
+ gitAutoStageTool,
486
+ gitFullAutoStatusTool
487
+ ]