@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,357 @@
1
+ import { homedir } from "node:os"
2
+ import { paint } from "../theme/color.mjs"
3
+
4
+ function stripAnsi(text) {
5
+ return String(text || "").replace(/\x1B\[[0-9;]*m/g, "")
6
+ }
7
+
8
+ function isFullWidthCodePoint(code) {
9
+ if (Number.isNaN(code)) return false
10
+ if (
11
+ code >= 0x1100 && (
12
+ code <= 0x115f ||
13
+ code === 0x2329 || code === 0x232a ||
14
+ (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
15
+ (code >= 0xac00 && code <= 0xd7a3) ||
16
+ (code >= 0xf900 && code <= 0xfaff) ||
17
+ (code >= 0xfe10 && code <= 0xfe19) ||
18
+ (code >= 0xfe30 && code <= 0xfe6f) ||
19
+ (code >= 0xff00 && code <= 0xff60) ||
20
+ (code >= 0xffe0 && code <= 0xffe6) ||
21
+ (code >= 0x1f300 && code <= 0x1f64f) ||
22
+ (code >= 0x1f900 && code <= 0x1f9ff) ||
23
+ (code >= 0x20000 && code <= 0x3fffd)
24
+ )
25
+ ) return true
26
+ return false
27
+ }
28
+
29
+ function visibleWidth(text) {
30
+ let width = 0
31
+ for (const ch of stripAnsi(text)) {
32
+ width += isFullWidthCodePoint(ch.codePointAt(0)) ? 2 : 1
33
+ }
34
+ return width
35
+ }
36
+
37
+ function clipPlainByWidth(text, maxWidth) {
38
+ if (maxWidth <= 0) return ""
39
+ let out = ""
40
+ let used = 0
41
+ for (const ch of String(text || "")) {
42
+ const w = isFullWidthCodePoint(ch.codePointAt(0)) ? 2 : 1
43
+ if (used + w > maxWidth) break
44
+ out += ch
45
+ used += w
46
+ }
47
+ return out
48
+ }
49
+
50
+ function padCell(text, width) {
51
+ const raw = stripAnsi(text)
52
+ const w = visibleWidth(raw)
53
+ if (w === width) return text
54
+ if (w < width) return text + " ".repeat(width - w)
55
+ if (width <= 1) return clipPlainByWidth(raw, width)
56
+ return clipPlainByWidth(raw, Math.max(1, width - 1)) + "…"
57
+ }
58
+
59
+ function wrapPlain(text, width) {
60
+ if (width <= 4) return [clipPlainByWidth(String(text || ""), Math.max(width, 1))]
61
+ const words = String(text || "").split(/\s+/).filter(Boolean)
62
+ if (!words.length) return [""]
63
+ const out = []
64
+ let line = ""
65
+ for (const word of words) {
66
+ const candidate = line ? `${line} ${word}` : word
67
+ if (visibleWidth(candidate) <= width) {
68
+ line = candidate
69
+ continue
70
+ }
71
+ if (line) out.push(line)
72
+ if (visibleWidth(word) <= width) {
73
+ line = word
74
+ continue
75
+ }
76
+ let rest = word
77
+ while (visibleWidth(rest) > width) {
78
+ const part = clipPlainByWidth(rest, width)
79
+ out.push(part)
80
+ rest = rest.slice(part.length)
81
+ }
82
+ line = rest || ""
83
+ }
84
+ if (line) out.push(line)
85
+ return out.length ? out : [""]
86
+ }
87
+
88
+ function terminalWidth() {
89
+ const cols = Number(process.stdout.columns || 120)
90
+ if (!Number.isFinite(cols) || cols <= 0) return 120
91
+ return Math.max(60, Math.min(cols, 220))
92
+ }
93
+
94
+ function centerLine(text, width) {
95
+ const rawWidth = visibleWidth(text)
96
+ if (rawWidth >= width) return text
97
+ const pad = Math.floor((width - rawWidth) / 2)
98
+ return `${" ".repeat(Math.max(0, pad))}${text}`
99
+ }
100
+
101
+ function shortenPath(path) {
102
+ const home = homedir()
103
+ if (!path) return ""
104
+ const replaced = path.startsWith(home) ? `~${path.slice(home.length)}` : path
105
+ if (replaced.length <= 72) return replaced
106
+ return `...${replaced.slice(-69)}`
107
+ }
108
+
109
+ function renderTag(theme, label, fg = "#0b0b0b", bg = theme.base.accent) {
110
+ return paint(` ${label} `, fg, { bg, bold: true })
111
+ }
112
+
113
+ function ageLabel(ms) {
114
+ const mins = Math.round(ms / 60000)
115
+ if (mins < 1) return "just now"
116
+ if (mins < 60) return `${mins}m ago`
117
+ const hours = Math.round(mins / 60)
118
+ if (hours < 24) return `${hours}h ago`
119
+ return `${Math.round(hours / 24)}d ago`
120
+ }
121
+
122
+ function flattenSections(sections, width) {
123
+ const lines = []
124
+ for (const [index, section] of sections.entries()) {
125
+ lines.push(paint(section.title, section.color, { bold: true }))
126
+ for (const item of section.items) {
127
+ if (!item) {
128
+ lines.push("")
129
+ continue
130
+ }
131
+ const wrapped = wrapPlain(item, width)
132
+ for (const line of wrapped) lines.push(line)
133
+ }
134
+ if (index !== sections.length - 1) lines.push("")
135
+ }
136
+ return lines
137
+ }
138
+
139
+ function frameLine(content, width, borderColor) {
140
+ const inner = width - 4
141
+ const padded = padCell(content, inner)
142
+ return paint(`| ${padded} |`, borderColor)
143
+ }
144
+
145
+ function drawSingleColumn({ width, theme, sections }) {
146
+ const border = paint(`+${"-".repeat(width - 2)}+`, theme.base.border)
147
+ const out = [border]
148
+ const cellWidth = width - 4
149
+ const lines = flattenSections(sections, cellWidth)
150
+ for (const line of lines) out.push(frameLine(line, width, theme.base.border))
151
+ out.push(border)
152
+ return out
153
+ }
154
+
155
+ function drawDoubleColumn({ width, theme, leftSections, rightSections }) {
156
+ const border = paint(`+${"-".repeat(width - 2)}+`, theme.base.border)
157
+ const out = [border]
158
+ const inner = width - 4
159
+ const gap = 3
160
+ const leftWidth = Math.floor((inner - gap) * 0.5)
161
+ const rightWidth = inner - gap - leftWidth
162
+
163
+ const leftLines = flattenSections(leftSections, leftWidth)
164
+ const rightLines = flattenSections(rightSections, rightWidth)
165
+ const rows = Math.max(leftLines.length, rightLines.length)
166
+
167
+ for (let i = 0; i < rows; i++) {
168
+ const left = padCell(leftLines[i] || "", leftWidth)
169
+ const right = padCell(rightLines[i] || "", rightWidth)
170
+ out.push(paint(`| ${left} | ${right} |`, theme.base.border))
171
+ }
172
+ out.push(border)
173
+ return out
174
+ }
175
+
176
+ export function renderReplDashboard({
177
+ theme,
178
+ state,
179
+ providers,
180
+ recentSessions,
181
+ customCommandCount,
182
+ cwd,
183
+ columns = null
184
+ }) {
185
+ const width = Number.isFinite(columns) ? Math.max(60, Math.min(Number(columns), 220)) : terminalWidth()
186
+ const title = `${renderTag(theme, "KKCODE", "#111111", theme.semantic.info)} ${paint("Interactive Coding CLI", theme.base.fg, { bold: true })}`
187
+ const subtitle = paint("Adaptive dashboard + richer command palette", theme.base.muted)
188
+
189
+ const recentLines = recentSessions.length
190
+ ? recentSessions.slice(0, 6).map((s) => `${s.id.slice(0, 12)} ${s.mode} ${ageLabel(Date.now() - s.updatedAt)}`)
191
+ : ["(no session yet)"]
192
+
193
+ const leftSections = [
194
+ {
195
+ title: "Workspace",
196
+ color: theme.semantic.info,
197
+ items: [shortenPath(cwd)]
198
+ },
199
+ {
200
+ title: "Runtime",
201
+ color: theme.semantic.success,
202
+ items: [
203
+ `Session: ${state.sessionId}`,
204
+ `Mode: ${state.mode}`,
205
+ `Provider: ${state.providerType}`,
206
+ `Model: ${state.model}`,
207
+ `Custom commands: ${customCommandCount}`
208
+ ]
209
+ },
210
+ {
211
+ title: "Quick Aliases",
212
+ color: theme.modes.agent,
213
+ items: [
214
+ "/h /? help",
215
+ "/n new session",
216
+ "/r resume latest",
217
+ "/m switch mode",
218
+ "/p switch provider",
219
+ "/k shortcuts"
220
+ ]
221
+ }
222
+ ]
223
+
224
+ const rightSections = [
225
+ {
226
+ title: "Tips",
227
+ color: theme.semantic.warn,
228
+ items: [
229
+ "Use /dash to redraw this panel",
230
+ "Use /clear to clear screen",
231
+ "Use /model <id> to override model",
232
+ "Use \"\"\" for multi-line prompts"
233
+ ]
234
+ },
235
+ {
236
+ title: "Recent Activity",
237
+ color: theme.modes.plan,
238
+ items: recentLines
239
+ },
240
+ {
241
+ title: "Providers",
242
+ color: theme.modes.ask,
243
+ items: [providers.length ? providers.join(" | ") : "(none configured)"]
244
+ },
245
+ {
246
+ title: "Useful Commands",
247
+ color: theme.modes.longagent,
248
+ items: [
249
+ "/history /resume /commands /reload",
250
+ "/ask /plan /agent /longagent"
251
+ ]
252
+ }
253
+ ]
254
+
255
+ const lines = [
256
+ title,
257
+ subtitle,
258
+ ""
259
+ ]
260
+
261
+ const useSingle = width < 110
262
+ const panel = useSingle
263
+ ? drawSingleColumn({ width, theme, sections: [...leftSections, ...rightSections] })
264
+ : drawDoubleColumn({ width, theme, leftSections, rightSections })
265
+
266
+ lines.push(...panel)
267
+ return lines.join("\n")
268
+ }
269
+
270
+ export function renderReplLogo({ theme, columns = null }) {
271
+ const width = Number.isFinite(columns) ? Math.max(60, Math.min(Number(columns), 220)) : terminalWidth()
272
+ const rawLogo = [
273
+ "██╗ ██╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗",
274
+ "██║ ██╔╝ ██║ ██╔╝ ██╔════╝ ██╔═══██╗ ██╔══██╗ ██╔════╝",
275
+ "█████╔╝ █████╔╝ ██║ ██║ ██║ ██║ ██║ █████╗ ",
276
+ "██╔═██╗ ██╔═██╗ ██║ ██║ ██║ ██║ ██║ ██╔══╝ ",
277
+ "██║ ██╗ ██║ ██╗ ╚██████╗ ╚██████╔╝ ██████╔╝ ███████╗",
278
+ "╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝"
279
+ ]
280
+ const wave = [
281
+ "#4af5f0", "#3de8f5", "#30dbfa", "#38c8ff", "#40b5ff",
282
+ "#58a0ff", "#708bff", "#8876ff", "#a061ff", "#b84cff",
283
+ "#d037ff", "#e828f0", "#f034d0", "#f040b0", "#f04c90",
284
+ "#f040b0", "#f034d0", "#e828f0", "#d037ff", "#b84cff",
285
+ "#a061ff", "#8876ff", "#708bff", "#58a0ff", "#40b5ff",
286
+ "#38c8ff", "#30dbfa", "#3de8f5"
287
+ ]
288
+ const coreLines = rawLogo.map((line, row) => {
289
+ let out = ""
290
+ for (let col = 0; col < line.length; col++) {
291
+ const ch = line[col]
292
+ if (ch === " ") { out += " "; continue }
293
+ const waveIdx = (col + row * 3) % wave.length
294
+ out += paint(ch, wave[waveIdx], { bold: true })
295
+ }
296
+ return out
297
+ })
298
+ coreLines.push(paint("AI Coding Agent", theme.base.fg, { bold: true }))
299
+ coreLines.push(paint("Type /status to open Workspace & Runtime panel", theme.base.muted))
300
+
301
+ const mascotRaw = [
302
+ " /\\ ",
303
+ " /__\\ ",
304
+ " /|[]|\\ ",
305
+ " /_|__|_\\ ",
306
+ " /||\\ ",
307
+ " /_||_\\ ",
308
+ " /\\ "
309
+ ]
310
+ const mascotPalette = [
311
+ "#6ec1ff",
312
+ "#52b7ff",
313
+ "#36d8d3",
314
+ "#3fd487",
315
+ "#f1c55b",
316
+ "#f39b52",
317
+ "#ff7f6e"
318
+ ]
319
+ const mascotLines = mascotRaw.map((line, idx) => paint(line, mascotPalette[idx % mascotPalette.length], { bold: true }))
320
+
321
+ // Narrow terminals: keep pure centered logo, avoid cramped side art.
322
+ if (width < 96) {
323
+ const lines = coreLines.map((line) => centerLine(line, width))
324
+ return lines.join("\n")
325
+ }
326
+
327
+ // Three-column layout keeps KKCODE visually centered in the full terminal:
328
+ // [mascot] [center logo block] [symmetric spacer]
329
+ const mascotWidth = Math.max(...mascotRaw.map((line) => visibleWidth(line)))
330
+ const sideWidth = mascotWidth + 2
331
+ const centerWidth = Math.max(24, width - (sideWidth * 2))
332
+
333
+ const rows = Math.max(coreLines.length, mascotLines.length)
334
+ const mascotTopPad = Math.max(0, Math.floor((rows - mascotLines.length) / 2))
335
+ const logoTopPad = Math.max(0, Math.floor((rows - coreLines.length) / 2))
336
+ const lines = []
337
+
338
+ for (let i = 0; i < rows; i++) {
339
+ const mascotIdx = i - mascotTopPad
340
+ const logoIdx = i - logoTopPad
341
+ const left = mascotIdx >= 0 && mascotIdx < mascotLines.length ? mascotLines[mascotIdx] : ""
342
+ const mid = logoIdx >= 0 && logoIdx < coreLines.length ? centerLine(coreLines[logoIdx], centerWidth) : ""
343
+ const leftCell = padCell(left, sideWidth)
344
+ const midCell = padCell(mid, centerWidth)
345
+ const rightCell = " ".repeat(sideWidth)
346
+ lines.push(`${leftCell}${midCell}${rightCell}`)
347
+ }
348
+
349
+ return lines.join("\n")
350
+ }
351
+
352
+ export function renderStartupHint(recentSessions = []) {
353
+ if (!recentSessions.length) return ""
354
+ const last = recentSessions[0]
355
+ const age = ageLabel(Date.now() - last.updatedAt)
356
+ return `last session: ${last.id} (${last.mode}, ${age})\n quick resume: /r ${last.id.slice(0, 12)}`
357
+ }
@@ -0,0 +1,121 @@
1
+ import path from "node:path"
2
+ import { access, readFile } from "node:fs/promises"
3
+ import YAML from "yaml"
4
+
5
+ const DEFAULT_PRICING = {
6
+ currency: "USD",
7
+ per_tokens: 1000000,
8
+ models: {
9
+ "claude-opus-4-6": { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
10
+ "claude-opus-4-5": { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
11
+ "claude-opus-4-1": { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 },
12
+ "claude-opus-4": { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 },
13
+ "claude-sonnet-4-6": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
14
+ "claude-sonnet-4-5": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
15
+ "claude-sonnet-4": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
16
+ "claude-haiku-4-5": { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 },
17
+ "claude-haiku-3-5": { input: 0.8, output: 4, cache_read: 0.08, cache_write: 1 },
18
+ "gpt-5.3-codex": { input: 15, output: 60, cache_read: 7.5, cache_write: 15 },
19
+ "gpt-4o": { input: 2.5, output: 10, cache_read: 1.25, cache_write: 2.5 },
20
+ "gpt-4o-mini": { input: 0.15, output: 0.6, cache_read: 0.075, cache_write: 0.15 },
21
+ "deepseek-chat": { input: 0.27, output: 1.1, cache_read: 0.07, cache_write: 0.27 },
22
+ "deepseek-coder": { input: 0.27, output: 1.1, cache_read: 0.07, cache_write: 0.27 },
23
+ "deepseek-v3.1-terminus": { input: 0.55, output: 1.65, cache_read: 0.07, cache_write: 0.55 },
24
+ "deepseek-v3.2": { input: 0.28, output: 0.41, cache_read: 0.03, cache_write: 0.28 },
25
+ "kimi-k2.5": { input: 0.55, output: 2.9, cache_read: 0, cache_write: 0 },
26
+ "kimi-k2-0905": { input: 0.55, output: 2.2, cache_read: 0.14, cache_write: 0 },
27
+ "qwen-plus": { input: 0.11, output: 1.11, cache_read: 0.02, cache_write: 0 },
28
+ "qwen-max": { input: 0.33, output: 1.33, cache_read: 0.07, cache_write: 0 },
29
+ "qwen-turbo": { input: 0.04, output: 0.08, cache_read: 0.01, cache_write: 0 },
30
+ "qwen3-coder-plus": { input: 0.55, output: 2.2, cache_read: 0.11, cache_write: 0 },
31
+ "qwen3-coder-flash": { input: 0.14, output: 0.55, cache_read: 0.03, cache_write: 0 },
32
+ "qwen3-coder-480b-a35b": { input: 0.83, output: 3.3, cache_read: 0.17, cache_write: 0 },
33
+ "doubao-seed-2.0-code": { input: 0.44, output: 2.2, cache_read: 0, cache_write: 0 },
34
+ "doubao-seed-2.0-pro": { input: 0.44, output: 2.2, cache_read: 0, cache_write: 0 },
35
+ "doubao-seed-1.8": { input: 0.11, output: 1.1, cache_read: 0, cache_write: 0 },
36
+ "doubao-seed-code": { input: 0.17, output: 1.1, cache_read: 0, cache_write: 0 },
37
+ "minimax-m2.5": { input: 0.29, output: 1.16, cache_read: 0.03, cache_write: 0 },
38
+ "minimax-m2.5-highspeed": { input: 0.58, output: 2.32, cache_read: 0.03, cache_write: 0 },
39
+ "minimax-m2.1": { input: 0.29, output: 1.16, cache_read: 0.03, cache_write: 0 },
40
+ "minimax-m2": { input: 0.29, output: 1.16, cache_read: 0.03, cache_write: 0 },
41
+ "glm-5": { input: 0.55, output: 3.05, cache_read: 0, cache_write: 0 },
42
+ "glm-4.7": { input: 0.41, output: 1.93, cache_read: 0, cache_write: 0 },
43
+ "glm-4.6": { input: 0.28, output: 1.1, cache_read: 0, cache_write: 0 }
44
+ },
45
+ default: {
46
+ input: 3,
47
+ output: 15,
48
+ cache_read: 0.3,
49
+ cache_write: 3.75
50
+ }
51
+ }
52
+
53
+ async function exists(file) {
54
+ try {
55
+ await access(file)
56
+ return true
57
+ } catch {
58
+ return false
59
+ }
60
+ }
61
+
62
+ function parse(file, raw) {
63
+ if (file.endsWith(".json")) return JSON.parse(raw)
64
+ return YAML.parse(raw)
65
+ }
66
+
67
+ function resolvePricingPath(configState) {
68
+ const projectPath = configState.source.projectRaw?.usage?.pricing_file
69
+ if (typeof projectPath === "string" && projectPath.trim()) {
70
+ return path.resolve(configState.source.projectDir ?? process.cwd(), projectPath)
71
+ }
72
+ const userPath = configState.source.userRaw?.usage?.pricing_file
73
+ if (typeof userPath === "string" && userPath.trim()) {
74
+ return path.resolve(configState.source.userDir ?? process.cwd(), userPath)
75
+ }
76
+ return null
77
+ }
78
+
79
+ export async function loadPricing(configState) {
80
+ const file = resolvePricingPath(configState)
81
+ if (!file || !(await exists(file))) {
82
+ return { pricing: DEFAULT_PRICING, source: "default", errors: [] }
83
+ }
84
+ try {
85
+ const raw = await readFile(file, "utf8")
86
+ const parsed = parse(file, raw)
87
+ const pricing = {
88
+ ...DEFAULT_PRICING,
89
+ ...parsed,
90
+ default: { ...DEFAULT_PRICING.default, ...(parsed.default ?? {}) }
91
+ }
92
+ return { pricing, source: file, errors: [] }
93
+ } catch (error) {
94
+ return { pricing: DEFAULT_PRICING, source: "default", errors: [`${file}: ${error.message}`] }
95
+ }
96
+ }
97
+
98
+ function findPricingEntry(models, model) {
99
+ if (models[model]) return models[model]
100
+ // Fuzzy: try prefix match (e.g. "claude-opus-4-6-20250601" → "claude-opus-4-6")
101
+ const m = String(model).toLowerCase()
102
+ for (const key of Object.keys(models)) {
103
+ if (m.startsWith(key)) return models[key]
104
+ }
105
+ return null
106
+ }
107
+
108
+ export function calculateCost(pricing, model, usage) {
109
+ const entry = findPricingEntry(pricing.models, model) ?? pricing.default
110
+ const per = pricing.per_tokens || 1000000
111
+ // All providers normalize input to non-cached tokens only (see provider/*.mjs)
112
+ const amount =
113
+ ((usage.input || 0) * (entry.input || 0) +
114
+ (usage.output || 0) * (entry.output || 0) +
115
+ (usage.cacheRead || 0) * (entry.cache_read || 0) +
116
+ (usage.cacheWrite || 0) * (entry.cache_write || 0)) /
117
+ per
118
+ const savings = ((usage.cacheRead || 0) * ((entry.input || 0) - (entry.cache_read || 0))) / per
119
+ const unknown = !findPricingEntry(pricing.models, model)
120
+ return { amount, savings, unknown, currency: pricing.currency }
121
+ }
@@ -0,0 +1,113 @@
1
+ import { ensureUserRoot, usageStorePath } from "../storage/paths.mjs"
2
+ import { readJson, writeJson } from "../storage/json-store.mjs"
3
+
4
+ export function emptyUsage() {
5
+ return {
6
+ input: 0,
7
+ output: 0,
8
+ cacheRead: 0,
9
+ cacheWrite: 0,
10
+ cost: 0,
11
+ turns: 0
12
+ }
13
+ }
14
+
15
+ function addUsage(target, delta, cost) {
16
+ target.input += delta.input || 0
17
+ target.output += delta.output || 0
18
+ target.cacheRead += delta.cacheRead || 0
19
+ target.cacheWrite += delta.cacheWrite || 0
20
+ target.cost += cost || 0
21
+ target.turns += 1
22
+ }
23
+
24
+ function todayKey() {
25
+ return new Date().toISOString().slice(0, 10) // "YYYY-MM-DD"
26
+ }
27
+
28
+ function defaultStore() {
29
+ return {
30
+ updatedAt: Date.now(),
31
+ globalDay: todayKey(),
32
+ global: emptyUsage(),
33
+ sessions: {}
34
+ }
35
+ }
36
+
37
+ function maybeRotateGlobal(store) {
38
+ const today = todayKey()
39
+ if (store.globalDay && store.globalDay !== today) {
40
+ store.global = emptyUsage()
41
+ store.globalDay = today
42
+ }
43
+ if (!store.globalDay) store.globalDay = today
44
+ }
45
+
46
+ export async function readUsageStore() {
47
+ await ensureUserRoot()
48
+ return readJson(usageStorePath(), defaultStore())
49
+ }
50
+
51
+ async function persist(store) {
52
+ store.updatedAt = Date.now()
53
+ await writeJson(usageStorePath(), store)
54
+ }
55
+
56
+ export async function recordTurn({ sessionId, usage, cost }) {
57
+ const store = await readUsageStore()
58
+ maybeRotateGlobal(store)
59
+ if (!store.sessions[sessionId]) store.sessions[sessionId] = emptyUsage()
60
+ addUsage(store.sessions[sessionId], usage, cost)
61
+ addUsage(store.global, usage, cost)
62
+ await persist(store)
63
+ return {
64
+ turn: {
65
+ input: usage.input || 0,
66
+ output: usage.output || 0,
67
+ cacheRead: usage.cacheRead || 0,
68
+ cacheWrite: usage.cacheWrite || 0,
69
+ cost: cost || 0,
70
+ turns: 1
71
+ },
72
+ session: store.sessions[sessionId],
73
+ global: store.global
74
+ }
75
+ }
76
+
77
+ export async function resetUsage(sessionId = null) {
78
+ if (!sessionId) {
79
+ await persist(defaultStore())
80
+ return
81
+ }
82
+ const store = await readUsageStore()
83
+ delete store.sessions[sessionId]
84
+ store.global = emptyUsage()
85
+ for (const session of Object.values(store.sessions)) {
86
+ store.global.input += session.input
87
+ store.global.output += session.output
88
+ store.global.cacheRead += session.cacheRead
89
+ store.global.cacheWrite += session.cacheWrite
90
+ store.global.cost += session.cost
91
+ store.global.turns += session.turns
92
+ }
93
+ await persist(store)
94
+ }
95
+
96
+ export async function exportUsageCsv() {
97
+ const store = await readUsageStore()
98
+ const rows = [["scope", "sessionId", "input", "output", "cacheRead", "cacheWrite", "cost", "turns"]]
99
+ rows.push([
100
+ "global",
101
+ "",
102
+ store.global.input,
103
+ store.global.output,
104
+ store.global.cacheRead,
105
+ store.global.cacheWrite,
106
+ store.global.cost,
107
+ store.global.turns
108
+ ])
109
+ for (const [sessionId, usage] of Object.entries(store.sessions)) {
110
+ rows.push(["session", sessionId, usage.input, usage.output, usage.cacheRead, usage.cacheWrite, usage.cost, usage.turns])
111
+ }
112
+ return rows.map((row) => row.join(",")).join("\n") + "\n"
113
+ }