@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,100 @@
1
+ import { Command } from "commander"
2
+ import { LongAgentManager } from "../orchestration/longagent-manager.mjs"
3
+
4
+ export function createLongagentCommand() {
5
+ const cmd = new Command("longagent").description("manage longagent sessions")
6
+
7
+ cmd
8
+ .command("status")
9
+ .description("show one longagent session or list all")
10
+ .option("--session <id>", "session id")
11
+ .action(async (options) => {
12
+ if (options.session) {
13
+ const item = await LongAgentManager.get(options.session)
14
+ if (!item) {
15
+ console.error(`not found: ${options.session}`)
16
+ process.exitCode = 1
17
+ return
18
+ }
19
+ console.log(JSON.stringify(item, null, 2))
20
+ return
21
+ }
22
+ const list = await LongAgentManager.list()
23
+ console.log(JSON.stringify(list, null, 2))
24
+ })
25
+
26
+ cmd
27
+ .command("plan")
28
+ .description("show frozen stage plan for a longagent session")
29
+ .requiredOption("--session <id>", "session id")
30
+ .action(async (options) => {
31
+ const item = await LongAgentManager.get(options.session)
32
+ if (!item) {
33
+ console.error(`not found: ${options.session}`)
34
+ process.exitCode = 1
35
+ return
36
+ }
37
+ if (!item.stagePlan) {
38
+ console.error(`no frozen plan found for session: ${options.session}`)
39
+ process.exitCode = 1
40
+ return
41
+ }
42
+ console.log(JSON.stringify(item.stagePlan, null, 2))
43
+ })
44
+
45
+ cmd
46
+ .command("stop")
47
+ .description("emergency stop for a running longagent session")
48
+ .requiredOption("--session <id>", "session id")
49
+ .option("--force", "confirm emergency stop")
50
+ .action(async (options) => {
51
+ if (!options.force) {
52
+ console.error("longagent stop is emergency-only. re-run with --force to confirm.")
53
+ process.exitCode = 1
54
+ return
55
+ }
56
+ const result = await LongAgentManager.stop(options.session)
57
+ if (!result) {
58
+ console.error(`not found: ${options.session}`)
59
+ process.exitCode = 1
60
+ return
61
+ }
62
+ console.log(`emergency stop requested: ${options.session}`)
63
+ })
64
+
65
+ cmd
66
+ .command("resume")
67
+ .description("clear stop flag for session")
68
+ .requiredOption("--session <id>", "session id")
69
+ .action(async (options) => {
70
+ const result = await LongAgentManager.clearStop(options.session)
71
+ if (!result) {
72
+ console.error(`not found: ${options.session}`)
73
+ process.exitCode = 1
74
+ return
75
+ }
76
+ console.log(`stop flag cleared: ${options.session}`)
77
+ })
78
+
79
+ cmd
80
+ .command("stage-retry")
81
+ .description("mark one stage for manual retry in longagent state")
82
+ .requiredOption("--session <id>", "session id")
83
+ .requiredOption("--stage <id>", "stage id")
84
+ .action(async (options) => {
85
+ const current = await LongAgentManager.get(options.session)
86
+ if (!current) {
87
+ console.error(`not found: ${options.session}`)
88
+ process.exitCode = 1
89
+ return
90
+ }
91
+ const out = await LongAgentManager.update(options.session, {
92
+ retryStageId: options.stage,
93
+ stageStatus: "retry_requested",
94
+ stopRequested: false
95
+ })
96
+ console.log(`stage retry requested: ${options.stage} (session=${out.sessionId})`)
97
+ })
98
+
99
+ return cmd
100
+ }
@@ -0,0 +1,89 @@
1
+ import { Command } from "commander"
2
+ import { buildContext, printContextWarnings } from "../context.mjs"
3
+ import { McpRegistry } from "../mcp/registry.mjs"
4
+
5
+ export function createMcpCommand() {
6
+ const cmd = new Command("mcp").description("manage MCP servers and tools")
7
+
8
+ cmd
9
+ .command("list")
10
+ .description("list configured and healthy MCP servers")
11
+ .action(async () => {
12
+ const ctx = await buildContext()
13
+ printContextWarnings(ctx)
14
+ await McpRegistry.initialize(ctx.configState.config)
15
+ console.log(JSON.stringify(McpRegistry.listServers(), null, 2))
16
+ })
17
+
18
+ cmd
19
+ .command("tools")
20
+ .description("list MCP tools")
21
+ .action(async () => {
22
+ const ctx = await buildContext()
23
+ printContextWarnings(ctx)
24
+ await McpRegistry.initialize(ctx.configState.config)
25
+ console.log(JSON.stringify(McpRegistry.listTools(), null, 2))
26
+ })
27
+
28
+ cmd
29
+ .command("resources")
30
+ .description("list resources for MCP server")
31
+ .requiredOption("--server <name>", "server name")
32
+ .action(async (options) => {
33
+ const ctx = await buildContext()
34
+ printContextWarnings(ctx)
35
+ await McpRegistry.initialize(ctx.configState.config)
36
+ const list = await McpRegistry.listResources(options.server)
37
+ console.log(JSON.stringify(list, null, 2))
38
+ })
39
+
40
+ cmd
41
+ .command("templates")
42
+ .description("list templates for MCP server")
43
+ .requiredOption("--server <name>", "server name")
44
+ .action(async (options) => {
45
+ const ctx = await buildContext()
46
+ printContextWarnings(ctx)
47
+ await McpRegistry.initialize(ctx.configState.config)
48
+ const list = await McpRegistry.listTemplates(options.server)
49
+ console.log(JSON.stringify(list, null, 2))
50
+ })
51
+
52
+ cmd
53
+ .command("test")
54
+ .description("test MCP health and tool discovery")
55
+ .option("--json", "print JSON output", false)
56
+ .action(async (options) => {
57
+ const ctx = await buildContext()
58
+ printContextWarnings(ctx)
59
+ await McpRegistry.initialize(ctx.configState.config)
60
+ const snapshot = McpRegistry.healthSnapshot()
61
+ const tools = McpRegistry.listTools()
62
+ const healthy = snapshot.filter((item) => item.ok).length
63
+ const unhealthy = snapshot.length - healthy
64
+
65
+ if (options.json) {
66
+ console.log(JSON.stringify({
67
+ configured: snapshot.length,
68
+ healthy,
69
+ unhealthy,
70
+ tools: tools.length,
71
+ servers: snapshot
72
+ }, null, 2))
73
+ return
74
+ }
75
+
76
+ console.log(`configured: ${snapshot.length}`)
77
+ console.log(`healthy: ${healthy}`)
78
+ console.log(`unhealthy: ${unhealthy}`)
79
+ console.log(`tools: ${tools.length}`)
80
+ for (const item of snapshot) {
81
+ const status = item.ok ? "ok" : "fail"
82
+ const reason = item.reason || "-"
83
+ const error = item.error ? ` | ${item.error}` : ""
84
+ console.log(`- ${item.name} [${item.transport}] ${status} (${reason})${error}`)
85
+ }
86
+ })
87
+
88
+ return cmd
89
+ }
@@ -0,0 +1,36 @@
1
+ import { Command } from "commander"
2
+ import { buildContext, printContextWarnings } from "../context.mjs"
3
+ import { PermissionEngine } from "../permission/engine.mjs"
4
+
5
+ export function createPermissionCommand() {
6
+ const cmd = new Command("permission").description("inspect permission rules and session grants")
7
+
8
+ cmd
9
+ .command("show")
10
+ .description("show configured permission policy")
11
+ .action(async () => {
12
+ const ctx = await buildContext()
13
+ printContextWarnings(ctx)
14
+ console.log(JSON.stringify(ctx.configState.config.permission, null, 2))
15
+ })
16
+
17
+ cmd
18
+ .command("session")
19
+ .description("show granted allow_session keys for one session")
20
+ .requiredOption("--id <id>", "session id")
21
+ .action(async (options) => {
22
+ const list = PermissionEngine.listSession(options.id)
23
+ console.log(JSON.stringify(list, null, 2))
24
+ })
25
+
26
+ cmd
27
+ .command("reset")
28
+ .description("clear in-memory grants for one session")
29
+ .requiredOption("--id <id>", "session id")
30
+ .action(async (options) => {
31
+ PermissionEngine.clearSession(options.id)
32
+ console.log(`permission cache cleared for session ${options.id}`)
33
+ })
34
+
35
+ return cmd
36
+ }
@@ -0,0 +1,42 @@
1
+ import path from "node:path"
2
+ import { readdir, readFile } from "node:fs/promises"
3
+ import { Command } from "commander"
4
+
5
+ const SESSION_PROMPT_DIR = path.resolve("src/session/prompt")
6
+ const TOOL_PROMPT_DIR = path.resolve("src/tool/prompt")
7
+
8
+ async function listFiles(dir) {
9
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
10
+ return entries.filter((entry) => entry.isFile()).map((entry) => entry.name)
11
+ }
12
+
13
+ export function createPromptCommand() {
14
+ const cmd = new Command("prompt").description("inspect prompt placement and files")
15
+
16
+ cmd
17
+ .command("list")
18
+ .description("list session/tool prompt files")
19
+ .action(async () => {
20
+ const sessionFiles = await listFiles(SESSION_PROMPT_DIR)
21
+ const toolFiles = await listFiles(TOOL_PROMPT_DIR)
22
+ console.log(`session prompts: ${SESSION_PROMPT_DIR}`)
23
+ for (const file of sessionFiles) console.log(`- ${file}`)
24
+ console.log(``)
25
+ console.log(`tool prompts: ${TOOL_PROMPT_DIR}`)
26
+ for (const file of toolFiles) console.log(`- ${file}`)
27
+ })
28
+
29
+ cmd
30
+ .command("show")
31
+ .description("show one prompt file")
32
+ .requiredOption("--type <type>", "session|tool")
33
+ .requiredOption("--name <name>", "prompt filename")
34
+ .action(async (options) => {
35
+ const dir = options.type === "session" ? SESSION_PROMPT_DIR : TOOL_PROMPT_DIR
36
+ const file = path.join(dir, options.name)
37
+ const content = await readFile(file, "utf8")
38
+ console.log(content.trim())
39
+ })
40
+
41
+ return cmd
42
+ }
@@ -0,0 +1,266 @@
1
+ import path from "node:path"
2
+ import { readFile } from "node:fs/promises"
3
+ import { execSync } from "node:child_process"
4
+ import { Command } from "commander"
5
+ import { buildContext, printContextWarnings } from "../context.mjs"
6
+ import { parseUnifiedDiff, previewLines } from "../review/diff-parser.mjs"
7
+ import { scoreRisk, sortReviewFiles } from "../review/risk-score.mjs"
8
+ import { defaultReviewState, readReviewState, writeReviewState } from "../review/review-store.mjs"
9
+ import { clearRejections, enqueueRejection, listRejections } from "../review/rejection-queue.mjs"
10
+ import { paint } from "../theme/color.mjs"
11
+ import { applyReviewDecision, getSession, listSessions } from "../session/store.mjs"
12
+
13
+ function getGitDiff() {
14
+ try {
15
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" })
16
+ return execSync("git diff --no-color", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
17
+ } catch {
18
+ return ""
19
+ }
20
+ }
21
+
22
+ async function loadDiff(diffFile) {
23
+ if (!diffFile) return getGitDiff()
24
+ return readFile(path.resolve(diffFile), "utf8")
25
+ }
26
+
27
+ function renderFile(file, index, lines, theme) {
28
+ const title = `${index + 1}. ${file.path} (+${file.added} -${file.removed}) risk=${file.riskScore}`
29
+ console.log(paint(title, file.riskScore >= 9 ? theme.semantic.error : file.riskScore >= 6 ? theme.semantic.warn : theme.semantic.info))
30
+ if (file.reasons.length) {
31
+ console.log(` reasons: ${file.reasons.join("; ")}`)
32
+ }
33
+ const preview = previewLines(file, lines)
34
+ for (const line of preview) {
35
+ if (line.startsWith("+") && !line.startsWith("+++")) {
36
+ console.log(paint(` ${line}`, theme.components.diff_add))
37
+ continue
38
+ }
39
+ if (line.startsWith("-") && !line.startsWith("---")) {
40
+ console.log(paint(` ${line}`, theme.components.diff_del))
41
+ continue
42
+ }
43
+ console.log(` ${line}`)
44
+ }
45
+ if (file.rawLines.length > lines) {
46
+ console.log(paint(` ... (${file.rawLines.length - lines} more lines, use "kkcode review expand --index ${index}" )`, theme.base.muted))
47
+ }
48
+ }
49
+
50
+ function summarize(files) {
51
+ const added = files.reduce((sum, file) => sum + file.added, 0)
52
+ const removed = files.reduce((sum, file) => sum + file.removed, 0)
53
+ const risk = files.reduce((sum, file) => sum + file.riskScore, 0)
54
+ return { fileCount: files.length, added, removed, risk }
55
+ }
56
+
57
+ async function resolveReviewSessionId(requestedSessionId, cwd) {
58
+ if (requestedSessionId) {
59
+ const data = await getSession(requestedSessionId)
60
+ if (!data) return null
61
+ return requestedSessionId
62
+ }
63
+ const latest = await listSessions({ cwd, limit: 1, includeChildren: true })
64
+ if (!latest.length) return null
65
+ return latest[0].id
66
+ }
67
+
68
+ export function createReviewCommand() {
69
+ const cmd = new Command("review").description("review code changes with risk-first previews")
70
+
71
+ cmd
72
+ .command("open")
73
+ .description("build review state from diff and show first previews")
74
+ .option("--diff-file <file>", "use a diff file instead of git diff")
75
+ .option("--session <id>", "bind review decisions to this session id")
76
+ .option("--lines <n>", "preview lines per file")
77
+ .action(async (options) => {
78
+ const ctx = await buildContext()
79
+ printContextWarnings(ctx)
80
+ const config = ctx.configState.config
81
+ const theme = ctx.themeState.theme
82
+ const previewCount = Number(options.lines ?? config.review.default_lines)
83
+ const diff = await loadDiff(options.diffFile ?? null)
84
+ const files = parseUnifiedDiff(diff).map((file) => {
85
+ const risk = scoreRisk(file)
86
+ return {
87
+ ...file,
88
+ riskScore: risk.score,
89
+ reasons: risk.reasons,
90
+ status: "pending"
91
+ }
92
+ })
93
+ if (files.length === 0) {
94
+ console.log("no diff content found")
95
+ return
96
+ }
97
+ const sorted = sortReviewFiles(files, config.review.sort)
98
+ const state = defaultReviewState()
99
+ const sessionId = await resolveReviewSessionId(options.session ?? null, process.cwd())
100
+ if (options.session && !sessionId) {
101
+ console.error(`session not found: ${options.session}`)
102
+ process.exitCode = 1
103
+ return
104
+ }
105
+ state.sessionId = sessionId
106
+ state.files = sorted
107
+ state.currentIndex = 0
108
+ await writeReviewState(state)
109
+ const summary = summarize(sorted)
110
+ console.log(`summary: files=${summary.fileCount} added=${summary.added} removed=${summary.removed} totalRisk=${summary.risk}`)
111
+ const risky = sorted.filter((file) => file.riskScore >= 6).slice(0, 5)
112
+ if (risky.length) {
113
+ console.log("high-risk files:")
114
+ for (const file of risky) {
115
+ console.log(`- ${file.path} (risk=${file.riskScore})`)
116
+ }
117
+ }
118
+ if (state.sessionId) {
119
+ console.log(`bound session: ${state.sessionId}`)
120
+ } else {
121
+ console.log("bound session: (none)")
122
+ }
123
+ for (const [index, file] of sorted.entries()) {
124
+ renderFile(file, index, previewCount, theme)
125
+ }
126
+ })
127
+
128
+ cmd
129
+ .command("next")
130
+ .description("move to next file preview")
131
+ .action(async () => {
132
+ const ctx = await buildContext()
133
+ const config = ctx.configState.config
134
+ const theme = ctx.themeState.theme
135
+ const state = await readReviewState()
136
+ if (!state.files.length) {
137
+ console.error("review state empty. Run `kkcode review open` first.")
138
+ process.exitCode = 1
139
+ return
140
+ }
141
+ state.currentIndex = Math.min(state.currentIndex + 1, state.files.length - 1)
142
+ await writeReviewState(state)
143
+ renderFile(state.files[state.currentIndex], state.currentIndex, config.review.default_lines, theme)
144
+ })
145
+
146
+ cmd
147
+ .command("expand")
148
+ .description("expand current or selected file preview")
149
+ .option("--index <n>", "file index, zero-based")
150
+ .action(async (options) => {
151
+ const ctx = await buildContext()
152
+ const theme = ctx.themeState.theme
153
+ const config = ctx.configState.config
154
+ const state = await readReviewState()
155
+ if (!state.files.length) {
156
+ console.error("review state empty. Run `kkcode review open` first.")
157
+ process.exitCode = 1
158
+ return
159
+ }
160
+ const index = options.index !== undefined ? Math.max(0, Number(options.index)) : state.currentIndex
161
+ const file = state.files[index]
162
+ if (!file) {
163
+ console.error(`invalid index: ${index}`)
164
+ process.exitCode = 1
165
+ return
166
+ }
167
+ const max = config.review.max_expand_lines
168
+ renderFile(file, index, max, theme)
169
+ })
170
+
171
+ cmd
172
+ .command("approve")
173
+ .description("approve current or selected review file")
174
+ .option("--index <n>", "file index, zero-based")
175
+ .action(async (options) => {
176
+ const state = await readReviewState()
177
+ if (!state.files.length) {
178
+ console.error("review state empty. Run `kkcode review open` first.")
179
+ process.exitCode = 1
180
+ return
181
+ }
182
+ const index = options.index !== undefined ? Math.max(0, Number(options.index)) : state.currentIndex
183
+ const file = state.files[index]
184
+ if (!file) {
185
+ console.error(`invalid index: ${index}`)
186
+ process.exitCode = 1
187
+ return
188
+ }
189
+ file.status = "approved"
190
+ await writeReviewState(state)
191
+ if (state.sessionId) {
192
+ await applyReviewDecision(state.sessionId, {
193
+ file: file.path,
194
+ status: "approved",
195
+ riskScore: file.riskScore
196
+ }).catch(() => {})
197
+ } else {
198
+ console.log("warning: no bound session id; decision not persisted to session history")
199
+ }
200
+ console.log(`approved: ${file.path}`)
201
+ })
202
+
203
+ cmd
204
+ .command("reject")
205
+ .description("reject current or selected review file")
206
+ .requiredOption("--reason <reason>", "reject reason")
207
+ .option("--index <n>", "file index, zero-based")
208
+ .action(async (options) => {
209
+ const state = await readReviewState()
210
+ if (!state.files.length) {
211
+ console.error("review state empty. Run `kkcode review open` first.")
212
+ process.exitCode = 1
213
+ return
214
+ }
215
+ const index = options.index !== undefined ? Math.max(0, Number(options.index)) : state.currentIndex
216
+ const file = state.files[index]
217
+ if (!file) {
218
+ console.error(`invalid index: ${index}`)
219
+ process.exitCode = 1
220
+ return
221
+ }
222
+ file.status = "rejected"
223
+ file.rejectReason = options.reason
224
+ await writeReviewState(state)
225
+ await enqueueRejection(
226
+ {
227
+ file: file.path,
228
+ reason: options.reason,
229
+ riskScore: file.riskScore
230
+ },
231
+ process.cwd()
232
+ )
233
+ if (state.sessionId) {
234
+ await applyReviewDecision(state.sessionId, {
235
+ file: file.path,
236
+ status: "rejected",
237
+ reason: options.reason,
238
+ riskScore: file.riskScore
239
+ }).catch(() => {})
240
+ } else {
241
+ console.log("warning: no bound session id; decision not persisted to session history")
242
+ }
243
+ console.log(`rejected: ${file.path}`)
244
+ console.log(`reason: ${options.reason}`)
245
+ })
246
+
247
+ cmd
248
+ .command("feedback")
249
+ .description("show or clear queued rejection feedback")
250
+ .option("--clear", "clear all queued feedback", false)
251
+ .action(async (options) => {
252
+ if (options.clear) {
253
+ await clearRejections(process.cwd())
254
+ console.log("rejection feedback queue cleared")
255
+ return
256
+ }
257
+ const list = await listRejections(process.cwd())
258
+ if (!list.length) {
259
+ console.log("no rejection feedback found")
260
+ return
261
+ }
262
+ console.log(JSON.stringify(list, null, 2))
263
+ })
264
+
265
+ return cmd
266
+ }
@@ -0,0 +1,34 @@
1
+ import { Command } from "commander"
2
+ import { loadRuleBlocks, renderRulesPrompt } from "../rules/load-rules.mjs"
3
+
4
+ export function createRuleCommand() {
5
+ const cmd = new Command("rule").description("inspect global/project rule prompts")
6
+
7
+ cmd
8
+ .command("list")
9
+ .description("list loaded rule files")
10
+ .action(async () => {
11
+ const blocks = await loadRuleBlocks(process.cwd())
12
+ if (!blocks.length) {
13
+ console.log("no rules found")
14
+ return
15
+ }
16
+ for (const block of blocks) {
17
+ console.log(`- [${block.scope}] ${block.file}`)
18
+ }
19
+ })
20
+
21
+ cmd
22
+ .command("show")
23
+ .description("show merged rules prompt")
24
+ .action(async () => {
25
+ const text = await renderRulesPrompt(process.cwd())
26
+ if (!text) {
27
+ console.log("no rules found")
28
+ return
29
+ }
30
+ console.log(text)
31
+ })
32
+
33
+ return cmd
34
+ }