@kkelly-offical/kkcode 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +445 -0
  3. package/package.json +46 -0
  4. package/src/agent/agent.mjs +170 -0
  5. package/src/agent/custom-agent-loader.mjs +158 -0
  6. package/src/agent/generator.mjs +115 -0
  7. package/src/agent/prompt/architect.txt +36 -0
  8. package/src/agent/prompt/build-fixer.txt +71 -0
  9. package/src/agent/prompt/build.txt +101 -0
  10. package/src/agent/prompt/compaction.txt +12 -0
  11. package/src/agent/prompt/explore.txt +29 -0
  12. package/src/agent/prompt/guide.txt +40 -0
  13. package/src/agent/prompt/longagent.txt +178 -0
  14. package/src/agent/prompt/plan.txt +50 -0
  15. package/src/agent/prompt/researcher.txt +23 -0
  16. package/src/agent/prompt/reviewer.txt +44 -0
  17. package/src/agent/prompt/security-reviewer.txt +62 -0
  18. package/src/agent/prompt/tdd-guide.txt +84 -0
  19. package/src/agent/prompt/title.txt +8 -0
  20. package/src/command/custom-commands.mjs +57 -0
  21. package/src/commands/agent.mjs +71 -0
  22. package/src/commands/audit.mjs +77 -0
  23. package/src/commands/background.mjs +86 -0
  24. package/src/commands/chat.mjs +114 -0
  25. package/src/commands/command.mjs +41 -0
  26. package/src/commands/config.mjs +44 -0
  27. package/src/commands/doctor.mjs +148 -0
  28. package/src/commands/hook.mjs +29 -0
  29. package/src/commands/init.mjs +141 -0
  30. package/src/commands/longagent.mjs +100 -0
  31. package/src/commands/mcp.mjs +89 -0
  32. package/src/commands/permission.mjs +36 -0
  33. package/src/commands/prompt.mjs +42 -0
  34. package/src/commands/review.mjs +266 -0
  35. package/src/commands/rule.mjs +34 -0
  36. package/src/commands/session.mjs +235 -0
  37. package/src/commands/theme.mjs +98 -0
  38. package/src/commands/usage.mjs +91 -0
  39. package/src/config/defaults.mjs +195 -0
  40. package/src/config/import-config.mjs +76 -0
  41. package/src/config/load-config.mjs +76 -0
  42. package/src/config/schema.mjs +509 -0
  43. package/src/context.mjs +40 -0
  44. package/src/core/constants.mjs +46 -0
  45. package/src/core/errors.mjs +57 -0
  46. package/src/core/events.mjs +29 -0
  47. package/src/core/types.mjs +57 -0
  48. package/src/github/api.mjs +78 -0
  49. package/src/github/auth.mjs +286 -0
  50. package/src/github/flow.mjs +298 -0
  51. package/src/github/workspace.mjs +212 -0
  52. package/src/index.mjs +82 -0
  53. package/src/knowledge/api-design.txt +9 -0
  54. package/src/knowledge/cpp.txt +10 -0
  55. package/src/knowledge/docker.txt +10 -0
  56. package/src/knowledge/dotnet.txt +9 -0
  57. package/src/knowledge/electron.txt +10 -0
  58. package/src/knowledge/flutter.txt +10 -0
  59. package/src/knowledge/go.txt +9 -0
  60. package/src/knowledge/graphql.txt +10 -0
  61. package/src/knowledge/java.txt +9 -0
  62. package/src/knowledge/kotlin.txt +10 -0
  63. package/src/knowledge/loader.mjs +125 -0
  64. package/src/knowledge/next.txt +8 -0
  65. package/src/knowledge/node.txt +8 -0
  66. package/src/knowledge/nuxt.txt +9 -0
  67. package/src/knowledge/php.txt +10 -0
  68. package/src/knowledge/python.txt +10 -0
  69. package/src/knowledge/react-native.txt +10 -0
  70. package/src/knowledge/react.txt +9 -0
  71. package/src/knowledge/ruby.txt +11 -0
  72. package/src/knowledge/rust.txt +9 -0
  73. package/src/knowledge/svelte.txt +9 -0
  74. package/src/knowledge/swift.txt +10 -0
  75. package/src/knowledge/tailwind.txt +10 -0
  76. package/src/knowledge/testing.txt +8 -0
  77. package/src/knowledge/typescript.txt +8 -0
  78. package/src/knowledge/vue.txt +9 -0
  79. package/src/mcp/client-http.mjs +157 -0
  80. package/src/mcp/client-sse.mjs +286 -0
  81. package/src/mcp/client-stdio.mjs +451 -0
  82. package/src/mcp/registry.mjs +394 -0
  83. package/src/mcp/stdio-framing.mjs +127 -0
  84. package/src/orchestration/background-manager.mjs +358 -0
  85. package/src/orchestration/background-worker.mjs +245 -0
  86. package/src/orchestration/longagent-manager.mjs +116 -0
  87. package/src/orchestration/stage-scheduler.mjs +489 -0
  88. package/src/orchestration/subagent-router.mjs +62 -0
  89. package/src/orchestration/task-scheduler.mjs +74 -0
  90. package/src/permission/engine.mjs +92 -0
  91. package/src/permission/exec-policy.mjs +372 -0
  92. package/src/permission/prompt.mjs +39 -0
  93. package/src/permission/rules.mjs +120 -0
  94. package/src/permission/workspace-trust.mjs +44 -0
  95. package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
  96. package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
  97. package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
  98. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
  99. package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
  100. package/src/plugin/hook-bus.mjs +154 -0
  101. package/src/provider/anthropic.mjs +389 -0
  102. package/src/provider/ollama.mjs +236 -0
  103. package/src/provider/openai-compatible.mjs +1 -0
  104. package/src/provider/openai.mjs +339 -0
  105. package/src/provider/retry-policy.mjs +68 -0
  106. package/src/provider/router.mjs +228 -0
  107. package/src/provider/sse.mjs +91 -0
  108. package/src/repl.mjs +2929 -0
  109. package/src/review/diff-parser.mjs +36 -0
  110. package/src/review/rejection-queue.mjs +62 -0
  111. package/src/review/review-store.mjs +21 -0
  112. package/src/review/risk-score.mjs +61 -0
  113. package/src/rules/load-rules.mjs +64 -0
  114. package/src/runtime.mjs +1 -0
  115. package/src/session/checkpoint.mjs +239 -0
  116. package/src/session/compaction.mjs +276 -0
  117. package/src/session/engine.mjs +225 -0
  118. package/src/session/instinct-manager.mjs +172 -0
  119. package/src/session/instruction-loader.mjs +25 -0
  120. package/src/session/longagent-plan.mjs +329 -0
  121. package/src/session/longagent-scaffold.mjs +100 -0
  122. package/src/session/longagent.mjs +1462 -0
  123. package/src/session/loop.mjs +905 -0
  124. package/src/session/memory-loader.mjs +75 -0
  125. package/src/session/project-context.mjs +367 -0
  126. package/src/session/prompt/anthropic.txt +151 -0
  127. package/src/session/prompt/beast.txt +37 -0
  128. package/src/session/prompt/max-steps.txt +6 -0
  129. package/src/session/prompt/plan.txt +9 -0
  130. package/src/session/prompt/qwen.txt +46 -0
  131. package/src/session/prompt-loader.mjs +18 -0
  132. package/src/session/recovery.mjs +52 -0
  133. package/src/session/store.mjs +503 -0
  134. package/src/session/system-prompt.mjs +260 -0
  135. package/src/session/task-validator.mjs +266 -0
  136. package/src/session/usability-gates.mjs +379 -0
  137. package/src/skill/builtin/backend-patterns.mjs +123 -0
  138. package/src/skill/builtin/commit.mjs +64 -0
  139. package/src/skill/builtin/debug.mjs +45 -0
  140. package/src/skill/builtin/frontend-patterns.mjs +120 -0
  141. package/src/skill/builtin/frontend.mjs +188 -0
  142. package/src/skill/builtin/init.mjs +220 -0
  143. package/src/skill/builtin/review.mjs +49 -0
  144. package/src/skill/builtin/security-checklist.mjs +80 -0
  145. package/src/skill/builtin/tdd.mjs +54 -0
  146. package/src/skill/generator.mjs +113 -0
  147. package/src/skill/registry.mjs +336 -0
  148. package/src/storage/audit-store.mjs +83 -0
  149. package/src/storage/event-log.mjs +82 -0
  150. package/src/storage/ghost-commit-store.mjs +235 -0
  151. package/src/storage/json-store.mjs +53 -0
  152. package/src/storage/paths.mjs +148 -0
  153. package/src/theme/color.mjs +64 -0
  154. package/src/theme/default-theme.mjs +29 -0
  155. package/src/theme/load-theme.mjs +71 -0
  156. package/src/theme/markdown.mjs +135 -0
  157. package/src/theme/schema.mjs +45 -0
  158. package/src/theme/status-bar.mjs +158 -0
  159. package/src/tool/audit-wrapper.mjs +38 -0
  160. package/src/tool/edit-transaction.mjs +126 -0
  161. package/src/tool/executor.mjs +109 -0
  162. package/src/tool/file-lock-manager.mjs +85 -0
  163. package/src/tool/git-auto.mjs +545 -0
  164. package/src/tool/git-full-auto.mjs +478 -0
  165. package/src/tool/image-util.mjs +276 -0
  166. package/src/tool/prompt/background_cancel.txt +1 -0
  167. package/src/tool/prompt/background_output.txt +1 -0
  168. package/src/tool/prompt/bash.txt +71 -0
  169. package/src/tool/prompt/codesearch.txt +18 -0
  170. package/src/tool/prompt/edit.txt +27 -0
  171. package/src/tool/prompt/enter_plan.txt +74 -0
  172. package/src/tool/prompt/exit_plan.txt +62 -0
  173. package/src/tool/prompt/glob.txt +33 -0
  174. package/src/tool/prompt/grep.txt +43 -0
  175. package/src/tool/prompt/list.txt +8 -0
  176. package/src/tool/prompt/multiedit.txt +20 -0
  177. package/src/tool/prompt/notebookedit.txt +21 -0
  178. package/src/tool/prompt/patch.txt +24 -0
  179. package/src/tool/prompt/question.txt +44 -0
  180. package/src/tool/prompt/read.txt +40 -0
  181. package/src/tool/prompt/task.txt +83 -0
  182. package/src/tool/prompt/todowrite.txt +117 -0
  183. package/src/tool/prompt/webfetch.txt +38 -0
  184. package/src/tool/prompt/websearch.txt +43 -0
  185. package/src/tool/prompt/write.txt +38 -0
  186. package/src/tool/prompt-loader.mjs +18 -0
  187. package/src/tool/question-prompt.mjs +86 -0
  188. package/src/tool/registry.mjs +1309 -0
  189. package/src/tool/task-tool.mjs +28 -0
  190. package/src/ui/activity-renderer.mjs +410 -0
  191. package/src/ui/repl-dashboard.mjs +357 -0
  192. package/src/usage/pricing.mjs +121 -0
  193. package/src/usage/usage-meter.mjs +113 -0
  194. package/src/util/git.mjs +496 -0
  195. package/src/util/template.mjs +10 -0
  196. package/src/util/yaml.mjs +100 -0
