@kkelly-offical/kkcode 0.1.7 → 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.
- package/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +228 -220
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +89 -89
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2981
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +298 -298
- package/src/session/engine.mjs +417 -232
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1097
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -900
- package/src/session/loop.mjs +1005 -930
- package/src/session/prompt/agent.txt +25 -25
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +31 -31
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +196 -195
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -519
- package/src/session/system-prompt.mjs +308 -273
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +99 -93
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- 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
|
|
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
|
+
}
|