@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 { writeFile } from "node:fs/promises"
3
+ import { Command } from "commander"
4
+ import { exportSession, getSession, listSessions, forkSession, fsckSessionStore, gcSessionStore, flushNow } from "../session/store.mjs"
5
+ import { newSessionId, executeTurn } from "../session/engine.mjs"
6
+ import { listRecoverableSessions, getResumeContext, isRecoveryEnabled } from "../session/recovery.mjs"
7
+ import { buildContext } from "../context.mjs"
8
+ import { ToolRegistry } from "../tool/registry.mjs"
9
+
10
+ function assertRecoveryEnabled(config, commandName) {
11
+ if (isRecoveryEnabled(config)) return true
12
+ console.error(`session recovery is disabled (session.recovery=false). command "${commandName}" is unavailable.`)
13
+ process.exitCode = 2
14
+ return false
15
+ }
16
+
17
+ export function createSessionCommand() {
18
+ const cmd = new Command("session").description("manage persisted kkcode sessions")
19
+
20
+ cmd
21
+ .command("fsck")
22
+ .description("check session index/data consistency")
23
+ .option("--json", "print as json", false)
24
+ .action(async (options) => {
25
+ const report = await fsckSessionStore()
26
+ if (options.json) {
27
+ console.log(JSON.stringify(report, null, 2))
28
+ return
29
+ }
30
+ console.log(`checked_at=${new Date(report.checkedAt).toLocaleString()}`)
31
+ console.log(`sessions_in_index=${report.sessionsInIndex} files_on_disk=${report.filesOnDisk}`)
32
+ console.log(`missing_data_files=${report.missingDataFiles.length}`)
33
+ console.log(`orphan_data_files=${report.orphanDataFiles.length}`)
34
+ console.log(`invalid_data_files=${report.invalidDataFiles.length}`)
35
+ for (const suggestion of report.suggestions) {
36
+ console.log(`suggestion: ${suggestion}`)
37
+ }
38
+ if (!report.ok) process.exitCode = 2
39
+ })
40
+
41
+ cmd
42
+ .command("gc")
43
+ .description("clean orphan/stale session data")
44
+ .option("--orphans-only", "only remove orphan session files", false)
45
+ .option("--max-age <days>", "remove stale sessions older than days", "30")
46
+ .option("--json", "print as json", false)
47
+ .action(async (options) => {
48
+ const result = await gcSessionStore({
49
+ orphansOnly: Boolean(options.orphansOnly),
50
+ maxAgeDays: Number(options.maxAge || 30)
51
+ })
52
+ await flushNow()
53
+ if (options.json) {
54
+ console.log(JSON.stringify(result, null, 2))
55
+ return
56
+ }
57
+ console.log(`removed orphan files: ${result.removed.orphanFiles.length}`)
58
+ console.log(`removed stale sessions: ${result.removed.staleSessions.length}`)
59
+ console.log(`removed checkpoint dirs: ${result.removed.checkpointDirs.length}`)
60
+ console.log(`total removed: ${result.totalRemoved}`)
61
+ })
62
+
63
+ cmd
64
+ .command("list")
65
+ .description("list sessions")
66
+ .option("--cwd-only", "filter by cwd", false)
67
+ .option("--roots", "only root sessions", false)
68
+ .option("--limit <n>", "max sessions", "30")
69
+ .action(async (options) => {
70
+ const list = await listSessions({
71
+ cwd: options.cwdOnly ? process.cwd() : null,
72
+ includeChildren: !options.roots,
73
+ limit: Number(options.limit || 30)
74
+ })
75
+ if (!list.length) {
76
+ console.log("no sessions found")
77
+ return
78
+ }
79
+ for (const session of list) {
80
+ const parent = session.parentSessionId ? ` parent=${session.parentSessionId}` : ""
81
+ console.log(
82
+ `${session.id} ${session.mode} ${session.providerType} ${session.model} ${new Date(session.updatedAt).toLocaleString()}${parent}`
83
+ )
84
+ }
85
+ })
86
+
87
+ cmd
88
+ .command("show")
89
+ .description("show one session")
90
+ .requiredOption("--id <id>", "session id")
91
+ .action(async (options) => {
92
+ const data = await getSession(options.id)
93
+ if (!data) {
94
+ console.error(`session not found: ${options.id}`)
95
+ process.exitCode = 1
96
+ return
97
+ }
98
+ console.log(JSON.stringify(data, null, 2))
99
+ })
100
+
101
+ cmd
102
+ .command("export")
103
+ .description("export session to json")
104
+ .requiredOption("--id <id>", "session id")
105
+ .option("--out <file>", "output file")
106
+ .action(async (options) => {
107
+ const data = await exportSession(options.id)
108
+ if (!data) {
109
+ console.error(`session not found: ${options.id}`)
110
+ process.exitCode = 1
111
+ return
112
+ }
113
+ const out = options.out ? path.resolve(options.out) : path.resolve(`session-${options.id}.json`)
114
+ await writeFile(out, JSON.stringify(data, null, 2) + "\n", "utf8")
115
+ console.log(`exported: ${out}`)
116
+ })
117
+
118
+ cmd
119
+ .command("fork")
120
+ .description("fork session into a new child session")
121
+ .requiredOption("--id <id>", "source session id")
122
+ .option("--new-id <id>", "new session id")
123
+ .option("--title <title>", "child title")
124
+ .action(async (options) => {
125
+ const newId = options.newId || newSessionId()
126
+ const out = await forkSession({
127
+ sessionId: options.id,
128
+ newSessionId: newId,
129
+ title: options.title || null
130
+ })
131
+ if (!out) {
132
+ console.error(`session not found: ${options.id}`)
133
+ process.exitCode = 1
134
+ return
135
+ }
136
+ console.log(`forked: ${newId} <- ${options.id}`)
137
+ })
138
+
139
+ cmd
140
+ .command("resume")
141
+ .description("resume a session from the last user message")
142
+ .requiredOption("--id <id>", "session id to resume")
143
+ .option("--mode <mode>", "override mode")
144
+ .option("--model <model>", "override model")
145
+ .action(async (options) => {
146
+ const ctx = await buildContext()
147
+ if (!assertRecoveryEnabled(ctx.configState.config, "session resume")) return
148
+
149
+ const resumeCtx = await getResumeContext(options.id, { enabled: true })
150
+ if (!resumeCtx) {
151
+ console.error(`session not found: ${options.id}`)
152
+ process.exitCode = 1
153
+ return
154
+ }
155
+ if (!resumeCtx.canResume) {
156
+ console.error(`no user message found in session ${options.id}`)
157
+ process.exitCode = 1
158
+ return
159
+ }
160
+ await ToolRegistry.initialize({ config: ctx.configState.config, cwd: process.cwd() })
161
+ const mode = options.mode || resumeCtx.session.mode
162
+ const model = options.model || resumeCtx.session.model
163
+ console.log(`resuming session ${options.id} (${resumeCtx.messageCount} messages)`)
164
+ console.log(`last prompt: ${resumeCtx.lastPrompt.slice(0, 100)}${resumeCtx.lastPrompt.length > 100 ? "..." : ""}`)
165
+ const result = await executeTurn({
166
+ prompt: resumeCtx.lastPrompt,
167
+ mode,
168
+ model,
169
+ sessionId: options.id,
170
+ configState: ctx.configState,
171
+ providerType: resumeCtx.session.providerType
172
+ })
173
+ console.log(result.reply)
174
+ })
175
+
176
+ cmd
177
+ .command("retry")
178
+ .description("retry the last failed turn in a session")
179
+ .requiredOption("--id <id>", "session id to retry")
180
+ .action(async (options) => {
181
+ const ctx = await buildContext()
182
+ if (!assertRecoveryEnabled(ctx.configState.config, "session retry")) return
183
+
184
+ const resumeCtx = await getResumeContext(options.id, { enabled: true })
185
+ if (!resumeCtx) {
186
+ console.error(`session not found: ${options.id}`)
187
+ process.exitCode = 1
188
+ return
189
+ }
190
+ if (!resumeCtx.canRetry) {
191
+ console.error(`session ${options.id} has no failed turn to retry`)
192
+ process.exitCode = 1
193
+ return
194
+ }
195
+ if (!resumeCtx.canResume) {
196
+ console.error(`no user message found in session ${options.id}`)
197
+ process.exitCode = 1
198
+ return
199
+ }
200
+ await ToolRegistry.initialize({ config: ctx.configState.config, cwd: process.cwd() })
201
+ console.log(`retrying failed turn in session ${options.id}`)
202
+ const result = await executeTurn({
203
+ prompt: resumeCtx.lastPrompt,
204
+ mode: resumeCtx.session.mode,
205
+ model: resumeCtx.session.model,
206
+ sessionId: options.id,
207
+ configState: ctx.configState,
208
+ providerType: resumeCtx.session.providerType
209
+ })
210
+ console.log(result.reply)
211
+ })
212
+
213
+ cmd
214
+ .command("recoverable")
215
+ .description("list sessions that can be resumed or retried")
216
+ .action(async () => {
217
+ const ctx = await buildContext()
218
+ if (!assertRecoveryEnabled(ctx.configState.config, "session recoverable")) return
219
+
220
+ const sessions = await listRecoverableSessions({
221
+ cwd: process.cwd(),
222
+ enabled: true
223
+ })
224
+ if (!sessions.length) {
225
+ console.log("no recoverable sessions")
226
+ return
227
+ }
228
+ for (const s of sessions) {
229
+ const reason = s.retryMeta?.inProgress ? "in-progress" : s.status === "error" ? "error" : "unknown"
230
+ console.log(`${s.id} ${s.mode} ${reason} ${new Date(s.updatedAt).toLocaleString()}`)
231
+ }
232
+ })
233
+
234
+ return cmd
235
+ }
@@ -0,0 +1,98 @@
1
+ import path from "node:path"
2
+ import { access, writeFile, readFile } from "node:fs/promises"
3
+ import YAML from "yaml"
4
+ import { Command } from "commander"
5
+ import { DEFAULT_THEME } from "../theme/default-theme.mjs"
6
+ import { validateTheme } from "../theme/schema.mjs"
7
+ import { ensureProjectRoot, projectRootDir } from "../storage/paths.mjs"
8
+ import { buildContext, printContextWarnings } from "../context.mjs"
9
+ import { renderStatusBar } from "../theme/status-bar.mjs"
10
+
11
+ async function fileExists(file) {
12
+ try {
13
+ await access(file)
14
+ return true
15
+ } catch {
16
+ return false
17
+ }
18
+ }
19
+
20
+ function parseTheme(file, raw) {
21
+ if (file.endsWith(".json")) return JSON.parse(raw)
22
+ return YAML.parse(raw)
23
+ }
24
+
25
+ export function createThemeCommand() {
26
+ const cmd = new Command("theme").description("manage kkcode theme files")
27
+
28
+ cmd
29
+ .command("init")
30
+ .description("create a neo-contrast theme template in the project")
31
+ .option("--path <file>", "output file path")
32
+ .option("--force", "overwrite existing file", false)
33
+ .action(async (options) => {
34
+ await ensureProjectRoot(process.cwd())
35
+ const target = options.path
36
+ ? path.resolve(options.path)
37
+ : path.join(projectRootDir(process.cwd()), "neo-contrast.theme.yaml")
38
+ if ((await fileExists(target)) && !options.force) {
39
+ console.error(`theme file exists: ${target} (use --force to overwrite)`)
40
+ process.exitCode = 1
41
+ return
42
+ }
43
+ await writeFile(target, YAML.stringify(DEFAULT_THEME), "utf8")
44
+ console.log(`theme template written: ${target}`)
45
+ })
46
+
47
+ cmd
48
+ .command("validate")
49
+ .description("validate a theme file")
50
+ .requiredOption("--file <file>", "theme file path")
51
+ .action(async (options) => {
52
+ const file = path.resolve(options.file)
53
+ const raw = await readFile(file, "utf8")
54
+ const parsed = parseTheme(file, raw)
55
+ const check = validateTheme(parsed)
56
+ if (!check.valid) {
57
+ console.error("theme is invalid:")
58
+ for (const error of check.errors) console.error(`- ${error}`)
59
+ process.exitCode = 1
60
+ return
61
+ }
62
+ console.log("theme is valid")
63
+ })
64
+
65
+ cmd
66
+ .command("preview")
67
+ .description("preview theme status bars by mode")
68
+ .option("--file <file>", "theme file path")
69
+ .action(async (options) => {
70
+ const ctx = await buildContext({ themeFile: options.file ?? null })
71
+ printContextWarnings(ctx)
72
+ const theme = ctx.themeState.theme
73
+ const config = ctx.configState.config
74
+ const modes = ["ask", "plan", "agent", "longagent"]
75
+ for (const mode of modes) {
76
+ const line = renderStatusBar({
77
+ mode,
78
+ model: "anthropic/claude-sonnet-4.5",
79
+ permission: "guarded",
80
+ tokenMeter: {
81
+ estimated: false,
82
+ turn: { input: 180, output: 240 },
83
+ session: { input: 1240, output: 2110 },
84
+ global: { input: 8920, output: 11110 }
85
+ },
86
+ aggregation: config.usage.aggregation,
87
+ cost: 0.0234,
88
+ showCost: config.ui.status.show_cost,
89
+ showTokenMeter: config.ui.status.show_token_meter,
90
+ theme
91
+ })
92
+ console.log(line)
93
+ }
94
+ console.log(`theme source: ${ctx.themeState.source}`)
95
+ })
96
+
97
+ return cmd
98
+ }
@@ -0,0 +1,91 @@
1
+ import path from "node:path"
2
+ import { writeFile } from "node:fs/promises"
3
+ import { Command } from "commander"
4
+ import { exportUsageCsv, readUsageStore, resetUsage } from "../usage/usage-meter.mjs"
5
+ import { buildContext } from "../context.mjs"
6
+
7
+ function printUsageLine(scope, usage) {
8
+ console.log(
9
+ `${scope.padEnd(12)} input=${usage.input} output=${usage.output} cacheRead=${usage.cacheRead} cacheWrite=${usage.cacheWrite} cost=$${usage.cost.toFixed(4)} turns=${usage.turns}`
10
+ )
11
+ }
12
+
13
+ export function createUsageCommand() {
14
+ const cmd = new Command("usage").description("inspect and manage token/cost usage")
15
+
16
+ cmd
17
+ .command("show")
18
+ .description("show usage summary")
19
+ .option("--session <id>", "show only one session")
20
+ .option("--json", "print as JSON", false)
21
+ .action(async (options) => {
22
+ const store = await readUsageStore()
23
+ const ctx = await buildContext().catch(() => null)
24
+ const budget = ctx?.configState?.config?.usage?.budget || {}
25
+ if (options.json) {
26
+ if (options.session) {
27
+ console.log(JSON.stringify(store.sessions[options.session] ?? null, null, 2))
28
+ return
29
+ }
30
+ console.log(JSON.stringify(store, null, 2))
31
+ return
32
+ }
33
+ printUsageLine("global", store.global)
34
+ if (budget.global_usd) {
35
+ const ratio = (store.global.cost / budget.global_usd) * 100
36
+ console.log(`global budget: $${store.global.cost.toFixed(4)} / $${budget.global_usd} (${ratio.toFixed(1)}%)`)
37
+ }
38
+ if (options.session) {
39
+ const session = store.sessions[options.session]
40
+ if (!session) {
41
+ console.error(`session not found: ${options.session}`)
42
+ process.exitCode = 1
43
+ return
44
+ }
45
+ printUsageLine(`session:${options.session}`, session)
46
+ if (budget.session_usd) {
47
+ const ratio = (session.cost / budget.session_usd) * 100
48
+ console.log(`session budget: $${session.cost.toFixed(4)} / $${budget.session_usd} (${ratio.toFixed(1)}%)`)
49
+ }
50
+ return
51
+ }
52
+ for (const [sessionId, usage] of Object.entries(store.sessions)) {
53
+ printUsageLine(`session:${sessionId}`, usage)
54
+ }
55
+ })
56
+
57
+ cmd
58
+ .command("reset")
59
+ .description("reset usage counters")
60
+ .option("--session <id>", "reset only one session")
61
+ .action(async (options) => {
62
+ await resetUsage(options.session ?? null)
63
+ console.log(options.session ? `usage reset: ${options.session}` : "usage reset: all")
64
+ })
65
+
66
+ cmd
67
+ .command("export")
68
+ .description("export usage as json or csv")
69
+ .option("--format <format>", "json|csv", "json")
70
+ .option("--out <file>", "output file path")
71
+ .action(async (options) => {
72
+ const format = String(options.format).toLowerCase()
73
+ const outFile = path.resolve(options.out ?? `usage-export.${format}`)
74
+ if (format === "json") {
75
+ const store = await readUsageStore()
76
+ await writeFile(outFile, JSON.stringify(store, null, 2) + "\n", "utf8")
77
+ console.log(`usage exported: ${outFile}`)
78
+ return
79
+ }
80
+ if (format === "csv") {
81
+ const csv = await exportUsageCsv()
82
+ await writeFile(outFile, csv, "utf8")
83
+ console.log(`usage exported: ${outFile}`)
84
+ return
85
+ }
86
+ console.error("unsupported format, expected json or csv")
87
+ process.exitCode = 1
88
+ })
89
+
90
+ return cmd
91
+ }
@@ -0,0 +1,195 @@
1
+ export const DEFAULT_CONFIG = {
2
+ language: "en",
3
+ provider: {
4
+ default: "openai",
5
+ openai: {
6
+ base_url: "https://api.openai.com/v1",
7
+ api_key_env: "OPENAI_API_KEY",
8
+ default_model: "gpt-5.3-codex",
9
+ models: ["gpt-5.3-codex", "gpt-5.2"],
10
+ timeout_ms: 120000,
11
+ stream_idle_timeout_ms: 120000,
12
+ max_tokens: 32768,
13
+ context_limit: null,
14
+ retry_attempts: 3,
15
+ retry_base_delay_ms: 800,
16
+ stream: true,
17
+ thinking: null
18
+ },
19
+ anthropic: {
20
+ base_url: "https://api.anthropic.com/v1",
21
+ api_key_env: "ANTHROPIC_API_KEY",
22
+ default_model: "claude-opus-4-6",
23
+ models: ["claude-sonnet-4-5", "claude-sonnet-4-6", "claude-haiku-4-5-20251001", "claude-opus-4-6"],
24
+ timeout_ms: 120000,
25
+ stream_idle_timeout_ms: 120000,
26
+ max_tokens: 32768,
27
+ context_limit: null,
28
+ retry_attempts: 3,
29
+ retry_base_delay_ms: 800,
30
+ stream: true,
31
+ thinking: null
32
+ },
33
+ ollama: {
34
+ base_url: "http://localhost:11434",
35
+ api_key_env: "",
36
+ default_model: "llama3.1",
37
+ timeout_ms: 300000,
38
+ stream_idle_timeout_ms: 300000,
39
+ max_tokens: 32768,
40
+ context_limit: null,
41
+ retry_attempts: 1,
42
+ retry_base_delay_ms: 1000,
43
+ stream: true,
44
+ thinking: null
45
+ },
46
+ model_context: {}
47
+ },
48
+ agent: {
49
+ default_mode: "agent",
50
+ max_steps: 8,
51
+ longagent: {
52
+ max_iterations: 0,
53
+ no_progress_warning: 3,
54
+ no_progress_limit: 5,
55
+ max_stage_recoveries: 3,
56
+ heartbeat_timeout_ms: 120000,
57
+ checkpoint_interval: 5,
58
+ parallel: {
59
+ enabled: true,
60
+ max_concurrency: 3,
61
+ stage_pass_rule: "all_success",
62
+ task_timeout_ms: 600000,
63
+ task_max_retries: 2
64
+ },
65
+ planner: {
66
+ intake_questions: {
67
+ enabled: true,
68
+ max_rounds: 6
69
+ },
70
+ ask_user_after_plan_frozen: false
71
+ },
72
+ resume_incomplete_files: true,
73
+ scaffold: {
74
+ enabled: true
75
+ },
76
+ git: {
77
+ enabled: "ask",
78
+ auto_branch: true,
79
+ auto_commit_stages: true,
80
+ auto_merge: true,
81
+ branch_prefix: "kkcode"
82
+ },
83
+ usability_gates: {
84
+ prompt_user: "first_run",
85
+ build: { enabled: true },
86
+ test: { enabled: true },
87
+ review: { enabled: true },
88
+ health: { enabled: true },
89
+ budget: { enabled: true }
90
+ }
91
+ },
92
+ subagents: {},
93
+ routing: {
94
+ categories: {}
95
+ }
96
+ },
97
+ mcp: {
98
+ servers: {},
99
+ auto_discover: true,
100
+ timeout_ms: 30000
101
+ },
102
+ skills: {
103
+ enabled: true,
104
+ dirs: [".kkcode/skills"]
105
+ },
106
+ permission: {
107
+ default_policy: "ask",
108
+ non_tty_default: "deny",
109
+ rules: []
110
+ },
111
+ storage: {
112
+ session_shard_enabled: true,
113
+ flush_interval_ms: 1000,
114
+ event_rotate_mb: 32,
115
+ event_retain_days: 14
116
+ },
117
+ background: {
118
+ mode: "worker_process",
119
+ worker_timeout_ms: 900000,
120
+ max_parallel: 2
121
+ },
122
+ runtime: {
123
+ tool_registry_cache_ttl_ms: 30000,
124
+ mcp_refresh_ttl_ms: 60000
125
+ },
126
+ tool: {
127
+ sources: {
128
+ builtin: true,
129
+ local: true,
130
+ mcp: true,
131
+ plugin: true
132
+ },
133
+ write_lock: {
134
+ mode: "file_lock",
135
+ wait_timeout_ms: 120000
136
+ },
137
+ local_dirs: [".kkcode/tools", ".kkcode/tool"],
138
+ plugin_dirs: [".kkcode/plugins", ".kkcode/plugin"]
139
+ },
140
+ session: {
141
+ max_history: 30,
142
+ recovery: true,
143
+ compaction_threshold_ratio: 0.7,
144
+ compaction_threshold_messages: 50,
145
+ context_cache_points: true
146
+ },
147
+ review: {
148
+ sort: "risk_first",
149
+ default_lines: 80,
150
+ max_expand_lines: 1200,
151
+ risk_weights: {
152
+ sensitive_path: 4,
153
+ large_change: 3,
154
+ medium_change: 2,
155
+ small_change: 1,
156
+ executable_script: 2,
157
+ command_pattern: 3
158
+ }
159
+ },
160
+ usage: {
161
+ pricing_file: null,
162
+ aggregation: ["turn", "session", "global"],
163
+ budget: {
164
+ session_usd: null,
165
+ global_usd: null,
166
+ warn_at_percent: 80,
167
+ strategy: "warn"
168
+ }
169
+ },
170
+ ui: {
171
+ theme_file: null,
172
+ mode_colors: {
173
+ ask: "#8da3b9",
174
+ plan: "#00b7c2",
175
+ agent: "#2ac26f",
176
+ longagent: "#ff7a33"
177
+ },
178
+ layout: "compact",
179
+ markdown_render: true,
180
+ status: {
181
+ show_cost: true,
182
+ show_token_meter: true
183
+ }
184
+ }
185
+ }
186
+
187
+ export const VALID_PROVIDER_TYPES = ["openai", "anthropic", "ollama", "openai-compatible"]
188
+
189
+ import { listProviders } from "../provider/router.mjs"
190
+ export function getValidProviderTypes() {
191
+ return listProviders()
192
+ }
193
+ export const VALID_MODES = ["ask", "plan", "agent", "longagent"]
194
+ export const VALID_REVIEW_SORT = ["risk_first", "time_order", "file_order"]
195
+ export const VALID_LANGUAGES = ["en", "zh"]
@@ -0,0 +1,76 @@
1
+ import { DEFAULT_CONFIG } from "./defaults.mjs"
2
+
3
+ function mergeObject(base, override) {
4
+ if (override === undefined || override === null) return base
5
+ if (Array.isArray(override)) return [...override]
6
+ if (!base || typeof base !== "object" || Array.isArray(base)) return override
7
+ if (typeof override !== "object") return override
8
+ const out = { ...base }
9
+ for (const key of Object.keys(override)) {
10
+ out[key] = mergeObject(base[key], override[key])
11
+ }
12
+ return out
13
+ }
14
+
15
+ function normalizePermissionRules(inputRules = {}) {
16
+ const rules = []
17
+ for (const [tool, value] of Object.entries(inputRules)) {
18
+ if (typeof value !== "boolean") continue
19
+ rules.push({
20
+ tool,
21
+ action: value ? "allow" : "deny"
22
+ })
23
+ }
24
+ return rules
25
+ }
26
+
27
+ export function importConfig(input = {}) {
28
+ const next = structuredClone(DEFAULT_CONFIG)
29
+
30
+ if (input.llm) {
31
+ if (input.llm.default_provider_type) next.provider.default = input.llm.default_provider_type
32
+ if (input.llm.max_steps) next.agent.max_steps = Number(input.llm.max_steps)
33
+
34
+ for (const key of ["openai", "anthropic"]) {
35
+ if (!input.llm[key]) continue
36
+ const src = input.llm[key]
37
+ const dst = next.provider[key]
38
+ if (src.base_url) dst.base_url = src.base_url
39
+ if (src.api_key_env) dst.api_key_env = src.api_key_env
40
+ if (src.default_model) dst.default_model = src.default_model
41
+ }
42
+ }
43
+
44
+ if (input.longagent?.max_iterations !== undefined) {
45
+ next.agent.longagent.max_iterations = Number(input.longagent.max_iterations)
46
+ }
47
+
48
+ if (input.usage) {
49
+ if (input.usage.pricing_file !== undefined) next.usage.pricing_file = input.usage.pricing_file
50
+ if (Array.isArray(input.usage.aggregation)) next.usage.aggregation = [...input.usage.aggregation]
51
+ }
52
+
53
+ if (input.ui) {
54
+ if (input.ui.layout) next.ui.layout = input.ui.layout
55
+ if (input.ui.theme?.file !== undefined) next.ui.theme_file = input.ui.theme.file
56
+ if (input.ui.theme?.mode_colors) next.ui.mode_colors = mergeObject(next.ui.mode_colors, input.ui.theme.mode_colors)
57
+ if (input.ui.status) next.ui.status = mergeObject(next.ui.status, input.ui.status)
58
+ if (input.ui.review?.sort) next.review.sort = input.ui.review.sort
59
+ if (input.ui.diff_preview?.default_lines) next.review.default_lines = Number(input.ui.diff_preview.default_lines)
60
+ if (input.ui.diff_preview?.max_expand_lines) next.review.max_expand_lines = Number(input.ui.diff_preview.max_expand_lines)
61
+ }
62
+
63
+ if (input.agents && typeof input.agents === "object") {
64
+ next.agent.subagents = mergeObject(next.agent.subagents, input.agents)
65
+ }
66
+
67
+ if (input.tools && typeof input.tools === "object") {
68
+ next.permission.rules.push(...normalizePermissionRules(input.tools))
69
+ }
70
+
71
+ if (input.permission?.rules && Array.isArray(input.permission.rules)) {
72
+ next.permission.rules.push(...input.permission.rules)
73
+ }
74
+
75
+ return next
76
+ }