lumencode 0.4.3

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.
@@ -0,0 +1,137 @@
1
+ function normalizeClassification(value) {
2
+ const v = String(value || '').toLowerCase();
3
+ if (['confirmed_ai', 'confirmed', 'ai', 'high'].includes(v)) return 'confirmed_ai';
4
+ if (['probable_ai', 'probable', 'medium'].includes(v)) return 'probable_ai';
5
+ if (['possible_ai', 'possible', 'low'].includes(v)) return 'possible_ai';
6
+ if (['human', 'excluded', 'unknown'].includes(v)) return v;
7
+ return 'unknown';
8
+ }
9
+
10
+ function unique(values = []) {
11
+ return [...new Set(values.filter(Boolean))];
12
+ }
13
+
14
+ export function classifyAttribution(input = {}) {
15
+ const override = input.override || null;
16
+ const evidence = Array.isArray(input.evidence) ? input.evidence : [];
17
+ const tools = unique([
18
+ ...(Array.isArray(input.tools) ? input.tools : []),
19
+ ...(input.primaryTool ? [input.primaryTool] : []),
20
+ ...(input.tool ? [input.tool] : []),
21
+ ...(input.detectedTool ? [input.detectedTool] : []),
22
+ ...evidence.map(e => e.tool),
23
+ ]);
24
+
25
+ if (override?.classification) {
26
+ const classification = normalizeClassification(override.classification);
27
+ return {
28
+ commitHash: input.commitHash || null,
29
+ classification,
30
+ primaryTool: override.primaryTool || input.primaryTool || tools[0] || null,
31
+ tools: unique([...(Array.isArray(override.tools) ? override.tools : []), ...tools]),
32
+ evidence,
33
+ source: 'manual',
34
+ reason: 'manual_override',
35
+ };
36
+ }
37
+
38
+ const attributionType = String(input.attributionType || '').toLowerCase();
39
+ const confidence = String(input.aiConfidence || '').toLowerCase();
40
+ const aiAssisted = input.aiAssisted === true || confidence !== 'none';
41
+ let classification = 'unknown';
42
+ let reason = 'no_evidence';
43
+
44
+ if (attributionType === 'explicit' || confidence === 'high') {
45
+ classification = 'confirmed_ai';
46
+ reason = attributionType === 'explicit' ? 'explicit_signature' : 'high_confidence';
47
+ } else if (attributionType.startsWith('session_strong')) {
48
+ classification = 'probable_ai';
49
+ reason = 'session_commit';
50
+ } else if (attributionType.startsWith('session_file_overlap')) {
51
+ classification = confidence === 'high' ? 'confirmed_ai' : 'probable_ai';
52
+ reason = 'file_overlap';
53
+ } else if (input.sessionAttribution === 'strong') {
54
+ classification = confidence === 'high' ? 'confirmed_ai' : 'probable_ai';
55
+ reason = 'session_commit';
56
+ } else if (input.sessionAttribution === 'weak' && aiAssisted) {
57
+ classification = 'possible_ai';
58
+ reason = 'time_window';
59
+ } else if (confidence === 'medium') {
60
+ classification = 'probable_ai';
61
+ reason = 'medium_confidence';
62
+ } else if (confidence === 'low' && aiAssisted) {
63
+ classification = 'possible_ai';
64
+ reason = 'low_confidence';
65
+ } else if (!aiAssisted || input.isAI === false) {
66
+ classification = 'human';
67
+ reason = 'human_default';
68
+ }
69
+
70
+ return {
71
+ commitHash: input.commitHash || null,
72
+ classification,
73
+ primaryTool: input.primaryTool || input.detectedTool || tools[0] || null,
74
+ tools,
75
+ evidence,
76
+ source: 'auto',
77
+ reason,
78
+ };
79
+ }
80
+
81
+ export function aggregateAttribution(items = []) {
82
+ const summary = {
83
+ confirmedAI: 0,
84
+ probableAI: 0,
85
+ possibleAI: 0,
86
+ unknown: 0,
87
+ human: 0,
88
+ excluded: 0,
89
+ confirmedAILines: 0,
90
+ probableAILines: 0,
91
+ possibleAILines: 0,
92
+ unknownLines: 0,
93
+ humanLines: 0,
94
+ excludedLines: 0,
95
+ totalLinesChanged: 0,
96
+ totalItems: 0,
97
+ unknownReasons: [],
98
+ };
99
+
100
+ for (const item of items || []) {
101
+ const classified = item?.classification ? item : classifyAttribution(item);
102
+ const lines = (item?.added || item?.linesAdded || 0) + (item?.deleted || item?.linesDeleted || 0);
103
+ summary.totalItems++;
104
+ summary.totalLinesChanged += lines;
105
+
106
+ switch (classified.classification) {
107
+ case 'confirmed_ai':
108
+ summary.confirmedAI++;
109
+ summary.confirmedAILines += lines;
110
+ break;
111
+ case 'probable_ai':
112
+ summary.probableAI++;
113
+ summary.probableAILines += lines;
114
+ break;
115
+ case 'possible_ai':
116
+ summary.possibleAI++;
117
+ summary.possibleAILines += lines;
118
+ break;
119
+ case 'human':
120
+ summary.human++;
121
+ summary.humanLines += lines;
122
+ break;
123
+ case 'excluded':
124
+ summary.excluded++;
125
+ summary.excludedLines += lines;
126
+ break;
127
+ default:
128
+ summary.unknown++;
129
+ summary.unknownLines += lines;
130
+ if (classified.reason) summary.unknownReasons.push(classified.reason);
131
+ break;
132
+ }
133
+ }
134
+
135
+ summary.unknownReasons = unique(summary.unknownReasons);
136
+ return summary;
137
+ }
package/lib/blocks.js ADDED
@@ -0,0 +1,86 @@
1
+ import { resolveModelPricing, computeCostFromRecords } from './aggregate.js';
2
+ import { getInputTokens, getOutputTokens, getCacheRead, getCacheCreate, isAssistantRecord } from './record-utils.js';
3
+
4
+ const DEFAULT_SESSION_DURATION_HOURS = 5;
5
+
6
+ function floorToHourMs(ms) {
7
+ return Math.floor(ms / (60 * 60 * 1000)) * 60 * 60 * 1000;
8
+ }
9
+
10
+ export function identifyBillingBlocks(records, sessionDurationHours = DEFAULT_SESSION_DURATION_HOURS, costMode = 'auto') {
11
+ const sorted = records
12
+ .filter(r => isAssistantRecord(r) && r.timestamp)
13
+ .sort((a, b) => a.timestamp.localeCompare(b.timestamp));
14
+
15
+ if (sorted.length === 0) return [];
16
+
17
+ const durationMs = sessionDurationHours * 60 * 60 * 1000;
18
+ const blocks = [];
19
+ let blockStartMs = null;
20
+ let blockRecords = [];
21
+
22
+ for (const record of sorted) {
23
+ const entryMs = new Date(record.timestamp).getTime();
24
+
25
+ if (blockStartMs === null) {
26
+ blockStartMs = floorToHourMs(entryMs);
27
+ blockRecords = [record];
28
+ } else {
29
+ const lastMs = new Date(blockRecords[blockRecords.length - 1].timestamp).getTime();
30
+ const sinceBlockStart = entryMs - blockStartMs;
31
+ const sinceLastEntry = entryMs - lastMs;
32
+
33
+ if (sinceBlockStart > durationMs || sinceLastEntry > durationMs) {
34
+ blocks.push(createBlock(blockStartMs, blockRecords, durationMs, costMode));
35
+ blockStartMs = floorToHourMs(entryMs);
36
+ blockRecords = [record];
37
+ } else {
38
+ blockRecords.push(record);
39
+ }
40
+ }
41
+ }
42
+
43
+ if (blockStartMs !== null) {
44
+ blocks.push(createBlock(blockStartMs, blockRecords, durationMs, costMode));
45
+ }
46
+
47
+ return blocks;
48
+ }
49
+
50
+ function createBlock(startMs, records, durationMs, costMode) {
51
+ const startTime = new Date(startMs);
52
+ const endTime = new Date(startMs + durationMs);
53
+ const lastRecord = records[records.length - 1];
54
+ const actualEndTime = lastRecord ? new Date(lastRecord.timestamp) : startTime;
55
+ const nowMs = Date.now();
56
+ const isActive = (nowMs - actualEndTime.getTime()) < durationMs && nowMs < endTime.getTime();
57
+
58
+ let inputTokens = 0, outputTokens = 0, cacheRead = 0, cacheCreate = 0;
59
+ const models = new Set();
60
+ const sessions = new Set();
61
+
62
+ for (const r of records) {
63
+ inputTokens += getInputTokens(r);
64
+ outputTokens += getOutputTokens(r);
65
+ cacheRead += getCacheRead(r);
66
+ cacheCreate += getCacheCreate(r);
67
+ if (r.model) models.add(r.model);
68
+ if (r.sessionId) sessions.add(r.sessionId);
69
+ }
70
+
71
+ const costUSD = computeCostFromRecords(records, costMode);
72
+
73
+ return {
74
+ id: startTime.toISOString(),
75
+ startTime: startTime.toISOString(),
76
+ endTime: endTime.toISOString(),
77
+ actualEndTime: actualEndTime.toISOString(),
78
+ isActive,
79
+ requests: records.length,
80
+ sessions: sessions.size,
81
+ tokenCounts: { inputTokens, outputTokens, cacheRead, cacheCreate },
82
+ totalTokens: inputTokens + outputTokens + cacheRead + cacheCreate,
83
+ costUSD: Math.round(costUSD * 100) / 100,
84
+ models: [...models],
85
+ };
86
+ }
package/lib/cache.js ADDED
@@ -0,0 +1,18 @@
1
+ import { statSync } from 'fs';
2
+ import { parseJsonlFile } from './parser.js';
3
+
4
+ const fileCache = new Map();
5
+
6
+ export function getCachedFileRecords(filePath) {
7
+ const { mtimeMs } = statSync(filePath);
8
+ const cached = fileCache.get(filePath);
9
+ if (cached && cached.mtime === mtimeMs) return cached.records;
10
+
11
+ const records = parseJsonlFile(filePath);
12
+ fileCache.set(filePath, { mtime: mtimeMs, records });
13
+ return records;
14
+ }
15
+
16
+ export function invalidateFileCache() {
17
+ fileCache.clear();
18
+ }
package/lib/config.js ADDED
@@ -0,0 +1,91 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const CONFIG_LOCATIONS = [
6
+ join(homedir(), '.lumencode.json'),
7
+ join(homedir(), '.claude', 'tools', 'lumencode', 'config.json'),
8
+ join(process.cwd(), 'config.json'),
9
+ ];
10
+
11
+ const DEFAULT_CONFIG = {
12
+ claudeDir: join(homedir(), '.claude'),
13
+ codexDir: '', // 空字符串表示自动检测
14
+ opencodeDir: '', // 空字符串表示自动检测
15
+ enabledTools: [], // 空数组表示自动检测启用
16
+ repos: [],
17
+ excludeProjects: [],
18
+ blockQuota: null, // 5h 计费窗口 token 上限(Max Pro=1000000, Max=450000 等),null=不限
19
+ costMode: 'auto', // 'auto' | 'calculate' | 'display'
20
+ scenarioKeywords: {
21
+ coding: ['实现', '功能', '开发', '添加', '修改代码', 'implement', 'feature', 'add', 'refactor', '重构', '组件'],
22
+ testing: ['测试', 'test', 'spec', '覆盖率', 'coverage', '单元测试', 'unit test', 'jest', 'vitest', 'mocha'],
23
+ debugging: ['修复', 'bug', 'debug', 'fix', '报错', '错误', '异常', 'error', 'issue', '问题', '排查', '堆栈'],
24
+ documentation: ['文档', 'doc', 'readme', 'md', '注释', 'comment', '说明', '指南', 'guide'],
25
+ review: ['review', '审查', '检查', '代码审查', '/review'],
26
+ planning: ['计划', 'plan', '设计', '架构', '方案', 'design', 'architect'],
27
+ },
28
+ };
29
+
30
+ export function loadConfig(configPath) {
31
+ let config = { ...DEFAULT_CONFIG };
32
+
33
+ // 如果传入了自定义配置路径,只检查该路径,不回退到全局配置
34
+ if (configPath) {
35
+ if (existsSync(configPath)) {
36
+ try {
37
+ const userConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
38
+ config = { ...config, ...userConfig };
39
+ return config;
40
+ } catch (e) {
41
+ console.error(`配置文件读取失败: ${configPath}`, e.message);
42
+ // 文件存在但解析失败,返回默认值
43
+ }
44
+ }
45
+ return config;
46
+ }
47
+
48
+ // 未传入配置路径时,按优先级检查默认位置
49
+ for (const p of CONFIG_LOCATIONS) {
50
+ if (existsSync(p)) {
51
+ try {
52
+ const userConfig = JSON.parse(readFileSync(p, 'utf-8'));
53
+ config = { ...config, ...userConfig };
54
+ return config;
55
+ } catch (e) {
56
+ console.error(`配置文件读取失败: ${p}`, e.message);
57
+ }
58
+ }
59
+ }
60
+
61
+ return config;
62
+ }
63
+
64
+ export function getConfigPath(configPath) {
65
+ if (configPath && existsSync(configPath)) return configPath;
66
+ for (const p of CONFIG_LOCATIONS) {
67
+ if (existsSync(p)) return p;
68
+ }
69
+ return CONFIG_LOCATIONS[0];
70
+ }
71
+
72
+ export function saveConfig(config, configPath) {
73
+ // 保存时固定使用用户级路径,避免写入 process.cwd() 下的 git 跟踪文件
74
+ const target = configPath || CONFIG_LOCATIONS[0];
75
+ const dir = dirname(target);
76
+ if (!existsSync(dir)) {
77
+ mkdirSync(dir, { recursive: true });
78
+ }
79
+ writeFileSync(target, JSON.stringify(config, null, 2), { encoding: 'utf8' });
80
+ return target;
81
+ }
82
+
83
+ export function initConfig(configPath) {
84
+ const target = configPath || CONFIG_LOCATIONS[0];
85
+ if (existsSync(target)) {
86
+ console.log(`配置文件已存在: ${target}`);
87
+ return;
88
+ }
89
+ writeFileSync(target, JSON.stringify(DEFAULT_CONFIG, null, 2), { encoding: 'utf8' });
90
+ console.log(`配置文件已创建: ${target}`);
91
+ }