tlc-claude-code 1.2.26 → 1.2.28
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/dashboard/dist/components/ActivityFeed.d.ts +17 -0
- package/dashboard/dist/components/ActivityFeed.js +42 -0
- package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
- package/dashboard/dist/components/ActivityFeed.test.js +162 -0
- package/dashboard/dist/components/BranchSelector.d.ts +16 -0
- package/dashboard/dist/components/BranchSelector.js +49 -0
- package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
- package/dashboard/dist/components/BranchSelector.test.js +166 -0
- package/dashboard/dist/components/CommandPalette.d.ts +17 -0
- package/dashboard/dist/components/CommandPalette.js +118 -0
- package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
- package/dashboard/dist/components/CommandPalette.test.js +181 -0
- package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
- package/dashboard/dist/components/ConnectionStatus.js +27 -0
- package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
- package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
- package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
- package/dashboard/dist/components/DeviceFrame.js +52 -0
- package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
- package/dashboard/dist/components/DeviceFrame.test.js +118 -0
- package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
- package/dashboard/dist/components/EnvironmentBadge.js +16 -0
- package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
- package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
- package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
- package/dashboard/dist/components/FocusIndicator.js +47 -0
- package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/FocusIndicator.test.js +117 -0
- package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
- package/dashboard/dist/components/KeyboardHelp.js +61 -0
- package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
- package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
- package/dashboard/dist/components/LogSearch.d.ts +13 -0
- package/dashboard/dist/components/LogSearch.js +43 -0
- package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
- package/dashboard/dist/components/LogSearch.test.js +100 -0
- package/dashboard/dist/components/LogStream.d.ts +21 -0
- package/dashboard/dist/components/LogStream.js +123 -0
- package/dashboard/dist/components/LogStream.test.d.ts +1 -0
- package/dashboard/dist/components/LogStream.test.js +159 -0
- package/dashboard/dist/components/PlanView.d.ts +7 -0
- package/dashboard/dist/components/PlanView.js +74 -2
- package/dashboard/dist/components/PlanView.test.js +70 -1
- package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
- package/dashboard/dist/components/PreviewPanel.js +73 -0
- package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
- package/dashboard/dist/components/PreviewPanel.test.js +124 -0
- package/dashboard/dist/components/ProjectCard.d.ts +18 -0
- package/dashboard/dist/components/ProjectCard.js +19 -0
- package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectCard.test.js +53 -0
- package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
- package/dashboard/dist/components/ProjectDetail.js +65 -0
- package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectDetail.test.js +196 -0
- package/dashboard/dist/components/ProjectList.d.ts +11 -0
- package/dashboard/dist/components/ProjectList.js +62 -0
- package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectList.test.js +93 -0
- package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
- package/dashboard/dist/components/SettingsPanel.js +154 -0
- package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
- package/dashboard/dist/components/SettingsPanel.test.js +196 -0
- package/dashboard/dist/components/StatusBar.d.ts +16 -0
- package/dashboard/dist/components/StatusBar.js +47 -0
- package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
- package/dashboard/dist/components/StatusBar.test.js +123 -0
- package/dashboard/dist/components/TaskBoard.d.ts +22 -0
- package/dashboard/dist/components/TaskBoard.js +102 -0
- package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskBoard.test.js +113 -0
- package/dashboard/dist/components/TaskCard.d.ts +17 -0
- package/dashboard/dist/components/TaskCard.js +29 -0
- package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskCard.test.js +109 -0
- package/dashboard/dist/components/TaskDetail.d.ts +36 -0
- package/dashboard/dist/components/TaskDetail.js +41 -0
- package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
- package/dashboard/dist/components/TaskDetail.test.js +164 -0
- package/dashboard/dist/components/TaskFilter.d.ts +12 -0
- package/dashboard/dist/components/TaskFilter.js +138 -0
- package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
- package/dashboard/dist/components/TaskFilter.test.js +109 -0
- package/dashboard/dist/components/TeamPanel.d.ts +15 -0
- package/dashboard/dist/components/TeamPanel.js +24 -0
- package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPanel.test.js +109 -0
- package/dashboard/dist/components/TeamPresence.d.ts +14 -0
- package/dashboard/dist/components/TeamPresence.js +31 -0
- package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPresence.test.js +144 -0
- package/dashboard/dist/components/layout/Header.d.ts +9 -0
- package/dashboard/dist/components/layout/Header.js +11 -0
- package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Header.test.js +35 -0
- package/dashboard/dist/components/layout/Shell.d.ts +10 -0
- package/dashboard/dist/components/layout/Shell.js +5 -0
- package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Shell.test.js +34 -0
- package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
- package/dashboard/dist/components/layout/Sidebar.js +8 -0
- package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
- package/dashboard/dist/components/ui/Badge.d.ts +9 -0
- package/dashboard/dist/components/ui/Badge.js +13 -0
- package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Badge.test.js +69 -0
- package/dashboard/dist/components/ui/Button.d.ts +12 -0
- package/dashboard/dist/components/ui/Button.js +14 -0
- package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Button.test.js +81 -0
- package/dashboard/dist/components/ui/Card.d.ts +21 -0
- package/dashboard/dist/components/ui/Card.js +20 -0
- package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Card.test.js +82 -0
- package/dashboard/dist/components/ui/Input.d.ts +13 -0
- package/dashboard/dist/components/ui/Input.js +8 -0
- package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Input.test.js +68 -0
- package/dashboard/dist/styles/tokens.d.ts +150 -0
- package/dashboard/dist/styles/tokens.js +184 -0
- package/dashboard/dist/styles/tokens.test.d.ts +1 -0
- package/dashboard/dist/styles/tokens.test.js +95 -0
- package/dashboard/dist/test/setup.d.ts +1 -0
- package/dashboard/dist/test/setup.js +1 -0
- package/dashboard/package.json +3 -0
- package/package.json +1 -1
- package/server/dashboard/index.html +157 -2
- package/server/index.js +38 -21
- package/server/lib/adapters/base-adapter.js +114 -0
- package/server/lib/adapters/base-adapter.test.js +90 -0
- package/server/lib/adapters/claude-adapter.js +141 -0
- package/server/lib/adapters/claude-adapter.test.js +180 -0
- package/server/lib/adapters/deepseek-adapter.js +153 -0
- package/server/lib/adapters/deepseek-adapter.test.js +193 -0
- package/server/lib/adapters/openai-adapter.js +190 -0
- package/server/lib/adapters/openai-adapter.test.js +231 -0
- package/server/lib/budget-tracker.js +169 -0
- package/server/lib/budget-tracker.test.js +165 -0
- package/server/lib/claude-injector.js +85 -0
- package/server/lib/claude-injector.test.js +161 -0
- package/server/lib/consensus-engine.js +135 -0
- package/server/lib/consensus-engine.test.js +152 -0
- package/server/lib/context-builder.js +112 -0
- package/server/lib/context-builder.test.js +120 -0
- package/server/lib/file-collector.js +322 -0
- package/server/lib/file-collector.test.js +307 -0
- package/server/lib/memory-classifier.js +175 -0
- package/server/lib/memory-classifier.test.js +169 -0
- package/server/lib/memory-committer.js +138 -0
- package/server/lib/memory-committer.test.js +136 -0
- package/server/lib/memory-hooks.js +127 -0
- package/server/lib/memory-hooks.test.js +136 -0
- package/server/lib/memory-init.js +104 -0
- package/server/lib/memory-init.test.js +119 -0
- package/server/lib/memory-observer.js +149 -0
- package/server/lib/memory-observer.test.js +158 -0
- package/server/lib/memory-reader.js +243 -0
- package/server/lib/memory-reader.test.js +216 -0
- package/server/lib/memory-storage.js +120 -0
- package/server/lib/memory-storage.test.js +136 -0
- package/server/lib/memory-writer.js +176 -0
- package/server/lib/memory-writer.test.js +231 -0
- package/server/lib/overdrive-command.js +30 -6
- package/server/lib/overdrive-command.test.js +8 -1
- package/server/lib/pattern-detector.js +216 -0
- package/server/lib/pattern-detector.test.js +241 -0
- package/server/lib/relevance-scorer.js +175 -0
- package/server/lib/relevance-scorer.test.js +107 -0
- package/server/lib/review-command.js +238 -0
- package/server/lib/review-command.test.js +245 -0
- package/server/lib/review-orchestrator.js +273 -0
- package/server/lib/review-orchestrator.test.js +300 -0
- package/server/lib/review-reporter.js +288 -0
- package/server/lib/review-reporter.test.js +240 -0
- package/server/lib/session-summary.js +90 -0
- package/server/lib/session-summary.test.js +156 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { OpenAIAdapter, OPENAI_PRICING, OPENAI_MODEL, DEFAULT_RATE_LIMITS } from './openai-adapter.js';
|
|
3
|
+
import { BaseAdapter } from './base-adapter.js';
|
|
4
|
+
|
|
5
|
+
describe('OpenAIAdapter', () => {
|
|
6
|
+
describe('constructor', () => {
|
|
7
|
+
it('sets name to openai by default', () => {
|
|
8
|
+
const adapter = new OpenAIAdapter();
|
|
9
|
+
expect(adapter.name).toBe('openai');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('initializes rate limit counters', () => {
|
|
13
|
+
const adapter = new OpenAIAdapter();
|
|
14
|
+
expect(adapter.requestsThisMinute).toBe(0);
|
|
15
|
+
expect(adapter.tokensThisMinute).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('accepts custom rate limits', () => {
|
|
19
|
+
const adapter = new OpenAIAdapter({
|
|
20
|
+
rateLimits: { requestsPerMinute: 100, tokensPerMinute: 50000 },
|
|
21
|
+
});
|
|
22
|
+
expect(adapter.rateLimits.requestsPerMinute).toBe(100);
|
|
23
|
+
expect(adapter.rateLimits.tokensPerMinute).toBe(50000);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('review', () => {
|
|
28
|
+
it('returns empty response for empty code', async () => {
|
|
29
|
+
const adapter = new OpenAIAdapter();
|
|
30
|
+
const result = await adapter.review('');
|
|
31
|
+
|
|
32
|
+
expect(result.issues).toEqual([]);
|
|
33
|
+
expect(result.model).toBe('openai');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns standardized response format', async () => {
|
|
37
|
+
const adapter = new OpenAIAdapter();
|
|
38
|
+
const result = await adapter.review('const x = 1;');
|
|
39
|
+
|
|
40
|
+
expect(BaseAdapter.validateResponse(result)).toBe(true);
|
|
41
|
+
expect(result.model).toBe('openai');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('throws when budget exceeded', async () => {
|
|
45
|
+
const mockTracker = {
|
|
46
|
+
canSpend: vi.fn(() => false),
|
|
47
|
+
record: vi.fn(),
|
|
48
|
+
getUsage: vi.fn(() => ({ daily: 10, monthly: 100, requests: 50 })),
|
|
49
|
+
};
|
|
50
|
+
const adapter = new OpenAIAdapter({ budgetTracker: mockTracker });
|
|
51
|
+
|
|
52
|
+
await expect(adapter.review('const x = 1;')).rejects.toThrow('Budget exceeded');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('throws when rate limit exceeded', async () => {
|
|
56
|
+
const adapter = new OpenAIAdapter({
|
|
57
|
+
rateLimits: { requestsPerMinute: 1, tokensPerMinute: 10 },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// First request succeeds
|
|
61
|
+
await adapter.review('const x = 1;');
|
|
62
|
+
|
|
63
|
+
// Second request exceeds rate limit
|
|
64
|
+
await expect(adapter.review('const y = 2;')).rejects.toThrow('Rate limit exceeded');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('records cost after successful review', async () => {
|
|
68
|
+
const mockTracker = {
|
|
69
|
+
canSpend: vi.fn(() => true),
|
|
70
|
+
record: vi.fn(),
|
|
71
|
+
getUsage: vi.fn(() => ({ daily: 0, monthly: 0, requests: 0 })),
|
|
72
|
+
};
|
|
73
|
+
const adapter = new OpenAIAdapter({ budgetTracker: mockTracker });
|
|
74
|
+
|
|
75
|
+
await adapter.review('const x = 1;');
|
|
76
|
+
|
|
77
|
+
expect(mockTracker.record).toHaveBeenCalledWith('openai', expect.any(Number));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('increments rate limit counters', async () => {
|
|
81
|
+
const adapter = new OpenAIAdapter();
|
|
82
|
+
|
|
83
|
+
await adapter.review('const x = 1;');
|
|
84
|
+
|
|
85
|
+
expect(adapter.requestsThisMinute).toBe(1);
|
|
86
|
+
expect(adapter.tokensThisMinute).toBeGreaterThan(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('handles API errors gracefully', async () => {
|
|
90
|
+
const adapter = new OpenAIAdapter();
|
|
91
|
+
adapter.callAPI = vi.fn().mockRejectedValue(new Error('API error'));
|
|
92
|
+
|
|
93
|
+
const result = await adapter.review('const x = 1;');
|
|
94
|
+
|
|
95
|
+
expect(result.error).toBe('API error');
|
|
96
|
+
expect(result.issues).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('rate limiting', () => {
|
|
101
|
+
it('resets counters after minute window', () => {
|
|
102
|
+
const adapter = new OpenAIAdapter();
|
|
103
|
+
adapter.requestsThisMinute = 100;
|
|
104
|
+
adapter.tokensThisMinute = 50000;
|
|
105
|
+
adapter.lastMinuteReset = Date.now() - 61000; // 61 seconds ago
|
|
106
|
+
|
|
107
|
+
adapter.checkRateLimitReset();
|
|
108
|
+
|
|
109
|
+
expect(adapter.requestsThisMinute).toBe(0);
|
|
110
|
+
expect(adapter.tokensThisMinute).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not reset within minute window', () => {
|
|
114
|
+
const adapter = new OpenAIAdapter();
|
|
115
|
+
adapter.requestsThisMinute = 100;
|
|
116
|
+
adapter.lastMinuteReset = Date.now() - 30000; // 30 seconds ago
|
|
117
|
+
|
|
118
|
+
adapter.checkRateLimitReset();
|
|
119
|
+
|
|
120
|
+
expect(adapter.requestsThisMinute).toBe(100);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('withinRateLimits checks both requests and tokens', () => {
|
|
124
|
+
const adapter = new OpenAIAdapter({
|
|
125
|
+
rateLimits: { requestsPerMinute: 10, tokensPerMinute: 1000 },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Within limits
|
|
129
|
+
expect(adapter.withinRateLimits(500)).toBe(true);
|
|
130
|
+
|
|
131
|
+
// Exceed token limit
|
|
132
|
+
expect(adapter.withinRateLimits(1500)).toBe(false);
|
|
133
|
+
|
|
134
|
+
// Exceed request limit
|
|
135
|
+
adapter.requestsThisMinute = 10;
|
|
136
|
+
expect(adapter.withinRateLimits(100)).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('estimateCost', () => {
|
|
141
|
+
it('calculates cost based on token count', () => {
|
|
142
|
+
const adapter = new OpenAIAdapter();
|
|
143
|
+
const cost = adapter.estimateCost(1000);
|
|
144
|
+
|
|
145
|
+
// At default pricing (o3): (500 * 10 + 500 * 40) / 1M = 0.025
|
|
146
|
+
expect(cost).toBeCloseTo(0.025, 4);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('uses custom pricing if provided', () => {
|
|
150
|
+
const adapter = new OpenAIAdapter({
|
|
151
|
+
pricing: { inputPerMillion: 5.00, outputPerMillion: 15.00 },
|
|
152
|
+
});
|
|
153
|
+
const cost = adapter.estimateCost(1000);
|
|
154
|
+
|
|
155
|
+
// (500 * 5 + 500 * 15) / 1M = 0.01
|
|
156
|
+
expect(cost).toBeCloseTo(0.01, 4);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('canAfford', () => {
|
|
161
|
+
it('returns true without budget tracker', () => {
|
|
162
|
+
const adapter = new OpenAIAdapter();
|
|
163
|
+
expect(adapter.canAfford(100)).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('checks budget tracker when available', () => {
|
|
167
|
+
const mockTracker = {
|
|
168
|
+
canSpend: vi.fn(() => true),
|
|
169
|
+
};
|
|
170
|
+
const adapter = new OpenAIAdapter({
|
|
171
|
+
budgetTracker: mockTracker,
|
|
172
|
+
budget: { budgetDaily: 10, budgetMonthly: 100 },
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
adapter.canAfford(0.5);
|
|
176
|
+
|
|
177
|
+
expect(mockTracker.canSpend).toHaveBeenCalledWith('openai', 0.5, expect.any(Object));
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('getUsage', () => {
|
|
182
|
+
it('returns zero without budget tracker', () => {
|
|
183
|
+
const adapter = new OpenAIAdapter();
|
|
184
|
+
expect(adapter.getUsage()).toEqual({ daily: 0, monthly: 0, requests: 0 });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('returns tracker usage when available', () => {
|
|
188
|
+
const mockTracker = {
|
|
189
|
+
getUsage: vi.fn(() => ({ daily: 5.00, monthly: 50.00, requests: 25 })),
|
|
190
|
+
};
|
|
191
|
+
const adapter = new OpenAIAdapter({ budgetTracker: mockTracker });
|
|
192
|
+
|
|
193
|
+
const usage = adapter.getUsage();
|
|
194
|
+
|
|
195
|
+
expect(usage.daily).toBe(5.00);
|
|
196
|
+
expect(usage.requests).toBe(25);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('getRateLimitStatus', () => {
|
|
201
|
+
it('returns current rate limit status', () => {
|
|
202
|
+
const adapter = new OpenAIAdapter();
|
|
203
|
+
adapter.requestsThisMinute = 5;
|
|
204
|
+
adapter.tokensThisMinute = 1000;
|
|
205
|
+
|
|
206
|
+
const status = adapter.getRateLimitStatus();
|
|
207
|
+
|
|
208
|
+
expect(status.requestsUsed).toBe(5);
|
|
209
|
+
expect(status.requestsLimit).toBe(DEFAULT_RATE_LIMITS.requestsPerMinute);
|
|
210
|
+
expect(status.tokensUsed).toBe(1000);
|
|
211
|
+
expect(status.tokensLimit).toBe(DEFAULT_RATE_LIMITS.tokensPerMinute);
|
|
212
|
+
expect(typeof status.resetsIn).toBe('number');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('exports', () => {
|
|
217
|
+
it('exports OPENAI_PRICING for o3', () => {
|
|
218
|
+
expect(OPENAI_PRICING.inputPerMillion).toBe(10.00);
|
|
219
|
+
expect(OPENAI_PRICING.outputPerMillion).toBe(40.00);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('exports OPENAI_MODEL', () => {
|
|
223
|
+
expect(OPENAI_MODEL).toBe('o3');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('exports DEFAULT_RATE_LIMITS', () => {
|
|
227
|
+
expect(DEFAULT_RATE_LIMITS.requestsPerMinute).toBe(500);
|
|
228
|
+
expect(DEFAULT_RATE_LIMITS.tokensPerMinute).toBe(150000);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget Tracker - Track and limit API spending across models
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
class BudgetTracker {
|
|
9
|
+
constructor(configPath = '.tlc/usage.json') {
|
|
10
|
+
this.configPath = configPath;
|
|
11
|
+
this.usage = {};
|
|
12
|
+
this.load();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load usage from file
|
|
17
|
+
*/
|
|
18
|
+
load() {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(this.configPath)) {
|
|
21
|
+
const data = fs.readFileSync(this.configPath, 'utf-8');
|
|
22
|
+
this.usage = JSON.parse(data);
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('Failed to load usage:', err.message);
|
|
26
|
+
this.usage = {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Initialize missing models
|
|
30
|
+
this.initializeModel('openai');
|
|
31
|
+
this.initializeModel('deepseek');
|
|
32
|
+
|
|
33
|
+
this.checkResets();
|
|
34
|
+
this.save();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize model with default values
|
|
39
|
+
*/
|
|
40
|
+
initializeModel(model) {
|
|
41
|
+
if (!this.usage[model]) {
|
|
42
|
+
this.usage[model] = {
|
|
43
|
+
daily: 0,
|
|
44
|
+
monthly: 0,
|
|
45
|
+
requests: 0,
|
|
46
|
+
lastDailyReset: new Date().toISOString(),
|
|
47
|
+
lastMonthlyReset: new Date().toISOString(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Save usage to file
|
|
54
|
+
*/
|
|
55
|
+
save() {
|
|
56
|
+
try {
|
|
57
|
+
const dir = path.dirname(this.configPath);
|
|
58
|
+
if (!fs.existsSync(dir)) {
|
|
59
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
fs.writeFileSync(this.configPath, JSON.stringify(this.usage, null, 2), 'utf-8');
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('Failed to save usage:', err.message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check and perform resets if needed
|
|
69
|
+
*/
|
|
70
|
+
checkResets() {
|
|
71
|
+
const now = new Date();
|
|
72
|
+
|
|
73
|
+
for (const model of Object.keys(this.usage)) {
|
|
74
|
+
const usage = this.usage[model];
|
|
75
|
+
|
|
76
|
+
// Daily reset
|
|
77
|
+
const lastDaily = new Date(usage.lastDailyReset);
|
|
78
|
+
if (lastDaily.toDateString() !== now.toDateString()) {
|
|
79
|
+
usage.daily = 0;
|
|
80
|
+
usage.lastDailyReset = now.toISOString();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Monthly reset
|
|
84
|
+
const lastMonthly = new Date(usage.lastMonthlyReset);
|
|
85
|
+
if (lastMonthly.getMonth() !== now.getMonth() || lastMonthly.getFullYear() !== now.getFullYear()) {
|
|
86
|
+
usage.monthly = 0;
|
|
87
|
+
usage.lastMonthlyReset = now.toISOString();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if can spend amount
|
|
94
|
+
* @param {string} model - Model name
|
|
95
|
+
* @param {number} amount - Amount in USD
|
|
96
|
+
* @param {Object} config - Budget config { budgetDaily, budgetMonthly }
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
canSpend(model, amount, config) {
|
|
100
|
+
this.initializeModel(model);
|
|
101
|
+
const usage = this.usage[model];
|
|
102
|
+
|
|
103
|
+
const withinDaily = (usage.daily + amount) <= config.budgetDaily;
|
|
104
|
+
const withinMonthly = (usage.monthly + amount) <= config.budgetMonthly;
|
|
105
|
+
|
|
106
|
+
return withinDaily && withinMonthly;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Record spending
|
|
111
|
+
* @param {string} model - Model name
|
|
112
|
+
* @param {number} amount - Amount in USD
|
|
113
|
+
*/
|
|
114
|
+
record(model, amount) {
|
|
115
|
+
this.initializeModel(model);
|
|
116
|
+
this.usage[model].daily += amount;
|
|
117
|
+
this.usage[model].monthly += amount;
|
|
118
|
+
this.usage[model].requests = (this.usage[model].requests || 0) + 1;
|
|
119
|
+
this.save();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if should alert about budget
|
|
124
|
+
* @param {string} model - Model name
|
|
125
|
+
* @param {Object} config - Budget config { budgetDaily, alertThreshold }
|
|
126
|
+
* @returns {boolean}
|
|
127
|
+
*/
|
|
128
|
+
shouldAlert(model, config) {
|
|
129
|
+
this.initializeModel(model);
|
|
130
|
+
const usage = this.usage[model];
|
|
131
|
+
const percentUsed = usage.daily / config.budgetDaily;
|
|
132
|
+
return percentUsed >= (config.alertThreshold || 0.8);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get usage for model
|
|
137
|
+
* @param {string} model - Model name
|
|
138
|
+
* @returns {Object} Usage { daily, monthly, requests }
|
|
139
|
+
*/
|
|
140
|
+
getUsage(model) {
|
|
141
|
+
if (!this.usage[model]) {
|
|
142
|
+
return { daily: 0, monthly: 0, requests: 0 };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
daily: this.usage[model].daily || 0,
|
|
146
|
+
monthly: this.usage[model].monthly || 0,
|
|
147
|
+
requests: this.usage[model].requests || 0,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Reset usage for model (admin)
|
|
153
|
+
* @param {string} model - Model name
|
|
154
|
+
*/
|
|
155
|
+
reset(model) {
|
|
156
|
+
this.usage[model] = {
|
|
157
|
+
daily: 0,
|
|
158
|
+
monthly: 0,
|
|
159
|
+
requests: 0,
|
|
160
|
+
lastDailyReset: new Date().toISOString(),
|
|
161
|
+
lastMonthlyReset: new Date().toISOString(),
|
|
162
|
+
};
|
|
163
|
+
this.save();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
BudgetTracker,
|
|
169
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { BudgetTracker } from './budget-tracker.js';
|
|
6
|
+
|
|
7
|
+
describe('BudgetTracker', () => {
|
|
8
|
+
let testDir;
|
|
9
|
+
let tracker;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-budget-test-'));
|
|
13
|
+
tracker = new BudgetTracker(path.join(testDir, '.tlc', 'usage.json'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('initialization', () => {
|
|
21
|
+
it('creates usage file if not exists', () => {
|
|
22
|
+
expect(fs.existsSync(tracker.configPath)).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('loads existing usage', () => {
|
|
26
|
+
const initialData = {
|
|
27
|
+
openai: { daily: 2.50, monthly: 25.00, lastDailyReset: new Date().toISOString() },
|
|
28
|
+
};
|
|
29
|
+
fs.mkdirSync(path.dirname(tracker.configPath), { recursive: true });
|
|
30
|
+
fs.writeFileSync(tracker.configPath, JSON.stringify(initialData));
|
|
31
|
+
|
|
32
|
+
const newTracker = new BudgetTracker(tracker.configPath);
|
|
33
|
+
expect(newTracker.usage.openai.daily).toBe(2.50);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('canSpend', () => {
|
|
38
|
+
it('allows spending within daily budget', () => {
|
|
39
|
+
const config = { budgetDaily: 5.00, budgetMonthly: 50.00 };
|
|
40
|
+
tracker.usage.openai = { daily: 2.00, monthly: 10.00 };
|
|
41
|
+
|
|
42
|
+
expect(tracker.canSpend('openai', 1.00, config)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('blocks spending over daily budget', () => {
|
|
46
|
+
const config = { budgetDaily: 5.00, budgetMonthly: 50.00 };
|
|
47
|
+
tracker.usage.openai = { daily: 4.50, monthly: 10.00 };
|
|
48
|
+
|
|
49
|
+
expect(tracker.canSpend('openai', 1.00, config)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('blocks spending over monthly budget', () => {
|
|
53
|
+
const config = { budgetDaily: 5.00, budgetMonthly: 50.00 };
|
|
54
|
+
tracker.usage.openai = { daily: 0.00, monthly: 49.50 };
|
|
55
|
+
|
|
56
|
+
expect(tracker.canSpend('openai', 1.00, config)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('record', () => {
|
|
61
|
+
it('records spending', () => {
|
|
62
|
+
tracker.record('openai', 1.50);
|
|
63
|
+
|
|
64
|
+
expect(tracker.usage.openai.daily).toBe(1.50);
|
|
65
|
+
expect(tracker.usage.openai.monthly).toBe(1.50);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('accumulates spending', () => {
|
|
69
|
+
tracker.record('openai', 1.00);
|
|
70
|
+
tracker.record('openai', 0.50);
|
|
71
|
+
|
|
72
|
+
expect(tracker.usage.openai.daily).toBe(1.50);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('persists to file', () => {
|
|
76
|
+
tracker.record('openai', 2.00);
|
|
77
|
+
|
|
78
|
+
const loaded = JSON.parse(fs.readFileSync(tracker.configPath, 'utf-8'));
|
|
79
|
+
expect(loaded.openai.daily).toBe(2.00);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('daily reset', () => {
|
|
84
|
+
it('resets daily at midnight', () => {
|
|
85
|
+
tracker.usage.openai = {
|
|
86
|
+
daily: 5.00,
|
|
87
|
+
monthly: 20.00,
|
|
88
|
+
lastDailyReset: new Date('2026-01-30').toISOString(),
|
|
89
|
+
lastMonthlyReset: new Date('2026-01-01').toISOString(), // Same month
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Simulate new day (same month)
|
|
93
|
+
vi.setSystemTime(new Date('2026-01-31T10:00:00'));
|
|
94
|
+
tracker.checkResets();
|
|
95
|
+
|
|
96
|
+
expect(tracker.usage.openai.daily).toBe(0);
|
|
97
|
+
expect(tracker.usage.openai.monthly).toBe(20.00); // Monthly preserved
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('does not reset if same day', () => {
|
|
101
|
+
const now = new Date('2026-01-31T10:00:00');
|
|
102
|
+
vi.setSystemTime(now);
|
|
103
|
+
|
|
104
|
+
tracker.usage.openai = {
|
|
105
|
+
daily: 3.00,
|
|
106
|
+
monthly: 15.00,
|
|
107
|
+
lastDailyReset: now.toISOString(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
tracker.checkResets();
|
|
111
|
+
|
|
112
|
+
expect(tracker.usage.openai.daily).toBe(3.00);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('monthly reset', () => {
|
|
117
|
+
it('resets monthly at month start', () => {
|
|
118
|
+
tracker.usage.openai = {
|
|
119
|
+
daily: 5.00,
|
|
120
|
+
monthly: 45.00,
|
|
121
|
+
lastMonthlyReset: new Date('2025-12-15').toISOString(),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Simulate new month
|
|
125
|
+
vi.setSystemTime(new Date('2026-01-15'));
|
|
126
|
+
tracker.checkResets();
|
|
127
|
+
|
|
128
|
+
expect(tracker.usage.openai.monthly).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('shouldAlert', () => {
|
|
133
|
+
it('returns true at threshold', () => {
|
|
134
|
+
tracker.usage.openai = { daily: 4.00, monthly: 0 };
|
|
135
|
+
const config = { budgetDaily: 5.00, alertThreshold: 0.8 };
|
|
136
|
+
|
|
137
|
+
expect(tracker.shouldAlert('openai', config)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns false below threshold', () => {
|
|
141
|
+
tracker.usage.openai = { daily: 2.00, monthly: 0 };
|
|
142
|
+
const config = { budgetDaily: 5.00, alertThreshold: 0.8 };
|
|
143
|
+
|
|
144
|
+
expect(tracker.shouldAlert('openai', config)).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('getUsage', () => {
|
|
149
|
+
it('returns usage for model', () => {
|
|
150
|
+
tracker.usage.openai = { daily: 2.50, monthly: 25.00, requests: 10 };
|
|
151
|
+
|
|
152
|
+
const usage = tracker.getUsage('openai');
|
|
153
|
+
|
|
154
|
+
expect(usage.daily).toBe(2.50);
|
|
155
|
+
expect(usage.monthly).toBe(25.00);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns zero for unknown model', () => {
|
|
159
|
+
const usage = tracker.getUsage('unknown');
|
|
160
|
+
|
|
161
|
+
expect(usage.daily).toBe(0);
|
|
162
|
+
expect(usage.monthly).toBe(0);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLAUDE.md Injector - Inject memory context into CLAUDE.md
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs').promises;
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Markers for the auto-generated memory section
|
|
10
|
+
*/
|
|
11
|
+
const MEMORY_SECTION_MARKERS = {
|
|
12
|
+
START: '<!-- TLC-MEMORY-START -->',
|
|
13
|
+
END: '<!-- TLC-MEMORY-END -->',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract memory section content from CLAUDE.md
|
|
18
|
+
* @param {string} content - CLAUDE.md content
|
|
19
|
+
* @returns {string|null} Extracted memory content or null if not found
|
|
20
|
+
*/
|
|
21
|
+
function extractMemorySection(content) {
|
|
22
|
+
if (!content) return null;
|
|
23
|
+
|
|
24
|
+
const startIdx = content.indexOf(MEMORY_SECTION_MARKERS.START);
|
|
25
|
+
const endIdx = content.indexOf(MEMORY_SECTION_MARKERS.END);
|
|
26
|
+
|
|
27
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const start = startIdx + MEMORY_SECTION_MARKERS.START.length;
|
|
32
|
+
return content.slice(start, endIdx).trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Inject memory context into CLAUDE.md
|
|
37
|
+
* @param {string} projectRoot - Project root directory
|
|
38
|
+
* @param {string} memoryContext - Memory context to inject
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
async function injectMemoryContext(projectRoot, memoryContext) {
|
|
42
|
+
const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
|
|
43
|
+
|
|
44
|
+
let existingContent = '';
|
|
45
|
+
try {
|
|
46
|
+
existingContent = await fs.readFile(claudeMdPath, 'utf-8');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (err.code !== 'ENOENT') throw err;
|
|
49
|
+
// File doesn't exist, will create new
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Build the new memory section
|
|
53
|
+
const memorySection = `${MEMORY_SECTION_MARKERS.START}
|
|
54
|
+
${memoryContext}
|
|
55
|
+
${MEMORY_SECTION_MARKERS.END}`;
|
|
56
|
+
|
|
57
|
+
let newContent;
|
|
58
|
+
|
|
59
|
+
// Check if memory section already exists
|
|
60
|
+
const startIdx = existingContent.indexOf(MEMORY_SECTION_MARKERS.START);
|
|
61
|
+
const endIdx = existingContent.indexOf(MEMORY_SECTION_MARKERS.END);
|
|
62
|
+
|
|
63
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
64
|
+
// Replace existing section
|
|
65
|
+
const before = existingContent.slice(0, startIdx);
|
|
66
|
+
const after = existingContent.slice(endIdx + MEMORY_SECTION_MARKERS.END.length);
|
|
67
|
+
newContent = before + memorySection + after;
|
|
68
|
+
} else {
|
|
69
|
+
// Append new section
|
|
70
|
+
const trimmed = existingContent.trim();
|
|
71
|
+
if (trimmed) {
|
|
72
|
+
newContent = trimmed + '\n\n' + memorySection + '\n';
|
|
73
|
+
} else {
|
|
74
|
+
newContent = memorySection + '\n';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await fs.writeFile(claudeMdPath, newContent, 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
injectMemoryContext,
|
|
83
|
+
extractMemorySection,
|
|
84
|
+
MEMORY_SECTION_MARKERS,
|
|
85
|
+
};
|