@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.
- package/LICENSE +674 -0
- package/README.md +445 -0
- package/package.json +46 -0
- package/src/agent/agent.mjs +170 -0
- package/src/agent/custom-agent-loader.mjs +158 -0
- package/src/agent/generator.mjs +115 -0
- package/src/agent/prompt/architect.txt +36 -0
- package/src/agent/prompt/build-fixer.txt +71 -0
- package/src/agent/prompt/build.txt +101 -0
- package/src/agent/prompt/compaction.txt +12 -0
- package/src/agent/prompt/explore.txt +29 -0
- package/src/agent/prompt/guide.txt +40 -0
- package/src/agent/prompt/longagent.txt +178 -0
- package/src/agent/prompt/plan.txt +50 -0
- package/src/agent/prompt/researcher.txt +23 -0
- package/src/agent/prompt/reviewer.txt +44 -0
- package/src/agent/prompt/security-reviewer.txt +62 -0
- package/src/agent/prompt/tdd-guide.txt +84 -0
- package/src/agent/prompt/title.txt +8 -0
- package/src/command/custom-commands.mjs +57 -0
- package/src/commands/agent.mjs +71 -0
- package/src/commands/audit.mjs +77 -0
- package/src/commands/background.mjs +86 -0
- package/src/commands/chat.mjs +114 -0
- package/src/commands/command.mjs +41 -0
- package/src/commands/config.mjs +44 -0
- package/src/commands/doctor.mjs +148 -0
- package/src/commands/hook.mjs +29 -0
- package/src/commands/init.mjs +141 -0
- package/src/commands/longagent.mjs +100 -0
- package/src/commands/mcp.mjs +89 -0
- package/src/commands/permission.mjs +36 -0
- package/src/commands/prompt.mjs +42 -0
- package/src/commands/review.mjs +266 -0
- package/src/commands/rule.mjs +34 -0
- package/src/commands/session.mjs +235 -0
- package/src/commands/theme.mjs +98 -0
- package/src/commands/usage.mjs +91 -0
- package/src/config/defaults.mjs +195 -0
- package/src/config/import-config.mjs +76 -0
- package/src/config/load-config.mjs +76 -0
- package/src/config/schema.mjs +509 -0
- package/src/context.mjs +40 -0
- package/src/core/constants.mjs +46 -0
- package/src/core/errors.mjs +57 -0
- package/src/core/events.mjs +29 -0
- package/src/core/types.mjs +57 -0
- package/src/github/api.mjs +78 -0
- package/src/github/auth.mjs +286 -0
- package/src/github/flow.mjs +298 -0
- package/src/github/workspace.mjs +212 -0
- package/src/index.mjs +82 -0
- package/src/knowledge/api-design.txt +9 -0
- package/src/knowledge/cpp.txt +10 -0
- package/src/knowledge/docker.txt +10 -0
- package/src/knowledge/dotnet.txt +9 -0
- package/src/knowledge/electron.txt +10 -0
- package/src/knowledge/flutter.txt +10 -0
- package/src/knowledge/go.txt +9 -0
- package/src/knowledge/graphql.txt +10 -0
- package/src/knowledge/java.txt +9 -0
- package/src/knowledge/kotlin.txt +10 -0
- package/src/knowledge/loader.mjs +125 -0
- package/src/knowledge/next.txt +8 -0
- package/src/knowledge/node.txt +8 -0
- package/src/knowledge/nuxt.txt +9 -0
- package/src/knowledge/php.txt +10 -0
- package/src/knowledge/python.txt +10 -0
- package/src/knowledge/react-native.txt +10 -0
- package/src/knowledge/react.txt +9 -0
- package/src/knowledge/ruby.txt +11 -0
- package/src/knowledge/rust.txt +9 -0
- package/src/knowledge/svelte.txt +9 -0
- package/src/knowledge/swift.txt +10 -0
- package/src/knowledge/tailwind.txt +10 -0
- package/src/knowledge/testing.txt +8 -0
- package/src/knowledge/typescript.txt +8 -0
- package/src/knowledge/vue.txt +9 -0
- package/src/mcp/client-http.mjs +157 -0
- package/src/mcp/client-sse.mjs +286 -0
- package/src/mcp/client-stdio.mjs +451 -0
- package/src/mcp/registry.mjs +394 -0
- package/src/mcp/stdio-framing.mjs +127 -0
- package/src/orchestration/background-manager.mjs +358 -0
- package/src/orchestration/background-worker.mjs +245 -0
- package/src/orchestration/longagent-manager.mjs +116 -0
- package/src/orchestration/stage-scheduler.mjs +489 -0
- package/src/orchestration/subagent-router.mjs +62 -0
- package/src/orchestration/task-scheduler.mjs +74 -0
- package/src/permission/engine.mjs +92 -0
- package/src/permission/exec-policy.mjs +372 -0
- package/src/permission/prompt.mjs +39 -0
- package/src/permission/rules.mjs +120 -0
- package/src/permission/workspace-trust.mjs +44 -0
- package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
- package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
- package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
- package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
- package/src/plugin/hook-bus.mjs +154 -0
- package/src/provider/anthropic.mjs +389 -0
- package/src/provider/ollama.mjs +236 -0
- package/src/provider/openai-compatible.mjs +1 -0
- package/src/provider/openai.mjs +339 -0
- package/src/provider/retry-policy.mjs +68 -0
- package/src/provider/router.mjs +228 -0
- package/src/provider/sse.mjs +91 -0
- package/src/repl.mjs +2929 -0
- package/src/review/diff-parser.mjs +36 -0
- package/src/review/rejection-queue.mjs +62 -0
- package/src/review/review-store.mjs +21 -0
- package/src/review/risk-score.mjs +61 -0
- package/src/rules/load-rules.mjs +64 -0
- package/src/runtime.mjs +1 -0
- package/src/session/checkpoint.mjs +239 -0
- package/src/session/compaction.mjs +276 -0
- package/src/session/engine.mjs +225 -0
- package/src/session/instinct-manager.mjs +172 -0
- package/src/session/instruction-loader.mjs +25 -0
- package/src/session/longagent-plan.mjs +329 -0
- package/src/session/longagent-scaffold.mjs +100 -0
- package/src/session/longagent.mjs +1462 -0
- package/src/session/loop.mjs +905 -0
- package/src/session/memory-loader.mjs +75 -0
- package/src/session/project-context.mjs +367 -0
- package/src/session/prompt/anthropic.txt +151 -0
- package/src/session/prompt/beast.txt +37 -0
- package/src/session/prompt/max-steps.txt +6 -0
- package/src/session/prompt/plan.txt +9 -0
- package/src/session/prompt/qwen.txt +46 -0
- package/src/session/prompt-loader.mjs +18 -0
- package/src/session/recovery.mjs +52 -0
- package/src/session/store.mjs +503 -0
- package/src/session/system-prompt.mjs +260 -0
- package/src/session/task-validator.mjs +266 -0
- package/src/session/usability-gates.mjs +379 -0
- package/src/skill/builtin/backend-patterns.mjs +123 -0
- package/src/skill/builtin/commit.mjs +64 -0
- package/src/skill/builtin/debug.mjs +45 -0
- package/src/skill/builtin/frontend-patterns.mjs +120 -0
- package/src/skill/builtin/frontend.mjs +188 -0
- package/src/skill/builtin/init.mjs +220 -0
- package/src/skill/builtin/review.mjs +49 -0
- package/src/skill/builtin/security-checklist.mjs +80 -0
- package/src/skill/builtin/tdd.mjs +54 -0
- package/src/skill/generator.mjs +113 -0
- package/src/skill/registry.mjs +336 -0
- package/src/storage/audit-store.mjs +83 -0
- package/src/storage/event-log.mjs +82 -0
- package/src/storage/ghost-commit-store.mjs +235 -0
- package/src/storage/json-store.mjs +53 -0
- package/src/storage/paths.mjs +148 -0
- package/src/theme/color.mjs +64 -0
- package/src/theme/default-theme.mjs +29 -0
- package/src/theme/load-theme.mjs +71 -0
- package/src/theme/markdown.mjs +135 -0
- package/src/theme/schema.mjs +45 -0
- package/src/theme/status-bar.mjs +158 -0
- package/src/tool/audit-wrapper.mjs +38 -0
- package/src/tool/edit-transaction.mjs +126 -0
- package/src/tool/executor.mjs +109 -0
- package/src/tool/file-lock-manager.mjs +85 -0
- package/src/tool/git-auto.mjs +545 -0
- package/src/tool/git-full-auto.mjs +478 -0
- package/src/tool/image-util.mjs +276 -0
- package/src/tool/prompt/background_cancel.txt +1 -0
- package/src/tool/prompt/background_output.txt +1 -0
- package/src/tool/prompt/bash.txt +71 -0
- package/src/tool/prompt/codesearch.txt +18 -0
- package/src/tool/prompt/edit.txt +27 -0
- package/src/tool/prompt/enter_plan.txt +74 -0
- package/src/tool/prompt/exit_plan.txt +62 -0
- package/src/tool/prompt/glob.txt +33 -0
- package/src/tool/prompt/grep.txt +43 -0
- package/src/tool/prompt/list.txt +8 -0
- package/src/tool/prompt/multiedit.txt +20 -0
- package/src/tool/prompt/notebookedit.txt +21 -0
- package/src/tool/prompt/patch.txt +24 -0
- package/src/tool/prompt/question.txt +44 -0
- package/src/tool/prompt/read.txt +40 -0
- package/src/tool/prompt/task.txt +83 -0
- package/src/tool/prompt/todowrite.txt +117 -0
- package/src/tool/prompt/webfetch.txt +38 -0
- package/src/tool/prompt/websearch.txt +43 -0
- package/src/tool/prompt/write.txt +38 -0
- package/src/tool/prompt-loader.mjs +18 -0
- package/src/tool/question-prompt.mjs +86 -0
- package/src/tool/registry.mjs +1309 -0
- package/src/tool/task-tool.mjs +28 -0
- package/src/ui/activity-renderer.mjs +410 -0
- package/src/ui/repl-dashboard.mjs +357 -0
- package/src/usage/pricing.mjs +121 -0
- package/src/usage/usage-meter.mjs +113 -0
- package/src/util/git.mjs +496 -0
- package/src/util/template.mjs +10 -0
- 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
|
+
}
|