@kkelly-offical/kkcode 0.1.2

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 (196) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +445 -0
  3. package/package.json +46 -0
  4. package/src/agent/agent.mjs +170 -0
  5. package/src/agent/custom-agent-loader.mjs +158 -0
  6. package/src/agent/generator.mjs +115 -0
  7. package/src/agent/prompt/architect.txt +36 -0
  8. package/src/agent/prompt/build-fixer.txt +71 -0
  9. package/src/agent/prompt/build.txt +101 -0
  10. package/src/agent/prompt/compaction.txt +12 -0
  11. package/src/agent/prompt/explore.txt +29 -0
  12. package/src/agent/prompt/guide.txt +40 -0
  13. package/src/agent/prompt/longagent.txt +178 -0
  14. package/src/agent/prompt/plan.txt +50 -0
  15. package/src/agent/prompt/researcher.txt +23 -0
  16. package/src/agent/prompt/reviewer.txt +44 -0
  17. package/src/agent/prompt/security-reviewer.txt +62 -0
  18. package/src/agent/prompt/tdd-guide.txt +84 -0
  19. package/src/agent/prompt/title.txt +8 -0
  20. package/src/command/custom-commands.mjs +57 -0
  21. package/src/commands/agent.mjs +71 -0
  22. package/src/commands/audit.mjs +77 -0
  23. package/src/commands/background.mjs +86 -0
  24. package/src/commands/chat.mjs +114 -0
  25. package/src/commands/command.mjs +41 -0
  26. package/src/commands/config.mjs +44 -0
  27. package/src/commands/doctor.mjs +148 -0
  28. package/src/commands/hook.mjs +29 -0
  29. package/src/commands/init.mjs +141 -0
  30. package/src/commands/longagent.mjs +100 -0
  31. package/src/commands/mcp.mjs +89 -0
  32. package/src/commands/permission.mjs +36 -0
  33. package/src/commands/prompt.mjs +42 -0
  34. package/src/commands/review.mjs +266 -0
  35. package/src/commands/rule.mjs +34 -0
  36. package/src/commands/session.mjs +235 -0
  37. package/src/commands/theme.mjs +98 -0
  38. package/src/commands/usage.mjs +91 -0
  39. package/src/config/defaults.mjs +195 -0
  40. package/src/config/import-config.mjs +76 -0
  41. package/src/config/load-config.mjs +76 -0
  42. package/src/config/schema.mjs +509 -0
  43. package/src/context.mjs +40 -0
  44. package/src/core/constants.mjs +46 -0
  45. package/src/core/errors.mjs +57 -0
  46. package/src/core/events.mjs +29 -0
  47. package/src/core/types.mjs +57 -0
  48. package/src/github/api.mjs +78 -0
  49. package/src/github/auth.mjs +286 -0
  50. package/src/github/flow.mjs +298 -0
  51. package/src/github/workspace.mjs +212 -0
  52. package/src/index.mjs +82 -0
  53. package/src/knowledge/api-design.txt +9 -0
  54. package/src/knowledge/cpp.txt +10 -0
  55. package/src/knowledge/docker.txt +10 -0
  56. package/src/knowledge/dotnet.txt +9 -0
  57. package/src/knowledge/electron.txt +10 -0
  58. package/src/knowledge/flutter.txt +10 -0
  59. package/src/knowledge/go.txt +9 -0
  60. package/src/knowledge/graphql.txt +10 -0
  61. package/src/knowledge/java.txt +9 -0
  62. package/src/knowledge/kotlin.txt +10 -0
  63. package/src/knowledge/loader.mjs +125 -0
  64. package/src/knowledge/next.txt +8 -0
  65. package/src/knowledge/node.txt +8 -0
  66. package/src/knowledge/nuxt.txt +9 -0
  67. package/src/knowledge/php.txt +10 -0
  68. package/src/knowledge/python.txt +10 -0
  69. package/src/knowledge/react-native.txt +10 -0
  70. package/src/knowledge/react.txt +9 -0
  71. package/src/knowledge/ruby.txt +11 -0
  72. package/src/knowledge/rust.txt +9 -0
  73. package/src/knowledge/svelte.txt +9 -0
  74. package/src/knowledge/swift.txt +10 -0
  75. package/src/knowledge/tailwind.txt +10 -0
  76. package/src/knowledge/testing.txt +8 -0
  77. package/src/knowledge/typescript.txt +8 -0
  78. package/src/knowledge/vue.txt +9 -0
  79. package/src/mcp/client-http.mjs +157 -0
  80. package/src/mcp/client-sse.mjs +286 -0
  81. package/src/mcp/client-stdio.mjs +451 -0
  82. package/src/mcp/registry.mjs +394 -0
  83. package/src/mcp/stdio-framing.mjs +127 -0
  84. package/src/orchestration/background-manager.mjs +358 -0
  85. package/src/orchestration/background-worker.mjs +245 -0
  86. package/src/orchestration/longagent-manager.mjs +116 -0
  87. package/src/orchestration/stage-scheduler.mjs +489 -0
  88. package/src/orchestration/subagent-router.mjs +62 -0
  89. package/src/orchestration/task-scheduler.mjs +74 -0
  90. package/src/permission/engine.mjs +92 -0
  91. package/src/permission/exec-policy.mjs +372 -0
  92. package/src/permission/prompt.mjs +39 -0
  93. package/src/permission/rules.mjs +120 -0
  94. package/src/permission/workspace-trust.mjs +44 -0
  95. package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
  96. package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
  97. package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
  98. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
  99. package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
  100. package/src/plugin/hook-bus.mjs +154 -0
  101. package/src/provider/anthropic.mjs +389 -0
  102. package/src/provider/ollama.mjs +236 -0
  103. package/src/provider/openai-compatible.mjs +1 -0
  104. package/src/provider/openai.mjs +339 -0
  105. package/src/provider/retry-policy.mjs +68 -0
  106. package/src/provider/router.mjs +228 -0
  107. package/src/provider/sse.mjs +91 -0
  108. package/src/repl.mjs +2929 -0
  109. package/src/review/diff-parser.mjs +36 -0
  110. package/src/review/rejection-queue.mjs +62 -0
  111. package/src/review/review-store.mjs +21 -0
  112. package/src/review/risk-score.mjs +61 -0
  113. package/src/rules/load-rules.mjs +64 -0
  114. package/src/runtime.mjs +1 -0
  115. package/src/session/checkpoint.mjs +239 -0
  116. package/src/session/compaction.mjs +276 -0
  117. package/src/session/engine.mjs +225 -0
  118. package/src/session/instinct-manager.mjs +172 -0
  119. package/src/session/instruction-loader.mjs +25 -0
  120. package/src/session/longagent-plan.mjs +329 -0
  121. package/src/session/longagent-scaffold.mjs +100 -0
  122. package/src/session/longagent.mjs +1462 -0
  123. package/src/session/loop.mjs +905 -0
  124. package/src/session/memory-loader.mjs +75 -0
  125. package/src/session/project-context.mjs +367 -0
  126. package/src/session/prompt/anthropic.txt +151 -0
  127. package/src/session/prompt/beast.txt +37 -0
  128. package/src/session/prompt/max-steps.txt +6 -0
  129. package/src/session/prompt/plan.txt +9 -0
  130. package/src/session/prompt/qwen.txt +46 -0
  131. package/src/session/prompt-loader.mjs +18 -0
  132. package/src/session/recovery.mjs +52 -0
  133. package/src/session/store.mjs +503 -0
  134. package/src/session/system-prompt.mjs +260 -0
  135. package/src/session/task-validator.mjs +266 -0
  136. package/src/session/usability-gates.mjs +379 -0
  137. package/src/skill/builtin/backend-patterns.mjs +123 -0
  138. package/src/skill/builtin/commit.mjs +64 -0
  139. package/src/skill/builtin/debug.mjs +45 -0
  140. package/src/skill/builtin/frontend-patterns.mjs +120 -0
  141. package/src/skill/builtin/frontend.mjs +188 -0
  142. package/src/skill/builtin/init.mjs +220 -0
  143. package/src/skill/builtin/review.mjs +49 -0
  144. package/src/skill/builtin/security-checklist.mjs +80 -0
  145. package/src/skill/builtin/tdd.mjs +54 -0
  146. package/src/skill/generator.mjs +113 -0
  147. package/src/skill/registry.mjs +336 -0
  148. package/src/storage/audit-store.mjs +83 -0
  149. package/src/storage/event-log.mjs +82 -0
  150. package/src/storage/ghost-commit-store.mjs +235 -0
  151. package/src/storage/json-store.mjs +53 -0
  152. package/src/storage/paths.mjs +148 -0
  153. package/src/theme/color.mjs +64 -0
  154. package/src/theme/default-theme.mjs +29 -0
  155. package/src/theme/load-theme.mjs +71 -0
  156. package/src/theme/markdown.mjs +135 -0
  157. package/src/theme/schema.mjs +45 -0
  158. package/src/theme/status-bar.mjs +158 -0
  159. package/src/tool/audit-wrapper.mjs +38 -0
  160. package/src/tool/edit-transaction.mjs +126 -0
  161. package/src/tool/executor.mjs +109 -0
  162. package/src/tool/file-lock-manager.mjs +85 -0
  163. package/src/tool/git-auto.mjs +545 -0
  164. package/src/tool/git-full-auto.mjs +478 -0
  165. package/src/tool/image-util.mjs +276 -0
  166. package/src/tool/prompt/background_cancel.txt +1 -0
  167. package/src/tool/prompt/background_output.txt +1 -0
  168. package/src/tool/prompt/bash.txt +71 -0
  169. package/src/tool/prompt/codesearch.txt +18 -0
  170. package/src/tool/prompt/edit.txt +27 -0
  171. package/src/tool/prompt/enter_plan.txt +74 -0
  172. package/src/tool/prompt/exit_plan.txt +62 -0
  173. package/src/tool/prompt/glob.txt +33 -0
  174. package/src/tool/prompt/grep.txt +43 -0
  175. package/src/tool/prompt/list.txt +8 -0
  176. package/src/tool/prompt/multiedit.txt +20 -0
  177. package/src/tool/prompt/notebookedit.txt +21 -0
  178. package/src/tool/prompt/patch.txt +24 -0
  179. package/src/tool/prompt/question.txt +44 -0
  180. package/src/tool/prompt/read.txt +40 -0
  181. package/src/tool/prompt/task.txt +83 -0
  182. package/src/tool/prompt/todowrite.txt +117 -0
  183. package/src/tool/prompt/webfetch.txt +38 -0
  184. package/src/tool/prompt/websearch.txt +43 -0
  185. package/src/tool/prompt/write.txt +38 -0
  186. package/src/tool/prompt-loader.mjs +18 -0
  187. package/src/tool/question-prompt.mjs +86 -0
  188. package/src/tool/registry.mjs +1309 -0
  189. package/src/tool/task-tool.mjs +28 -0
  190. package/src/ui/activity-renderer.mjs +410 -0
  191. package/src/ui/repl-dashboard.mjs +357 -0
  192. package/src/usage/pricing.mjs +121 -0
  193. package/src/usage/usage-meter.mjs +113 -0
  194. package/src/util/git.mjs +496 -0
  195. package/src/util/template.mjs +10 -0
  196. package/src/util/yaml.mjs +100 -0
