@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.
- package/LICENSE +674 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- 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
|
+
}
|
package/src/runtime.mjs
ADDED
|
@@ -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
|
+
}
|