@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -23,19 +23,26 @@ export async function readAuditStore() {
23
23
  return readJson(auditStorePath(), defaults())
24
24
  }
25
25
 
26
+ let writeLock = Promise.resolve()
27
+
26
28
  export async function appendAuditEntry(entry) {
27
- const store = await readAuditStore()
28
- store.entries.push({
29
- id: `aud_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
30
- createdAt: Date.now(),
31
- ...entry
32
- })
33
- if (store.entries.length > state.maxEntries) {
34
- store.entries = store.entries.slice(-state.maxEntries)
29
+ const run = async () => {
30
+ const store = await readAuditStore()
31
+ store.entries.push({
32
+ id: `aud_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
33
+ createdAt: Date.now(),
34
+ ...entry
35
+ })
36
+ if (store.entries.length > state.maxEntries) {
37
+ store.entries = store.entries.slice(-state.maxEntries)
38
+ }
39
+ store.updatedAt = Date.now()
40
+ await writeJson(auditStorePath(), store)
41
+ return store.entries[store.entries.length - 1]
35
42
  }
36
- store.updatedAt = Date.now()
37
- await writeJson(auditStorePath(), store)
38
- return store.entries[store.entries.length - 1]
43
+ const result = writeLock.then(run, run)
44
+ writeLock = result.then(() => undefined, () => undefined)
45
+ return result
39
46
  }
40
47
 
41
48
  export async function listAuditEntries(options = {}) {
@@ -43,11 +43,17 @@ async function cleanupOldLogs() {
43
43
  }
44
44
  }
45
45
 
46
+ let lastCleanupAt = 0
47
+ const CLEANUP_INTERVAL_MS = 60 * 60 * 1000 // 1 hour
48
+
46
49
  export async function appendEventLog(event) {
47
50
  await ensureUserRoot()
48
51
  await maybeRotate()
49
52
  await appendFile(eventLogPath(), JSON.stringify(event) + "\n", "utf8")
50
- await cleanupOldLogs()
53
+ if (Date.now() - lastCleanupAt > CLEANUP_INTERVAL_MS) {
54
+ lastCleanupAt = Date.now()
55
+ await cleanupOldLogs()
56
+ }
51
57
  }
52
58
 
