@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,86 @@
1
+ import { Command } from "commander"
2
+ import { BackgroundManager } from "../orchestration/background-manager.mjs"
3
+ import { buildContext, printContextWarnings } from "../context.mjs"
4
+
5
+ async function withContext(action) {
6
+ const ctx = await buildContext()
7
+ printContextWarnings(ctx)
8
+ await BackgroundManager.tick(ctx.configState.config)
9
+ return action(ctx)
10
+ }
11
+
12
+ export function createBackgroundCommand() {
13
+ const cmd = new Command("background").description("inspect background delegated tasks")
14
+
15
+ cmd
16
+ .command("list")
17
+ .description("list background tasks")
18
+ .action(async () => {
19
+ await withContext(async () => {
20
+ const list = await BackgroundManager.list()
21
+ console.log(JSON.stringify(list, null, 2))
22
+ })
23
+ })
24
+
25
+ cmd
26
+ .command("show")
27
+ .description("show one background task")
28
+ .requiredOption("--id <id>", "task id")
29
+ .action(async (options) => {
30
+ await withContext(async () => {
31
+ const task = await BackgroundManager.get(options.id)
32
+ if (!task) {
33
+ console.error(`not found: ${options.id}`)
34
+ process.exitCode = 1
35
+ return
36
+ }
37
+ console.log(JSON.stringify(task, null, 2))
38
+ })
39
+ })
40
+
41
+ cmd
42
+ .command("cancel")
43
+ .description("cancel one background task")
44
+ .requiredOption("--id <id>", "task id")
45
+ .action(async (options) => {
46
+ await withContext(async () => {
47
+ const ok = await BackgroundManager.cancel(options.id)
48
+ if (!ok) {
49
+ console.error(`not found: ${options.id}`)
50
+ process.exitCode = 1
51
+ return
52
+ }
53
+ console.log(`cancel requested: ${options.id}`)
54
+ })
55
+ })
56
+
57
+ cmd
58
+ .command("retry")
59
+ .description("retry one interrupted/error background task")
60
+ .requiredOption("--id <id>", "task id")
61
+ .action(async (options) => {
62
+ await withContext(async (ctx) => {
63
+ const task = await BackgroundManager.retry(options.id, ctx.configState.config)
64
+ if (!task) {
65
+ console.error(`task not retryable or not found: ${options.id}`)
66
+ process.exitCode = 1
67
+ return
68
+ }
69
+ console.log(`retry queued: ${task.id} (attempt=${task.attempt})`)
70
+ })
71
+ })
72
+
73
+ cmd
74
+ .command("clean")
75
+ .description("remove old completed/cancelled/error/interrupted tasks")
76
+ .option("--max-age <days>", "max age in days", "7")
77
+ .action(async (options) => {
78
+ await withContext(async () => {
79
+ const maxAge = Number(options.maxAge || 7) * 24 * 60 * 60 * 1000
80
+ const removed = await BackgroundManager.clean({ maxAge })
81
+ console.log(`removed ${removed.length} task(s)`)
82
+ })
83
+ })
84
+
85
+ return cmd
86
+ }
@@ -0,0 +1,114 @@
1
+ import { Command } from "commander"
2
+ import { buildContext, printContextWarnings } from "../context.mjs"
3
+ import { executeTurn, newSessionId, resolveMode } from "../session/engine.mjs"
4
+ import { renderStatusBar } from "../theme/status-bar.mjs"
5
+ import { applyCommandTemplate, loadCustomCommands } from "../command/custom-commands.mjs"
6
+ import { ToolRegistry } from "../tool/registry.mjs"
7
+ import { HookBus, initHookBus } from "../plugin/hook-bus.mjs"
8
+ import { listProviders } from "../provider/router.mjs"
9
+
10
+ export function createChatCommand() {
11
+ const providers = listProviders()
12
+ return new Command("chat")
13
+ .description("run one prompt in ask/plan/agent/longagent mode")
14
+ .argument("<prompt...>", "prompt text")
15
+ .option("--mode <mode>", "ask|plan|agent|longagent", "agent")
16
+ .option("--model <model>", "model id")
17
+ .option("--provider-type <type>", `provider type (${providers.join("|")})`)
18
+ .option("--base-url <url>", "provider base url override")
19
+ .option("--api-key-env <name>", "api key env override")
20
+ .option("--max-iterations <n>", "longagent max iterations (0 = unlimited)")
21
+ .option("--session <id>", "session id")
22
+ .action(async (promptParts, options) => {
23
+ const ctx = await buildContext()
24
+ printContextWarnings(ctx)
25
+ let prompt = promptParts.join(" ").trim()
26
+ if (prompt.startsWith("/")) {
27
+ const commands = await loadCustomCommands(process.cwd())
28
+ const [name, ...argTokens] = prompt.slice(1).split(/\s+/)
29
+ const custom = commands.find((item) => item.name === name)
30
+ if (custom) {
31
+ const args = argTokens.join(" ").trim()
32
+ prompt = applyCommandTemplate(custom.template, args, {
33
+ path: process.cwd()
34
+ })
35
+ }
36
+ }
37
+
38
+ const mode = resolveMode(options.mode)
39
+ const providerType = options.providerType ?? ctx.configState.config.provider.default
40
+ const providerDefaults = ctx.configState.config.provider[providerType]
41
+ if (!providerDefaults) {
42
+ throw new Error(`unknown provider type: ${providerType}`)
43
+ }
44
+ const model = options.model ?? providerDefaults.default_model
45
+ const sessionId = options.session || newSessionId()
46
+
47
+ await ToolRegistry.initialize({
48
+ config: ctx.configState.config,
49
+ cwd: process.cwd()
50
+ })
51
+
52
+ await initHookBus()
53
+ const chatParams = await HookBus.chatParams({
54
+ prompt,
55
+ mode,
56
+ model,
57
+ providerType,
58
+ sessionId,
59
+ baseUrl: options.baseUrl ?? null,
60
+ apiKeyEnv: options.apiKeyEnv ?? null
61
+ })
62
+
63
+ const result = await executeTurn({
64
+ prompt: chatParams.prompt ?? prompt,
65
+ mode: chatParams.mode ?? mode,
66
+ model: chatParams.model ?? model,
67
+ sessionId,
68
+ configState: ctx.configState,
69
+ providerType: chatParams.providerType ?? providerType,
70
+ baseUrl: chatParams.baseUrl ?? options.baseUrl ?? null,
71
+ apiKeyEnv: chatParams.apiKeyEnv ?? options.apiKeyEnv ?? null,
72
+ maxIterations: options.maxIterations !== undefined ? Number(options.maxIterations) : null,
73
+ output: ctx.configState.config.provider[chatParams.providerType ?? providerType]?.stream !== false
74
+ ? { write: (chunk) => process.stdout.write(String(chunk || "")) }
75
+ : null
76
+ })
77
+
78
+ const status = renderStatusBar({
79
+ mode,
80
+ model: result.model,
81
+ permission: ctx.configState.config.permission.default_policy,
82
+ tokenMeter: result.tokenMeter,
83
+ aggregation: ctx.configState.config.usage.aggregation,
84
+ cost: result.cost,
85
+ showCost: ctx.configState.config.ui.status.show_cost,
86
+ showTokenMeter: ctx.configState.config.ui.status.show_token_meter,
87
+ theme: ctx.themeState.theme,
88
+ layout: ctx.configState.config.ui.layout,
89
+ longagentState: mode === "longagent" ? result.longagent : null
90
+ })
91
+
92
+ console.log(status)
93
+ console.log("")
94
+ const streamEnabled = ctx.configState.config.provider[chatParams.providerType ?? providerType]?.stream !== false
95
+ if (!streamEnabled || !result.emittedText) {
96
+ console.log(result.reply)
97
+ }
98
+ console.log("")
99
+ if (result.toolEvents.length) {
100
+ console.log(`tool events: ${result.toolEvents.length}`)
101
+ }
102
+ if (result.pricingWarnings.length) {
103
+ for (const warning of result.pricingWarnings) {
104
+ console.log(`pricing warning: ${warning}`)
105
+ }
106
+ }
107
+ if (result.budgetWarnings.length) {
108
+ for (const warning of result.budgetWarnings) {
109
+ console.log(`budget warning: ${warning}`)
110
+ }
111
+ }
112
+ console.log(`session: ${sessionId}`)
113
+ })
114
+ }
@@ -0,0 +1,41 @@
1
+ import { Command } from "commander"
2
+ import { applyCommandTemplate, loadCustomCommands } from "../command/custom-commands.mjs"
3
+
4
+ export function createCommandCommand() {
5
+ const cmd = new Command("command").description("inspect custom slash commands")
6
+
7
+ cmd
8
+ .command("list")
9
+ .description("list discovered custom commands")
10
+ .action(async () => {
11
+ const list = await loadCustomCommands(process.cwd())
12
+ if (!list.length) {
13
+ console.log("no custom commands found")
14
+ return
15
+ }
16
+ for (const item of list) {
17
+ console.log(`/${item.name} (${item.scope}) -> ${item.source}`)
18
+ }
19
+ })
20
+
21
+ cmd
22
+ .command("preview")
23
+ .description("preview command template expansion")
24
+ .requiredOption("--name <name>", "command name")
25
+ .option("--args <text>", "arguments text", "")
26
+ .action(async (options) => {
27
+ const list = await loadCustomCommands(process.cwd())
28
+ const item = list.find((cmd) => cmd.name === options.name)
29
+ if (!item) {
30
+ console.error(`command not found: ${options.name}`)
31
+ process.exitCode = 1
32
+ return
33
+ }
34
+ const expanded = applyCommandTemplate(item.template, options.args, {
35
+ path: process.cwd()
36
+ })
37
+ console.log(expanded)
38
+ })
39
+
40
+ return cmd
41
+ }
@@ -0,0 +1,44 @@
1
+ import path from "node:path"
2
+ import { readFile, writeFile } from "node:fs/promises"
3
+ import { Command } from "commander"
4
+ import YAML from "yaml"
5
+ import { importConfig } from "../config/import-config.mjs"
6
+ import { validateConfig } from "../config/schema.mjs"
7
+
8
+ function parseInput(file, raw) {
9
+ if (file.endsWith(".json")) return JSON.parse(raw)
10
+ return YAML.parse(raw)
11
+ }
12
+
13
+ function stringifyOutput(file, data) {
14
+ if (file.endsWith(".json")) return JSON.stringify(data, null, 2) + "\n"
15
+ return YAML.stringify(data)
16
+ }
17
+
18
+ export function createConfigCommand() {
19
+ const cmd = new Command("config").description("manage kkcode config")
20
+
21
+ cmd
22
+ .command("import")
23
+ .description("import external config into kkcode format")
24
+ .requiredOption("--from <file>", "source config file path")
25
+ .option("--to <file>", "output file path", "kkcode.config.yaml")
26
+ .action(async (options) => {
27
+ const from = path.resolve(options.from)
28
+ const to = path.resolve(options.to)
29
+ const raw = await readFile(from, "utf8")
30
+ const parsed = parseInput(from, raw)
31
+ const imported = importConfig(parsed)
32
+ const check = validateConfig(imported)
33
+ if (!check.valid) {
34
+ console.error("import produced invalid config:")
35
+ for (const error of check.errors) console.error(`- ${error}`)
36
+ process.exitCode = 1
37
+ return
38
+ }
39
+ await writeFile(to, stringifyOutput(to, imported), "utf8")
40
+ console.log(`imported config written: ${to}`)
41
+ })
42
+
43
+ return cmd
44
+ }
@@ -0,0 +1,148 @@
1
+ import { Command } from "commander"
2
+ import { exec as execCb } from "node:child_process"
3
+ import { promisify } from "node:util"
4
+ import { buildContext } from "../context.mjs"
5
+ import { listProviders } from "../provider/router.mjs"
6
+ import { eventLogStats } from "../storage/event-log.mjs"
7
+ import { auditStats } from "../storage/audit-store.mjs"
8
+ import { fsckSessionStore, flushNow } from "../session/store.mjs"
9
+ import { BackgroundManager } from "../orchestration/background-manager.mjs"
10
+ import { McpRegistry } from "../mcp/registry.mjs"
11
+
12
+ const exec = promisify(execCb)
13
+
14
+ async function hasCommand(cmd) {
15
+ const query = process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`
16
+ try {
17
+ await exec(query)
18
+ return true
19
+ } catch {
20
+ return false
21
+ }
22
+ }
23
+
24
+ function summarizeBackground(tasks) {
25
+ const counters = {
26
+ total: tasks.length,
27
+ pending: 0,
28
+ running: 0,
29
+ completed: 0,
30
+ error: 0,
31
+ cancelled: 0,
32
+ interrupted: 0
33
+ }
34
+ for (const task of tasks) {
35
+ if (counters[task.status] !== undefined) counters[task.status] += 1
36
+ }
37
+ return counters
38
+ }
39
+
40
+ async function buildDoctorReport() {
41
+ const ctx = await buildContext()
42
+ await flushNow()
43
+ await BackgroundManager.tick(ctx.configState.config)
44
+
45
+ const checks = {
46
+ node: true,
47
+ rg: await hasCommand("rg"),
48
+ git: await hasCommand("git")
49
+ }
50
+
51
+ const config = ctx.configState.config
52
+ const providers = []
53
+ for (const [name, provider] of Object.entries(config.provider || {})) {
54
+ if (name === "default") continue
55
+ if (!provider || typeof provider !== "object") continue
56
+ const keyEnv = provider.api_key_env || ""
57
+ providers.push({
58
+ name,
59
+ type: provider.type || name,
60
+ model: provider.default_model || null,
61
+ baseUrl: provider.base_url || null,
62
+ apiKeyEnv: keyEnv || null,
63
+ apiKeyConfigured: keyEnv ? Boolean(process.env[keyEnv]) : true
64
+ })
65
+ }
66
+
67
+ const events = await eventLogStats()
68
+ const audit = await auditStats()
69
+ const storage = await fsckSessionStore()
70
+ const backgroundTasks = await BackgroundManager.list()
71
+ await McpRegistry.initialize(config)
72
+ const mcpSnapshot = McpRegistry.healthSnapshot()
73
+ const mcpHealthy = mcpSnapshot.filter((item) => item.ok).length
74
+
75
+ return {
76
+ ok: storage.ok,
77
+ timestamp: new Date().toISOString(),
78
+ cwd: process.cwd(),
79
+ themeWarnings: ctx.themeState.errors,
80
+ config: {
81
+ defaultProvider: config.provider?.default || null,
82
+ userPath: ctx.configState.source.userPath,
83
+ projectPath: ctx.configState.source.projectPath,
84
+ warnings: ctx.configState.errors
85
+ },
86
+ runtime: {
87
+ providersRegistered: listProviders(),
88
+ providersConfigured: providers
89
+ },
90
+ checks,
91
+ mcp: {
92
+ configured: mcpSnapshot.length,
93
+ healthy: mcpHealthy,
94
+ unhealthy: mcpSnapshot.length - mcpHealthy,
95
+ servers: mcpSnapshot
96
+ },
97
+ storage: {
98
+ sessions: storage,
99
+ eventLog: events,
100
+ audit
101
+ },
102
+ background: summarizeBackground(backgroundTasks)
103
+ }
104
+ }
105
+
106
+ function printTextReport(report, themeWarnings = []) {
107
+ console.log("kkcode doctor")
108
+ console.log(`time: ${report.timestamp}`)
109
+ console.log(`cwd: ${report.cwd}`)
110
+ console.log(`default provider: ${report.config.defaultProvider}`)
111
+ console.log(`config.user: ${report.config.userPath || "(none)"}`)
112
+ console.log(`config.project: ${report.config.projectPath || "(none)"}`)
113
+ if (report.config.warnings.length) {
114
+ for (const warning of report.config.warnings) {
115
+ console.log(`config warning: ${warning}`)
116
+ }
117
+ }
118
+ for (const warning of themeWarnings) {
119
+ console.log(`theme warning: ${warning}`)
120
+ }
121
+ for (const p of report.runtime.providersConfigured) {
122
+ console.log(
123
+ `provider:${p.name} type=${p.type} model=${p.model || "?"} key=${p.apiKeyEnv || "-"} (${p.apiKeyConfigured ? "set" : "missing"})`
124
+ )
125
+ }
126
+ console.log(`check node=${report.checks.node ? "ok" : "missing"} rg=${report.checks.rg ? "ok" : "missing"} git=${report.checks.git ? "ok" : "missing"}`)
127
+ console.log(`mcp: configured=${report.mcp.configured} healthy=${report.mcp.healthy} unhealthy=${report.mcp.unhealthy}`)
128
+ console.log(`sessions: ok=${report.storage.sessions.ok} index=${report.storage.sessions.sessionsInIndex} files=${report.storage.sessions.filesOnDisk}`)
129
+ console.log(`events: active=${report.storage.eventLog.activeBytes} rotated=${report.storage.eventLog.rotatedFiles}`)
130
+ console.log(`audit: total=${report.storage.audit.total} error1h=${report.storage.audit.error1h} error24h=${report.storage.audit.error24h}`)
131
+ console.log(
132
+ `background: total=${report.background.total} running=${report.background.running} pending=${report.background.pending} interrupted=${report.background.interrupted} error=${report.background.error}`
133
+ )
134
+ }
135
+
136
+ export function createDoctorCommand() {
137
+ return new Command("doctor")
138
+ .description("run environment diagnostics")
139
+ .option("--json", "print structured diagnostics", false)
140
+ .action(async (options) => {
141
+ const report = await buildDoctorReport()
142
+ if (options.json) {
143
+ console.log(JSON.stringify(report, null, 2))
144
+ return
145
+ }
146
+ printTextReport(report, report.themeWarnings || [])
147
+ })
148
+ }
@@ -0,0 +1,29 @@
1
+ import { Command } from "commander"
2
+ import { initHookBus, HookBus } from "../plugin/hook-bus.mjs"
3
+
4
+ export function createHookCommand() {
5
+ const cmd = new Command("hook").description("inspect loaded hooks")
6
+
7
+ cmd
8
+ .command("list")
9
+ .description("list loaded hooks and loading errors")
10
+ .action(async () => {
11
+ await initHookBus(process.cwd())
12
+ const hooks = HookBus.list()
13
+ const errors = HookBus.errors()
14
+ console.log(`supported events: ${HookBus.supportedEvents().join(", ")}`)
15
+ if (!hooks.length) {
16
+ console.log("no hooks loaded")
17
+ } else {
18
+ for (const hook of hooks) {
19
+ console.log(`- ${hook.name} (${hook.source})`)
20
+ }
21
+ }
22
+ if (errors.length) {
23
+ console.log("hook loading errors:")
24
+ for (const err of errors) console.log(`- ${err}`)
25
+ }
26
+ })
27
+
28
+ return cmd
29
+ }
@@ -0,0 +1,141 @@
1
+ import { join } from "node:path"
2
+ import { mkdir, writeFile, access } from "node:fs/promises"
3
+ import { createInterface } from "node:readline/promises"
4
+ import { Command } from "commander"
5
+ import YAML from "yaml"
6
+ import { listProviders } from "../provider/router.mjs"
7
+ import { validateConfig } from "../config/schema.mjs"
8
+
9
+ async function exists(target) {
10
+ try { await access(target); return true } catch { return false }
11
+ }
12
+
13
+ async function askQuestion(rl, question, defaultValue = "") {
14
+ const suffix = defaultValue ? ` (${defaultValue})` : ""
15
+ const answer = await rl.question(`${question}${suffix}: `)
16
+ return answer.trim() || defaultValue
17
+ }
18
+
19
+ async function askChoice(rl, question, choices, defaultValue) {
20
+ const choiceStr = choices.map((c, i) => `${i + 1}. ${c}`).join("\n")
21
+ console.log(`\n${question}\n${choiceStr}`)
22
+ const answer = await rl.question(`choose [${defaultValue}]: `)
23
+ const trimmed = answer.trim()
24
+ if (!trimmed) return defaultValue
25
+ const idx = parseInt(trimmed, 10)
26
+ if (idx >= 1 && idx <= choices.length) return choices[idx - 1]
27
+ if (choices.includes(trimmed)) return trimmed
28
+ return defaultValue
29
+ }
30
+
31
+ const PROVIDER_DEFAULTS = {
32
+ openai: { api_key_env: "OPENAI_API_KEY", default_model: "gpt-5.3-codex" },
33
+ anthropic: { api_key_env: "ANTHROPIC_API_KEY", default_model: "claude-opus-4-6" },
34
+ ollama: { base_url: "http://localhost:11434", api_key_env: "", default_model: "llama3.1" },
35
+ "openai-compatible": { base_url: "", api_key_env: "", default_model: "" }
36
+ }
37
+
38
+ function buildConfig(answers) {
39
+ const config = {
40
+ provider: {
41
+ default: answers.provider
42
+ },
43
+ permission: {
44
+ default_policy: answers.permissionPolicy || "ask"
45
+ }
46
+ }
47
+ const block = {}
48
+ if (answers.baseUrl) block.base_url = answers.baseUrl
49
+ if (answers.apiKeyEnv) block.api_key_env = answers.apiKeyEnv
50
+ if (answers.model) block.default_model = answers.model
51
+ if (Object.keys(block).length) {
52
+ config.provider[answers.provider] = block
53
+ }
54
+ return config
55
+ }
56
+
57
+ async function runInteractive(rl) {
58
+ const providers = listProviders()
59
+ const provider = await askChoice(rl, "select default provider:", providers, "openai")
60
+ const defaults = PROVIDER_DEFAULTS[provider] || {}
61
+
62
+ let baseUrl = ""
63
+ let apiKeyEnv = ""
64
+ let model = ""
65
+
66
+ if (provider === "ollama") {
67
+ baseUrl = await askQuestion(rl, "ollama base URL", defaults.base_url)
68
+ model = await askQuestion(rl, "default model", defaults.default_model)
69
+ } else if (provider === "openai-compatible") {
70
+ baseUrl = await askQuestion(rl, "base URL", "")
71
+ apiKeyEnv = await askQuestion(rl, "API key env var", "")
72
+ model = await askQuestion(rl, "default model", "")
73
+ } else {
74
+ apiKeyEnv = await askQuestion(rl, "API key env var", defaults.api_key_env)
75
+ model = await askQuestion(rl, "default model", defaults.default_model)
76
+ }
77
+
78
+ const permissionPolicy = await askChoice(
79
+ rl,
80
+ "default permission policy:",
81
+ ["allow", "ask", "deny"],
82
+ "ask"
83
+ )
84
+
85
+ return { provider, baseUrl, apiKeyEnv, model, permissionPolicy }
86
+ }
87
+
88
+ export function createInitCommand() {
89
+ return new Command("init")
90
+ .description("initialize kkcode in current project")
91
+ .option("-y, --yes", "use defaults without prompting")
92
+ .action(async (options) => {
93
+ const cwd = process.cwd()
94
+ const configDir = join(cwd, ".kkcode")
95
+ const configFile = join(configDir, "config.yaml")
96
+
97
+ if (await exists(configFile)) {
98
+ console.log(`config already exists: ${configFile}`)
99
+ console.log("use 'kkcode config' to modify it")
100
+ return
101
+ }
102
+
103
+ let answers
104
+ if (options.yes) {
105
+ answers = {
106
+ provider: "openai",
107
+ baseUrl: "",
108
+ apiKeyEnv: "OPENAI_API_KEY",
109
+ model: "gpt-5.3-codex",
110
+ permissionPolicy: "ask"
111
+ }
112
+ } else {
113
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
114
+ try {
115
+ answers = await runInteractive(rl)
116
+ } finally {
117
+ rl.close()
118
+ }
119
+ }
120
+
121
+ const config = buildConfig(answers)
122
+ const check = validateConfig(config)
123
+ if (!check.valid) {
124
+ console.error("generated config is invalid:")
125
+ for (const e of check.errors) console.error(` - ${e}`)
126
+ process.exitCode = 1
127
+ return
128
+ }
129
+
130
+ await mkdir(configDir, { recursive: true })
131
+ await writeFile(configFile, YAML.stringify(config), "utf8")
132
+ console.log(`created: ${configFile}`)
133
+
134
+ const defaultProvider = config.provider.default
135
+ const providerCfg = config.provider[defaultProvider] || {}
136
+ if (providerCfg.api_key_env && !process.env[providerCfg.api_key_env]) {
137
+ console.log(`note: ${providerCfg.api_key_env} is not set in environment`)
138
+ }
139
+ console.log("run 'kkcode doctor' for full diagnostics")
140
+ })
141
+ }