@@ -0,0 +1,235 @@
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
+ /**
10
+ * Ghost Commit 存储管理
11
+ *
12
+ * 提供幽灵提交的持久化存储、查询、清理等功能。
13
+ * 存储位置: ~/.kkcode/ghost-commits/
14
+ */
15
+
16
+ /** 获取幽灵提交存储目录 */
17
+ export function getGhostCommitDir() {
18
+ return path.join(userRootDir(), GHOST_COMMIT_DIR)
19
+ }
20
+
21
+ /** 获取仓库的存储目录(基于 repoPath 的哈希) */
22
+ function getRepoDir(repoPath) {
23
+ // 使用简单的哈希来避免路径中的特殊字符问题
24
+ const hash = Buffer.from(repoPath).toString("base64url")
25
+ return path.join(getGhostCommitDir(), hash)
26
+ }
27
+
28
+ /** 确保目录存在 */
29
+ async function ensureDir(dir) {
30
+ await mkdir(dir, { recursive: true })
31
+ }
32
+
33
+ /**
34
+ * 保存幽灵提交元数据
35
+ *
36
+ * @param {Object} ghostCommit - 幽灵提交信息
37
+ * @param {string} ghostCommit.id - 唯一ID
38
+ * @param {string} ghostCommit.commitHash - Git commit hash
39
+ * @param {string} ghostCommit.repoPath - 仓库路径
40
+ * @param {string} ghostCommit.parentHash - 父提交hash
41
+ * @param {string} ghostCommit.message - 提交信息
42
+ * @param {number} ghostCommit.createdAt - 创建时间戳
43
+ * @param {string[]} ghostCommit.files - 包含的文件列表
44
+ */
45
+ export async function saveGhostCommit(ghostCommit) {
46
+ const repoDir = getRepoDir(ghostCommit.repoPath)
47
+ await ensureDir(repoDir)
48
+
49
+ const filePath = path.join(repoDir, `${ghostCommit.id}.json`)
50
+ const data = {
51
+ ...ghostCommit,
52
+ savedAt: Date.now()
53
+ }
54
+
55
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf8")
56
+
57
+ // 清理旧的幽灵提交
58
+ await cleanupOldGhostCommits(ghostCommit.repoPath)
59
+
60
+ return { ok: true, path: filePath }
61
+ }
62
+
63
+ /**
64
+ * 加载幽灵提交元数据
65
+ *
66
+ * @param {string} repoPath - 仓库路径
67
+ * @param {string} ghostCommitId - 幽灵提交ID
68
+ * @returns {Promise<Object|null>}
69
+ */
70
+ export async function loadGhostCommit(repoPath, ghostCommitId) {
71
+ const repoDir = getRepoDir(repoPath)
72
+ const filePath = path.join(repoDir, `${ghostCommitId}.json`)
73
+
74
+ try {
75
+ const content = await readFile(filePath, "utf8")
76
+ return JSON.parse(content)
77
+ } catch {
78
+ return null
79
+ }
80
+ }
81
+
82
+ /**
83
+ * 列出仓库的所有幽灵提交
84
+ *
85
+ * @param {string} repoPath - 仓库路径
86
+ * @param {Object} options - 选项
87
+ * @param {boolean} [options.includeExpired=false] - 是否包含已过期的
88
+ * @returns {Promise<Array<Object>>}
89
+ */
90
+ export async function listGhostCommits(repoPath, options = {}) {
91
+ const { includeExpired = false } = options
92
+ const repoDir = getRepoDir(repoPath)
93
+
94
+ try {
95
+ const entries = await readdir(repoDir, { withFileTypes: true })
96
+ const commits = []
97
+
98
+ for (const entry of entries) {
99
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue
100
+
101
+ try {
102
+ const content = await readFile(path.join(repoDir, entry.name), "utf8")
103
+ const commit = JSON.parse(content)
104
+
105
+ // 检查是否过期
106
+ const isExpired = Date.now() - commit.createdAt > GHOST_COMMIT_TTL_MS
107
+ if (!includeExpired && isExpired) {
108
+ // 删除过期文件
109
+ await unlink(path.join(repoDir, entry.name)).catch(() => {})
110
+ continue
111
+ }
112
+
113
+ commits.push({
114
+ ...commit,
115
+ isExpired
116
+ })
117
+ } catch {
118
+ // 跳过无效文件
119
+ }
120
+ }
121
+
122
+ // 按创建时间降序排列
123
+ return commits.sort((a, b) => b.createdAt - a.createdAt)
124
+ } catch {
125
+ return []
126
+ }
127
+ }
128
+
129
+ /**
130
+ * 删除幽灵提交
131
+ *
132
+ * @param {string} repoPath - 仓库路径
133
+ * @param {string} ghostCommitId - 幽灵提交ID
134
+ * @returns {Promise<boolean>}
135
+ */
136
+ export async function deleteGhostCommit(repoPath, ghostCommitId) {
137
+ const repoDir = getRepoDir(repoPath)
138
+ const filePath = path.join(repoDir, `${ghostCommitId}.json`)
139
+
140
+ try {
141
+ await unlink(filePath)
142
+ return true
143
+ } catch {
144
+ return false
145
+ }
146
+ }
147
+
148
+ /**
149
+ * 清理旧的幽灵提交
150
+ * 保留最新的 MAX_GHOST_COMMITS_PER_REPO 个
151
+ *
152
+ * @param {string} repoPath - 仓库路径
153
+ */
154
+ export async function cleanupOldGhostCommits(repoPath) {
155
+ const commits = await listGhostCommits(repoPath, { includeExpired: true })
156
+
157
+ if (commits.length <= MAX_GHOST_COMMITS_PER_REPO) {
158
+ return
159
+ }
160
+
161
+ // 删除多余的旧提交
162
+ const toDelete = commits.slice(MAX_GHOST_COMMITS_PER_REPO)
163
+ for (const commit of toDelete) {
164
+ await deleteGhostCommit(repoPath, commit.id)
165
+ }
166
+ }
167
+
168
+ /**
169
+ * 清理所有过期的幽灵提交
170
+ *
171
+ * @returns {Promise<{deleted: number}>}
172
+ */
173
+ export async function cleanupAllExpired() {
174
+ const baseDir = getGhostCommitDir()
175
+ let deleted = 0
176
+
177
+ try {
178
+ const repoDirs = await readdir(baseDir, { withFileTypes: true })
179
+
180
+ for (const dir of repoDirs) {
181
+ if (!dir.isDirectory()) continue
182
+
183
+ const repoPath = path.join(baseDir, dir.name)
184
+ try {
185
+ const files = await readdir(repoPath)
186
+
187
+ for (const file of files) {
188
+ if (!file.endsWith(".json")) continue
189
+
190
+ try {
191
+ const content = await readFile(path.join(repoPath, file), "utf8")
192
+ const commit = JSON.parse(content)
193
+
194
+ const isExpired = Date.now() - commit.createdAt > GHOST_COMMIT_TTL_MS
195
+ if (isExpired) {
196
+ await unlink(path.join(repoPath, file))
197
+ deleted++
198
+ }
199
+ } catch {
200
+ // 跳过无效文件
201
+ }
202
+ }
203
+ } catch {
204
+ // 跳过无法读取的目录
205
+ }
206
+ }
207
+ } catch {
208
+ // 目录可能不存在
209
+ }
210
+
211
+ return { deleted }
212
+ }
213
+
214
+ /**
215
+ * 获取最新的幽灵提交
216
+ *
217
+ * @param {string} repoPath - 仓库路径
218
+ * @returns {Promise<Object|null>}
219
+ */
220
+ export async function getLatestGhostCommit(repoPath) {
221
+ const commits = await listGhostCommits(repoPath)
222
+ return commits[0] || null
223
+ }
224
+
225
+ /**
226
+ * 统计幽灵提交数量
227
+ *
228
+ * @param {string} repoPath - 仓库路径
229
+ * @returns {Promise<{total: number, expired: number}>}
230
+ */
231
+ export async function countGhostCommits(repoPath) {
232
+ const commits = await listGhostCommits(repoPath, { includeExpired: true })
233
+ const expired = commits.filter(c => c.isExpired).length
234
+ return { total: commits.length, expired }
235
+ }
@@ -0,0 +1,53 @@
1
+ import path from "node:path"
2
+ import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises"
3
+
4
+ export async function readJson(file, fallback) {
5
+ try {
6
+ const content = await readFile(file, "utf8")
7
+ return JSON.parse(content)
8
+ } catch {
9
+ return fallback
10
+ }
11
+ }
12
+
13
+ export async function writeJsonAtomic(file, value) {
14
+ const dir = path.dirname(file)
15
+ const tmp = `${file}.tmp.${process.pid}.${Date.now()}`
16
+ await mkdir(dir, { recursive: true })
17
+ await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", "utf8")
18
+
19
+ // Windows can temporarily lock files and make atomic rename fail with EPERM/EBUSY.
20
+ // Retry briefly, then fall back to direct write to keep data durable.
21
+ let renamed = false
22
+ let lastError = null
23
+ const retries = [10, 30, 80, 160, 300]
24
+ for (const delay of retries) {
25
+ try {
26
+ await rename(tmp, file)
27
+ renamed = true
28
+ break
29
+ } catch (error) {
30
+ lastError = error
31
+ if (!["EPERM", "EBUSY", "EACCES"].includes(error?.code)) {
32
+ break
33
+ }
34
+ await new Promise((resolve) => setTimeout(resolve, delay))
35
+ }
36
+ }
37
+
38
+ if (!renamed) {
39
+ try {
40
+ await writeFile(file, JSON.stringify(value, null, 2) + "\n", "utf8")
41
+ renamed = true
42
+ } catch (fallbackError) {
43
+ lastError = fallbackError
44
+ }
45
+ }
46
+
47
+ await unlink(tmp).catch(() => {})
48
+ if (!renamed && lastError) throw lastError
49
+ }
50
+
51
+ export async function writeJson(file, value) {
52
+ await writeJsonAtomic(file, value)
53
+ }
@@ -0,0 +1,148 @@
1
+ import os from "node:os"
2
+ import path from "node:path"
3
+ import { createHash } from "node:crypto"
4
+ import { mkdir } from "node:fs/promises"
5
+
6
+ export function userRootDir() {
7
+ return process.env.KKCODE_HOME || path.join(os.homedir(), ".kkcode")
8
+ }
9
+
10
+ export function projectRootDir(cwd = process.cwd()) {
11
+ return path.join(cwd, ".kkcode")
12
+ }
13
+
14
+ export function userConfigCandidates() {
15
+ const root = userRootDir()
16
+ return [
17
+ path.join(root, "config.yaml"),
18
+ path.join(root, "config.yml"),
19
+ path.join(root, "config.json"),
20
+ path.join(root, "kkcode.config.yaml"),
21
+ path.join(root, "kkcode.config.yml"),
22
+ path.join(root, "kkcode.config.json")
23
+ ]
24
+ }
25
+
26
+ export function projectConfigCandidates(cwd = process.cwd()) {
27
+ return [
28
+ path.join(cwd, "kkcode.config.yaml"),
29
+ path.join(cwd, "kkcode.config.yml"),
30
+ path.join(cwd, "kkcode.config.json"),
31
+ path.join(projectRootDir(cwd), "config.yaml"),
32
+ path.join(projectRootDir(cwd), "config.yml"),
33
+ path.join(projectRootDir(cwd), "config.json")
34
+ ]
35
+ }
36
+
37
+ export function usageStorePath() {
38
+ return path.join(userRootDir(), "usage.json")
39
+ }
40
+
41
+ export function trustFilePath(cwd = process.cwd()) {
42
+ return path.join(projectRootDir(cwd), "trust.json")
43
+ }
44
+
45
+ export function reviewStorePath(cwd = process.cwd()) {
46
+ return path.join(projectRootDir(cwd), "review-state.json")
47
+ }
48
+
49
+ export function sessionStorePath() {
50
+ // Legacy monolithic store path kept for migration.
51
+ return path.join(userRootDir(), "session-store.json")
52
+ }
53
+
54
+ export function sessionShardRootPath() {
55
+ return path.join(userRootDir(), "sessions")
56
+ }
57
+
58
+ export function sessionIndexPath() {
59
+ return path.join(sessionShardRootPath(), "index.json")
60
+ }
61
+
62
+ export function sessionDataPath(sessionId) {
63
+ return path.join(sessionShardRootPath(), `${sessionId}.json`)
64
+ }
65
+
66
+ export function backgroundTaskStorePath() {
67
+ return path.join(userRootDir(), "background-tasks.json")
68
+ }
69
+
70
+ export function backgroundTaskRuntimeDir() {
71
+ return path.join(userRootDir(), "tasks")
72
+ }
73
+
74
+ export function backgroundTaskLogPath(taskId) {
75
+ return path.join(backgroundTaskRuntimeDir(), `${taskId}.log`)
76
+ }
77
+
78
+ export function backgroundTaskCheckpointPath(taskId) {
79
+ return path.join(backgroundTaskRuntimeDir(), `${taskId}.json`)
80
+ }
81
+
82
+ export function sessionCheckpointRootPath() {
83
+ return path.join(userRootDir(), "checkpoints")
84
+ }
85
+
86
+ export function sessionCheckpointPath(sessionId, name = "latest") {
87
+ return path.join(sessionCheckpointRootPath(), sessionId, `${name}.json`)
88
+ }
89
+
90
+ export function legacySessionStorePath() {
91
+ return path.join(userRootDir(), "session-store.json")
92
+ }
93
+
94
+ export function reviewRejectionQueuePath(cwd = process.cwd()) {
95
+ return path.join(projectRootDir(cwd), "review-rejections.json")
96
+ }
97
+
98
+ export function eventLogPath() {
99
+ return path.join(userRootDir(), "events.log")
100
+ }
101
+
102
+ export function auditStorePath() {
103
+ return path.join(userRootDir(), "audit-log.json")
104
+ }
105
+
106
+ export async function ensureUserRoot() {
107
+ await mkdir(userRootDir(), { recursive: true })
108
+ }
109
+
110
+ export async function ensureProjectRoot(cwd = process.cwd()) {
111
+ await mkdir(projectRootDir(cwd), { recursive: true })
112
+ }
113
+
114
+ export async function ensureSessionShardRoot() {
115
+ await mkdir(sessionShardRootPath(), { recursive: true })
116
+ }
117
+
118
+ export async function ensureBackgroundTaskRuntimeDir() {
119
+ await mkdir(backgroundTaskRuntimeDir(), { recursive: true })
120
+ }
121
+
122
+ // Auto Memory — persistent per-project memory directory
123
+ export function memoryDir(cwd = process.cwd()) {
124
+ const hash = createHash("md5").update(cwd).digest("hex").slice(0, 12)
125
+ const safeName = path.basename(cwd).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 30)
126
+ return path.join(userRootDir(), "projects", `${safeName}_${hash}`, "memory")
127
+ }
128
+
129
+ export function memoryFilePath(cwd = process.cwd()) {
130
+ return path.join(memoryDir(cwd), "MEMORY.md")
131
+ }
132
+
133
+ export async function ensureMemoryDir(cwd = process.cwd()) {
134
+ await mkdir(memoryDir(cwd), { recursive: true })
135
+ }
136
+
137
+ // GitHub integration
138
+ export function githubTokenPath() {
139
+ return path.join(userRootDir(), "github-token.json")
140
+ }
141
+
142
+ export function githubReposDir() {
143
+ return path.join(userRootDir(), "repos")
144
+ }
145
+
146
+ export async function ensureGithubReposDir() {
147
+ await mkdir(githubReposDir(), { recursive: true })
148
+ }
@@ -0,0 +1,64 @@
1
+ const ANSI = {
2
+ reset: "\u001b[0m",
3
+ bold: "\u001b[1m",
4
+ dim: "\u001b[2m",
5
+ italic: "\u001b[3m",
6
+ underline: "\u001b[4m"
7
+ }
8
+
9
+ const NAMED = {
10
+ black: "\u001b[30m",
11
+ red: "\u001b[31m",
12
+ green: "\u001b[32m",
13
+ yellow: "\u001b[33m",
14
+ blue: "\u001b[34m",
15
+ magenta: "\u001b[35m",
16
+ cyan: "\u001b[36m",
17
+ white: "\u001b[37m"
18
+ }
19
+
20
+ function hexToRgb(hex) {
21
+ const raw = hex.replace("#", "")
22
+ return {
23
+ r: parseInt(raw.slice(0, 2), 16),
24
+ g: parseInt(raw.slice(2, 4), 16),
25
+ b: parseInt(raw.slice(4, 6), 16)
26
+ }
27
+ }
28
+
29
+ function fgColorCode(color) {
30
+ if (!color) return ""
31
+ if (NAMED[color]) return NAMED[color]
32
+ if (/^#([A-Fa-f0-9]{6})$/.test(color)) {
33
+ const { r, g, b } = hexToRgb(color)
34
+ return `\u001b[38;2;${r};${g};${b}m`
35
+ }
36
+ return ""
37
+ }
38
+
39
+ function bgColorCode(color) {
40
+ if (!color) return ""
41
+ if (NAMED[color]) {
42
+ const fg = NAMED[color]
43
+ return fg.replace("[3", "[4")
44
+ }
45
+ if (/^#([A-Fa-f0-9]{6})$/.test(color)) {
46
+ const { r, g, b } = hexToRgb(color)
47
+ return `\u001b[48;2;${r};${g};${b}m`
48
+ }
49
+ return ""
50
+ }
51
+
52
+ export function paint(text, color, options = {}) {
53
+ if (!process.stdout.isTTY || process.env.NO_COLOR) return text
54
+ const styles = []
55
+ if (options.bold) styles.push(ANSI.bold)
56
+ if (options.dim) styles.push(ANSI.dim)
57
+ if (options.italic) styles.push(ANSI.italic)
58
+ if (options.underline) styles.push(ANSI.underline)
59
+ const style = styles.join("")
60
+ const fg = fgColorCode(color)
61
+ const bg = bgColorCode(options.bg || null)
62
+ if (!fg && !bg && !style) return text
63
+ return `${style}${fg}${bg}${text}${ANSI.reset}`
64
+ }
@@ -0,0 +1,29 @@
1
+ export const DEFAULT_THEME = {
2
+ name: "neo-contrast",
3
+ base: {
4
+ bg: "#0b0b0b",
5
+ fg: "#f5f7fa",
6
+ muted: "#c3d2e8",
7
+ border: "#5b6b85",
8
+ accent: "#22d3ee"
9
+ },
10
+ semantic: {
11
+ info: "#60a5fa",
12
+ warn: "#fbbf24",
13
+ error: "#f87171",
14
+ success: "#34d399"
15
+ },
16
+ modes: {
17
+ ask: "#7aa2f7",
18
+ plan: "#2dd4bf",
19
+ agent: "#4ade80",
20
+ longagent: "#fb923c"
21
+ },
22
+ components: {
23
+ panel: "#111827",
24
+ header: "#e2e8f0",
25
+ footer: "#94a3b8",
26
+ diff_add: "#22c55e",
27
+ diff_del: "#ef4444"
28
+ }
29
+ }
@@ -0,0 +1,71 @@
1
+ import path from "node:path"
2
+ import { access, readFile } from "node:fs/promises"
3
+ import YAML from "yaml"
4
+ import { DEFAULT_THEME } from "./default-theme.mjs"
5
+ import { validateTheme } from "./schema.mjs"
6
+
7
+ async function exists(file) {
8
+ try {
9
+ await access(file)
10
+ return true
11
+ } catch {
12
+ return false
13
+ }
14
+ }
15
+
16
+ function parseTheme(file, raw) {
17
+ if (file.endsWith(".json")) return JSON.parse(raw)
18
+ return YAML.parse(raw)
19
+ }
20
+
21
+ function deepMerge(base, override) {
22
+ if (override === null || override === undefined) return base
23
+ if (Array.isArray(override)) return [...override]
24
+ if (typeof override !== "object") return override
25
+ if (!base || typeof base !== "object" || Array.isArray(base)) return override
26
+ const out = { ...base }
27
+ for (const key of Object.keys(override)) {
28
+ out[key] = deepMerge(base[key], override[key])
29
+ }
30
+ return out
31
+ }
32
+
33
+ function resolveConfiguredThemePath(configState) {
34
+ const projectTheme = configState.source.projectRaw?.ui?.theme_file
35
+ if (typeof projectTheme === "string" && projectTheme.trim()) {
36
+ return path.resolve(configState.source.projectDir ?? process.cwd(), projectTheme)
37
+ }
38
+ const userTheme = configState.source.userRaw?.ui?.theme_file
39
+ if (typeof userTheme === "string" && userTheme.trim()) {
40
+ return path.resolve(configState.source.userDir ?? process.cwd(), userTheme)
41
+ }
42
+ return null
43
+ }
44
+
45
+ export async function loadTheme(configState, fileOverride = null) {
46
+ const target = fileOverride ? path.resolve(fileOverride) : resolveConfiguredThemePath(configState)
47
+ if (!target || !(await exists(target))) {
48
+ return { theme: deepMerge(DEFAULT_THEME, { modes: configState.config.ui.mode_colors }), source: "default", errors: [] }
49
+ }
50
+ try {
51
+ const raw = await readFile(target, "utf8")
52
+ const parsed = parseTheme(target, raw)
53
+ const merged = deepMerge(DEFAULT_THEME, parsed)
54
+ merged.modes = deepMerge(merged.modes, configState.config.ui.mode_colors)
55
+ const check = validateTheme(merged)
56
+ if (!check.valid) {
57
+ return {
58
+ theme: deepMerge(DEFAULT_THEME, { modes: configState.config.ui.mode_colors }),
59
+ source: "default",
60
+ errors: check.errors.map((error) => `${target}: ${error}`)
61
+ }
62
+ }
63
+ return { theme: merged, source: target, errors: [] }
64
+ } catch (error) {
65
+ return {
66
+ theme: deepMerge(DEFAULT_THEME, { modes: configState.config.ui.mode_colors }),
67
+ source: "default",
68
+ errors: [`${target}: ${error.message}`]
69
+ }
70
+ }
71
+ }