@@ -0,0 +1,36 @@
1
+ export function parseUnifiedDiff(diffText) {
2
+ const lines = diffText.split(/\r?\n/)
3
+ const files = []
4
+ let current = null
5
+
6
+ for (const line of lines) {
7
+ if (line.startsWith("diff --git ")) {
8
+ if (current) files.push(current)
9
+ const parts = line.split(" ")
10
+ const bPath = parts[3] || ""
11
+ current = {
12
+ path: bPath.replace(/^b\//, ""),
13
+ added: 0,
14
+ removed: 0,
15
+ rawLines: [],
16
+ addedLines: []
17
+ }
18
+ continue
19
+ }
20
+ if (!current) continue
21
+ current.rawLines.push(line)
22
+ if (line.startsWith("+") && !line.startsWith("+++")) {
23
+ current.added += 1
24
+ current.addedLines.push(line.slice(1))
25
+ }
26
+ if (line.startsWith("-") && !line.startsWith("---")) {
27
+ current.removed += 1
28
+ }
29
+ }
30
+ if (current) files.push(current)
31
+ return files.filter((file) => file.path.length > 0)
32
+ }
33
+
34
+ export function previewLines(file, limit = 80) {
35
+ return file.rawLines.slice(0, limit)
36
+ }
@@ -0,0 +1,62 @@
1
+ import { ensureProjectRoot, reviewRejectionQueuePath } from "../storage/paths.mjs"
2
+ import { readJson, writeJson } from "../storage/json-store.mjs"
3
+
4
+ function now() {
5
+ return Date.now()
6
+ }
7
+
8
+ function defaults() {
9
+ return {
10
+ updatedAt: now(),
11
+ entries: []
12
+ }
13
+ }
14
+
15
+ async function load(cwd = process.cwd()) {
16
+ await ensureProjectRoot(cwd)
17
+ return readJson(reviewRejectionQueuePath(cwd), defaults())
18
+ }
19
+
20
+ async function save(data, cwd = process.cwd()) {
21
+ data.updatedAt = now()
22
+ await writeJson(reviewRejectionQueuePath(cwd), data)
23
+ }
24
+
25
+ export async function enqueueRejection(entry, cwd = process.cwd()) {
26
+ const data = await load(cwd)
27
+ data.entries.push({
28
+ id: `rej_${now()}_${Math.random().toString(36).slice(2, 8)}`,
29
+ createdAt: now(),
30
+ consumed: false,
31
+ ...entry
32
+ })
33
+ await save(data, cwd)
34
+ return data.entries[data.entries.length - 1]
35
+ }
36
+
37
+ export async function listRejections(cwd = process.cwd()) {
38
+ const data = await load(cwd)
39
+ return data.entries.sort((a, b) => b.createdAt - a.createdAt)
40
+ }
41
+
42
+ export async function pendingRejections(cwd = process.cwd()) {
43
+ const data = await load(cwd)
44
+ return data.entries.filter((entry) => !entry.consumed)
45
+ }
46
+
47
+ export async function markRejectionsConsumed(ids, sessionId, cwd = process.cwd()) {
48
+ if (!ids.length) return
49
+ const data = await load(cwd)
50
+ const set = new Set(ids)
51
+ for (const entry of data.entries) {
52
+ if (!set.has(entry.id)) continue
53
+ entry.consumed = true
54
+ entry.consumedAt = now()
55
+ entry.consumedBy = sessionId
56
+ }
57
+ await save(data, cwd)
58
+ }
59
+
60
+ export async function clearRejections(cwd = process.cwd()) {
61
+ await save(defaults(), cwd)
62
+ }
@@ -0,0 +1,21 @@
1
+ import { ensureProjectRoot, reviewStorePath } from "../storage/paths.mjs"
2
+ import { readJson, writeJson } from "../storage/json-store.mjs"
3
+
4
+ export function defaultReviewState() {
5
+ return {
6
+ createdAt: Date.now(),
7
+ sessionId: null,
8
+ currentIndex: 0,
9
+ files: []
10
+ }
11
+ }
12
+
13
+ export async function readReviewState(cwd = process.cwd()) {
14
+ await ensureProjectRoot(cwd)
15
+ return readJson(reviewStorePath(cwd), defaultReviewState())
16
+ }
17
+
18
+ export async function writeReviewState(state, cwd = process.cwd()) {
19
+ await ensureProjectRoot(cwd)
20
+ await writeJson(reviewStorePath(cwd), state)
21
+ }
@@ -0,0 +1,61 @@
1
+ const DEFAULT_PATH_RISK = ["config", "auth", "permission", "migration", "infra", "script"]
2
+ const COMMAND_RISK_RE = /\b(curl|wget|powershell|pwsh|bash|sh|chmod|sudo|eval|exec|rm\s+-rf)\b/i
3
+
4
+ const DEFAULT_WEIGHTS = {
5
+ sensitive_path: 4,
6
+ large_change: 3,
7
+ medium_change: 2,
8
+ small_change: 1,
9
+ executable_script: 2,
10
+ command_pattern: 3
11
+ }
12
+
13
+ export function scoreRisk(file, options = {}) {
14
+ const w = { ...DEFAULT_WEIGHTS, ...options.weights }
15
+ const sensitiveKeys = options.sensitive_paths || DEFAULT_PATH_RISK
16
+
17
+ let score = 1
18
+ const reasons = []
19
+ const lowerPath = file.path.toLowerCase()
20
+
21
+ for (const key of sensitiveKeys) {
22
+ if (lowerPath.includes(key)) {
23
+ score += w.sensitive_path
24
+ reasons.push(`path contains "${key}"`)
25
+ }
26
+ }
27
+
28
+ const changed = file.added + file.removed
29
+ if (changed > 200) {
30
+ score += w.large_change
31
+ reasons.push("large change size (>200 lines)")
32
+ } else if (changed > 80) {
33
+ score += w.medium_change
34
+ reasons.push("medium change size (>80 lines)")
35
+ } else if (changed > 30) {
36
+ score += w.small_change
37
+ reasons.push("noticeable change size (>30 lines)")
38
+ }
39
+
40
+ if (/\.(sh|ps1|bat|cmd)$/i.test(file.path)) {
41
+ score += w.executable_script
42
+ reasons.push("executable script file")
43
+ }
44
+
45
+ if (file.addedLines.some((line) => COMMAND_RISK_RE.test(line))) {
46
+ score += w.command_pattern
47
+ reasons.push("contains executable command patterns")
48
+ }
49
+
50
+ return { score, reasons }
51
+ }
52
+
53
+ export function sortReviewFiles(files, sortMode) {
54
+ if (sortMode === "file_order") {
55
+ return [...files].sort((a, b) => a.path.localeCompare(b.path))
56
+ }
57
+ if (sortMode === "time_order") {
58
+ return [...files]
59
+ }
60
+ return [...files].sort((a, b) => b.riskScore - a.riskScore || a.path.localeCompare(b.path))
61
+ }
@@ -0,0 +1,64 @@
1
+ import path from "node:path"
2
+ import { access, readdir, readFile } from "node:fs/promises"
3
+
4
+ async function exists(file) {
5
+ try {
6
+ await access(file)
7
+ return true
8
+ } catch {
9
+ return false
10
+ }
11
+ }
12
+
13
+ async function readRuleDir(dir, scope) {
14
+ if (!(await exists(dir))) return []
15
+ const entries = await readdir(dir, { withFileTypes: true })
16
+ const files = entries
17
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
18
+ .map((entry) => entry.name)
19
+ .sort((a, b) => a.localeCompare(b))
20
+ const blocks = []
21
+ for (const file of files) {
22
+ const target = path.join(dir, file)
23
+ const content = (await readFile(target, "utf8")).trim()
24
+ if (!content) continue
25
+ blocks.push({
26
+ scope,
27
+ file: target,
28
+ content
29
+ })
30
+ }
31
+ return blocks
32
+ }
33
+
34
+ async function readSingleRuleFile(filePath, scope) {
35
+ if (!(await exists(filePath))) return []
36
+ const content = (await readFile(filePath, "utf8")).trim()
37
+ if (!content) return []
38
+ return [{ scope, file: filePath, content }]
39
+ }
40
+
41
+ export async function loadRuleBlocks(cwd = process.cwd()) {
42
+ const userHome = process.env.USERPROFILE || process.env.HOME || cwd
43
+ const userRuleFile = path.join(userHome, ".kkcode", "rule.md")
44
+ const userRulesDir = path.join(userHome, ".kkcode", "rules")
45
+ const projectRuleFile = path.join(cwd, ".kkcode", "rule.md")
46
+ const projectRulesDir = path.join(cwd, ".kkcode", "rules")
47
+ const [globalSingle, globalDir, projectSingle, projectDir] = await Promise.all([
48
+ readSingleRuleFile(userRuleFile, "global"),
49
+ readRuleDir(userRulesDir, "global"),
50
+ readSingleRuleFile(projectRuleFile, "project"),
51
+ readRuleDir(projectRulesDir, "project")
52
+ ])
53
+ return [...globalSingle, ...globalDir, ...projectSingle, ...projectDir]
54
+ }
55
+
56
+ export async function renderRulesPrompt(cwd = process.cwd()) {
57
+ const blocks = await loadRuleBlocks(cwd)
58
+ if (!blocks.length) return ""
59
+ return blocks
60
+ .map((block) => {
61
+ return [`<rule scope="${block.scope}" source="${block.file}">`, block.content, `</rule>`].join("\n")
62
+ })
63
+ .join("\n\n")
64
+ }
@@ -0,0 +1 @@
1
+ export { resolveMode, newSessionId, executeTurn } from "./session/engine.mjs"
@@ -0,0 +1,239 @@
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
+ // ============================================================================
50
+ // Git Snapshot Integration - AI Agent 自动 Git 快照功能
51
+ // ============================================================================
52
+
53
+ /**
54
+ * 在 AI 修改前自动创建 Git 快照
55
+ *
56
+ * @param {string} sessionId - 会话ID
57
+ * @param {string} cwd - 工作目录
58
+ * @param {Object} config - 配置对象
59
+ * @param {Object} options - 选项
60
+ * @param {string} [options.reason] - 快照原因
61
+ * @returns {Promise<{ok: boolean, snapshot?: Object, skipped?: boolean, reason?: string}>}
62
+ */
63
+ export async function autoSnapshotBeforeEdit(sessionId, cwd, config = {}, options = {}) {
64
+ // 检查 Git 自动化是否启用
65
+ if (config.git_auto?.enabled === false) {
66
+ return { ok: true, skipped: true, reason: "git_auto_disabled" }
67
+ }
68
+
69
+ // 检查自动快照是否启用
70
+ if (config.git_auto?.auto_snapshot === false) {
71
+ return { ok: true, skipped: true, reason: "auto_snapshot_disabled" }
72
+ }
73
+
74
+ // 检查是否是 Git 仓库
75
+ if (!(await isGitRepo(cwd))) {
76
+ return { ok: true, skipped: true, reason: "not_a_git_repo" }
77
+ }
78
+
79
+ try {
80
+ const result = await gitSnapshotTool.execute(
81
+ {
82
+ auto: true,
83
+ message: options.reason || `Auto snapshot before AI edit (session: ${sessionId})`
84
+ },
85
+ { cwd, sessionId, config }
86
+ )
87
+
88
+ if (result.ok) {
89
+ return {
90
+ ok: true,
91
+ snapshot: result.snapshot,
92
+ skipped: false
93
+ }
94
+ } else {
95
+ return {
96
+ ok: false,
97
+ skipped: true,
98
+ reason: result.message || "snapshot_failed"
99
+ }
100
+ }
101
+ } catch (error) {
102
+ return {
103
+ ok: false,
104
+ skipped: true,
105
+ reason: error.message
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 获取会话的 Git 快照历史
112
+ *
113
+ * @param {string} sessionId - 会话ID
114
+ * @param {string} cwd - 工作目录
115
+ * @returns {Promise<Array<Object>>}
116
+ */
117
+ export async function getSessionSnapshots(sessionId, cwd) {
118
+ if (!(await isGitRepo(cwd))) {
119
+ return []
120
+ }
121
+
122
+ const snapshots = await listGhostCommits(cwd)
123
+ // 过滤出当前会话的快照
124
+ return snapshots.filter(s =>
125
+ s.message?.includes(`session: ${sessionId}`) ||
126
+ s.message?.includes("Auto snapshot")
127
+ )
128
+ }
129
+
130
+ /**
131
+ * 恢复到会话的最近一次快照
132
+ *
133
+ * @param {string} sessionId - 会话ID
134
+ * @param {string} cwd - 工作目录
135
+ * @returns {Promise<{ok: boolean, message?: string, error?: string}>}
136
+ */
137
+ export async function restoreLastSessionSnapshot(sessionId, cwd) {
138
+ if (!(await isGitRepo(cwd))) {
139
+ return { ok: false, error: "Not a git repository" }
140
+ }
141
+
142
+ const snapshots = await getSessionSnapshots(sessionId, cwd)
143
+ if (snapshots.length === 0) {
144
+ return { ok: false, error: "No snapshots found for this session" }
145
+ }
146
+
147
+ const latest = snapshots[0]
148
+ const { gitRestoreTool } = await import("../tool/git-auto.mjs")
149
+
150
+ const result = await gitRestoreTool.execute(
151
+ { snapshot_id: latest.id },
152
+ { cwd, sessionId }
153
+ )
154
+
155
+ return result
156
+ }
157
+
158
+ /**
159
+ * Checkpoint Manager - 统一的管理器
160
+ *
161
+ * 协调 JSON checkpoint 和 Git snapshot 两种机制:
162
+ * - JSON checkpoint: 保存会话状态(内存中的数据)
163
+ * - Git snapshot: 保存工作目录状态(文件系统状态)
164
+ */
165
+ export class CheckpointManager {
166
+ constructor(sessionId, cwd, config = {}) {
167
+ this.sessionId = sessionId
168
+ this.cwd = cwd
169
+ this.config = config
170
+ this.lastSnapshotId = null
171
+ }
172
+
173
+ /**
174
+ * 在修改前创建检查点(自动决定使用哪种机制)
175
+ */
176
+ async beforeEdit(reason = "AI edit") {
177
+ const results = {
178
+ jsonCheckpoint: null,
179
+ gitSnapshot: null
180
+ }
181
+
182
+ // 1. 创建 JSON checkpoint(如果配置启用)
183
+ if (this.config.checkpoint?.enabled !== false) {
184
+ // 这里可以扩展保存更多会话状态
185
+ results.jsonCheckpoint = await saveCheckpoint(this.sessionId, {
186
+ type: "pre_edit",
187
+ reason,
188
+ timestamp: Date.now()
189
+ })
190
+ }
191
+
192
+ // 2. 创建 Git snapshot(如果配置启用)
193
+ if (this.config.git_auto?.enabled !== false && this.config.git_auto?.auto_snapshot !== false) {
194
+ const snapshotResult = await autoSnapshotBeforeEdit(
195
+ this.sessionId,
196
+ this.cwd,
197
+ this.config,
198
+ { reason }
199
+ )
200
+
201
+ if (snapshotResult.ok && !snapshotResult.skipped) {
202
+ results.gitSnapshot = snapshotResult.snapshot
203
+ this.lastSnapshotId = snapshotResult.snapshot.id
204
+ }
205
+ }
206
+
207
+ return results
208
+ }
209
+
210
+ /**
211
+ * 恢复到最近一次检查点
212
+ */
213
+ async restore() {
214
+ if (this.lastSnapshotId) {
215
+ const { gitRestoreTool } = await import("../tool/git-auto.mjs")
216
+ return await gitRestoreTool.execute(
217
+ { snapshot_id: this.lastSnapshotId },
218
+ { cwd: this.cwd, sessionId: this.sessionId }
219
+ )
220
+ }
221
+
222
+ // 如果没有快照ID,尝试恢复到最近一次会话快照
223
+ return await restoreLastSessionSnapshot(this.sessionId, this.cwd)
224
+ }
225
+
226
+ /**
227
+ * 获取当前会话的所有快照
228
+ */
229
+ async listSnapshots() {
230
+ return await getSessionSnapshots(this.sessionId, this.cwd)
231
+ }
232
+ }
233
+
234
+ /**
235
+ * 创建 CheckpointManager 实例的工厂函数
236
+ */
237
+ export function createCheckpointManager(sessionId, cwd, config) {
238
+ return new CheckpointManager(sessionId, cwd, config)
239
+ }