@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,302 +1,302 @@
1
- import path from "node:path"
2
- import { mkdir, readdir } from "node:fs/promises"
3
- import { readJson, writeJson } from "../storage/json-store.mjs"
4
- import { userRootDir } from "../storage/paths.mjs"
5
- import { isGitRepo } from "../util/git.mjs"
6
- import { gitSnapshotTool } from "../tool/git-auto.mjs"
7
- import { listGhostCommits, getLatestGhostCommit } from "../storage/ghost-commit-store.mjs"
8
-
9
- function checkpointDir(sessionId) {
10
- return path.join(userRootDir(), "checkpoints", sessionId)
11
- }
12
-
13
- function checkpointFile(sessionId, name) {
14
- return path.join(checkpointDir(sessionId), `${name}.json`)
15
- }
16
-
17
- function latestFile(sessionId) {
18
- return checkpointFile(sessionId, "latest")
19
- }
20
-
21
- export async function saveCheckpoint(sessionId, data) {
22
- const dir = checkpointDir(sessionId)
23
- await mkdir(dir, { recursive: true })
24
- const checkpoint = {
25
- sessionId,
26
- savedAt: Date.now(),
27
- ...data
28
- }
29
- await writeJson(latestFile(sessionId), checkpoint)
30
- const numbered = checkpointFile(sessionId, `cp_${data.iteration || 0}`)
31
- await writeJson(numbered, checkpoint)
32
- return checkpoint
33
- }
34
-
35
- export async function loadCheckpoint(sessionId, name = "latest") {
36
- const file = name === "latest" ? latestFile(sessionId) : checkpointFile(sessionId, name)
37
- return readJson(file, null)
38
- }
39
-
40
- export async function listCheckpoints(sessionId) {
41
- const dir = checkpointDir(sessionId)
42
- const files = await readdir(dir, { withFileTypes: true }).catch(() => [])
43
- return files
44
- .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
45
- .map((entry) => entry.name.replace(/\.json$/, ""))
46
- .sort()
47
- }
48
-
49
- // ========== Phase 7: Task 级 Checkpoint ==========
50
-
51
- export async function saveTaskCheckpoint(sessionId, stageId, taskId, data) {
52
- const dir = checkpointDir(sessionId)
53
- await mkdir(dir, { recursive: true })
54
- const name = `task_${stageId}_${taskId}`
55
- const checkpoint = {
56
- sessionId,
57
- stageId,
58
- taskId,
59
- savedAt: Date.now(),
60
- ...data
61
- }
62
- await writeJson(checkpointFile(sessionId, name), checkpoint)
63
- return checkpoint
64
- }
65
-
66
- export async function loadTaskCheckpoints(sessionId, stageId) {
67
- const dir = checkpointDir(sessionId)
68
- const files = await readdir(dir, { withFileTypes: true }).catch(() => [])
69
- const prefix = `task_${stageId}_`
70
- const results = {}
71
- for (const entry of files) {
72
- if (entry.isFile() && entry.name.startsWith(prefix) && entry.name.endsWith(".json")) {
73
- const data = await readJson(path.join(dir, entry.name), null)
74
- if (data?.taskId) results[data.taskId] = data
75
- }
76
- }
77
- return results
78
- }
79
-
80
- // ========== Phase 10: Checkpoint 清理策略 ==========
81
-
82
- export async function cleanupCheckpoints(sessionId, options = {}) {
83
- const maxKeep = options.maxKeep || 10
84
- const keepStageCheckpoints = options.keepStageCheckpoints !== false
85
- const dir = checkpointDir(sessionId)
86
- const all = await listCheckpoints(sessionId)
87
- if (all.length <= maxKeep + 1) return { removed: 0 }
88
-
89
- const toKeep = new Set(["latest"])
90
- // 保留 stage 级和 task 级 checkpoint
91
- if (keepStageCheckpoints) {
92
- for (const name of all) {
93
- if (name.startsWith("hybrid_stage_") || name.startsWith("task_")) {
94
- toKeep.add(name)
95
- }
96
- }
97
- }
98
- // 保留最近 maxKeep 个编号 checkpoint
99
- const numbered = all.filter(n => n.startsWith("cp_")).sort()
100
- for (const n of numbered.slice(-maxKeep)) toKeep.add(n)
101
-
102
- let removed = 0
103
- for (const name of all) {
104
- if (toKeep.has(name)) continue
105
- try {
106
- const { unlink: unlinkFile } = await import("node:fs/promises")
107
- await unlinkFile(checkpointFile(sessionId, name)).catch(() => {})
108
- removed++
109
- } catch { /* ignore */ }
110
- }
111
- return { removed }
112
- }
113
-
114
- // ============================================================================
115
- // Git Snapshot Integration - AI Agent 自动 Git 快照功能
116
- // ============================================================================
117
-
118
- /**
119
- * 在 AI 修改前自动创建 Git 快照
120
- *
121
- * @param {string} sessionId - 会话ID
122
- * @param {string} cwd - 工作目录
123
- * @param {Object} config - 配置对象
124
- * @param {Object} options - 选项
125
- * @param {string} [options.reason] - 快照原因
126
- * @returns {Promise<{ok: boolean, snapshot?: Object, skipped?: boolean, reason?: string}>}
127
- */
128
- export async function autoSnapshotBeforeEdit(sessionId, cwd, config = {}, options = {}) {
129
- // 检查 Git 自动化是否启用(默认启用,只有显式关闭才跳过)
130
- if (config.git_auto?.enabled === false) {
131
- return { ok: true, skipped: true, reason: "git_auto_disabled" }
132
- }
133
- if (config.git_auto?.auto_snapshot === false) {
134
- return { ok: true, skipped: true, reason: "auto_snapshot_disabled" }
135
- }
136
-
137
- // 检查是否是 Git 仓库
138
- if (!(await isGitRepo(cwd))) {
139
- return { ok: true, skipped: true, reason: "not_a_git_repo" }
140
- }
141
-
142
- try {
143
- const result = await gitSnapshotTool.execute(
144
- {
145
- auto: true,
146
- message: options.reason || `Auto snapshot before AI edit (session: ${sessionId})`
147
- },
148
- { cwd, sessionId, config }
149
- )
150
-
151
- if (result.ok) {
152
- return {
153
- ok: true,
154
- snapshot: result.snapshot,
155
- skipped: false
156
- }
157
- } else {
158
- return {
159
- ok: false,
160
- skipped: true,
161
- reason: result.message || "snapshot_failed"
162
- }
163
- }
164
- } catch (error) {
165
- return {
166
- ok: false,
167
- skipped: true,
168
- reason: error.message
169
- }
170
- }
171
- }
172
-
173
- /**
174
- * 获取会话的 Git 快照历史
175
- *
176
- * @param {string} sessionId - 会话ID
177
- * @param {string} cwd - 工作目录
178
- * @returns {Promise<Array<Object>>}
179
- */
180
- export async function getSessionSnapshots(sessionId, cwd) {
181
- if (!(await isGitRepo(cwd))) {
182
- return []
183
- }
184
-
185
- const snapshots = await listGhostCommits(cwd)
186
- // 过滤出当前会话的快照
187
- return snapshots.filter(s =>
188
- s.message?.includes(`session: ${sessionId}`) ||
189
- s.message?.includes("Auto snapshot")
190
- )
191
- }
192
-
193
- /**
194
- * 恢复到会话的最近一次快照
195
- *
196
- * @param {string} sessionId - 会话ID
197
- * @param {string} cwd - 工作目录
198
- * @returns {Promise<{ok: boolean, message?: string, error?: string}>}
199
- */
200
- export async function restoreLastSessionSnapshot(sessionId, cwd) {
201
- if (!(await isGitRepo(cwd))) {
202
- return { ok: false, error: "Not a git repository" }
203
- }
204
-
205
- const snapshots = await getSessionSnapshots(sessionId, cwd)
206
- if (snapshots.length === 0) {
207
- return { ok: false, error: "No snapshots found for this session" }
208
- }
209
-
210
- const latest = snapshots[0]
211
- const { gitRestoreTool } = await import("../tool/git-auto.mjs")
212
-
213
- const result = await gitRestoreTool.execute(
214
- { snapshot_id: latest.id },
215
- { cwd, sessionId }
216
- )
217
-
218
- return result
219
- }
220
-
221
- /**
222
- * Checkpoint Manager - 统一的管理器
223
- *
224
- * 协调 JSON checkpoint 和 Git snapshot 两种机制:
225
- * - JSON checkpoint: 保存会话状态(内存中的数据)
226
- * - Git snapshot: 保存工作目录状态(文件系统状态)
227
- */
228
- export class CheckpointManager {
229
- constructor(sessionId, cwd, config = {}) {
230
- this.sessionId = sessionId
231
- this.cwd = cwd
232
- this.config = config
233
- this.lastSnapshotId = null
234
- }
235
-
236
- /**
237
- * 在修改前创建检查点(自动决定使用哪种机制)
238
- */
239
- async beforeEdit(reason = "AI edit") {
240
- const results = {
241
- jsonCheckpoint: null,
242
- gitSnapshot: null
243
- }
244
-
245
- // 1. 创建 JSON checkpoint(如果配置启用)
246
- if (this.config.checkpoint?.enabled !== false) {
247
- // 这里可以扩展保存更多会话状态
248
- results.jsonCheckpoint = await saveCheckpoint(this.sessionId, {
249
- type: "pre_edit",
250
- reason,
251
- timestamp: Date.now()
252
- })
253
- }
254
-
255
- // 2. 创建 Git snapshot(如果配置启用)
256
- if (this.config.git_auto?.enabled !== false && this.config.git_auto?.auto_snapshot !== false) {
257
- const snapshotResult = await autoSnapshotBeforeEdit(
258
- this.sessionId,
259
- this.cwd,
260
- this.config,
261
- { reason }
262
- )
263
-
264
- if (snapshotResult.ok && !snapshotResult.skipped) {
265
- results.gitSnapshot = snapshotResult.snapshot
266
- this.lastSnapshotId = snapshotResult.snapshot.id
267
- }
268
- }
269
-
270
- return results
271
- }
272
-
273
- /**
274
- * 恢复到最近一次检查点
275
- */
276
- async restore() {
277
- if (this.lastSnapshotId) {
278
- const { gitRestoreTool } = await import("../tool/git-auto.mjs")
279
- return await gitRestoreTool.execute(
280
- { snapshot_id: this.lastSnapshotId },
281
- { cwd: this.cwd, sessionId: this.sessionId }
282
- )
283
- }
284
-
285
- // 如果没有快照ID,尝试恢复到最近一次会话快照
286
- return await restoreLastSessionSnapshot(this.sessionId, this.cwd)
287
- }
288
-
289
- /**
290
- * 获取当前会话的所有快照
291
- */
292
- async listSnapshots() {
293
- return await getSessionSnapshots(this.sessionId, this.cwd)
294
- }
295
- }
296
-
297
- /**
298
- * 创建 CheckpointManager 实例的工厂函数
299
- */
300
- export function createCheckpointManager(sessionId, cwd, config) {
301
- return new CheckpointManager(sessionId, cwd, config)
302
- }
1
+ import path from "node:path"
2
+ import { mkdir, readdir } from "node:fs/promises"
3
+ import { readJson, writeJson } from "../storage/json-store.mjs"
4
+ import { userRootDir } from "../storage/paths.mjs"
5
+ import { isGitRepo } from "../util/git.mjs"
6
+ import { gitSnapshotTool } from "../tool/git-auto.mjs"
7
+ import { listGhostCommits, getLatestGhostCommit } from "../storage/ghost-commit-store.mjs"
8
+
9
+ function checkpointDir(sessionId) {
10
+ return path.join(userRootDir(), "checkpoints", sessionId)
11
+ }
12
+
13
+ function checkpointFile(sessionId, name) {
14
+ return path.join(checkpointDir(sessionId), `${name}.json`)
15
+ }
16
+
17
+ function latestFile(sessionId) {
18
+ return checkpointFile(sessionId, "latest")
19
+ }
20
+
21
+ export async function saveCheckpoint(sessionId, data) {
22
+ const dir = checkpointDir(sessionId)
23
+ await mkdir(dir, { recursive: true })
24
+ const checkpoint = {
25
+ sessionId,
26
+ savedAt: Date.now(),
27
+ ...data
28
+ }
29
+ await writeJson(latestFile(sessionId), checkpoint)
30
+ const numbered = checkpointFile(sessionId, `cp_${data.iteration || 0}`)
31
+ await writeJson(numbered, checkpoint)
32
+ return checkpoint
33
+ }
34
+
35
+ export async function loadCheckpoint(sessionId, name = "latest") {
36
+ const file = name === "latest" ? latestFile(sessionId) : checkpointFile(sessionId, name)
37
+ return readJson(file, null)
38
+ }
39
+
40
+ export async function listCheckpoints(sessionId) {
41
+ const dir = checkpointDir(sessionId)
42
+ const files = await readdir(dir, { withFileTypes: true }).catch(() => [])
43
+ return files
44
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
45
+ .map((entry) => entry.name.replace(/\.json$/, ""))
46
+ .sort()
47
+ }
48
+
49
+ // ========== Phase 7: Task 级 Checkpoint ==========
50
+
51
+ export async function saveTaskCheckpoint(sessionId, stageId, taskId, data) {
52
+ const dir = checkpointDir(sessionId)
53
+ await mkdir(dir, { recursive: true })
54
+ const name = `task_${stageId}_${taskId}`
55
+ const checkpoint = {
56
+ sessionId,
57
+ stageId,
58
+ taskId,
59
+ savedAt: Date.now(),
60
+ ...data
61
+ }
62
+ await writeJson(checkpointFile(sessionId, name), checkpoint)
63
+ return checkpoint
64
+ }
65
+
66
+ export async function loadTaskCheckpoints(sessionId, stageId) {
67
+ const dir = checkpointDir(sessionId)
68
+ const files = await readdir(dir, { withFileTypes: true }).catch(() => [])
69
+ const prefix = `task_${stageId}_`
70
+ const results = {}
71
+ for (const entry of files) {
72
+ if (entry.isFile() && entry.name.startsWith(prefix) && entry.name.endsWith(".json")) {
73
+ const data = await readJson(path.join(dir, entry.name), null)
74
+ if (data?.taskId) results[data.taskId] = data
75
+ }
76
+ }
77
+ return results
78
+ }
79
+
80
+ // ========== Phase 10: Checkpoint 清理策略 ==========
81
+
82
+ export async function cleanupCheckpoints(sessionId, options = {}) {
83
+ const maxKeep = options.maxKeep || 10
84
+ const keepStageCheckpoints = options.keepStageCheckpoints !== false
85
+ const dir = checkpointDir(sessionId)
86
+ const all = await listCheckpoints(sessionId)
87
+ if (all.length <= maxKeep + 1) return { removed: 0 }
88
+
89
+ const toKeep = new Set(["latest"])
90
+ // 保留 stage 级和 task 级 checkpoint
91
+ if (keepStageCheckpoints) {
92
+ for (const name of all) {
93
+ if (name.startsWith("hybrid_stage_") || name.startsWith("task_")) {
94
+ toKeep.add(name)
95
+ }
96
+ }
97
+ }
98
+ // 保留最近 maxKeep 个编号 checkpoint
99
+ const numbered = all.filter(n => n.startsWith("cp_")).sort()
100
+ for (const n of numbered.slice(-maxKeep)) toKeep.add(n)
101
+
102
+ let removed = 0
103
+ for (const name of all) {
104
+ if (toKeep.has(name)) continue
105
+ try {
106
+ const { unlink: unlinkFile } = await import("node:fs/promises")
107
+ await unlinkFile(checkpointFile(sessionId, name)).catch(() => {})
108
+ removed++
109
+ } catch { /* ignore */ }
110
+ }
111
+ return { removed }
112
+ }
113
+
114
+ // ============================================================================
115
+ // Git Snapshot Integration - AI Agent 自动 Git 快照功能
116
+ // ============================================================================
117
+
118
+ /**
119
+ * 在 AI 修改前自动创建 Git 快照
120
+ *
121
+ * @param {string} sessionId - 会话ID
122
+ * @param {string} cwd - 工作目录
123
+ * @param {Object} config - 配置对象
124
+ * @param {Object} options - 选项
125
+ * @param {string} [options.reason] - 快照原因
126
+ * @returns {Promise<{ok: boolean, snapshot?: Object, skipped?: boolean, reason?: string}>}
127
+ */
128
+ export async function autoSnapshotBeforeEdit(sessionId, cwd, config = {}, options = {}) {
129
+ // 检查 Git 自动化是否启用(默认启用,只有显式关闭才跳过)
130
+ if (config.git_auto?.enabled === false) {
131
+ return { ok: true, skipped: true, reason: "git_auto_disabled" }
132
+ }
133
+ if (config.git_auto?.auto_snapshot === false) {
134
+ return { ok: true, skipped: true, reason: "auto_snapshot_disabled" }
135
+ }
136
+
137
+ // 检查是否是 Git 仓库
138
+ if (!(await isGitRepo(cwd))) {
139
+ return { ok: true, skipped: true, reason: "not_a_git_repo" }
140
+ }
141
+
142
+ try {
143
+ const result = await gitSnapshotTool.execute(
144
+ {
145
+ auto: true,
146
+ message: options.reason || `Auto snapshot before AI edit (session: ${sessionId})`
147
+ },
148
+ { cwd, sessionId, config }
149
+ )
150
+
151
+ if (result.ok) {
152
+ return {
153
+ ok: true,
154
+ snapshot: result.snapshot,
155
+ skipped: false
156
+ }
157
+ } else {
158
+ return {
159
+ ok: false,
160
+ skipped: true,
161
+ reason: result.message || "snapshot_failed"
162
+ }
163
+ }
164
+ } catch (error) {
165
+ return {
166
+ ok: false,
167
+ skipped: true,
168
+ reason: error.message
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 获取会话的 Git 快照历史
175
+ *
176
+ * @param {string} sessionId - 会话ID
177
+ * @param {string} cwd - 工作目录
178
+ * @returns {Promise<Array<Object>>}
179
+ */
180
+ export async function getSessionSnapshots(sessionId, cwd) {
181
+ if (!(await isGitRepo(cwd))) {
182
+ return []
183
+ }
184
+
185
+ const snapshots = await listGhostCommits(cwd)
186
+ // 过滤出当前会话的快照
187
+ return snapshots.filter(s =>
188
+ s.message?.includes(`session: ${sessionId}`) ||
189
+ s.message?.includes("Auto snapshot")
190
+ )
191
+ }
192
+
193
+ /**
194
+ * 恢复到会话的最近一次快照
195
+ *
196
+ * @param {string} sessionId - 会话ID
197
+ * @param {string} cwd - 工作目录
198
+ * @returns {Promise<{ok: boolean, message?: string, error?: string}>}
199
+ */
200
+ export async function restoreLastSessionSnapshot(sessionId, cwd) {
201
+ if (!(await isGitRepo(cwd))) {
202
+ return { ok: false, error: "Not a git repository" }
203
+ }
204
+
205
+ const snapshots = await getSessionSnapshots(sessionId, cwd)
206
+ if (snapshots.length === 0) {
207
+ return { ok: false, error: "No snapshots found for this session" }
208
+ }
209
+
210
+ const latest = snapshots[0]
211
+ const { gitRestoreTool } = await import("../tool/git-auto.mjs")
212
+
213
+ const result = await gitRestoreTool.execute(
214
+ { snapshot_id: latest.id },
215
+ { cwd, sessionId }
216
+ )
217
+
218
+ return result
219
+ }
220
+
221
+ /**
222
+ * Checkpoint Manager - 统一的管理器
223
+ *
224
+ * 协调 JSON checkpoint 和 Git snapshot 两种机制:
225
+ * - JSON checkpoint: 保存会话状态(内存中的数据)
226
+ * - Git snapshot: 保存工作目录状态(文件系统状态)
227
+ */
228
+ export class CheckpointManager {
229
+ constructor(sessionId, cwd, config = {}) {
230
+ this.sessionId = sessionId
231
+ this.cwd = cwd
232
+ this.config = config
233
+ this.lastSnapshotId = null
234
+ }
235
+
236
+ /**
237
+ * 在修改前创建检查点(自动决定使用哪种机制)
238
+ */
239
+ async beforeEdit(reason = "AI edit") {
240
+ const results = {
241
+ jsonCheckpoint: null,
242
+ gitSnapshot: null
243
+ }
244
+
245
+ // 1. 创建 JSON checkpoint(如果配置启用)
246
+ if (this.config.checkpoint?.enabled !== false) {
247
+ // 这里可以扩展保存更多会话状态
248
+ results.jsonCheckpoint = await saveCheckpoint(this.sessionId, {
249
+ type: "pre_edit",
250
+ reason,
251
+ timestamp: Date.now()
252
+ })
253
+ }
254
+
255
+ // 2. 创建 Git snapshot(如果配置启用)
256
+ if (this.config.git_auto?.enabled !== false && this.config.git_auto?.auto_snapshot !== false) {
257
+ const snapshotResult = await autoSnapshotBeforeEdit(
258
+ this.sessionId,
259
+ this.cwd,
260
+ this.config,
261
+ { reason }
262
+ )
263
+
264
+ if (snapshotResult.ok && !snapshotResult.skipped) {
265
+ results.gitSnapshot = snapshotResult.snapshot
266
+ this.lastSnapshotId = snapshotResult.snapshot?.id ?? null
267
+ }
268
+ }
269
+
270
+ return results
271
+ }
272
+
273
+ /**
274
+ * 恢复到最近一次检查点
275
+ */
276
+ async restore() {
277
+ if (this.lastSnapshotId) {
278
+ const { gitRestoreTool } = await import("../tool/git-auto.mjs")
279
+ return await gitRestoreTool.execute(
280
+ { snapshot_id: this.lastSnapshotId },
281
+ { cwd: this.cwd, sessionId: this.sessionId }
282
+ )
283
+ }
284
+
285
+ // 如果没有快照ID,尝试恢复到最近一次会话快照
286
+ return await restoreLastSessionSnapshot(this.sessionId, this.cwd)
287
+ }
288
+
289
+ /**
290
+ * 获取当前会话的所有快照
291
+ */
292
+ async listSnapshots() {
293
+ return await getSessionSnapshots(this.sessionId, this.cwd)
294
+ }
295
+ }
296
+
297
+ /**
298
+ * 创建 CheckpointManager 实例的工厂函数
299
+ */
300
+ export function createCheckpointManager(sessionId, cwd, config) {
301
+ return new CheckpointManager(sessionId, cwd, config)
302
+ }