codebuddy-stats 1.1.0 → 1.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.
@@ -1,128 +0,0 @@
1
- export interface PricingTier {
2
- limit: number
3
- pricePerMTok: number
4
- }
5
-
6
- export interface ModelPricing {
7
- prompt: PricingTier[]
8
- completion: PricingTier[]
9
- cacheRead: PricingTier[]
10
- cacheWrite: PricingTier[]
11
- }
12
-
13
- // 模型价格 (USD / 1M tokens)
14
- function createPricing(
15
- inputPrice: number,
16
- cachedInputPrice: number,
17
- outputPrice: number,
18
- cacheWritePrice?: number
19
- ): ModelPricing {
20
- return {
21
- prompt: [{ limit: Number.POSITIVE_INFINITY, pricePerMTok: inputPrice }],
22
- completion: [{ limit: Number.POSITIVE_INFINITY, pricePerMTok: outputPrice }],
23
- cacheRead: [{ limit: Number.POSITIVE_INFINITY, pricePerMTok: cachedInputPrice }],
24
- cacheWrite: [
25
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: cacheWritePrice ?? inputPrice },
26
- ],
27
- }
28
- }
29
-
30
- export const MODEL_PRICING: Record<string, ModelPricing> = {
31
- 'gpt-5.2': createPricing(1.75, 0.175, 14.0),
32
- 'gpt-5.1': createPricing(1.25, 0.125, 10.0),
33
- 'gpt-5': createPricing(1.25, 0.125, 10.0),
34
- 'gpt-5-mini': createPricing(0.25, 0.025, 2.0),
35
- 'gpt-5-nano': createPricing(0.05, 0.005, 0.4),
36
- 'gpt-5.1-chat-latest': createPricing(1.25, 0.125, 10.0),
37
- 'gpt-5-chat-latest': createPricing(1.25, 0.125, 10.0),
38
- 'gpt-5.1-codex': createPricing(1.25, 0.125, 10.0),
39
- 'gpt-5-codex': createPricing(1.25, 0.125, 10.0),
40
-
41
- 'claude-opus-4.5': createPricing(5.0, 0.5, 25.0, 10.0),
42
-
43
- 'claude-4.5': {
44
- prompt: [
45
- { limit: 200_000, pricePerMTok: 3.0 },
46
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 6.0 },
47
- ],
48
- completion: [
49
- { limit: 200_000, pricePerMTok: 15.0 },
50
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 22.5 },
51
- ],
52
- cacheRead: [
53
- { limit: 200_000, pricePerMTok: 0.3 },
54
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.6 },
55
- ],
56
- cacheWrite: [
57
- { limit: 200_000, pricePerMTok: 6.0 },
58
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 12.0 },
59
- ],
60
- },
61
-
62
- 'gemini-3-pro': {
63
- prompt: [
64
- { limit: 200_000, pricePerMTok: 2.0 },
65
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 4.0 },
66
- ],
67
- completion: [
68
- { limit: 200_000, pricePerMTok: 12.0 },
69
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 18.0 },
70
- ],
71
- cacheRead: [
72
- { limit: 200_000, pricePerMTok: 0.2 },
73
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.4 },
74
- ],
75
- cacheWrite: [
76
- { limit: 200_000, pricePerMTok: 0.2 },
77
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.4 },
78
- ],
79
- },
80
-
81
- 'gemini-2.5-pro': {
82
- prompt: [
83
- { limit: 200_000, pricePerMTok: 1.25 },
84
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 2.5 },
85
- ],
86
- completion: [
87
- { limit: 200_000, pricePerMTok: 10.0 },
88
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 15.0 },
89
- ],
90
- cacheRead: [
91
- { limit: 200_000, pricePerMTok: 0.125 },
92
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.25 },
93
- ],
94
- cacheWrite: [
95
- { limit: 200_000, pricePerMTok: 0.125 },
96
- { limit: Number.POSITIVE_INFINITY, pricePerMTok: 0.25 },
97
- ],
98
- },
99
- }
100
-
101
- export const DEFAULT_MODEL_ID = 'gpt-5.1' as const
102
-
103
- export function selectTierPrice(tokens: number, tiers: PricingTier[]): number {
104
- if (tokens <= 0) return tiers[0]?.pricePerMTok ?? 0
105
- for (const tier of tiers) {
106
- if (tokens <= tier.limit) {
107
- return tier.pricePerMTok
108
- }
109
- }
110
- return tiers[tiers.length - 1]?.pricePerMTok ?? 0
111
- }
112
-
113
- export function tokensToCost(tokens: number, tiers: PricingTier[]): number {
114
- if (!tokens) return 0
115
- const price = selectTierPrice(tokens, tiers)
116
- return (tokens / 1_000_000) * price
117
- }
118
-
119
- export function getPricingForModel(modelId: string | null | undefined): ModelPricing {
120
- if (modelId && MODEL_PRICING[modelId]) {
121
- return MODEL_PRICING[modelId]
122
- }
123
- const fallback = MODEL_PRICING[DEFAULT_MODEL_ID]
124
- if (!fallback) {
125
- throw new Error(`Missing pricing for default model: ${DEFAULT_MODEL_ID}`)
126
- }
127
- return fallback
128
- }
package/src/lib/utils.ts DELETED
@@ -1,61 +0,0 @@
1
- /**
2
- * 格式化数字,添加千分位分隔符
3
- */
4
- export function formatNumber(num: number | string | null | undefined): string {
5
- const n = typeof num === 'number' ? num : Number(num ?? 0)
6
- if (!Number.isFinite(n)) return '0'
7
- return n.toLocaleString('en-US')
8
- }
9
-
10
- /**
11
- * 格式化金额
12
- */
13
- export function formatCost(cost: number): string {
14
- const n = Number.isFinite(cost) ? cost : 0
15
- return `$${n.toFixed(2)}`
16
- }
17
-
18
- /**
19
- * 格式化 tokens 数量 (K/M/B)
20
- */
21
- export function formatTokens(tokens: number): string {
22
- if (tokens >= 1_000_000_000) {
23
- return `${(tokens / 1_000_000_000).toFixed(1)}B`
24
- }
25
- if (tokens >= 1_000_000) {
26
- return `${(tokens / 1_000_000).toFixed(1)}M`
27
- }
28
- if (tokens >= 1_000) {
29
- return `${(tokens / 1_000).toFixed(1)}K`
30
- }
31
- return String(tokens)
32
- }
33
-
34
- /**
35
- * 格式化百分比
36
- */
37
- export function formatPercent(ratio: number): string {
38
- return `${(ratio * 100).toFixed(1)}%`
39
- }
40
-
41
- /**
42
- * 截断字符串
43
- */
44
- export function truncate(str: string, maxLen: number): string {
45
- if (str.length <= maxLen) return str
46
- return str.slice(0, maxLen - 3) + '...'
47
- }
48
-
49
- /**
50
- * 右对齐字符串
51
- */
52
- export function padLeft(str: string, len: number): string {
53
- return str.padStart(len)
54
- }
55
-
56
- /**
57
- * 左对齐字符串
58
- */
59
- export function padRight(str: string, len: number): string {
60
- return str.padEnd(len)
61
- }
@@ -1,235 +0,0 @@
1
- import fs from 'node:fs/promises'
2
- import fsSync from 'node:fs'
3
- import path from 'node:path'
4
- import os from 'node:os'
5
- import crypto from 'node:crypto'
6
-
7
- import { getWorkspaceStorageDir } from './paths.js'
8
-
9
- export interface WorkspaceMapping {
10
- hash: string
11
- folderUri: string
12
- displayPath: string
13
- }
14
-
15
- // 缓存已解析的 CodeBuddy Code 路径名
16
- const codePathCache = new Map<string, string>()
17
-
18
- /**
19
- * 从 folder URI 提取纯路径(用于计算 MD5)
20
- */
21
- function extractPathFromUri(folderUri: string): string | null {
22
- // 处理本地文件路径: file:///path/to/folder
23
- if (folderUri.startsWith('file://')) {
24
- try {
25
- const url = new URL(folderUri)
26
- return decodeURIComponent(url.pathname)
27
- } catch {
28
- return decodeURIComponent(folderUri.replace('file://', ''))
29
- }
30
- }
31
-
32
- // 处理远程路径: vscode-remote://codebuddy-remote-ssh%2B.../path
33
- if (folderUri.startsWith('vscode-remote://')) {
34
- try {
35
- const url = new URL(folderUri)
36
- return decodeURIComponent(url.pathname)
37
- } catch {
38
- const match = folderUri.match(/vscode-remote:\/\/[^/]+(.+)$/)
39
- if (match?.[1]) {
40
- return decodeURIComponent(match[1])
41
- }
42
- }
43
- }
44
-
45
- return null
46
- }
47
-
48
- /**
49
- * 格式化路径用于显示(简化 home 目录)
50
- */
51
- function formatDisplayPath(p: string): string {
52
- const home = os.homedir()
53
- if (p.startsWith(home)) {
54
- return '~' + p.slice(home.length)
55
- }
56
- return p
57
- }
58
-
59
- /**
60
- * 从 folder URI 生成用于显示的友好路径
61
- */
62
- function getDisplayPath(folderUri: string): string {
63
- // 本地路径
64
- if (folderUri.startsWith('file://')) {
65
- const p = extractPathFromUri(folderUri)
66
- if (p) {
67
- return formatDisplayPath(p)
68
- }
69
- }
70
-
71
- // 远程路径
72
- if (folderUri.startsWith('vscode-remote://')) {
73
- const p = extractPathFromUri(folderUri)
74
- if (p) {
75
- const hostMatch = folderUri.match(/vscode-remote:\/\/codebuddy-remote-ssh%2B([^/]+)/)
76
- if (hostMatch?.[1]) {
77
- let host = decodeURIComponent(hostMatch[1])
78
- host = host.replace(/_x([0-9a-fA-F]{2})_/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
79
- host = host.replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
80
- if (host.includes('@')) {
81
- const parts = host.split('@')
82
- host = parts[parts.length - 1]?.split(':')[0] || host
83
- }
84
- if (host.length > 20) {
85
- host = host.slice(0, 17) + '...'
86
- }
87
- return `[${host}]${p}`
88
- }
89
- return `[remote]${p}`
90
- }
91
- }
92
-
93
- return folderUri
94
- }
95
-
96
- /**
97
- * 计算路径的 MD5 hash(CodeBuddyExtension 使用纯路径计算)
98
- */
99
- function computePathHash(p: string): string {
100
- return crypto.createHash('md5').update(p).digest('hex')
101
- }
102
-
103
- /**
104
- * 加载所有工作区映射
105
- */
106
- export async function loadWorkspaceMappings(): Promise<Map<string, WorkspaceMapping>> {
107
- const mappings = new Map<string, WorkspaceMapping>()
108
- const storageDir = getWorkspaceStorageDir()
109
-
110
- let entries: fsSync.Dirent[] = []
111
- try {
112
- entries = await fs.readdir(storageDir, { withFileTypes: true })
113
- } catch {
114
- return mappings
115
- }
116
-
117
- for (const entry of entries) {
118
- if (!entry.isDirectory()) continue
119
-
120
- const workspaceJsonPath = path.join(storageDir, entry.name, 'workspace.json')
121
- try {
122
- const content = await fs.readFile(workspaceJsonPath, 'utf8')
123
- const data = JSON.parse(content) as { folder?: string }
124
- const folderUri = data.folder
125
- if (!folderUri) continue
126
-
127
- const extractedPath = extractPathFromUri(folderUri)
128
- if (!extractedPath) continue
129
-
130
- const hash = computePathHash(extractedPath)
131
- const displayPath = getDisplayPath(folderUri)
132
-
133
- mappings.set(hash, { hash, folderUri, displayPath })
134
- } catch {
135
- // 跳过无法读取的文件
136
- }
137
- }
138
-
139
- return mappings
140
- }
141
-
142
- /**
143
- * 检查路径是否存在(同步版本,用于路径探测)
144
- */
145
- function pathExistsSync(p: string): boolean {
146
- try {
147
- fsSync.accessSync(p)
148
- return true
149
- } catch {
150
- return false
151
- }
152
- }
153
-
154
- /**
155
- * 尝试将 CodeBuddy Code 的项目名(路径中 / 替换为 -)还原为真实路径
156
- * 使用回溯搜索,因为目录名本身可能包含 -
157
- *
158
- * 例如: "Users-anoti-Documents-project-codebudy-cost-analyzer"
159
- * -> "/Users/anoti/Documents/project/codebudy-cost-analyzer"
160
- */
161
- function tryResolveCodePath(name: string): string | null {
162
- // 检查缓存
163
- const cached = codePathCache.get(name)
164
- if (cached !== undefined) {
165
- return cached || null
166
- }
167
-
168
- const parts = name.split('-')
169
- if (parts.length < 2) {
170
- codePathCache.set(name, '')
171
- return null
172
- }
173
-
174
- // 回溯搜索:尝试不同的分割方式
175
- function backtrack(index: number, currentPath: string): string | null {
176
- if (index >= parts.length) {
177
- // 检查完整路径是否存在
178
- if (pathExistsSync(currentPath)) {
179
- return currentPath
180
- }
181
- return null
182
- }
183
-
184
- // 尝试从当前位置开始,合并不同数量的 parts
185
- for (let end = index; end < parts.length; end++) {
186
- const segment = parts.slice(index, end + 1).join('-')
187
- const newPath = currentPath ? `${currentPath}/${segment}` : `/${segment}`
188
-
189
- // 如果这不是最后一段,检查目录是否存在
190
- if (end < parts.length - 1) {
191
- if (pathExistsSync(newPath)) {
192
- const result = backtrack(end + 1, newPath)
193
- if (result) return result
194
- }
195
- } else {
196
- // 最后一段,检查完整路径
197
- if (pathExistsSync(newPath)) {
198
- return newPath
199
- }
200
- }
201
- }
202
-
203
- return null
204
- }
205
-
206
- const result = backtrack(0, '')
207
- codePathCache.set(name, result || '')
208
- return result
209
- }
210
-
211
- /**
212
- * 解析项目名称
213
- * - MD5 hash (32位十六进制): 从 IDE workspaceMappings 查找
214
- * - 路径格式 (包含 -): 尝试还原 CodeBuddy Code 的路径格式
215
- */
216
- export function resolveProjectName(name: string, mappings?: Map<string, WorkspaceMapping>): string {
217
- // IDE source: MD5 hash
218
- if (mappings && /^[a-f0-9]{32}$/.test(name)) {
219
- const mapping = mappings.get(name)
220
- if (mapping) {
221
- return mapping.displayPath
222
- }
223
- }
224
-
225
- // Code source: 路径中 / 替换为 - 的格式
226
- // 特征:以大写字母开头(如 Users-、home-),包含 -
227
- if (/^[A-Za-z]/.test(name) && name.includes('-')) {
228
- const resolved = tryResolveCodePath(name)
229
- if (resolved) {
230
- return formatDisplayPath(resolved)
231
- }
232
- }
233
-
234
- return name
235
- }
package/tsconfig.json DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "$schema": "https://json.schemastore.org/tsconfig",
3
- "compilerOptions": {
4
- "target": "ES2022",
5
- "lib": ["ES2022"],
6
-
7
- "module": "NodeNext",
8
- "moduleResolution": "NodeNext",
9
- "verbatimModuleSyntax": true,
10
-
11
- "rootDir": "./src",
12
- "outDir": "./dist",
13
-
14
- "strict": true,
15
- "noUncheckedIndexedAccess": true,
16
-
17
- "esModuleInterop": true,
18
- "resolveJsonModule": true,
19
- "skipLibCheck": true,
20
-
21
- "sourceMap": true
22
- },
23
- "include": ["src/**/*"],
24
- "exclude": ["node_modules", "dist"]
25
- }