token-studio 4.8.0
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/.nvmrc +1 -0
- package/CHANGELOG.md +89 -0
- package/Dockerfile +17 -0
- package/LICENSE +22 -0
- package/NOTICE.md +21 -0
- package/PRIVACY.md +68 -0
- package/README.en.md +220 -0
- package/README.md +220 -0
- package/config/collectors.json +54 -0
- package/data/.gitkeep +1 -0
- package/docker-compose.yml +17 -0
- package/docs/assets/.gitkeep +1 -0
- package/docs/assets/token-studio-v44-dashboard.png +0 -0
- package/docs/assets/token-studio-v44-live.png +0 -0
- package/docs/assets/token-studio-v44-review-mobile.png +0 -0
- package/docs/assets/token-studio-v44-review.png +0 -0
- package/docs/assets/token-studio-v45-dashboard.png +0 -0
- package/docs/assets/token-studio-v45-live.png +0 -0
- package/docs/assets/token-studio-v45-review-mobile.png +0 -0
- package/docs/assets/token-studio-v45-review.png +0 -0
- package/docs/blog-case-study.md +34 -0
- package/docs/collector-support-matrix.md +65 -0
- package/docs/competitive-notes.md +87 -0
- package/docs/demo-data/README.md +12 -0
- package/docs/demo-data/token-studio-v2-demo.json +146 -0
- package/docs/demo-flow.md +39 -0
- package/docs/first-run.md +95 -0
- package/docs/local-collectors.md +49 -0
- package/docs/public-launch-checklist.md +45 -0
- package/docs/resume-bullets.md +7 -0
- package/docs/statusline.md +52 -0
- package/index.html +16 -0
- package/package.json +36 -0
- package/render.yaml +17 -0
- package/src/auto-attribution.mjs +396 -0
- package/src/ccusage-bridge.mjs +74 -0
- package/src/ccusage-import.mjs +415 -0
- package/src/cli.mjs +643 -0
- package/src/client/dashboard/App.jsx +1734 -0
- package/src/client/dashboard/annotation-presets.js +138 -0
- package/src/client/dashboard/attribution.js +328 -0
- package/src/client/dashboard/components-charts.jsx +622 -0
- package/src/client/dashboard/components-tables.jsx +1531 -0
- package/src/client/dashboard/components-top.jsx +307 -0
- package/src/client/dashboard/import-budget.js +41 -0
- package/src/client/dashboard/model-usage.js +108 -0
- package/src/client/dashboard/onboarding.js +80 -0
- package/src/client/dashboard/styles.css +2606 -0
- package/src/client/live/LiveApp.jsx +226 -0
- package/src/client/live/styles.css +446 -0
- package/src/client/main.jsx +20 -0
- package/src/client/review/ReviewApp.jsx +507 -0
- package/src/client/review/closure-progress.js +165 -0
- package/src/client/review/markdown-report.js +401 -0
- package/src/client/review/model-strategy.js +273 -0
- package/src/client/review/roi-advisor.js +255 -0
- package/src/client/review/roi-evidence.js +78 -0
- package/src/client/review/savings-simulator.js +252 -0
- package/src/client/review/sections-1.jsx +277 -0
- package/src/client/review/sections-2.jsx +927 -0
- package/src/client/review/styles.css +2321 -0
- package/src/client/review/utils.js +345 -0
- package/src/client/shared/utils.js +236 -0
- package/src/closure-check.mjs +537 -0
- package/src/closure-import.mjs +646 -0
- package/src/collect.mjs +247 -0
- package/src/collector-config.mjs +82 -0
- package/src/collector-registry.mjs +333 -0
- package/src/collectors/claude-code.mjs +355 -0
- package/src/collectors/codex.mjs +418 -0
- package/src/collectors/copilot.mjs +19 -0
- package/src/collectors/cursor.mjs +23 -0
- package/src/collectors/gemini.mjs +530 -0
- package/src/collectors/goose.mjs +15 -0
- package/src/collectors/hermes.mjs +206 -0
- package/src/collectors/kimi.mjs +15 -0
- package/src/collectors/openclaw.mjs +400 -0
- package/src/collectors/opencode.mjs +349 -0
- package/src/collectors/qwen.mjs +15 -0
- package/src/collectors/structured-usage.mjs +437 -0
- package/src/collectors/utils.mjs +93 -0
- package/src/db.mjs +1397 -0
- package/src/demo-seed.mjs +39 -0
- package/src/dev.mjs +43 -0
- package/src/live.mjs +428 -0
- package/src/model-policy.mjs +147 -0
- package/src/pricing.mjs +434 -0
- package/src/privacy-check.mjs +126 -0
- package/src/server.mjs +1240 -0
- package/src/source-health.mjs +195 -0
- package/src/statusline.mjs +156 -0
- package/src/terminal-report.mjs +245 -0
- package/src/update-pricing.mjs +8 -0
- package/test/annotation-presets.test.mjs +137 -0
- package/test/api-annotations.test.mjs +202 -0
- package/test/api-auto-attribution.test.mjs +169 -0
- package/test/api-source-health.test.mjs +109 -0
- package/test/api-v2.test.mjs +278 -0
- package/test/api-v43.test.mjs +151 -0
- package/test/api-v44.test.mjs +128 -0
- package/test/attribution-summary.test.mjs +164 -0
- package/test/auto-attribution.test.mjs +116 -0
- package/test/ccusage-bridge.test.mjs +36 -0
- package/test/ccusage-import.test.mjs +93 -0
- package/test/cli-v43.test.mjs +64 -0
- package/test/cli-v45.test.mjs +34 -0
- package/test/cli-v46.test.mjs +129 -0
- package/test/cli-v47.test.mjs +98 -0
- package/test/closure-check.test.mjs +202 -0
- package/test/closure-import.test.mjs +263 -0
- package/test/collector-config.test.mjs +25 -0
- package/test/collector-registry.test.mjs +56 -0
- package/test/csv.test.mjs +19 -0
- package/test/db-annotations.test.mjs +186 -0
- package/test/db-v2.test.mjs +200 -0
- package/test/db-v4.test.mjs +178 -0
- package/test/experimental-collectors.test.mjs +103 -0
- package/test/fixtures/collectors/copilot/usage.jsonl +2 -0
- package/test/fixtures/collectors/cursor/usage.jsonl +2 -0
- package/test/fixtures/collectors/goose/usage.jsonl +2 -0
- package/test/fixtures/collectors/kimi/usage.jsonl +2 -0
- package/test/fixtures/collectors/qwen/usage.jsonl +2 -0
- package/test/import-budget.test.mjs +40 -0
- package/test/live.test.mjs +256 -0
- package/test/markdown-report.test.mjs +193 -0
- package/test/model-policy.test.mjs +34 -0
- package/test/model-strategy.test.mjs +116 -0
- package/test/model-usage.test.mjs +99 -0
- package/test/official-pricing.test.mjs +70 -0
- package/test/onboarding.test.mjs +55 -0
- package/test/privacy-check.test.mjs +33 -0
- package/test/review-closure-progress.test.mjs +99 -0
- package/test/roi-advisor.test.mjs +188 -0
- package/test/roi-evidence.test.mjs +48 -0
- package/test/roi-summary.test.mjs +101 -0
- package/test/savings-simulator.test.mjs +141 -0
- package/test/source-health.test.mjs +62 -0
- package/test/statusline.test.mjs +148 -0
- package/vite.config.js +23 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import {
|
|
7
|
+
applyAutoSessionAnnotations,
|
|
8
|
+
deleteSessionAnnotation,
|
|
9
|
+
openDb,
|
|
10
|
+
undoAutoSessionAnnotations,
|
|
11
|
+
upsertSession,
|
|
12
|
+
upsertSessionAnnotation
|
|
13
|
+
} from '../src/db.mjs';
|
|
14
|
+
|
|
15
|
+
function withDb(fn) {
|
|
16
|
+
const dir = mkdtempSync(join(tmpdir(), 'token-studio-db-'));
|
|
17
|
+
const db = openDb(join(dir, 'usage.sqlite'));
|
|
18
|
+
try {
|
|
19
|
+
upsertSession(db, {
|
|
20
|
+
device: 'devbox',
|
|
21
|
+
source: 'Codex CLI',
|
|
22
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
23
|
+
lastActivity: '2026-06-10T01:00:00.000Z',
|
|
24
|
+
projectPath: 'D:\\Project',
|
|
25
|
+
inputTokens: 100,
|
|
26
|
+
outputTokens: 30,
|
|
27
|
+
cacheCreationTokens: 10,
|
|
28
|
+
cacheReadTokens: 20,
|
|
29
|
+
reasoningOutputTokens: 5,
|
|
30
|
+
totalTokens: 165,
|
|
31
|
+
costUSD: 0.01
|
|
32
|
+
});
|
|
33
|
+
fn(db);
|
|
34
|
+
} finally {
|
|
35
|
+
db.close();
|
|
36
|
+
rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('session annotation upsert stores normalized values', () => withDb((db) => {
|
|
41
|
+
const saved = upsertSessionAnnotation(db, {
|
|
42
|
+
device: 'devbox',
|
|
43
|
+
source: 'Codex CLI',
|
|
44
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
45
|
+
projectAlias: ' AI 选题雷达 ',
|
|
46
|
+
taskType: '功能开发',
|
|
47
|
+
outputStatus: '已完成',
|
|
48
|
+
workPurpose: '方案设计',
|
|
49
|
+
workStage: '实现',
|
|
50
|
+
valueLevel: '高',
|
|
51
|
+
note: ' 首版归因 UI '
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
assert.equal(saved.projectAlias, 'AI 选题雷达');
|
|
55
|
+
assert.equal(saved.taskType, '功能开发');
|
|
56
|
+
assert.equal(saved.outputStatus, '已完成');
|
|
57
|
+
assert.equal(saved.workPurpose, '方案设计');
|
|
58
|
+
assert.equal(saved.workStage, '实现');
|
|
59
|
+
assert.equal(saved.valueLevel, '高');
|
|
60
|
+
assert.equal(saved.note, '首版归因 UI');
|
|
61
|
+
assert.equal(saved.annotationSource, 'manual');
|
|
62
|
+
assert.equal(saved.annotationConfidence, 100);
|
|
63
|
+
assert.ok(saved.annotationUpdatedAt);
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
test('session annotation defaults stay explicit', () => withDb((db) => {
|
|
67
|
+
const saved = upsertSessionAnnotation(db, {
|
|
68
|
+
device: 'devbox',
|
|
69
|
+
source: 'Codex CLI',
|
|
70
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
71
|
+
projectAlias: '',
|
|
72
|
+
note: ''
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
assert.equal(saved.projectAlias, null);
|
|
76
|
+
assert.equal(saved.taskType, '未分类');
|
|
77
|
+
assert.equal(saved.outputStatus, '未标注');
|
|
78
|
+
assert.equal(saved.workPurpose, '未说明');
|
|
79
|
+
assert.equal(saved.workStage, '未说明');
|
|
80
|
+
assert.equal(saved.valueLevel, '未评估');
|
|
81
|
+
assert.equal(saved.note, null);
|
|
82
|
+
assert.equal(saved.annotationSource, 'manual');
|
|
83
|
+
assert.equal(saved.annotationConfidence, 100);
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
test('session annotation validates enums and length', () => withDb((db) => {
|
|
87
|
+
assert.throws(() => upsertSessionAnnotation(db, {
|
|
88
|
+
device: 'devbox',
|
|
89
|
+
source: 'Codex CLI',
|
|
90
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
91
|
+
taskType: '随便填'
|
|
92
|
+
}), /taskType must be one of/);
|
|
93
|
+
|
|
94
|
+
assert.throws(() => upsertSessionAnnotation(db, {
|
|
95
|
+
device: 'devbox',
|
|
96
|
+
source: 'Codex CLI',
|
|
97
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
98
|
+
valueLevel: '爆款'
|
|
99
|
+
}), /valueLevel must be one of/);
|
|
100
|
+
|
|
101
|
+
assert.throws(() => upsertSessionAnnotation(db, {
|
|
102
|
+
device: 'devbox',
|
|
103
|
+
source: 'Codex CLI',
|
|
104
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
105
|
+
note: 'x'.repeat(501)
|
|
106
|
+
}), /note must be 500 characters or less/);
|
|
107
|
+
|
|
108
|
+
assert.throws(() => upsertSessionAnnotation(db, {
|
|
109
|
+
device: 'devbox',
|
|
110
|
+
source: 'Codex CLI',
|
|
111
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
112
|
+
annotationSource: 'guessed'
|
|
113
|
+
}), /annotationSource must be one of/);
|
|
114
|
+
|
|
115
|
+
assert.throws(() => upsertSessionAnnotation(db, {
|
|
116
|
+
device: 'devbox',
|
|
117
|
+
source: 'Codex CLI',
|
|
118
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
119
|
+
annotationSource: 'auto',
|
|
120
|
+
annotationConfidence: 101
|
|
121
|
+
}), /annotationConfidence must be an integer/);
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
test('session annotation delete is scoped by identity', () => withDb((db) => {
|
|
125
|
+
upsertSessionAnnotation(db, {
|
|
126
|
+
device: 'devbox',
|
|
127
|
+
source: 'Codex CLI',
|
|
128
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
129
|
+
taskType: '问题修复'
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const deleted = deleteSessionAnnotation(db, {
|
|
133
|
+
device: 'devbox',
|
|
134
|
+
source: 'Codex CLI',
|
|
135
|
+
sessionId: 'local:codex:D:\\Project:codex-mini'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.equal(deleted, 1);
|
|
139
|
+
const remaining = db.prepare('SELECT COUNT(*) AS total FROM session_annotations').get();
|
|
140
|
+
assert.equal(remaining.total, 0);
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
test('auto annotations write provenance, protect manual rows, and undo by run id', () => withDb((db) => {
|
|
144
|
+
const first = applyAutoSessionAnnotations(db, [{
|
|
145
|
+
device: 'devbox',
|
|
146
|
+
source: 'Codex CLI',
|
|
147
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
148
|
+
values: {
|
|
149
|
+
projectAlias: 'Project',
|
|
150
|
+
taskType: '功能开发',
|
|
151
|
+
outputStatus: '已完成',
|
|
152
|
+
workPurpose: '功能开发',
|
|
153
|
+
workStage: '实现',
|
|
154
|
+
valueLevel: '中'
|
|
155
|
+
},
|
|
156
|
+
annotationConfidence: 85,
|
|
157
|
+
annotationReason: '路径和产出链接命中',
|
|
158
|
+
autoVersion: 'v3.15.0'
|
|
159
|
+
}], { runId: 'auto-test', threshold: 80 });
|
|
160
|
+
|
|
161
|
+
assert.equal(first.applied, 1);
|
|
162
|
+
const auto = db.prepare('SELECT annotation_source AS source, annotation_confidence AS confidence, auto_run_id AS runId FROM session_annotations').get();
|
|
163
|
+
assert.equal(auto.source, 'auto');
|
|
164
|
+
assert.equal(auto.confidence, 85);
|
|
165
|
+
assert.equal(auto.runId, 'auto-test');
|
|
166
|
+
|
|
167
|
+
upsertSessionAnnotation(db, {
|
|
168
|
+
device: 'devbox',
|
|
169
|
+
source: 'Codex CLI',
|
|
170
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
171
|
+
taskType: '问题修复'
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const protectedResult = applyAutoSessionAnnotations(db, [{
|
|
175
|
+
device: 'devbox',
|
|
176
|
+
source: 'Codex CLI',
|
|
177
|
+
sessionId: 'local:codex:D:\\Project:codex-mini',
|
|
178
|
+
values: { taskType: '功能开发' },
|
|
179
|
+
annotationConfidence: 90
|
|
180
|
+
}], { runId: 'auto-test-2', threshold: 80 });
|
|
181
|
+
assert.equal(protectedResult.applied, 0);
|
|
182
|
+
assert.equal(protectedResult.skippedProtected, 1);
|
|
183
|
+
|
|
184
|
+
assert.equal(undoAutoSessionAnnotations(db, { runId: 'auto-test' }), 0);
|
|
185
|
+
assert.equal(db.prepare('SELECT task_type AS taskType, annotation_source AS source FROM session_annotations').get().taskType, '问题修复');
|
|
186
|
+
}));
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import {
|
|
7
|
+
batchUpsertSessionAnnotations,
|
|
8
|
+
deleteProjectAliasRule,
|
|
9
|
+
deleteSessionOutput,
|
|
10
|
+
exportAnnotationData,
|
|
11
|
+
importAnnotationData,
|
|
12
|
+
listProjectAliasRules,
|
|
13
|
+
matchProjectAliasRule,
|
|
14
|
+
openDb,
|
|
15
|
+
upsertProjectAliasRule,
|
|
16
|
+
upsertSession,
|
|
17
|
+
upsertSessionAnnotation,
|
|
18
|
+
upsertSessionOutput
|
|
19
|
+
} from '../src/db.mjs';
|
|
20
|
+
|
|
21
|
+
function withDb(fn) {
|
|
22
|
+
const dir = mkdtempSync(join(tmpdir(), 'token-studio-v2-db-'));
|
|
23
|
+
const dbPath = join(dir, 'usage.sqlite');
|
|
24
|
+
const db = openDb(dbPath);
|
|
25
|
+
try {
|
|
26
|
+
seedSessions(db);
|
|
27
|
+
fn(db, dbPath);
|
|
28
|
+
} finally {
|
|
29
|
+
db.close();
|
|
30
|
+
rmSync(dir, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function seedSessions(db) {
|
|
35
|
+
for (const row of [
|
|
36
|
+
{
|
|
37
|
+
device: 'devbox',
|
|
38
|
+
source: 'Codex CLI',
|
|
39
|
+
sessionId: 'codex:one',
|
|
40
|
+
lastActivity: '2026-06-10T01:00:00.000Z',
|
|
41
|
+
projectPath: 'D:\\HighROIProjects\\TokenStudio',
|
|
42
|
+
totalTokens: 500,
|
|
43
|
+
costUSD: 0.05
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
device: 'devbox',
|
|
47
|
+
source: 'Claude Code',
|
|
48
|
+
sessionId: 'claude:two',
|
|
49
|
+
lastActivity: '2026-06-10T02:00:00.000Z',
|
|
50
|
+
projectPath: 'D:\\HighROIProjects\\Other',
|
|
51
|
+
totalTokens: 100,
|
|
52
|
+
costUSD: 0.01
|
|
53
|
+
}
|
|
54
|
+
]) {
|
|
55
|
+
upsertSession(db, row);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test('v2 schema migration is repeatable', () => withDb((db, dbPath) => {
|
|
60
|
+
const reopened = openDb(dbPath);
|
|
61
|
+
try {
|
|
62
|
+
const tables = reopened.prepare(`
|
|
63
|
+
SELECT name FROM sqlite_master
|
|
64
|
+
WHERE type = 'table' AND name IN ('project_alias_rules', 'session_outputs')
|
|
65
|
+
ORDER BY name
|
|
66
|
+
`).all().map(row => row.name);
|
|
67
|
+
assert.deepEqual(tables, ['project_alias_rules', 'session_outputs']);
|
|
68
|
+
const annotationColumns = reopened.prepare('PRAGMA table_info(session_annotations)').all().map(row => row.name);
|
|
69
|
+
const outputColumns = reopened.prepare('PRAGMA table_info(session_outputs)').all().map(row => row.name);
|
|
70
|
+
assert.equal(annotationColumns.includes('work_purpose'), true);
|
|
71
|
+
assert.equal(annotationColumns.includes('work_stage'), true);
|
|
72
|
+
assert.equal(annotationColumns.includes('value_level'), true);
|
|
73
|
+
assert.equal(outputColumns.includes('output_type'), true);
|
|
74
|
+
} finally {
|
|
75
|
+
reopened.close();
|
|
76
|
+
}
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
test('project alias rules match by prefix and can be disabled', () => withDb((db) => {
|
|
80
|
+
const rule = upsertProjectAliasRule(db, {
|
|
81
|
+
pattern: 'D:\\HighROIProjects\\TokenStudio',
|
|
82
|
+
matchType: 'prefix',
|
|
83
|
+
projectAlias: 'Token Studio',
|
|
84
|
+
enabled: true
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
assert.equal(matchProjectAliasRule('D:\\HighROIProjects\\TokenStudio\\src', [rule]), 'Token Studio');
|
|
88
|
+
|
|
89
|
+
const disabled = upsertProjectAliasRule(db, { ...rule, enabled: false });
|
|
90
|
+
assert.equal(matchProjectAliasRule('D:\\HighROIProjects\\TokenStudio\\src', [disabled]), null);
|
|
91
|
+
assert.equal(listProjectAliasRules(db)[0].enabled, false);
|
|
92
|
+
|
|
93
|
+
assert.equal(deleteProjectAliasRule(db, { id: rule.id }), 1);
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
test('session outputs upsert and delete by session identity', () => withDb((db) => {
|
|
97
|
+
const saved = upsertSessionOutput(db, {
|
|
98
|
+
device: 'devbox',
|
|
99
|
+
source: 'Codex CLI',
|
|
100
|
+
sessionId: 'codex:one',
|
|
101
|
+
outputUrl: 'https://github.com/example/repo/pull/42',
|
|
102
|
+
outputLabel: 'PR #42',
|
|
103
|
+
outputType: 'PR'
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
assert.equal(saved.outputUrl, 'https://github.com/example/repo/pull/42');
|
|
107
|
+
assert.equal(saved.outputLabel, 'PR #42');
|
|
108
|
+
assert.equal(saved.outputType, 'PR');
|
|
109
|
+
assert.throws(() => upsertSessionOutput(db, {
|
|
110
|
+
device: 'devbox',
|
|
111
|
+
source: 'Codex CLI',
|
|
112
|
+
sessionId: 'codex:one',
|
|
113
|
+
outputUrl: 'file:///secret.txt'
|
|
114
|
+
}), /http or https/);
|
|
115
|
+
|
|
116
|
+
assert.equal(deleteSessionOutput(db, {
|
|
117
|
+
device: 'devbox',
|
|
118
|
+
source: 'Codex CLI',
|
|
119
|
+
sessionId: 'codex:one'
|
|
120
|
+
}), 1);
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
test('batch annotation only changes selected sessions', () => withDb((db) => {
|
|
124
|
+
const result = batchUpsertSessionAnnotations(db, {
|
|
125
|
+
sessions: [{ device: 'devbox', source: 'Codex CLI', sessionId: 'codex:one' }],
|
|
126
|
+
values: { taskType: '功能开发', outputStatus: '已完成', workPurpose: '调试修复', workStage: '验证', valueLevel: '中', note: '批量标注' }
|
|
127
|
+
});
|
|
128
|
+
assert.equal(result.updated, 1);
|
|
129
|
+
|
|
130
|
+
const rows = db.prepare(`
|
|
131
|
+
SELECT source, task_type AS taskType, output_status AS outputStatus,
|
|
132
|
+
work_purpose AS workPurpose, work_stage AS workStage, value_level AS valueLevel, note
|
|
133
|
+
FROM session_annotations
|
|
134
|
+
ORDER BY source
|
|
135
|
+
`).all();
|
|
136
|
+
assert.equal(rows.length, 1);
|
|
137
|
+
assert.equal(rows[0].source, 'Codex CLI');
|
|
138
|
+
assert.equal(rows[0].taskType, '功能开发');
|
|
139
|
+
assert.equal(rows[0].outputStatus, '已完成');
|
|
140
|
+
assert.equal(rows[0].workPurpose, '调试修复');
|
|
141
|
+
assert.equal(rows[0].workStage, '验证');
|
|
142
|
+
assert.equal(rows[0].valueLevel, '中');
|
|
143
|
+
assert.equal(rows[0].note, '批量标注');
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
test('annotation export and import round-trip v3 data', () => withDb((db) => {
|
|
147
|
+
upsertSessionAnnotation(db, {
|
|
148
|
+
device: 'devbox',
|
|
149
|
+
source: 'Codex CLI',
|
|
150
|
+
sessionId: 'codex:one',
|
|
151
|
+
projectAlias: 'Token Studio',
|
|
152
|
+
taskType: '功能开发',
|
|
153
|
+
outputStatus: '已发布',
|
|
154
|
+
workPurpose: '功能开发',
|
|
155
|
+
workStage: '发布',
|
|
156
|
+
valueLevel: '关键'
|
|
157
|
+
});
|
|
158
|
+
upsertSessionOutput(db, {
|
|
159
|
+
device: 'devbox',
|
|
160
|
+
source: 'Codex CLI',
|
|
161
|
+
sessionId: 'codex:one',
|
|
162
|
+
outputUrl: 'https://example.com/token-studio',
|
|
163
|
+
outputLabel: 'Demo',
|
|
164
|
+
outputType: '部署'
|
|
165
|
+
});
|
|
166
|
+
upsertProjectAliasRule(db, {
|
|
167
|
+
pattern: 'D:\\HighROIProjects\\TokenStudio',
|
|
168
|
+
matchType: 'prefix',
|
|
169
|
+
projectAlias: 'Token Studio',
|
|
170
|
+
enabled: true
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const exported = exportAnnotationData(db);
|
|
174
|
+
assert.equal(exported.version, 3);
|
|
175
|
+
assert.equal(exported.sessionAnnotations.length, 1);
|
|
176
|
+
assert.equal(exported.sessionAnnotations[0].workPurpose, '功能开发');
|
|
177
|
+
assert.equal(exported.sessionAnnotations[0].workStage, '发布');
|
|
178
|
+
assert.equal(exported.sessionAnnotations[0].valueLevel, '关键');
|
|
179
|
+
assert.equal(exported.sessionOutputs.length, 1);
|
|
180
|
+
assert.equal(exported.sessionOutputs[0].outputType, '部署');
|
|
181
|
+
assert.equal(exported.projectAliasRules.length, 1);
|
|
182
|
+
|
|
183
|
+
db.prepare('DELETE FROM session_annotations').run();
|
|
184
|
+
db.prepare('DELETE FROM session_outputs').run();
|
|
185
|
+
db.prepare('DELETE FROM project_alias_rules').run();
|
|
186
|
+
|
|
187
|
+
const imported = importAnnotationData(db, exported);
|
|
188
|
+
assert.deepEqual(imported, {
|
|
189
|
+
sessionAnnotations: 1,
|
|
190
|
+
sessionOutputs: 1,
|
|
191
|
+
projectAliasRules: 1
|
|
192
|
+
});
|
|
193
|
+
assert.equal(db.prepare('SELECT COUNT(*) AS total FROM session_outputs').get().total, 1);
|
|
194
|
+
const importedAnnotation = db.prepare('SELECT work_purpose AS workPurpose, work_stage AS workStage, value_level AS valueLevel FROM session_annotations').get();
|
|
195
|
+
const importedOutput = db.prepare('SELECT output_type AS outputType FROM session_outputs').get();
|
|
196
|
+
assert.equal(importedAnnotation.workPurpose, '功能开发');
|
|
197
|
+
assert.equal(importedAnnotation.workStage, '发布');
|
|
198
|
+
assert.equal(importedAnnotation.valueLevel, '关键');
|
|
199
|
+
assert.equal(importedOutput.outputType, '部署');
|
|
200
|
+
}));
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
deleteAdvisorAction,
|
|
8
|
+
deleteBudgetProfile,
|
|
9
|
+
listAdvisorActions,
|
|
10
|
+
listBudgetProfiles,
|
|
11
|
+
linkWorkItemSessions,
|
|
12
|
+
listTokenEvents,
|
|
13
|
+
listWorkItems,
|
|
14
|
+
openDb,
|
|
15
|
+
upsertAdvisorAction,
|
|
16
|
+
upsertBudgetProfile,
|
|
17
|
+
upsertSession,
|
|
18
|
+
upsertTokenEvent,
|
|
19
|
+
upsertWorkItem
|
|
20
|
+
} from '../src/db.mjs';
|
|
21
|
+
|
|
22
|
+
function tempDb() {
|
|
23
|
+
const dir = mkdtempSync(join(tmpdir(), 'token-studio-roi-v4-'));
|
|
24
|
+
return openDb(join(dir, 'usage.sqlite'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('token_events upsert is idempotent and privacy bounded', () => {
|
|
28
|
+
const db = tempDb();
|
|
29
|
+
upsertTokenEvent(db, {
|
|
30
|
+
eventId: 'evt-1',
|
|
31
|
+
device: 'demo',
|
|
32
|
+
source: 'Codex CLI',
|
|
33
|
+
sessionId: 's1',
|
|
34
|
+
timestamp: '2026-06-17T00:00:00Z',
|
|
35
|
+
model: 'codex-mini',
|
|
36
|
+
inputTokens: 10,
|
|
37
|
+
outputTokens: 3,
|
|
38
|
+
toolCategory: 'edit',
|
|
39
|
+
fileExtension: '.js',
|
|
40
|
+
repoPathHash: 'abc',
|
|
41
|
+
privacyLevel: 'hashed'
|
|
42
|
+
});
|
|
43
|
+
upsertTokenEvent(db, {
|
|
44
|
+
eventId: 'evt-1',
|
|
45
|
+
device: 'demo',
|
|
46
|
+
source: 'Codex CLI',
|
|
47
|
+
sessionId: 's1',
|
|
48
|
+
timestamp: '2026-06-17T00:00:00Z',
|
|
49
|
+
model: 'codex-mini',
|
|
50
|
+
inputTokens: 20,
|
|
51
|
+
outputTokens: 5,
|
|
52
|
+
privacyLevel: 'safe'
|
|
53
|
+
});
|
|
54
|
+
const rows = listTokenEvents(db);
|
|
55
|
+
assert.equal(rows.length, 1);
|
|
56
|
+
assert.equal(rows[0].inputTokens, 20);
|
|
57
|
+
assert.equal(rows[0].privacyLevel, 'safe');
|
|
58
|
+
db.close();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('work items can be created and linked to sessions', () => {
|
|
62
|
+
const db = tempDb();
|
|
63
|
+
upsertSession(db, {
|
|
64
|
+
device: 'demo',
|
|
65
|
+
source: 'Codex CLI',
|
|
66
|
+
sessionId: 's1',
|
|
67
|
+
lastActivity: '2026-06-17',
|
|
68
|
+
totalTokens: 100
|
|
69
|
+
});
|
|
70
|
+
const item = upsertWorkItem(db, {
|
|
71
|
+
title: 'Ship Token Studio ROI',
|
|
72
|
+
projectAlias: 'Token Studio ROI',
|
|
73
|
+
workType: '功能开发',
|
|
74
|
+
status: '已发布',
|
|
75
|
+
valueLevel: '高',
|
|
76
|
+
outputUrl: 'https://example.com/pr/1',
|
|
77
|
+
outputType: 'PR'
|
|
78
|
+
});
|
|
79
|
+
const linked = linkWorkItemSessions(db, {
|
|
80
|
+
workItemId: item.id,
|
|
81
|
+
sessions: [{ device: 'demo', source: 'Codex CLI', sessionId: 's1' }]
|
|
82
|
+
});
|
|
83
|
+
assert.equal(linked.linked, 1);
|
|
84
|
+
const items = listWorkItems(db);
|
|
85
|
+
assert.equal(items.length, 1);
|
|
86
|
+
assert.equal(items[0].sessions.length, 1);
|
|
87
|
+
db.close();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('budget profiles validate custom local budgets', () => {
|
|
91
|
+
const db = tempDb();
|
|
92
|
+
const profile = upsertBudgetProfile(db, {
|
|
93
|
+
source: 'claude',
|
|
94
|
+
label: 'Claude 5h',
|
|
95
|
+
windowMinutes: 300,
|
|
96
|
+
tokenBudget: 500000
|
|
97
|
+
});
|
|
98
|
+
assert.equal(profile.source, 'claude');
|
|
99
|
+
assert.equal(profile.enabled, true);
|
|
100
|
+
assert.equal(listBudgetProfiles(db).length, 1);
|
|
101
|
+
assert.throws(() => upsertBudgetProfile(db, {
|
|
102
|
+
source: 'codex',
|
|
103
|
+
label: 'invalid',
|
|
104
|
+
windowMinutes: 0,
|
|
105
|
+
tokenBudget: 100
|
|
106
|
+
}), /windowMinutes/);
|
|
107
|
+
assert.equal(deleteBudgetProfile(db, { id: profile.id }), 1);
|
|
108
|
+
db.close();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('budget profiles support fixed reset windows and warning thresholds', () => {
|
|
112
|
+
const db = tempDb();
|
|
113
|
+
const profile = upsertBudgetProfile(db, {
|
|
114
|
+
source: 'Codex CLI',
|
|
115
|
+
label: 'Codex fixed 5h',
|
|
116
|
+
windowType: 'fixed',
|
|
117
|
+
windowMinutes: 300,
|
|
118
|
+
resetAnchor: '2026-06-17T00:00:00Z',
|
|
119
|
+
warningThreshold: 0.6,
|
|
120
|
+
tokenBudget: 100000
|
|
121
|
+
});
|
|
122
|
+
assert.equal(profile.windowType, 'fixed');
|
|
123
|
+
assert.equal(profile.resetAnchor, '2026-06-17T00:00:00.000Z');
|
|
124
|
+
assert.equal(profile.warningThreshold, 0.6);
|
|
125
|
+
|
|
126
|
+
const rolling = upsertBudgetProfile(db, {
|
|
127
|
+
id: profile.id,
|
|
128
|
+
source: 'Codex CLI',
|
|
129
|
+
label: 'Codex rolling',
|
|
130
|
+
windowType: 'rolling',
|
|
131
|
+
windowMinutes: 60,
|
|
132
|
+
resetAnchor: '2026-06-17T00:00:00Z',
|
|
133
|
+
warningThreshold: 0.75,
|
|
134
|
+
tokenBudget: 100000
|
|
135
|
+
});
|
|
136
|
+
assert.equal(rolling.windowType, 'rolling');
|
|
137
|
+
assert.equal(rolling.resetAnchor, null);
|
|
138
|
+
assert.throws(() => upsertBudgetProfile(db, {
|
|
139
|
+
source: 'Codex CLI',
|
|
140
|
+
label: 'bad threshold',
|
|
141
|
+
windowType: 'fixed',
|
|
142
|
+
windowMinutes: 300,
|
|
143
|
+
resetAnchor: '2026-06-17T00:00:00Z',
|
|
144
|
+
warningThreshold: 1.5,
|
|
145
|
+
tokenBudget: 1000
|
|
146
|
+
}), /warningThreshold/);
|
|
147
|
+
db.close();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('advisor actions upsert by period and source rule', () => {
|
|
151
|
+
const db = tempDb();
|
|
152
|
+
const first = upsertAdvisorAction(db, {
|
|
153
|
+
periodStart: '2026-06-01',
|
|
154
|
+
periodEnd: '2026-06-07',
|
|
155
|
+
category: '节省模拟',
|
|
156
|
+
title: '测试验证换轻量模型',
|
|
157
|
+
action: '下周测试验证默认先用轻量模型',
|
|
158
|
+
evidence: '2 sessions',
|
|
159
|
+
sourceRule: 'savings:test',
|
|
160
|
+
status: 'open'
|
|
161
|
+
});
|
|
162
|
+
const updated = upsertAdvisorAction(db, {
|
|
163
|
+
periodStart: '2026-06-01',
|
|
164
|
+
periodEnd: '2026-06-07',
|
|
165
|
+
category: '节省模拟',
|
|
166
|
+
title: '测试验证换轻量模型',
|
|
167
|
+
action: '下周测试验证默认先用轻量模型',
|
|
168
|
+
evidence: '2 sessions',
|
|
169
|
+
sourceRule: 'savings:test',
|
|
170
|
+
status: 'done'
|
|
171
|
+
});
|
|
172
|
+
assert.equal(updated.id, first.id);
|
|
173
|
+
assert.equal(updated.status, 'done');
|
|
174
|
+
assert.ok(updated.completedAt);
|
|
175
|
+
assert.equal(listAdvisorActions(db, { periodStart: '2026-06-01', periodEnd: '2026-06-07' }).length, 1);
|
|
176
|
+
assert.equal(deleteAdvisorAction(db, { id: updated.id }), 1);
|
|
177
|
+
db.close();
|
|
178
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { auditStructuredUsage, collectStructuredUsage, normalizeUsageRecord } from '../src/collectors/structured-usage.mjs';
|
|
8
|
+
|
|
9
|
+
const EXPERIMENTAL = ['cursor', 'copilot', 'qwen', 'kimi', 'goose'];
|
|
10
|
+
|
|
11
|
+
test('experimental collector fixtures contain no transcript or full path fields', () => {
|
|
12
|
+
for (const id of EXPERIMENTAL) {
|
|
13
|
+
const text = readFileSync(join('test', 'fixtures', 'collectors', id, 'usage.jsonl'), 'utf8');
|
|
14
|
+
assert.equal(/prompt|response|content|diff|transcript|messages/i.test(text), false, `${id} fixture contains conversation-like fields`);
|
|
15
|
+
assert.equal(/[A-Z]:[\\/]|\/Users\/|\/home\//.test(text), false, `${id} fixture contains full local paths`);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('experimental structured collector imports explicit token rows only', async () => {
|
|
20
|
+
for (const id of EXPERIMENTAL) {
|
|
21
|
+
const result = await collectStructuredUsage({
|
|
22
|
+
clientKey: id,
|
|
23
|
+
roots: [join(process.cwd(), 'test', 'fixtures', 'collectors', id)]
|
|
24
|
+
});
|
|
25
|
+
assert.equal(result.modelsJson.entries.length, 1, `${id} should skip missing-token fixture rows`);
|
|
26
|
+
assert.equal(result.tokenEvents.length, 1, `${id} should emit one token event`);
|
|
27
|
+
assert.equal(result.tokenEvents[0].source, id);
|
|
28
|
+
assert.ok(result.tokenEvents[0].inputTokens + result.tokenEvents[0].outputTokens > 0);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('structured usage normalizer rejects conversation-shaped rows', () => {
|
|
33
|
+
assert.deepEqual(normalizeUsageRecord({
|
|
34
|
+
sessionId: 'unsafe',
|
|
35
|
+
model: 'gpt-5.3-codex',
|
|
36
|
+
prompt: 'do not ingest this',
|
|
37
|
+
inputTokens: 100,
|
|
38
|
+
outputTokens: 20
|
|
39
|
+
}), []);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('structured usage audit counts usable, no-token and conversation-like records', async () => {
|
|
43
|
+
const dir = mkdtempSync(join(tmpdir(), 'token-studio-audit-'));
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(join(dir, 'usage.jsonl'), [
|
|
46
|
+
JSON.stringify({ eventId: 'ok', sessionId: 's1', model: 'gpt-5.3-codex', inputTokens: 100, outputTokens: 20 }),
|
|
47
|
+
JSON.stringify({ eventId: 'no-token', sessionId: 's2', model: 'gpt-5.3-codex' }),
|
|
48
|
+
JSON.stringify({ eventId: 'unsafe', sessionId: 's3', model: 'gpt-5.3-codex', prompt: 'do not read', inputTokens: 100 })
|
|
49
|
+
].join('\n'), 'utf8');
|
|
50
|
+
|
|
51
|
+
const audit = await auditStructuredUsage({ roots: [dir] });
|
|
52
|
+
assert.equal(audit.candidateFiles, 1);
|
|
53
|
+
assert.equal(audit.usableTokenRecords, 1);
|
|
54
|
+
assert.equal(audit.skippedNoTokenRecords, 1);
|
|
55
|
+
assert.equal(audit.skippedConversationLikeRecords, 1);
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(dir, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('collectors audit CLI emits safe summary without full paths', async () => {
|
|
62
|
+
const dir = mkdtempSync(join(tmpdir(), 'token-studio-cli-audit-'));
|
|
63
|
+
const configPath = join(dir, 'collectors.json');
|
|
64
|
+
const root = join(process.cwd(), 'test', 'fixtures', 'collectors');
|
|
65
|
+
writeFileSync(configPath, JSON.stringify({
|
|
66
|
+
collectors: Object.fromEntries(EXPERIMENTAL.map(id => [id, { roots: [join(root, id)] }]))
|
|
67
|
+
}), 'utf8');
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = await runCli(['collectors', '--audit', '--json'], {
|
|
71
|
+
TOKEN_STUDIO_CONFIG: configPath
|
|
72
|
+
});
|
|
73
|
+
assert.equal(result.code, 0, result.stderr);
|
|
74
|
+
const json = JSON.parse(result.stdout);
|
|
75
|
+
assert.equal(json.collectors.length, EXPERIMENTAL.length);
|
|
76
|
+
assert.equal(json.totals.usableTokenRecords, EXPERIMENTAL.length);
|
|
77
|
+
assert.equal(json.totals.skippedNoTokenRecords, EXPERIMENTAL.length);
|
|
78
|
+
assert.equal(json.totals.skippedConversationLikeRecords, 0);
|
|
79
|
+
assert.equal(result.stdout.includes(root), false);
|
|
80
|
+
assert.equal(/[A-Z]:[\\/].*fixtures/.test(result.stdout), false);
|
|
81
|
+
} finally {
|
|
82
|
+
rmSync(dir, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
function runCli(argv, env = {}) {
|
|
87
|
+
return new Promise(resolve => {
|
|
88
|
+
const child = spawn(process.execPath, ['src/cli.mjs', ...argv], {
|
|
89
|
+
cwd: process.cwd(),
|
|
90
|
+
env: { ...process.env, ...env },
|
|
91
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
92
|
+
windowsHide: true
|
|
93
|
+
});
|
|
94
|
+
let stdout = '';
|
|
95
|
+
let stderr = '';
|
|
96
|
+
child.stdout.setEncoding('utf8');
|
|
97
|
+
child.stderr.setEncoding('utf8');
|
|
98
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
99
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
100
|
+
child.on('close', code => resolve({ code, stdout, stderr }));
|
|
101
|
+
child.on('error', error => resolve({ code: 1, stdout, stderr: `${stderr}${error.message}` }));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"eventId":"copilot-1","sessionId":"copilot-session-1","timestamp":"2026-06-17T02:04:00Z","model":"gpt-5.3-codex","project":"token-studio-roi","input_tokens":900,"output_tokens":210,"cache_read_tokens":120,"toolCategory":"terminal","fileExtension":".md"}
|
|
2
|
+
{"eventId":"copilot-skip-no-tokens","sessionId":"copilot-session-2","timestamp":"2026-06-17T02:05:00Z","model":"gpt-5.3-codex","project":"token-studio-roi"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"eventId":"cursor-1","sessionId":"cursor-session-1","timestamp":"2026-06-17T02:00:00Z","model":"gpt-5.3-codex","project":"token-studio-roi","tokens":{"input":1200,"output":340,"cacheRead":800},"toolCategory":"edit","fileExtension":".js"}
|
|
2
|
+
{"eventId":"cursor-skip-no-tokens","sessionId":"cursor-session-2","timestamp":"2026-06-17T02:02:00Z","model":"gpt-5.3-codex","project":"token-studio-roi"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"eventId":"goose-1","sessionId":"goose-session-1","timestamp":"2026-06-17T02:10:00Z","model":"claude-sonnet","project":"token-studio-roi","tokens":{"input":1800,"output":260,"cacheRead":500},"toolCategory":"agent","fileExtension":".ts"}
|
|
2
|
+
{"eventId":"goose-skip-no-tokens","sessionId":"goose-session-2","timestamp":"2026-06-17T02:11:00Z","model":"claude-sonnet","project":"token-studio-roi"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"eventId":"kimi-1","sessionId":"kimi-session-1","timestamp":"2026-06-17T02:08:00Z","model":"kimi-k2","project":"token-studio-roi","inputTokens":1100,"outputTokens":380,"cacheCreationTokens":70,"toolCategory":"review","fileExtension":".jsx"}
|
|
2
|
+
{"eventId":"kimi-skip-no-tokens","sessionId":"kimi-session-2","timestamp":"2026-06-17T02:09:00Z","model":"kimi-k2","project":"token-studio-roi"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"eventId":"qwen-1","sessionId":"qwen-session-1","timestamp":"2026-06-17T02:06:00Z","model":"qwen3-coder","project":"token-studio-roi","usage":{"inputTokens":1500,"outputTokens":420,"reasoningTokens":60},"toolCategory":"research","fileExtension":".json"}
|
|
2
|
+
{"eventId":"qwen-skip-no-tokens","sessionId":"qwen-session-2","timestamp":"2026-06-17T02:07:00Z","model":"qwen3-coder","project":"token-studio-roi"}
|