@kkelly-offical/kkcode 0.1.3 → 0.1.7

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 (66) hide show
  1. package/README.md +110 -172
  2. package/package.json +46 -46
  3. package/src/agent/agent.mjs +220 -170
  4. package/src/agent/prompt/bug-hunter.txt +90 -0
  5. package/src/agent/prompt/frontend-designer.txt +58 -0
  6. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
  7. package/src/agent/prompt/longagent-coding-agent.txt +37 -0
  8. package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
  9. package/src/agent/prompt/longagent-preview-agent.txt +63 -0
  10. package/src/config/defaults.mjs +260 -195
  11. package/src/config/schema.mjs +71 -6
  12. package/src/core/constants.mjs +91 -46
  13. package/src/index.mjs +1 -1
  14. package/src/knowledge/frontend-aesthetics.txt +39 -0
  15. package/src/knowledge/loader.mjs +2 -1
  16. package/src/knowledge/tailwind.txt +12 -3
  17. package/src/mcp/client-http.mjs +141 -157
  18. package/src/mcp/client-sse.mjs +288 -286
  19. package/src/mcp/client-stdio.mjs +533 -451
  20. package/src/mcp/constants.mjs +2 -0
  21. package/src/mcp/registry.mjs +479 -394
  22. package/src/mcp/stdio-framing.mjs +133 -127
  23. package/src/mcp/tool-result.mjs +24 -0
  24. package/src/observability/index.mjs +42 -0
  25. package/src/observability/metrics.mjs +137 -0
  26. package/src/observability/tracer.mjs +137 -0
  27. package/src/orchestration/background-manager.mjs +372 -358
  28. package/src/orchestration/background-worker.mjs +305 -245
  29. package/src/orchestration/longagent-manager.mjs +171 -116
  30. package/src/orchestration/stage-scheduler.mjs +728 -489
  31. package/src/permission/exec-policy.mjs +9 -11
  32. package/src/provider/anthropic.mjs +1 -0
  33. package/src/provider/openai.mjs +340 -339
  34. package/src/provider/retry-policy.mjs +68 -68
  35. package/src/provider/router.mjs +241 -228
  36. package/src/provider/sse.mjs +104 -91
  37. package/src/repl.mjs +59 -7
  38. package/src/session/checkpoint.mjs +66 -3
  39. package/src/session/compaction.mjs +298 -276
  40. package/src/session/engine.mjs +232 -225
  41. package/src/session/longagent-4stage.mjs +460 -0
  42. package/src/session/longagent-hybrid.mjs +1097 -0
  43. package/src/session/longagent-plan.mjs +365 -329
  44. package/src/session/longagent-project-memory.mjs +53 -0
  45. package/src/session/longagent-scaffold.mjs +291 -100
  46. package/src/session/longagent-task-bus.mjs +54 -0
  47. package/src/session/longagent-utils.mjs +472 -0
  48. package/src/session/longagent.mjs +900 -1462
  49. package/src/session/loop.mjs +65 -40
  50. package/src/session/project-context.mjs +30 -0
  51. package/src/session/prompt/agent.txt +25 -0
  52. package/src/session/prompt/plan.txt +31 -9
  53. package/src/session/rollback.mjs +196 -0
  54. package/src/session/store.mjs +519 -503
  55. package/src/session/system-prompt.mjs +273 -260
  56. package/src/session/task-validator.mjs +4 -3
  57. package/src/skill/builtin/design.mjs +76 -0
  58. package/src/skill/builtin/frontend.mjs +8 -0
  59. package/src/skill/registry.mjs +390 -336
  60. package/src/storage/ghost-commit-store.mjs +18 -8
  61. package/src/tool/executor.mjs +11 -0
  62. package/src/tool/git-auto.mjs +0 -19
  63. package/src/tool/question-prompt.mjs +93 -86
  64. package/src/tool/registry.mjs +71 -37
  65. package/src/ui/activity-renderer.mjs +664 -410
  66. package/src/util/git.mjs +23 -0
