@vibescope/mcp-server 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +194 -138
  3. package/dist/api-client.d.ts +276 -8
  4. package/dist/api-client.js +123 -8
  5. package/dist/cli.d.ts +6 -3
  6. package/dist/cli.js +28 -10
  7. package/dist/handlers/blockers.d.ts +11 -0
  8. package/dist/handlers/blockers.js +37 -2
  9. package/dist/handlers/bodies-of-work.d.ts +2 -0
  10. package/dist/handlers/bodies-of-work.js +30 -1
  11. package/dist/handlers/connectors.js +2 -2
  12. package/dist/handlers/decisions.d.ts +11 -0
  13. package/dist/handlers/decisions.js +37 -2
  14. package/dist/handlers/deployment.d.ts +6 -0
  15. package/dist/handlers/deployment.js +33 -5
  16. package/dist/handlers/discovery.js +27 -11
  17. package/dist/handlers/fallback.js +12 -6
  18. package/dist/handlers/file-checkouts.d.ts +1 -0
  19. package/dist/handlers/file-checkouts.js +17 -2
  20. package/dist/handlers/findings.d.ts +5 -0
  21. package/dist/handlers/findings.js +19 -2
  22. package/dist/handlers/git-issues.js +4 -2
  23. package/dist/handlers/ideas.d.ts +5 -0
  24. package/dist/handlers/ideas.js +19 -2
  25. package/dist/handlers/progress.js +2 -2
  26. package/dist/handlers/project.d.ts +1 -0
  27. package/dist/handlers/project.js +35 -2
  28. package/dist/handlers/requests.js +6 -3
  29. package/dist/handlers/roles.js +13 -2
  30. package/dist/handlers/session.d.ts +12 -0
  31. package/dist/handlers/session.js +288 -25
  32. package/dist/handlers/sprints.d.ts +2 -0
  33. package/dist/handlers/sprints.js +30 -1
  34. package/dist/handlers/tasks.d.ts +25 -2
  35. package/dist/handlers/tasks.js +228 -35
  36. package/dist/handlers/tool-docs.js +834 -767
  37. package/dist/index.js +73 -73
  38. package/dist/knowledge.d.ts +6 -0
  39. package/dist/knowledge.js +218 -0
  40. package/dist/setup.d.ts +22 -0
  41. package/dist/setup.js +313 -0
  42. package/dist/templates/agent-guidelines.d.ts +18 -0
  43. package/dist/templates/agent-guidelines.js +207 -0
  44. package/dist/tools.js +527 -174
  45. package/dist/utils.d.ts +5 -2
  46. package/dist/utils.js +101 -62
  47. package/docs/TOOLS.md +2053 -2053
  48. package/package.json +51 -46
  49. package/scripts/generate-docs.ts +212 -212
  50. package/scripts/version-bump.ts +203 -0
  51. package/src/api-client.test.ts +723 -723
  52. package/src/api-client.ts +2499 -2140
  53. package/src/cli.ts +27 -10
  54. package/src/handlers/__test-setup__.ts +236 -231
  55. package/src/handlers/__test-utils__.ts +87 -87
  56. package/src/handlers/blockers.test.ts +468 -392
  57. package/src/handlers/blockers.ts +163 -109
  58. package/src/handlers/bodies-of-work.test.ts +704 -704
  59. package/src/handlers/bodies-of-work.ts +526 -468
  60. package/src/handlers/connectors.test.ts +834 -834
  61. package/src/handlers/connectors.ts +229 -229
  62. package/src/handlers/cost.test.ts +462 -462
  63. package/src/handlers/cost.ts +285 -285
  64. package/src/handlers/decisions.test.ts +382 -313
  65. package/src/handlers/decisions.ts +153 -99
  66. package/src/handlers/deployment.test.ts +551 -470
  67. package/src/handlers/deployment.ts +541 -508
  68. package/src/handlers/discovery.test.ts +206 -206
  69. package/src/handlers/discovery.ts +390 -374
  70. package/src/handlers/fallback.test.ts +537 -536
  71. package/src/handlers/fallback.ts +194 -188
  72. package/src/handlers/file-checkouts.test.ts +750 -670
  73. package/src/handlers/file-checkouts.ts +185 -165
  74. package/src/handlers/findings.test.ts +633 -633
  75. package/src/handlers/findings.ts +239 -203
  76. package/src/handlers/git-issues.test.ts +631 -631
  77. package/src/handlers/git-issues.ts +136 -134
  78. package/src/handlers/ideas.test.ts +644 -644
  79. package/src/handlers/ideas.ts +207 -175
  80. package/src/handlers/index.ts +84 -84
  81. package/src/handlers/milestones.test.ts +475 -475
  82. package/src/handlers/milestones.ts +180 -180
  83. package/src/handlers/organizations.test.ts +826 -826
  84. package/src/handlers/organizations.ts +315 -315
  85. package/src/handlers/progress.test.ts +269 -269
  86. package/src/handlers/progress.ts +77 -77
  87. package/src/handlers/project.test.ts +546 -546
  88. package/src/handlers/project.ts +239 -194
  89. package/src/handlers/requests.test.ts +303 -272
  90. package/src/handlers/requests.ts +99 -96
  91. package/src/handlers/roles.test.ts +303 -303
  92. package/src/handlers/roles.ts +226 -208
  93. package/src/handlers/session.test.ts +875 -576
  94. package/src/handlers/session.ts +738 -425
  95. package/src/handlers/sprints.test.ts +732 -732
  96. package/src/handlers/sprints.ts +537 -477
  97. package/src/handlers/tasks.test.ts +907 -980
  98. package/src/handlers/tasks.ts +945 -716
  99. package/src/handlers/tool-categories.test.ts +66 -66
  100. package/src/handlers/tool-docs.ts +1096 -1024
  101. package/src/handlers/types.test.ts +259 -0
  102. package/src/handlers/types.ts +175 -175
  103. package/src/handlers/validation.test.ts +582 -582
  104. package/src/handlers/validation.ts +97 -97
  105. package/src/index.ts +792 -792
  106. package/src/setup.test.ts +231 -0
  107. package/src/setup.ts +370 -0
  108. package/src/templates/agent-guidelines.ts +210 -0
  109. package/src/token-tracking.test.ts +453 -453
  110. package/src/token-tracking.ts +164 -164
  111. package/src/tools.ts +3562 -3208
  112. package/src/utils.test.ts +683 -681
  113. package/src/utils.ts +436 -392
  114. package/src/validators.test.ts +223 -223
  115. package/src/validators.ts +249 -249
  116. package/tsconfig.json +16 -16
  117. package/vitest.config.ts +14 -14
