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