53
59
  export async function eventLogStats() {
@@ -1,245 +1,243 @@
1
- import path from "node:path"
2
- import { mkdir, readdir, readFile, writeFile, unlink } from "node:fs/promises"
3
- import { userRootDir } from "./paths.mjs"
4
-
5
- const GHOST_COMMIT_DIR = "ghost-commits"
6
- const MAX_GHOST_COMMITS_PER_REPO = 50 // 每个仓库最多保留的幽灵提交数
7
- const GHOST_COMMIT_TTL_MS = 7 * 24 * 60 * 60 * 1000 // 7天过期
8
-
9
- // 防止并发 cleanup 竞态:per-repo 锁
10
- const cleanupLocks = new Map()
11
-
12
- /**
13
- * Ghost Commit 存储管理
14
- *
15
- * 提供幽灵提交的持久化存储、查询、清理等功能。
16
- * 存储位置: ~/.kkcode/ghost-commits/
17
- */
18
-
19
- /** 获取幽灵提交存储目录 */
20
- export function getGhostCommitDir() {
21
- return path.join(userRootDir(), GHOST_COMMIT_DIR)
22
- }
23
-
24
- /** 获取仓库的存储目录(基于 repoPath 的哈希) */
25
- function getRepoDir(repoPath) {
26
- // 使用简单的哈希来避免路径中的特殊字符问题
27
- const hash = Buffer.from(repoPath).toString("base64url")
28
- return path.join(getGhostCommitDir(), hash)
29
- }
30
-
31
- /** 确保目录存在 */
32
- async function ensureDir(dir) {
33
- await mkdir(dir, { recursive: true })
34
- }
35
-
36
- /**
37
- * 保存幽灵提交元数据
38
- *
39
- * @param {Object} ghostCommit - 幽灵提交信息
40
- * @param {string} ghostCommit.id - 唯一ID
41
- * @param {string} ghostCommit.commitHash - Git commit hash
42
- * @param {string} ghostCommit.repoPath - 仓库路径
43
- * @param {string} ghostCommit.parentHash - 父提交hash
44
- * @param {string} ghostCommit.message - 提交信息
45
- * @param {number} ghostCommit.createdAt - 创建时间戳
46
- * @param {string[]} ghostCommit.files - 包含的文件列表
47
- */
48
- export async function saveGhostCommit(ghostCommit) {
49
- const repoDir = getRepoDir(ghostCommit.repoPath)
50
- await ensureDir(repoDir)
51
-
52
- const filePath = path.join(repoDir, `${ghostCommit.id}.json`)
53
- const data = {
54
- ...ghostCommit,
55
- savedAt: Date.now()
56
- }
57
-
58
- await writeFile(filePath, JSON.stringify(data, null, 2), "utf8")
59
-
60
- // 清理旧的幽灵提交
61
- await cleanupOldGhostCommits(ghostCommit.repoPath)
62
-
63
- return { ok: true, path: filePath }
64
- }
65
-
66
- /**
67
- * 加载幽灵提交元数据
68
- *
69
- * @param {string} repoPath - 仓库路径
70
- * @param {string} ghostCommitId - 幽灵提交ID
71
- * @returns {Promise<Object|null>}
72
- */
73
- export async function loadGhostCommit(repoPath, ghostCommitId) {
74
- const repoDir = getRepoDir(repoPath)
75
- const filePath = path.join(repoDir, `${ghostCommitId}.json`)
76
-
77
- try {
78
- const content = await readFile(filePath, "utf8")
79
- return JSON.parse(content)
80
- } catch {
81
- return null
82
- }
83
- }
84
-
85
- /**
86
- * 列出仓库的所有幽灵提交
87
- *
88
- * @param {string} repoPath - 仓库路径
89
- * @param {Object} options - 选项
90
- * @param {boolean} [options.includeExpired=false] - 是否包含已过期的
91
- * @returns {Promise<Array<Object>>}
92
- */
93
- export async function listGhostCommits(repoPath, options = {}) {
94
- const { includeExpired = false } = options
95
- const repoDir = getRepoDir(repoPath)
96
-
97
- try {
98
- const entries = await readdir(repoDir, { withFileTypes: true })
99
- const commits = []
100
-
101
- for (const entry of entries) {
102
- if (!entry.isFile() || !entry.name.endsWith(".json")) continue
103
-
104
- try {
105
- const content = await readFile(path.join(repoDir, entry.name), "utf8")
106
- const commit = JSON.parse(content)
107
-
108
- // 检查是否过期
109
- const isExpired = Date.now() - commit.createdAt > GHOST_COMMIT_TTL_MS
110
- if (!includeExpired && isExpired) {
111
- // 删除过期文件
112
- await unlink(path.join(repoDir, entry.name)).catch(() => {})
113
- continue
114
- }
115
-
116
- commits.push({
117
- ...commit,
118
- isExpired
119
- })
120
- } catch {
121
- // 跳过无效文件
122
- }
123
- }
124
-
125
- // 按创建时间降序排列
126
- return commits.sort((a, b) => b.createdAt - a.createdAt)
127
- } catch {
128
- return []
129
- }
130
- }
131
-
132
- /**
133
- * 删除幽灵提交
134
- *
135
- * @param {string} repoPath - 仓库路径
136
- * @param {string} ghostCommitId - 幽灵提交ID
137
- * @returns {Promise<boolean>}
138
- */
139
- export async function deleteGhostCommit(repoPath, ghostCommitId) {
140
- const repoDir = getRepoDir(repoPath)
141
- const filePath = path.join(repoDir, `${ghostCommitId}.json`)
142
-
143
- try {
144
- await unlink(filePath)
145
- return true
146
- } catch {
147
- return false
148
- }
149
- }
150
-
151
- /**
152
- * 清理旧的幽灵提交
153
- * 保留最新的 MAX_GHOST_COMMITS_PER_REPO
154
- *
155
- * @param {string} repoPath - 仓库路径
156
- */
157
- export async function cleanupOldGhostCommits(repoPath) {
158
- // 同一 repo 的 cleanup 串行化,防止并发竞态
159
- if (cleanupLocks.get(repoPath)) return
160
- cleanupLocks.set(repoPath, true)
161
- try {
162
- const commits = await listGhostCommits(repoPath, { includeExpired: true })
163
-
164
- if (commits.length <= MAX_GHOST_COMMITS_PER_REPO) {
165
- return
166
- }
167
-
168
- // 删除多余的旧提交
169
- const toDelete = commits.slice(MAX_GHOST_COMMITS_PER_REPO)
170
- for (const commit of toDelete) {
171
- await deleteGhostCommit(repoPath, commit.id)
172
- }
173
- } finally {
174
- cleanupLocks.delete(repoPath)
175
- }
176
- }
177
-
178
- /**
179
- * 清理所有过期的幽灵提交
180
- *
181
- * @returns {Promise<{deleted: number}>}
182
- */
183
- export async function cleanupAllExpired() {
184
- const baseDir = getGhostCommitDir()
185
- let deleted = 0
186
-
187
- try {
188
- const repoDirs = await readdir(baseDir, { withFileTypes: true })
189
-
190
- for (const dir of repoDirs) {
191
- if (!dir.isDirectory()) continue
192
-
193
- const repoPath = path.join(baseDir, dir.name)
194
- try {
195
- const files = await readdir(repoPath)
196
-
197
- for (const file of files) {
198
- if (!file.endsWith(".json")) continue
199
-
200
- try {
201
- const content = await readFile(path.join(repoPath, file), "utf8")
202
- const commit = JSON.parse(content)
203
-
204
- const isExpired = Date.now() - commit.createdAt > GHOST_COMMIT_TTL_MS
205
- if (isExpired) {
206
- await unlink(path.join(repoPath, file))
207
- deleted++
208
- }
209
- } catch {
210
- // 跳过无效文件
211
- }
212
- }
213
- } catch {
214
- // 跳过无法读取的目录
215
- }
216
- }
217
- } catch {
218
- // 目录可能不存在
219
- }
220
-
221
- return { deleted }
222
- }
223
-
224
- /**
225
- * 获取最新的幽灵提交
226
- *
227
- * @param {string} repoPath - 仓库路径
228
- * @returns {Promise<Object|null>}
229
- */
230
- export async function getLatestGhostCommit(repoPath) {
231
- const commits = await listGhostCommits(repoPath)
232
- return commits[0] || null
233
- }
234
-
235
- /**
236
- * 统计幽灵提交数量
237
- *
238
- * @param {string} repoPath - 仓库路径
239
- * @returns {Promise<{total: number, expired: number}>}
240
- */
241
- export async function countGhostCommits(repoPath) {
242
- const commits = await listGhostCommits(repoPath, { includeExpired: true })
243
- const expired = commits.filter(c => c.isExpired).length
244
- return { total: commits.length, expired }
245
- }
1
+ import path from "node:path"
2
+ import { mkdir, readdir, readFile, writeFile, unlink } from "node:fs/promises"
3
+ import { userRootDir } from "./paths.mjs"
4
+
5
+ const GHOST_COMMIT_DIR = "ghost-commits"
6
+ const MAX_GHOST_COMMITS_PER_REPO = 50 // 每个仓库最多保留的幽灵提交数
7
+ const GHOST_COMMIT_TTL_MS = 7 * 24 * 60 * 60 * 1000 // 7天过期
8
+
9
+ // 防止并发 cleanup 竞态:per-repo 锁
10
+ const cleanupLocks = new Map()
11
+
12
+ /**
13
+ * Ghost Commit 存储管理
14
+ *
15
+ * 提供幽灵提交的持久化存储、查询、清理等功能。
16
+ * 存储位置: ~/.kkcode/ghost-commits/
17
+ */
18
+
19
+ /** 获取幽灵提交存储目录 */
20
+ export function getGhostCommitDir() {
21
+ return path.join(userRootDir(), GHOST_COMMIT_DIR)
22
+ }
23
+
24
+ /** 获取仓库的存储目录(基于 repoPath 的哈希) */
25
+ function getRepoDir(repoPath) {
26
+ // 使用简单的哈希来避免路径中的特殊字符问题
27
+ const hash = Buffer.from(repoPath).toString("base64url")
28
+ return path.join(getGhostCommitDir(), hash)
29
+ }
30
+
31
+ /** 确保目录存在 */
32
+ async function ensureDir(dir) {
33
+ await mkdir(dir, { recursive: true })
34
+ }
35
+
36
+ /**
37
+ * 保存幽灵提交元数据
38
+ *
39
+ * @param {Object} ghostCommit - 幽灵提交信息
40
+ * @param {string} ghostCommit.id - 唯一ID
41
+ * @param {string} ghostCommit.commitHash - Git commit hash
42
+ * @param {string} ghostCommit.repoPath - 仓库路径
43
+ * @param {string} ghostCommit.parentHash - 父提交hash
44
+ * @param {string} ghostCommit.message - 提交信息
45
+ * @param {number} ghostCommit.createdAt - 创建时间戳
46
+ * @param {string[]} ghostCommit.files - 包含的文件列表
47
+ */
48
+ export async function saveGhostCommit(ghostCommit) {
49
+ const repoDir = getRepoDir(ghostCommit.repoPath)
50
+ await ensureDir(repoDir)
51
+
52
+ const filePath = path.join(repoDir, `${ghostCommit.id}.json`)
53
+ const data = {
54
+ ...ghostCommit,
55
+ savedAt: Date.now()
56
+ }
57
+
58
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf8")
59
+
60
+ // 清理旧的幽灵提交
61
+ await cleanupOldGhostCommits(ghostCommit.repoPath)
62
+
63
+ return { ok: true, path: filePath }
64
+ }
65
+
66
+ /**
67
+ * 加载幽灵提交元数据
68
+ *
69
+ * @param {string} repoPath - 仓库路径
70
+ * @param {string} ghostCommitId - 幽灵提交ID
71
+ * @returns {Promise<Object|null>}
72
+ */
73
+ export async function loadGhostCommit(repoPath, ghostCommitId) {
74
+ const repoDir = getRepoDir(repoPath)
75
+ const filePath = path.join(repoDir, `${ghostCommitId}.json`)
76
+
77
+ try {
78
+ const content = await readFile(filePath, "utf8")
79
+ return JSON.parse(content)
80
+ } catch {
81
+ return null
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 列出仓库的所有幽灵提交
87
+ *
88
+ * @param {string} repoPath - 仓库路径
89
+ * @param {Object} options - 选项
90
+ * @param {boolean} [options.includeExpired=false] - 是否包含已过期的
91
+ * @returns {Promise<Array<Object>>}
92
+ */
93
+ export async function listGhostCommits(repoPath, options = {}) {
94
+ const { includeExpired = false } = options
95
+ const repoDir = getRepoDir(repoPath)
96
+
97
+ try {
98
+ const entries = await readdir(repoDir, { withFileTypes: true })
99
+ const commits = []
100
+
101
+ for (const entry of entries) {
102
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue
103
+
104
+ try {
105
+ const content = await readFile(path.join(repoDir, entry.name), "utf8")
106
+ const commit = JSON.parse(content)
107
+
108
+ // 检查是否过期
109
+ const isExpired = Date.now() - commit.createdAt > GHOST_COMMIT_TTL_MS
110
+ if (!includeExpired && isExpired) {
111
+ continue
112
+ }
113
+
114
+ commits.push({
115
+ ...commit,
116
+ isExpired
117
+ })
118
+ } catch {
119
+ // 跳过无效文件
120
+ }
121
+ }
122
+
123
+ // 按创建时间降序排列
124
+ return commits.sort((a, b) => b.createdAt - a.createdAt)
125
+ } catch {
126
+ return []
127
+ }
128
+ }
129
+
130
+ /**
131
+ * 删除幽灵提交
132
+ *
133
+ * @param {string} repoPath - 仓库路径
134
+ * @param {string} ghostCommitId - 幽灵提交ID
135
+ * @returns {Promise<boolean>}
136
+ */
137
+ export async function deleteGhostCommit(repoPath, ghostCommitId) {
138
+ const repoDir = getRepoDir(repoPath)
139
+ const filePath = path.join(repoDir, `${ghostCommitId}.json`)
140
+
141
+ try {
142
+ await unlink(filePath)
143
+ return true
144
+ } catch {
145
+ return false
146
+ }
147
+ }
148
+
149
+ /**
150
+ * 清理旧的幽灵提交
151
+ * 保留最新的 MAX_GHOST_COMMITS_PER_REPO 个
152
+ *
153
+ * @param {string} repoPath - 仓库路径
154
+ */
155
+ export async function cleanupOldGhostCommits(repoPath) {
156
+ // 同一 repo 的 cleanup 串行化,防止并发竞态
157
+ if (cleanupLocks.get(repoPath)) return
158
+ cleanupLocks.set(repoPath, true)
159
+ try {
160
+ const commits = await listGhostCommits(repoPath, { includeExpired: true })
161
+
162
+ if (commits.length <= MAX_GHOST_COMMITS_PER_REPO) {
163
+ return
164
+ }
165
+
166
+ // 删除多余的旧提交
167
+ const toDelete = commits.slice(MAX_GHOST_COMMITS_PER_REPO)
168
+ for (const commit of toDelete) {
169
+ await deleteGhostCommit(repoPath, commit.id)
170
+ }
171
+ } finally {
172
+ cleanupLocks.delete(repoPath)
173
+ }
174
+ }
175
+
176
+ /**
177
+ * 清理所有过期的幽灵提交
178
+ *
179
+ * @returns {Promise<{deleted: number}>}
180
+ */
181
+ export async function cleanupAllExpired() {
182
+ const baseDir = getGhostCommitDir()
183
+ let deleted = 0
184
+
185
+ try {
186
+ const repoDirs = await readdir(baseDir, { withFileTypes: true })
187
+
188
+ for (const dir of repoDirs) {
189
+ if (!dir.isDirectory()) continue
190
+
191
+ const repoPath = path.join(baseDir, dir.name)
192
+ try {
193
+ const files = await readdir(repoPath)
194
+
195
+ for (const file of files) {
196
+ if (!file.endsWith(".json")) continue
197
+
198
+ try {
199
+ const content = await readFile(path.join(repoPath, file), "utf8")
200
+ const commit = JSON.parse(content)
201
+
202
+ const isExpired = Date.now() - commit.createdAt > GHOST_COMMIT_TTL_MS
203
+ if (isExpired) {
204
+ await unlink(path.join(repoPath, file))
205
+ deleted++
206
+ }
207
+ } catch {
208
+ // 跳过无效文件
209
+ }
210
+ }
211
+ } catch {
212
+ // 跳过无法读取的目录
213
+ }
214
+ }
215
+ } catch {
216
+ // 目录可能不存在
217
+ }
218
+
219
+ return { deleted }
220
+ }
221
+
222
+ /**
223
+ * 获取最新的幽灵提交
224
+ *
225
+ * @param {string} repoPath - 仓库路径
226
+ * @returns {Promise<Object|null>}
227
+ */
228
+ export async function getLatestGhostCommit(repoPath) {
229
+ const commits = await listGhostCommits(repoPath)
230
+ return commits[0] || null
231
+ }
232
+
233
+ /**
234
+ * 统计幽灵提交数量
235
+ *
236
+ * @param {string} repoPath - 仓库路径
237
+ * @returns {Promise<{total: number, expired: number}>}
238
+ */
239
+ export async function countGhostCommits(repoPath) {
240
+ const commits = await listGhostCommits(repoPath, { includeExpired: true })
241
+ const expired = commits.filter(c => c.isExpired).length
242
+ return { total: commits.length, expired }
243
+ }
@@ -34,6 +34,14 @@ export function projectConfigCandidates(cwd = process.cwd()) {
34
34
  ]
35
35
  }
36
36
 
37
+ export function envFileCandidates(cwd = process.cwd()) {
38
+ return [
39
+ path.join(cwd, ".env"),
40
+ path.join(projectRootDir(cwd), ".env"),
41
+ path.join(userRootDir(), ".env")
42
+ ]
43
+ }
44
+
37
45
  export function usageStorePath() {
38
46
  return path.join(userRootDir(), "usage.json")
39
47
  }
@@ -103,6 +111,10 @@ export function auditStorePath() {
103
111
  return path.join(userRootDir(), "audit-log.json")
104
112
  }
105
113
 
114
+ export function updateStatePath() {
115
+ return path.join(userRootDir(), "update-state.json")
116
+ }
117
+
106
118
  export async function ensureUserRoot() {
107
119
  await mkdir(userRootDir(), { recursive: true })
108
120
  }
@@ -146,3 +158,8 @@ export function githubReposDir() {
146
158
  export async function ensureGithubReposDir() {
147
159
  await mkdir(githubReposDir(), { recursive: true })
148
160
  }
161
+
162
+ // User onboarding profile — stores tech stack, style preferences, etc.
163
+ export function profilePath() {
164
+ return path.join(userRootDir(), "profile.yaml")
165
+ }
@@ -14,7 +14,7 @@ export const DEFAULT_THEME = {
14
14
  success: "#34d399"
15
15
  },
16
16
  modes: {
17
- ask: "#7aa2f7",
17
+ assistant: "#22d3ee",
18
18
  plan: "#2dd4bf",
19
19
  agent: "#4ade80",
20
20
  longagent: "#fb923c"
@@ -38,6 +38,10 @@ function renderLine(line) {
38
38
 
39
39
  function renderInline(text) {
40
40
  return text
41
+ // 先处理组合格式:**`code`** → 粗体+青色
42
+ .replace(/\*\*`([^`]+)`\*\*/g, (_, c) => paint(c, COLORS.code, { bold: true }))
43
+ .replace(/__`([^`]+)`__/g, (_, c) => paint(c, COLORS.code, { bold: true }))
44
+ // 再处理单独格式
41
45
  .replace(/`([^`]+)`/g, (_, code) => paint(code, COLORS.code))
42
46
  .replace(/\*\*([^*]+)\*\*/g, (_, b) => paint(b, null, { bold: true }))
43
47
  .replace(/__([^_]+)__/g, (_, b) => paint(b, null, { bold: true }))
@@ -1,5 +1,5 @@
1
1
  const REQUIRED_GROUPS = ["base", "semantic", "modes", "components"]
2
- const MODE_KEYS = ["ask", "plan", "agent", "longagent"]
2
+ const MODE_KEYS = ["assistant", "plan", "agent", "longagent"]
3
3
  const HEX_RE = /^#([A-Fa-f0-9]{6})$/
4
4
 
5
5
  function validateColor(value, path, errors) {