@@ -1,453 +1,453 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import {
3
- estimateTokens,
4
- createTokenUsage,
5
- trackTokenUsage,
6
- setCurrentModel,
7
- resetTokenUsage,
8
- getTokenUsageSummary,
9
- type TokenUsage,
10
- } from './token-tracking.js';
11
-
12
- // ============================================================================
13
- // estimateTokens Tests
14
- // ============================================================================
15
-
16
- describe('estimateTokens', () => {
17
- it('should return 1 for empty object', () => {
18
- // "{}" is 2 chars, ceil(2/4) = 1
19
- expect(estimateTokens({})).toBe(1);
20
- });
21
-
22
- it('should return 1 for empty array', () => {
23
- // "[]" is 2 chars, ceil(2/4) = 1
24
- expect(estimateTokens([])).toBe(1);
25
- });
26
-
27
- it('should return 1 for empty string', () => {
28
- // '""' is 2 chars, ceil(2/4) = 1
29
- expect(estimateTokens('')).toBe(1);
30
- });
31
-
32
- it('should return 1 for null', () => {
33
- // "null" is 4 chars, ceil(4/4) = 1
34
- expect(estimateTokens(null)).toBe(1);
35
- });
36
-
37
- it('should return 1 for boolean', () => {
38
- // "true" is 4 chars, ceil(4/4) = 1
39
- expect(estimateTokens(true)).toBe(1);
40
- // "false" is 5 chars, ceil(5/4) = 2
41
- expect(estimateTokens(false)).toBe(2);
42
- });
43
-
44
- it('should estimate tokens for simple object', () => {
45
- const obj = { name: 'test' };
46
- // {"name":"test"} is 15 chars, ceil(15/4) = 4
47
- expect(estimateTokens(obj)).toBe(4);
48
- });
49
-
50
- it('should estimate tokens for array of strings', () => {
51
- const arr = ['one', 'two', 'three'];
52
- // ["one","two","three"] is 21 chars, ceil(21/4) = 6
53
- expect(estimateTokens(arr)).toBe(6);
54
- });
55
-
56
- it('should estimate tokens for nested object', () => {
57
- const obj = {
58
- user: {
59
- name: 'John',
60
- age: 30,
61
- },
62
- active: true,
63
- };
64
- // Complex object - just verify it returns a reasonable positive number
65
- const tokens = estimateTokens(obj);
66
- expect(tokens).toBeGreaterThan(0);
67
- expect(tokens).toBeLessThan(100); // Sanity check
68
- });
69
-
70
- it('should estimate tokens for large object', () => {
71
- const obj = {
72
- tasks: Array(100)
73
- .fill(null)
74
- .map((_, i) => ({
75
- id: `task-${i}`,
76
- title: `Task number ${i}`,
77
- status: 'pending',
78
- })),
79
- };
80
- const tokens = estimateTokens(obj);
81
- // Should be a large number for 100 tasks
82
- expect(tokens).toBeGreaterThan(500);
83
- });
84
-
85
- it('should handle numbers', () => {
86
- // "12345" is 5 chars, ceil(5/4) = 2
87
- expect(estimateTokens(12345)).toBe(2);
88
- // "3.14159" is 7 chars, ceil(7/4) = 2
89
- expect(estimateTokens(3.14159)).toBe(2);
90
- });
91
-
92
- it('should handle undefined by treating as null', () => {
93
- // JSON.stringify(undefined) returns undefined, not a string
94
- // Our function handles this gracefully
95
- const tokens = estimateTokens(undefined);
96
- expect(tokens).toBeGreaterThanOrEqual(1);
97
- });
98
-
99
- it('should handle circular reference gracefully', () => {
100
- const obj: Record<string, unknown> = { name: 'test' };
101
- obj.self = obj; // Create circular reference
102
-
103
- // Should not throw, should return minimal estimate
104
- const tokens = estimateTokens(obj);
105
- expect(tokens).toBe(1);
106
- });
107
-
108
- it('should handle objects with toJSON method', () => {
109
- const obj = {
110
- data: 'test',
111
- toJSON() {
112
- return { serialized: true };
113
- },
114
- };
115
- // toJSON returns {"serialized":true} which is 18 chars, ceil(18/4) = 5
116
- expect(estimateTokens(obj)).toBe(5);
117
- });
118
- });
119
-
120
- // ============================================================================
121
- // createTokenUsage Tests
122
- // ============================================================================
123
-
124
- describe('createTokenUsage', () => {
125
- it('should create fresh token usage object', () => {
126
- const usage = createTokenUsage();
127
-
128
- expect(usage.callCount).toBe(0);
129
- expect(usage.totalTokens).toBe(0);
130
- expect(usage.byTool).toEqual({});
131
- expect(usage.byModel).toEqual({});
132
- expect(usage.currentModel).toBeNull();
133
- });
134
-
135
- it('should create independent instances', () => {
136
- const usage1 = createTokenUsage();
137
- const usage2 = createTokenUsage();
138
-
139
- usage1.callCount = 5;
140
- usage1.byTool['test'] = { calls: 1, tokens: 10 };
141
-
142
- expect(usage2.callCount).toBe(0);
143
- expect(usage2.byTool).toEqual({});
144
- });
145
- });
146
-
147
- // ============================================================================
148
- // trackTokenUsage Tests
149
- // ============================================================================
150
-
151
- describe('trackTokenUsage', () => {
152
- let usage: TokenUsage;
153
-
154
- beforeEach(() => {
155
- usage = createTokenUsage();
156
- });
157
-
158
- it('should increment call count', () => {
159
- trackTokenUsage(usage, 'test_tool', {}, {});
160
- expect(usage.callCount).toBe(1);
161
-
162
- trackTokenUsage(usage, 'test_tool', {}, {});
163
- expect(usage.callCount).toBe(2);
164
- });
165
-
166
- it('should accumulate total tokens', () => {
167
- trackTokenUsage(usage, 'tool1', { key: 'value' }, { result: 'ok' });
168
- const firstTotal = usage.totalTokens;
169
- expect(firstTotal).toBeGreaterThan(0);
170
-
171
- trackTokenUsage(usage, 'tool2', { key: 'value' }, { result: 'ok' });
172
- expect(usage.totalTokens).toBeGreaterThan(firstTotal);
173
- });
174
-
175
- it('should track by tool name', () => {
176
- trackTokenUsage(usage, 'add_task', { title: 'Test' }, { success: true });
177
- trackTokenUsage(usage, 'add_task', { title: 'Test 2' }, { success: true });
178
- trackTokenUsage(usage, 'complete_task', { id: '123' }, { success: true });
179
-
180
- expect(usage.byTool['add_task'].calls).toBe(2);
181
- expect(usage.byTool['complete_task'].calls).toBe(1);
182
- expect(usage.byTool['add_task'].tokens).toBeGreaterThan(0);
183
- });
184
-
185
- it('should track by model when set', () => {
186
- setCurrentModel(usage, 'opus');
187
- trackTokenUsage(usage, 'tool1', { data: 'input' }, { data: 'output' });
188
-
189
- expect(usage.byModel['opus']).toBeDefined();
190
- expect(usage.byModel['opus'].input).toBeGreaterThan(0);
191
- expect(usage.byModel['opus'].output).toBeGreaterThan(0);
192
- });
193
-
194
- it('should not track by model when not set', () => {
195
- trackTokenUsage(usage, 'tool1', { data: 'input' }, { data: 'output' });
196
-
197
- expect(Object.keys(usage.byModel)).toHaveLength(0);
198
- });
199
-
200
- it('should track multiple models separately', () => {
201
- setCurrentModel(usage, 'opus');
202
- trackTokenUsage(usage, 'tool1', { x: 1 }, { y: 2 });
203
-
204
- setCurrentModel(usage, 'sonnet');
205
- trackTokenUsage(usage, 'tool2', { x: 1 }, { y: 2 });
206
- trackTokenUsage(usage, 'tool3', { x: 1 }, { y: 2 });
207
-
208
- expect(Object.keys(usage.byModel)).toHaveLength(2);
209
- expect(usage.byModel['opus']).toBeDefined();
210
- expect(usage.byModel['sonnet']).toBeDefined();
211
- });
212
-
213
- it('should handle empty args and response', () => {
214
- trackTokenUsage(usage, 'empty_tool', {}, {});
215
-
216
- expect(usage.callCount).toBe(1);
217
- expect(usage.totalTokens).toBeGreaterThanOrEqual(2); // At least 2 for {} + {}
218
- expect(usage.byTool['empty_tool'].calls).toBe(1);
219
- });
220
-
221
- it('should handle large args and response', () => {
222
- const largeArgs = { items: Array(1000).fill('item') };
223
- const largeResponse = { results: Array(1000).fill({ ok: true }) };
224
-
225
- trackTokenUsage(usage, 'large_tool', largeArgs, largeResponse);
226
-
227
- expect(usage.totalTokens).toBeGreaterThan(1000);
228
- });
229
- });
230
-
231
- // ============================================================================
232
- // setCurrentModel Tests
233
- // ============================================================================
234
-
235
- describe('setCurrentModel', () => {
236
- it('should set the current model', () => {
237
- const usage = createTokenUsage();
238
-
239
- setCurrentModel(usage, 'opus');
240
- expect(usage.currentModel).toBe('opus');
241
-
242
- setCurrentModel(usage, 'sonnet');
243
- expect(usage.currentModel).toBe('sonnet');
244
- });
245
-
246
- it('should allow clearing the model with null', () => {
247
- const usage = createTokenUsage();
248
-
249
- setCurrentModel(usage, 'opus');
250
- expect(usage.currentModel).toBe('opus');
251
-
252
- setCurrentModel(usage, null);
253
- expect(usage.currentModel).toBeNull();
254
- });
255
- });
256
-
257
- // ============================================================================
258
- // resetTokenUsage Tests
259
- // ============================================================================
260
-
261
- describe('resetTokenUsage', () => {
262
- it('should reset all tracking data', () => {
263
- const usage = createTokenUsage();
264
-
265
- // Add some data
266
- setCurrentModel(usage, 'opus');
267
- trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
268
- trackTokenUsage(usage, 'tool2', { c: 3 }, { d: 4 });
269
-
270
- expect(usage.callCount).toBe(2);
271
- expect(usage.totalTokens).toBeGreaterThan(0);
272
- expect(Object.keys(usage.byTool)).toHaveLength(2);
273
-
274
- // Reset
275
- resetTokenUsage(usage);
276
-
277
- expect(usage.callCount).toBe(0);
278
- expect(usage.totalTokens).toBe(0);
279
- expect(usage.byTool).toEqual({});
280
- expect(usage.byModel).toEqual({});
281
- expect(usage.currentModel).toBeNull();
282
- });
283
-
284
- it('should allow tracking after reset', () => {
285
- const usage = createTokenUsage();
286
-
287
- trackTokenUsage(usage, 'tool1', {}, {});
288
- resetTokenUsage(usage);
289
-
290
- trackTokenUsage(usage, 'tool2', { x: 1 }, { y: 2 });
291
-
292
- expect(usage.callCount).toBe(1);
293
- expect(usage.byTool['tool1']).toBeUndefined();
294
- expect(usage.byTool['tool2']).toBeDefined();
295
- });
296
- });
297
-
298
- // ============================================================================
299
- // getTokenUsageSummary Tests
300
- // ============================================================================
301
-
302
- describe('getTokenUsageSummary', () => {
303
- it('should return empty summary for new usage', () => {
304
- const usage = createTokenUsage();
305
- const summary = getTokenUsageSummary(usage);
306
-
307
- expect(summary.total_calls).toBe(0);
308
- expect(summary.total_tokens).toBe(0);
309
- expect(summary.average_tokens_per_call).toBe(0);
310
- expect(summary.by_tool).toEqual({});
311
- expect(summary.by_model).toEqual({});
312
- expect(summary.current_model).toBeNull();
313
- });
314
-
315
- it('should calculate average tokens per call', () => {
316
- const usage = createTokenUsage();
317
-
318
- // Track 3 calls with roughly similar sizes
319
- trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
320
- trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
321
- trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
322
-
323
- const summary = getTokenUsageSummary(usage);
324
-
325
- expect(summary.total_calls).toBe(3);
326
- expect(summary.average_tokens_per_call).toBeGreaterThan(0);
327
- expect(summary.average_tokens_per_call).toBe(
328
- Math.round(summary.total_tokens / summary.total_calls)
329
- );
330
- });
331
-
332
- it('should include per-tool averages', () => {
333
- const usage = createTokenUsage();
334
-
335
- trackTokenUsage(usage, 'small_tool', { x: 1 }, { y: 2 });
336
- trackTokenUsage(usage, 'small_tool', { x: 1 }, { y: 2 });
337
-
338
- const summary = getTokenUsageSummary(usage);
339
-
340
- expect(summary.by_tool['small_tool'].calls).toBe(2);
341
- expect(summary.by_tool['small_tool'].avg).toBeGreaterThan(0);
342
- expect(summary.by_tool['small_tool'].avg).toBe(
343
- Math.round(summary.by_tool['small_tool'].tokens / summary.by_tool['small_tool'].calls)
344
- );
345
- });
346
-
347
- it('should include model breakdown', () => {
348
- const usage = createTokenUsage();
349
-
350
- setCurrentModel(usage, 'opus');
351
- trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
352
-
353
- setCurrentModel(usage, 'haiku');
354
- trackTokenUsage(usage, 'tool2', { c: 3 }, { d: 4 });
355
-
356
- const summary = getTokenUsageSummary(usage);
357
-
358
- expect(summary.by_model['opus']).toBeDefined();
359
- expect(summary.by_model['haiku']).toBeDefined();
360
- expect(summary.current_model).toBe('haiku');
361
- });
362
-
363
- it('should return a copy of byModel to prevent mutation', () => {
364
- const usage = createTokenUsage();
365
-
366
- setCurrentModel(usage, 'opus');
367
- trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
368
-
369
- const summary = getTokenUsageSummary(usage);
370
-
371
- // Modify the summary
372
- summary.by_model['opus'].input = 999;
373
-
374
- // Original should be unchanged
375
- expect(usage.byModel['opus'].input).not.toBe(999);
376
- });
377
- });
378
-
379
- // ============================================================================
380
- // Integration Tests
381
- // ============================================================================
382
-
383
- describe('Token Tracking Integration', () => {
384
- it('should track a realistic session workflow', () => {
385
- const usage = createTokenUsage();
386
-
387
- // Agent starts with opus model
388
- setCurrentModel(usage, 'opus');
389
-
390
- // Start session
391
- trackTokenUsage(
392
- usage,
393
- 'start_work_session',
394
- { git_url: 'https://github.com/org/repo', model: 'opus' },
395
- {
396
- session_id: '123',
397
- persona: 'Atlas',
398
- next_task: { id: 'task-1', title: 'Fix bug' },
399
- }
400
- );
401
-
402
- // Update task
403
- trackTokenUsage(
404
- usage,
405
- 'update_task',
406
- { task_id: 'task-1', status: 'in_progress', progress_percentage: 25 },
407
- { success: true }
408
- );
409
-
410
- // Complete task
411
- trackTokenUsage(
412
- usage,
413
- 'complete_task',
414
- { task_id: 'task-1', summary: 'Fixed the bug' },
415
- { success: true, next_task: null }
416
- );
417
-
418
- const summary = getTokenUsageSummary(usage);
419
-
420
- expect(summary.total_calls).toBe(3);
421
- expect(summary.by_tool['start_work_session'].calls).toBe(1);
422
- expect(summary.by_tool['update_task'].calls).toBe(1);
423
- expect(summary.by_tool['complete_task'].calls).toBe(1);
424
- expect(summary.by_model['opus']).toBeDefined();
425
- expect(summary.by_model['opus'].input).toBeGreaterThan(0);
426
- expect(summary.by_model['opus'].output).toBeGreaterThan(0);
427
- });
428
-
429
- it('should handle session reset', () => {
430
- const usage = createTokenUsage();
431
-
432
- // First session
433
- setCurrentModel(usage, 'sonnet');
434
- trackTokenUsage(usage, 'tool1', {}, {});
435
- trackTokenUsage(usage, 'tool2', {}, {});
436
-
437
- const firstSummary = getTokenUsageSummary(usage);
438
- expect(firstSummary.total_calls).toBe(2);
439
-
440
- // Reset for new session
441
- resetTokenUsage(usage);
442
-
443
- // Second session
444
- setCurrentModel(usage, 'haiku');
445
- trackTokenUsage(usage, 'tool3', {}, {});
446
-
447
- const secondSummary = getTokenUsageSummary(usage);
448
- expect(secondSummary.total_calls).toBe(1);
449
- expect(secondSummary.by_tool['tool1']).toBeUndefined();
450
- expect(secondSummary.by_tool['tool3']).toBeDefined();
451
- expect(secondSummary.current_model).toBe('haiku');
452
- });
453
- });
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import {
3
+ estimateTokens,
4
+ createTokenUsage,
5
+ trackTokenUsage,
6
+ setCurrentModel,
7
+ resetTokenUsage,
8
+ getTokenUsageSummary,
9
+ type TokenUsage,
10
+ } from './token-tracking.js';
11
+
12
+ // ============================================================================
13
+ // estimateTokens Tests
14
+ // ============================================================================
15
+
16
+ describe('estimateTokens', () => {
17
+ it('should return 1 for empty object', () => {
18
+ // "{}" is 2 chars, ceil(2/4) = 1
19
+ expect(estimateTokens({})).toBe(1);
20
+ });
21
+
22
+ it('should return 1 for empty array', () => {
23
+ // "[]" is 2 chars, ceil(2/4) = 1
24
+ expect(estimateTokens([])).toBe(1);
25
+ });
26
+
27
+ it('should return 1 for empty string', () => {
28
+ // '""' is 2 chars, ceil(2/4) = 1
29
+ expect(estimateTokens('')).toBe(1);
30
+ });
31
+
32
+ it('should return 1 for null', () => {
33
+ // "null" is 4 chars, ceil(4/4) = 1
34
+ expect(estimateTokens(null)).toBe(1);
35
+ });
36
+
37
+ it('should return 1 for boolean', () => {
38
+ // "true" is 4 chars, ceil(4/4) = 1
39
+ expect(estimateTokens(true)).toBe(1);
40
+ // "false" is 5 chars, ceil(5/4) = 2
41
+ expect(estimateTokens(false)).toBe(2);
42
+ });
43
+
44
+ it('should estimate tokens for simple object', () => {
45
+ const obj = { name: 'test' };
46
+ // {"name":"test"} is 15 chars, ceil(15/4) = 4
47
+ expect(estimateTokens(obj)).toBe(4);
48
+ });
49
+
50
+ it('should estimate tokens for array of strings', () => {
51
+ const arr = ['one', 'two', 'three'];
52
+ // ["one","two","three"] is 21 chars, ceil(21/4) = 6
53
+ expect(estimateTokens(arr)).toBe(6);
54
+ });
55
+
56
+ it('should estimate tokens for nested object', () => {
57
+ const obj = {
58
+ user: {
59
+ name: 'John',
60
+ age: 30,
61
+ },
62
+ active: true,
63
+ };
64
+ // Complex object - just verify it returns a reasonable positive number
65
+ const tokens = estimateTokens(obj);
66
+ expect(tokens).toBeGreaterThan(0);
67
+ expect(tokens).toBeLessThan(100); // Sanity check
68
+ });
69
+
70
+ it('should estimate tokens for large object', () => {
71
+ const obj = {
72
+ tasks: Array(100)
73
+ .fill(null)
74
+ .map((_, i) => ({
75
+ id: `task-${i}`,
76
+ title: `Task number ${i}`,
77
+ status: 'pending',
78
+ })),
79
+ };
80
+ const tokens = estimateTokens(obj);
81
+ // Should be a large number for 100 tasks
82
+ expect(tokens).toBeGreaterThan(500);
83
+ });
84
+
85
+ it('should handle numbers', () => {
86
+ // "12345" is 5 chars, ceil(5/4) = 2
87
+ expect(estimateTokens(12345)).toBe(2);
88
+ // "3.14159" is 7 chars, ceil(7/4) = 2
89
+ expect(estimateTokens(3.14159)).toBe(2);
90
+ });
91
+
92
+ it('should handle undefined by treating as null', () => {
93
+ // JSON.stringify(undefined) returns undefined, not a string
94
+ // Our function handles this gracefully
95
+ const tokens = estimateTokens(undefined);
96
+ expect(tokens).toBeGreaterThanOrEqual(1);
97
+ });
98
+
99
+ it('should handle circular reference gracefully', () => {
100
+ const obj: Record<string, unknown> = { name: 'test' };
101
+ obj.self = obj; // Create circular reference
102
+
103
+ // Should not throw, should return minimal estimate
104
+ const tokens = estimateTokens(obj);
105
+ expect(tokens).toBe(1);
106
+ });
107
+
108
+ it('should handle objects with toJSON method', () => {
109
+ const obj = {
110
+ data: 'test',
111
+ toJSON() {
112
+ return { serialized: true };
113
+ },
114
+ };
115
+ // toJSON returns {"serialized":true} which is 18 chars, ceil(18/4) = 5
116
+ expect(estimateTokens(obj)).toBe(5);
117
+ });
118
+ });
119
+
120
+ // ============================================================================
121
+ // createTokenUsage Tests
122
+ // ============================================================================
123
+
124
+ describe('createTokenUsage', () => {
125
+ it('should create fresh token usage object', () => {
126
+ const usage = createTokenUsage();
127
+
128
+ expect(usage.callCount).toBe(0);
129
+ expect(usage.totalTokens).toBe(0);
130
+ expect(usage.byTool).toEqual({});
131
+ expect(usage.byModel).toEqual({});
132
+ expect(usage.currentModel).toBeNull();
133
+ });
134
+
135
+ it('should create independent instances', () => {
136
+ const usage1 = createTokenUsage();
137
+ const usage2 = createTokenUsage();
138
+
139
+ usage1.callCount = 5;
140
+ usage1.byTool['test'] = { calls: 1, tokens: 10 };
141
+
142
+ expect(usage2.callCount).toBe(0);
143
+ expect(usage2.byTool).toEqual({});
144
+ });
145
+ });
146
+
147
+ // ============================================================================
148
+ // trackTokenUsage Tests
149
+ // ============================================================================
150
+
151
+ describe('trackTokenUsage', () => {
152
+ let usage: TokenUsage;
153
+
154
+ beforeEach(() => {
155
+ usage = createTokenUsage();
156
+ });
157
+
158
+ it('should increment call count', () => {
159
+ trackTokenUsage(usage, 'test_tool', {}, {});
160
+ expect(usage.callCount).toBe(1);
161
+
162
+ trackTokenUsage(usage, 'test_tool', {}, {});
163
+ expect(usage.callCount).toBe(2);
164
+ });
165
+
166
+ it('should accumulate total tokens', () => {
167
+ trackTokenUsage(usage, 'tool1', { key: 'value' }, { result: 'ok' });
168
+ const firstTotal = usage.totalTokens;
169
+ expect(firstTotal).toBeGreaterThan(0);
170
+
171
+ trackTokenUsage(usage, 'tool2', { key: 'value' }, { result: 'ok' });
172
+ expect(usage.totalTokens).toBeGreaterThan(firstTotal);
173
+ });
174
+
175
+ it('should track by tool name', () => {
176
+ trackTokenUsage(usage, 'add_task', { title: 'Test' }, { success: true });
177
+ trackTokenUsage(usage, 'add_task', { title: 'Test 2' }, { success: true });
178
+ trackTokenUsage(usage, 'complete_task', { id: '123' }, { success: true });
179
+
180
+ expect(usage.byTool['add_task'].calls).toBe(2);
181
+ expect(usage.byTool['complete_task'].calls).toBe(1);
182
+ expect(usage.byTool['add_task'].tokens).toBeGreaterThan(0);
183
+ });
184
+
185
+ it('should track by model when set', () => {
186
+ setCurrentModel(usage, 'opus');
187
+ trackTokenUsage(usage, 'tool1', { data: 'input' }, { data: 'output' });
188
+
189
+ expect(usage.byModel['opus']).toBeDefined();
190
+ expect(usage.byModel['opus'].input).toBeGreaterThan(0);
191
+ expect(usage.byModel['opus'].output).toBeGreaterThan(0);
192
+ });
193
+
194
+ it('should not track by model when not set', () => {
195
+ trackTokenUsage(usage, 'tool1', { data: 'input' }, { data: 'output' });
196
+
197
+ expect(Object.keys(usage.byModel)).toHaveLength(0);
198
+ });
199
+
200
+ it('should track multiple models separately', () => {
201
+ setCurrentModel(usage, 'opus');
202
+ trackTokenUsage(usage, 'tool1', { x: 1 }, { y: 2 });
203
+
204
+ setCurrentModel(usage, 'sonnet');
205
+ trackTokenUsage(usage, 'tool2', { x: 1 }, { y: 2 });
206
+ trackTokenUsage(usage, 'tool3', { x: 1 }, { y: 2 });
207
+
208
+ expect(Object.keys(usage.byModel)).toHaveLength(2);
209
+ expect(usage.byModel['opus']).toBeDefined();
210
+ expect(usage.byModel['sonnet']).toBeDefined();
211
+ });
212
+
213
+ it('should handle empty args and response', () => {
214
+ trackTokenUsage(usage, 'empty_tool', {}, {});
215
+
216
+ expect(usage.callCount).toBe(1);
217
+ expect(usage.totalTokens).toBeGreaterThanOrEqual(2); // At least 2 for {} + {}
218
+ expect(usage.byTool['empty_tool'].calls).toBe(1);
219
+ });
220
+
221
+ it('should handle large args and response', () => {
222
+ const largeArgs = { items: Array(1000).fill('item') };
223
+ const largeResponse = { results: Array(1000).fill({ ok: true }) };
224
+
225
+ trackTokenUsage(usage, 'large_tool', largeArgs, largeResponse);
226
+
227
+ expect(usage.totalTokens).toBeGreaterThan(1000);
228
+ });
229
+ });
230
+
231
+ // ============================================================================
232
+ // setCurrentModel Tests
233
+ // ============================================================================
234
+
235
+ describe('setCurrentModel', () => {
236
+ it('should set the current model', () => {
237
+ const usage = createTokenUsage();
238
+
239
+ setCurrentModel(usage, 'opus');
240
+ expect(usage.currentModel).toBe('opus');
241
+
242
+ setCurrentModel(usage, 'sonnet');
243
+ expect(usage.currentModel).toBe('sonnet');
244
+ });
245
+
246
+ it('should allow clearing the model with null', () => {
247
+ const usage = createTokenUsage();
248
+
249
+ setCurrentModel(usage, 'opus');
250
+ expect(usage.currentModel).toBe('opus');
251
+
252
+ setCurrentModel(usage, null);
253
+ expect(usage.currentModel).toBeNull();
254
+ });
255
+ });
256
+
257
+ // ============================================================================
258
+ // resetTokenUsage Tests
259
+ // ============================================================================
260
+
261
+ describe('resetTokenUsage', () => {
262
+ it('should reset all tracking data', () => {
263
+ const usage = createTokenUsage();
264
+
265
+ // Add some data
266
+ setCurrentModel(usage, 'opus');
267
+ trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
268
+ trackTokenUsage(usage, 'tool2', { c: 3 }, { d: 4 });
269
+
270
+ expect(usage.callCount).toBe(2);
271
+ expect(usage.totalTokens).toBeGreaterThan(0);
272
+ expect(Object.keys(usage.byTool)).toHaveLength(2);
273
+
274
+ // Reset
275
+ resetTokenUsage(usage);
276
+
277
+ expect(usage.callCount).toBe(0);
278
+ expect(usage.totalTokens).toBe(0);
279
+ expect(usage.byTool).toEqual({});
280
+ expect(usage.byModel).toEqual({});
281
+ expect(usage.currentModel).toBeNull();
282
+ });
283
+
284
+ it('should allow tracking after reset', () => {
285
+ const usage = createTokenUsage();
286
+
287
+ trackTokenUsage(usage, 'tool1', {}, {});
288
+ resetTokenUsage(usage);
289
+
290
+ trackTokenUsage(usage, 'tool2', { x: 1 }, { y: 2 });
291
+
292
+ expect(usage.callCount).toBe(1);
293
+ expect(usage.byTool['tool1']).toBeUndefined();
294
+ expect(usage.byTool['tool2']).toBeDefined();
295
+ });
296
+ });
297
+
298
+ // ============================================================================
299
+ // getTokenUsageSummary Tests
300
+ // ============================================================================
301
+
302
+ describe('getTokenUsageSummary', () => {
303
+ it('should return empty summary for new usage', () => {
304
+ const usage = createTokenUsage();
305
+ const summary = getTokenUsageSummary(usage);
306
+
307
+ expect(summary.total_calls).toBe(0);
308
+ expect(summary.total_tokens).toBe(0);
309
+ expect(summary.average_tokens_per_call).toBe(0);
310
+ expect(summary.by_tool).toEqual({});
311
+ expect(summary.by_model).toEqual({});
312
+ expect(summary.current_model).toBeNull();
313
+ });
314
+
315
+ it('should calculate average tokens per call', () => {
316
+ const usage = createTokenUsage();
317
+
318
+ // Track 3 calls with roughly similar sizes
319
+ trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
320
+ trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
321
+ trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
322
+
323
+ const summary = getTokenUsageSummary(usage);
324
+
325
+ expect(summary.total_calls).toBe(3);
326
+ expect(summary.average_tokens_per_call).toBeGreaterThan(0);
327
+ expect(summary.average_tokens_per_call).toBe(
328
+ Math.round(summary.total_tokens / summary.total_calls)
329
+ );
330
+ });
331
+
332
+ it('should include per-tool averages', () => {
333
+ const usage = createTokenUsage();
334
+
335
+ trackTokenUsage(usage, 'small_tool', { x: 1 }, { y: 2 });
336
+ trackTokenUsage(usage, 'small_tool', { x: 1 }, { y: 2 });
337
+
338
+ const summary = getTokenUsageSummary(usage);
339
+
340
+ expect(summary.by_tool['small_tool'].calls).toBe(2);
341
+ expect(summary.by_tool['small_tool'].avg).toBeGreaterThan(0);
342
+ expect(summary.by_tool['small_tool'].avg).toBe(
343
+ Math.round(summary.by_tool['small_tool'].tokens / summary.by_tool['small_tool'].calls)
344
+ );
345
+ });
346
+
347
+ it('should include model breakdown', () => {
348
+ const usage = createTokenUsage();
349
+
350
+ setCurrentModel(usage, 'opus');
351
+ trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
352
+
353
+ setCurrentModel(usage, 'haiku');
354
+ trackTokenUsage(usage, 'tool2', { c: 3 }, { d: 4 });
355
+
356
+ const summary = getTokenUsageSummary(usage);
357
+
358
+ expect(summary.by_model['opus']).toBeDefined();
359
+ expect(summary.by_model['haiku']).toBeDefined();
360
+ expect(summary.current_model).toBe('haiku');
361
+ });
362
+
363
+ it('should return a copy of byModel to prevent mutation', () => {
364
+ const usage = createTokenUsage();
365
+
366
+ setCurrentModel(usage, 'opus');
367
+ trackTokenUsage(usage, 'tool1', { a: 1 }, { b: 2 });
368
+
369
+ const summary = getTokenUsageSummary(usage);
370
+
371
+ // Modify the summary
372
+ summary.by_model['opus'].input = 999;
373
+
374
+ // Original should be unchanged
375
+ expect(usage.byModel['opus'].input).not.toBe(999);
376
+ });
377
+ });
378
+
379
+ // ============================================================================
380
+ // Integration Tests
381
+ // ============================================================================
382
+
383
+ describe('Token Tracking Integration', () => {
384
+ it('should track a realistic session workflow', () => {
385
+ const usage = createTokenUsage();
386
+
387
+ // Agent starts with opus model
388
+ setCurrentModel(usage, 'opus');
389
+
390
+ // Start session
391
+ trackTokenUsage(
392
+ usage,
393
+ 'start_work_session',
394
+ { git_url: 'https://github.com/org/repo', model: 'opus' },
395
+ {
396
+ session_id: '123',
397
+ persona: 'Atlas',
398
+ next_task: { id: 'task-1', title: 'Fix bug' },
399
+ }
400
+ );
401
+
402
+ // Update task
403
+ trackTokenUsage(
404
+ usage,
405
+ 'update_task',
406
+ { task_id: 'task-1', status: 'in_progress', progress_percentage: 25 },
407
+ { success: true }
408
+ );
409
+
410
+ // Complete task
411
+ trackTokenUsage(
412
+ usage,
413
+ 'complete_task',
414
+ { task_id: 'task-1', summary: 'Fixed the bug' },
415
+ { success: true, next_task: null }
416
+ );
417
+
418
+ const summary = getTokenUsageSummary(usage);
419
+
420
+ expect(summary.total_calls).toBe(3);
421
+ expect(summary.by_tool['start_work_session'].calls).toBe(1);
422
+ expect(summary.by_tool['update_task'].calls).toBe(1);
423
+ expect(summary.by_tool['complete_task'].calls).toBe(1);
424
+ expect(summary.by_model['opus']).toBeDefined();
425
+ expect(summary.by_model['opus'].input).toBeGreaterThan(0);
426
+ expect(summary.by_model['opus'].output).toBeGreaterThan(0);
427
+ });
428
+
429
+ it('should handle session reset', () => {
430
+ const usage = createTokenUsage();
431
+
432
+ // First session
433
+ setCurrentModel(usage, 'sonnet');
434
+ trackTokenUsage(usage, 'tool1', {}, {});
435
+ trackTokenUsage(usage, 'tool2', {}, {});
436
+
437
+ const firstSummary = getTokenUsageSummary(usage);
438
+ expect(firstSummary.total_calls).toBe(2);
439
+
440
+ // Reset for new session
441
+ resetTokenUsage(usage);
442
+
443
+ // Second session
444
+ setCurrentModel(usage, 'haiku');
445
+ trackTokenUsage(usage, 'tool3', {}, {});
446
+
447
+ const secondSummary = getTokenUsageSummary(usage);
448
+ expect(secondSummary.total_calls).toBe(1);
449
+ expect(secondSummary.by_tool['tool1']).toBeUndefined();
450
+ expect(secondSummary.by_tool['tool3']).toBeDefined();
451
+ expect(secondSummary.current_model).toBe('haiku');
452
+ });
453
+ });