@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,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
+ }