@@ -1,91 +1,104 @@
1
- /**
2
- * 将 fetch response.body (ReadableStream) 解析为 SSE 事件的 AsyncIterator。
3
- * 同时支持 OpenAI(纯 data: 行)和 Anthropic(event: + data: 对)格式。
4
- *
5
- * @param {ReadableStream} body
6
- * @param {AbortSignal} [signal]
7
- * @param {object} [options]
8
- * @param {number} [options.idleTimeoutMs] - per-chunk idle timeout (resets on each chunk)
9
- * @yields {{ event: string|null, data: string }}
10
- */
11
- export async function* parseSSE(body, signal, options = {}) {
12
- const reader = body.getReader()
13
- const decoder = new TextDecoder()
14
- let buffer = ""
15
- const idleMs = options.idleTimeoutMs || 0
16
-
17
- try {
18
- while (true) {
19
- if (signal?.aborted) break
20
-
21
- let readResult
22
- if (idleMs > 0) {
23
- // Race between next chunk and idle timeout
24
- readResult = await Promise.race([
25
- reader.read(),
26
- idleTimeout(idleMs, signal)
27
- ])
28
- } else {
29
- readResult = await reader.read()
30
- }
31
-
32
- const { done, value } = readResult
33
- if (done) break
34
- buffer += decoder.decode(value, { stream: true })
35
-
36
- const parts = buffer.split("\n\n")
37
- buffer = parts.pop()
38
-
39
- for (const part of parts) {
40
- const result = parsePart(part)
41
- if (result === null) return // [DONE]
42
- if (result) yield result
43
- }
44
- }
45
- // flush remaining buffer
46
- if (buffer.trim()) {
47
- const result = parsePart(buffer)
48
- if (result && result !== null) yield result
49
- }
50
- } finally {
51
- try { reader.releaseLock() } catch { /* reader may have pending read if generator was force-closed */ }
52
- }
53
- }
54
-
55
- function idleTimeout(ms, signal) {
56
- return new Promise((resolve, reject) => {
57
- const timer = setTimeout(() => {
58
- const err = new Error(`stream idle timeout: no data received for ${ms}ms`)
59
- err.code = "STREAM_IDLE_TIMEOUT"
60
- reject(err)
61
- }, ms)
62
- if (signal) {
63
- const onAbort = () => {
64
- clearTimeout(timer)
65
- const err = new Error("aborted")
66
- err.code = "ABORT_ERR"
67
- reject(err)
68
- }
69
- if (signal.aborted) { clearTimeout(timer); onAbort(); return }
70
- signal.addEventListener("abort", onAbort, { once: true })
71
- }
72
- })
73
- }
74
-
75
- function parsePart(part) {
76
- const trimmed = part.trim()
77
- if (!trimmed) return undefined
78
- let event = null
79
- let data = ""
80
- for (const line of trimmed.split("\n")) {
81
- if (line.startsWith("event:")) {
82
- event = line.slice(6).trim()
83
- } else if (line.startsWith("data:")) {
84
- const payload = line.slice(5).trim()
85
- if (payload === "[DONE]") return null
86
- data = payload
87
- }
88
- }
89
- if (!data) return undefined
90
- return { event, data }
91
- }
1
+ /**
2
+ * 将 fetch response.body (ReadableStream) 解析为 SSE 事件的 AsyncIterator。
3
+ * 同时支持 OpenAI(纯 data: 行)和 Anthropic(event: + data: 对)格式。
4
+ *
5
+ * @param {ReadableStream} body
6
+ * @param {AbortSignal} [signal]
7
+ * @param {object} [options]
8
+ * @param {number} [options.idleTimeoutMs] - per-chunk idle timeout (resets on each chunk)
9
+ * @yields {{ event: string|null, data: string }}
10
+ */
11
+ export async function* parseSSE(body, signal, options = {}) {
12
+ const reader = body.getReader()
13
+ const decoder = new TextDecoder()
14
+ let buffer = ""
15
+ const idleMs = options.idleTimeoutMs || 0
16
+ let currentTimeout = null
17
+
18
+ try {
19
+ while (true) {
20
+ if (signal?.aborted) break
21
+
22
+ let readResult
23
+ if (idleMs > 0) {
24
+ if (currentTimeout) currentTimeout.cancel()
25
+ currentTimeout = idleTimeout(idleMs, signal)
26
+ readResult = await Promise.race([
27
+ reader.read(),
28
+ currentTimeout.promise
29
+ ])
30
+ } else {
31
+ readResult = await reader.read()
32
+ }
33
+
34
+ const { done, value } = readResult
35
+ if (done) break
36
+ buffer += decoder.decode(value, { stream: true })
37
+
38
+ const parts = buffer.split("\n\n")
39
+ buffer = parts.pop()
40
+
41
+ for (const part of parts) {
42
+ const result = parsePart(part)
43
+ if (result === null) return // [DONE]
44
+ if (result) yield result
45
+ }
46
+ }
47
+ // flush remaining buffer
48
+ if (buffer.trim()) {
49
+ const result = parsePart(buffer)
50
+ if (result && result !== null) yield result
51
+ }
52
+ } finally {
53
+ if (currentTimeout) currentTimeout.cancel()
54
+ try { reader.releaseLock() } catch { /* reader may have pending read if generator was force-closed */ }
55
+ }
56
+ }
57
+
58
+ function idleTimeout(ms, signal) {
59
+ let timer = null
60
+ let onAbort = null
61
+ const promise = new Promise((resolve, reject) => {
62
+ timer = setTimeout(() => {
63
+ const err = new Error(`stream idle timeout: no data received for ${ms}ms`)
64
+ err.code = "STREAM_IDLE_TIMEOUT"
65
+ reject(err)
66
+ }, ms)
67
+ if (signal) {
68
+ onAbort = () => {
69
+ clearTimeout(timer)
70
+ const err = new Error("aborted")
71
+ err.code = "ABORT_ERR"
72
+ reject(err)
73
+ }
74
+ if (signal.aborted) { clearTimeout(timer); onAbort(); return }
75
+ signal.addEventListener("abort", onAbort, { once: true })
76
+ }
77
+ })
78
+ function cancel() {
79
+ if (timer !== null) { clearTimeout(timer); timer = null }
80
+ if (onAbort && signal) {
81
+ signal.removeEventListener("abort", onAbort)
82
+ onAbort = null
83
+ }
84
+ }
85
+ return { promise, cancel }
86
+ }
87
+
88
+ function parsePart(part) {
89
+ const trimmed = part.trim()
90
+ if (!trimmed) return undefined
91
+ let event = null
92
+ let data = ""
93
+ for (const line of trimmed.split("\n")) {
94
+ if (line.startsWith("event:")) {
95
+ event = line.slice(6).trim()
96
+ } else if (line.startsWith("data:")) {
97
+ const payload = line.slice(5).trim()
98
+ if (payload === "[DONE]") return null
99
+ data = payload
100
+ }
101
+ }
102
+ if (!data) return undefined
103
+ return { event, data }
104
+ }
package/src/repl.mjs CHANGED
@@ -29,6 +29,7 @@ import { extractImageRefs, buildContentBlocks, readClipboardImage, readClipboard
29
29
  import { generateSkill, saveSkillGlobal } from "./skill/generator.mjs"
30
30
  import { userConfigCandidates, projectConfigCandidates, memoryFilePath } from "./storage/paths.mjs"
31
31
  import { persistTrust, revokeTrust } from "./permission/workspace-trust.mjs"
32
+ import { confirmRollback, executeRollback } from "./session/rollback.mjs"
32
33
 
33
34
  const HIST_DIR = join(homedir(), ".kkcode")
34
35
  const HIST_FILE = join(HIST_DIR, "repl_history")
@@ -88,6 +89,7 @@ const BUILTIN_SLASH = [
88
89
  { name: "longagent", desc: "switch to longagent mode" },
89
90
  { name: "create-skill", desc: "generate a new skill via AI" },
90
91
  { name: "create-agent", desc: "generate a new sub-agent via AI" },
92
+ { name: "undo", desc: "undo last code changes" },
91
93
  { name: "trust", desc: "trust this workspace" },
92
94
  { name: "untrust", desc: "revoke workspace trust" },
93
95
  { name: "exit", desc: "quit" }
@@ -747,7 +749,9 @@ async function processInputLine({
747
749
  else {
748
750
  for (const s of sessions) {
749
751
  const age = ageLabel(Date.now() - s.updatedAt)
750
- print(` ${s.id.slice(0, 12)} ${padRight(s.mode, 9)} ${padRight(s.model || "?", 20)} ${padRight(s.status || "-", 14)} ${age}`)
752
+ const title = s.title || `${s.mode}:${s.model || "?"}`
753
+ const titleClipped = title.length > 35 ? title.slice(0, 32) + "..." : title
754
+ print(` ${s.id.slice(0, 12)} ${padRight(titleClipped, 36)} ${padRight(s.mode, 9)} ${padRight(s.status || "-", 10)} ${age}`)
751
755
  }
752
756
  }
753
757
  return { exit: false }
@@ -756,12 +760,42 @@ async function processInputLine({
756
760
  if (normalized === "/resume" || normalized.startsWith("/resume ") || normalized === "/r" || normalized.startsWith("/r ")) {
757
761
  const arg = normalized.replace(/^\/(resume|r)/, "").trim()
758
762
  const sessions = await listSessions({ cwd: process.cwd(), limit: 20, includeChildren: false })
763
+
764
+ if (!sessions.length) {
765
+ print("no sessions found in current directory")
766
+ return { exit: false }
767
+ }
768
+
759
769
  let target = null
760
- if (!arg) target = sessions[0] || null
761
- else target = sessions.find((s) => s.id === arg || s.id.startsWith(arg)) || null
770
+
771
+ if (!arg) {
772
+ // Show interactive numbered list
773
+ print(`\n Sessions in ${paint(process.cwd(), "cyan")}:\n`)
774
+ for (let i = 0; i < sessions.length; i++) {
775
+ const s = sessions[i]
776
+ const num = paint(` ${String(i + 1).padStart(2)}.`, "yellow")
777
+ const title = s.title || `${s.mode}:${s.model || "?"}`
778
+ const titleClipped = title.length > 45 ? title.slice(0, 42) + "..." : title
779
+ const age = ageLabel(Date.now() - s.updatedAt)
780
+ const mode = paint(padRight(s.mode, 9), "cyan")
781
+ const status = s.status === "active" ? paint("active", "green") : paint(s.status || "-", null, { dim: true })
782
+ print(`${num} ${padRight(titleClipped, 46)} ${mode} ${padRight(status, 14)} ${paint(age, null, { dim: true })}`)
783
+ }
784
+ print(`\n usage: ${paint("/resume <number>", "yellow")} or ${paint("/resume <session-id>", "yellow")}`)
785
+ return { exit: false }
786
+ }
787
+
788
+ // Try numeric index first (1-based)
789
+ const idx = parseInt(arg, 10)
790
+ if (!Number.isNaN(idx) && idx >= 1 && idx <= sessions.length) {
791
+ target = sessions[idx - 1]
792
+ } else {
793
+ // Fallback to ID prefix match
794
+ target = sessions.find((s) => s.id === arg || s.id.startsWith(arg)) || null
795
+ }
762
796
 
763
797
  if (!target) {
764
- print(arg ? `no session matching "${arg}"` : "no sessions to resume")
798
+ print(`no session matching "${arg}"`)
765
799
  return { exit: false }
766
800
  }
767
801
 
@@ -769,15 +803,33 @@ async function processInputLine({
769
803
  state.mode = target.mode || state.mode
770
804
  state.providerType = target.providerType || state.providerType
771
805
  state.model = target.model || state.model
772
- print(`resumed session: ${target.id} (${target.mode}, ${target.model || "?"})`)
806
+ const title = target.title || `${target.mode}:${target.model || "?"}`
807
+ print(`resumed: ${paint(title, "cyan")} (${target.mode}, ${target.model || "?"})`)
773
808
  const msgs = await getConversationHistory(target.id, 3)
774
809
  for (const m of msgs) {
775
- const preview = m.content.length > 84 ? `${m.content.slice(0, 84)}...` : m.content
810
+ const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content)
811
+ const preview = text.length > 84 ? `${text.slice(0, 84)}...` : text
776
812
  print(` [${m.role}] ${preview}`)
777
813
  }
778
814
  return { exit: false }
779
815
  }
780
816
 
817
+ if (normalized === "/undo") {
818
+ const language = ctx.configState.config.language || "en"
819
+ const cwd = process.cwd()
820
+ const confirmation = await confirmRollback({ cwd, language })
821
+ print(confirmation.message)
822
+ if (!confirmation.confirmed) return { exit: false }
823
+ const result = await executeRollback({
824
+ cwd,
825
+ commitHash: confirmation.commitHash,
826
+ sessionId: state.sessionId,
827
+ language
828
+ })
829
+ print(result.message)
830
+ return { exit: false }
831
+ }
832
+
781
833
  if (["/ask", "/plan", "/agent", "/longagent"].includes(normalized)) {
782
834
  state.mode = resolveMode(normalized.slice(1))
783
835
  print(`mode switched: ${state.mode}`)
@@ -2720,7 +2772,7 @@ function startSplash() {
2720
2772
  " ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ "
2721
2773
  ]
2722
2774
  const tagline = "AI Coding Agent"
2723
- const version = "v0.1.3"
2775
+ const version = "v0.1.6"
2724
2776
 
2725
2777
  // Gradient colors for the wave animation (cyan → blue → purple → pink → back)
2726
2778
  const wave = [
@@ -46,6 +46,71 @@ export async function listCheckpoints(sessionId) {
46
46
  .sort()
47
47
  }
48
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
+
49
114
  // ============================================================================
50
115
  // Git Snapshot Integration - AI Agent 自动 Git 快照功能
51
116
  // ============================================================================
@@ -61,12 +126,10 @@ export async function listCheckpoints(sessionId) {
61
126
  * @returns {Promise<{ok: boolean, snapshot?: Object, skipped?: boolean, reason?: string}>}
62
127
  */
63
128
  export async function autoSnapshotBeforeEdit(sessionId, cwd, config = {}, options = {}) {
64
- // 检查 Git 自动化是否启用
129
+ // 检查 Git 自动化是否启用(默认启用,只有显式关闭才跳过)
65
130
  if (config.git_auto?.enabled === false) {
66
131
  return { ok: true, skipped: true, reason: "git_auto_disabled" }
67
132
  }
68
-
69
- // 检查自动快照是否启用
70
133
  if (config.git_auto?.auto_snapshot === false) {
71
134
  return { ok: true, skipped: true, reason: "auto_snapshot_disabled" }
72
135
  }