@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.
Files changed (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +19 -2
  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 +90 -0
  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/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2929
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +36 -14
  96. package/src/session/engine.mjs +417 -227
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1081
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -884
  105. package/src/session/loop.mjs +1005 -905
  106. package/src/session/prompt/agent.txt +25 -0
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +28 -6
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +197 -0
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -510
  116. package/src/session/system-prompt.mjs +56 -8
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +17 -4
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. package/src/util/template.mjs +6 -1
@@ -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
+ ]