@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.1

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 (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -0,0 +1,184 @@
1
+ import { spawn } from "node:child_process"
2
+ import { readFile, writeFile } from "node:fs/promises"
3
+ import { dirname } from "node:path"
4
+ import { mkdir } from "node:fs/promises"
5
+ import { PACKAGE_NAME, PACKAGE_VERSION } from "../version.mjs"
6
+ import { updateStatePath } from "../storage/paths.mjs"
7
+
8
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org"
9
+ const DEFAULT_TIMEOUT_MS = 2500
10
+
11
+ function normalizeRegistry(registry = DEFAULT_REGISTRY) {
12
+ return String(registry || DEFAULT_REGISTRY).replace(/\/+$/, "")
13
+ }
14
+
15
+ function encodePackageName(name) {
16
+ return String(name).replace("/", "%2F")
17
+ }
18
+
19
+ function parseVersion(version) {
20
+ const match = String(version || "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/)
21
+ if (!match) return null
22
+ return {
23
+ major: Number(match[1]),
24
+ minor: Number(match[2]),
25
+ patch: Number(match[3]),
26
+ prerelease: match[4] ? match[4].split(".").map((part) => (/^\d+$/.test(part) ? Number(part) : part)) : []
27
+ }
28
+ }
29
+
30
+ function compareIdentifier(a, b) {
31
+ if (a === b) return 0
32
+ const aNum = typeof a === "number"
33
+ const bNum = typeof b === "number"
34
+ if (aNum && bNum) return a > b ? 1 : -1
35
+ if (aNum) return -1
36
+ if (bNum) return 1
37
+ return String(a) > String(b) ? 1 : -1
38
+ }
39
+
40
+ export function compareVersions(a, b) {
41
+ const av = parseVersion(a)
42
+ const bv = parseVersion(b)
43
+ if (!av || !bv) return String(a || "").localeCompare(String(b || ""))
44
+ for (const key of ["major", "minor", "patch"]) {
45
+ if (av[key] !== bv[key]) return av[key] > bv[key] ? 1 : -1
46
+ }
47
+ const aPre = av.prerelease
48
+ const bPre = bv.prerelease
49
+ if (!aPre.length && !bPre.length) return 0
50
+ if (!aPre.length) return 1
51
+ if (!bPre.length) return -1
52
+ const len = Math.max(aPre.length, bPre.length)
53
+ for (let i = 0; i < len; i++) {
54
+ if (aPre[i] === undefined) return -1
55
+ if (bPre[i] === undefined) return 1
56
+ const cmp = compareIdentifier(aPre[i], bPre[i])
57
+ if (cmp !== 0) return cmp
58
+ }
59
+ return 0
60
+ }
61
+
62
+ export function updateConfig(config = {}) {
63
+ return {
64
+ enabled: config.update?.enabled !== false,
65
+ notifyOnStartup: config.update?.notify_on_startup !== false,
66
+ autoInstall: Boolean(config.update?.auto_install),
67
+ channel: config.update?.channel || "latest",
68
+ checkIntervalHours: Number(config.update?.check_interval_hours ?? 12),
69
+ registry: config.update?.registry || DEFAULT_REGISTRY,
70
+ timeoutMs: Number(config.update?.timeout_ms ?? DEFAULT_TIMEOUT_MS)
71
+ }
72
+ }
73
+
74
+ async function readUpdateState(file = updateStatePath()) {
75
+ try {
76
+ return JSON.parse(await readFile(file, "utf8"))
77
+ } catch {
78
+ return {}
79
+ }
80
+ }
81
+
82
+ async function writeUpdateState(state, file = updateStatePath()) {
83
+ await mkdir(dirname(file), { recursive: true })
84
+ await writeFile(file, `${JSON.stringify(state, null, 2)}\n`)
85
+ }
86
+
87
+ export async function fetchPackageMetadata({ packageName = PACKAGE_NAME, registry = DEFAULT_REGISTRY, timeoutMs = DEFAULT_TIMEOUT_MS, fetchImpl = globalThis.fetch } = {}) {
88
+ if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this Node runtime")
89
+ const controller = new AbortController()
90
+ const timer = setTimeout(() => controller.abort(), Math.max(100, Number(timeoutMs || DEFAULT_TIMEOUT_MS)))
91
+ try {
92
+ const url = `${normalizeRegistry(registry)}/${encodePackageName(packageName)}`
93
+ const res = await fetchImpl(url, {
94
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
95
+ signal: controller.signal
96
+ })
97
+ if (!res.ok) throw new Error(`npm registry returned HTTP ${res.status}`)
98
+ return await res.json()
99
+ } finally {
100
+ clearTimeout(timer)
101
+ }
102
+ }
103
+
104
+ export async function checkForUpdate(config = {}, options = {}) {
105
+ const cfg = updateConfig(config)
106
+ if (!cfg.enabled && !options.force) return { ok: false, skipped: true, reason: "disabled" }
107
+
108
+ const now = Number(options.now ?? Date.now())
109
+ const stateFile = options.stateFile || updateStatePath()
110
+ const state = options.state ?? await readUpdateState(stateFile)
111
+ const intervalMs = Math.max(0, cfg.checkIntervalHours) * 60 * 60 * 1000
112
+ if (!options.force && intervalMs > 0 && state.checkedAt && now - Date.parse(state.checkedAt) < intervalMs) {
113
+ return { ok: true, skipped: true, reason: "interval", state }
114
+ }
115
+
116
+ const metadata = await fetchPackageMetadata({
117
+ packageName: options.packageName || PACKAGE_NAME,
118
+ registry: cfg.registry,
119
+ timeoutMs: cfg.timeoutMs,
120
+ fetchImpl: options.fetchImpl
121
+ })
122
+ const distTags = metadata["dist-tags"] || {}
123
+ const latestVersion = distTags[cfg.channel] || distTags.latest || metadata.version
124
+ const currentVersion = options.currentVersion || PACKAGE_VERSION
125
+ const hasUpdate = Boolean(latestVersion && compareVersions(latestVersion, currentVersion) > 0)
126
+ const result = {
127
+ ok: true,
128
+ packageName: options.packageName || PACKAGE_NAME,
129
+ channel: cfg.channel,
130
+ currentVersion,
131
+ latestVersion,
132
+ hasUpdate,
133
+ installSpec: `${options.packageName || PACKAGE_NAME}@${cfg.channel}`,
134
+ checkedAt: new Date(now).toISOString()
135
+ }
136
+ await writeUpdateState(result, stateFile)
137
+ return result
138
+ }
139
+
140
+ export function updateMessage(result) {
141
+ if (!result?.hasUpdate) return null
142
+ return `Update available: kkcode ${result.currentVersion} -> ${result.latestVersion} (${result.channel}). Run: kkcode update --channel ${result.channel}`
143
+ }
144
+
145
+ export async function maybeNotifyUpdateOnStartup(config = {}, options = {}) {
146
+ const cfg = updateConfig(config)
147
+ if (!cfg.enabled || !cfg.notifyOnStartup || process.env.KKCODE_DISABLE_UPDATE_CHECK === "1") return null
148
+ try {
149
+ const result = await checkForUpdate(config, options)
150
+ const message = updateMessage(result)
151
+ if (message) {
152
+ const print = options.print || console.error
153
+ print(message)
154
+ if (cfg.autoInstall) {
155
+ const install = await installUpdate(config, { channel: result.channel, stdio: "ignore" })
156
+ if (install.ok) print(`kkcode update installed ${result.latestVersion}; restart kkcode to use it.`)
157
+ else print(`kkcode auto-update failed: ${install.error}`)
158
+ }
159
+ }
160
+ return result
161
+ } catch (error) {
162
+ if (options.verbose) (options.print || console.error)(`kkcode update check failed: ${error.message}`)
163
+ return { ok: false, error: error.message }
164
+ }
165
+ }
166
+
167
+ function runCommand(command, args, { cwd = process.cwd(), env = process.env, stdio = "inherit" } = {}) {
168
+ return new Promise((resolve) => {
169
+ const child = spawn(command, args, { cwd, env, stdio, shell: process.platform === "win32" })
170
+ child.on("exit", (code) => resolve({ ok: code === 0, code }))
171
+ child.on("error", (error) => resolve({ ok: false, code: 1, error: error.message }))
172
+ })
173
+ }
174
+
175
+ export async function installUpdate(config = {}, options = {}) {
176
+ const cfg = updateConfig(config)
177
+ const channel = options.channel || cfg.channel || "latest"
178
+ const packageName = options.packageName || PACKAGE_NAME
179
+ const npm = options.npmCommand || process.env.npm_execpath || "npm"
180
+ const args = ["install", "-g", `${packageName}@${channel}`]
181
+ const result = await (options.runCommand || runCommand)(npm, args, options)
182
+ if (!result.ok) return { ok: false, code: result.code, error: result.error || `npm exited with ${result.code}` }
183
+ return { ok: true, command: npm, args }
184
+ }
@@ -1,121 +1,122 @@
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
- }
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
+ models: { ...DEFAULT_PRICING.models, ...(parsed.models ?? {}) },
91
+ default: { ...DEFAULT_PRICING.default, ...(parsed.default ?? {}) }
92
+ }
93
+ return { pricing, source: file, errors: [] }
94
+ } catch (error) {
95
+ return { pricing: DEFAULT_PRICING, source: "default", errors: [`${file}: ${error.message}`] }
96
+ }
97
+ }
98
+
99
+ function findPricingEntry(models, model) {
100
+ if (models[model]) return models[model]
101
+ // Fuzzy: try prefix match (e.g. "claude-opus-4-6-20250601" → "claude-opus-4-6")
102
+ const m = String(model).toLowerCase()
103
+ for (const key of Object.keys(models)) {
104
+ if (m.startsWith(key)) return models[key]
105
+ }
106
+ return null
107
+ }
108
+
109
+ export function calculateCost(pricing, model, usage) {
110
+ const entry = findPricingEntry(pricing.models, model) ?? pricing.default
111
+ const per = pricing.per_tokens || 1000000
112
+ // All providers normalize input to non-cached tokens only (see provider/*.mjs)
113
+ const amount =
114
+ ((usage.input || 0) * (entry.input || 0) +
115
+ (usage.output || 0) * (entry.output || 0) +
116
+ (usage.cacheRead || 0) * (entry.cache_read || 0) +
117
+ (usage.cacheWrite || 0) * (entry.cache_write || 0)) /
118
+ per
119
+ const savings = ((usage.cacheRead || 0) * ((entry.input || 0) - (entry.cache_read || 0))) / per
120
+ const unknown = !findPricingEntry(pricing.models, model)
121
+ return { amount, savings, unknown, currency: pricing.currency }
122
+ }
@@ -80,6 +80,7 @@ export async function resetUsage(sessionId = null) {
80
80
  return
81
81
  }
82
82
  const store = await readUsageStore()
83
+ maybeRotateGlobal(store)
83
84
  delete store.sessions[sessionId]
84
85
  store.global = emptyUsage()
85
86
  for (const session of Object.values(store.sessions)) {