runwork 0.5.1 → 0.6.1

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 (118) hide show
  1. package/dist/agents/__tests__/claude-code-stats.test.d.ts +1 -0
  2. package/dist/agents/__tests__/claude-code-stats.test.js +153 -0
  3. package/dist/agents/__tests__/claude-desktop-stats.test.d.ts +1 -0
  4. package/dist/agents/__tests__/claude-desktop-stats.test.js +280 -0
  5. package/dist/agents/__tests__/codex-stats.test.d.ts +1 -0
  6. package/dist/agents/__tests__/codex-stats.test.js +262 -0
  7. package/dist/agents/__tests__/cursor-stats.test.d.ts +1 -0
  8. package/dist/agents/__tests__/cursor-stats.test.js +183 -0
  9. package/dist/agents/__tests__/graceful-degradation.test.d.ts +1 -0
  10. package/dist/agents/__tests__/graceful-degradation.test.js +123 -0
  11. package/dist/agents/__tests__/instruction-hint.test.d.ts +1 -0
  12. package/dist/agents/__tests__/instruction-hint.test.js +69 -0
  13. package/dist/agents/__tests__/intro-skill.test.d.ts +1 -0
  14. package/dist/agents/__tests__/intro-skill.test.js +148 -0
  15. package/dist/agents/__tests__/schema-validation.test.d.ts +1 -0
  16. package/dist/agents/__tests__/schema-validation.test.js +212 -0
  17. package/dist/agents/claude-code.d.ts +16 -1
  18. package/dist/agents/claude-code.js +344 -21
  19. package/dist/agents/claude-desktop.d.ts +10 -1
  20. package/dist/agents/claude-desktop.js +250 -8
  21. package/dist/agents/cline.d.ts +13 -0
  22. package/dist/agents/cline.js +88 -0
  23. package/dist/agents/codex.d.ts +7 -2
  24. package/dist/agents/codex.js +130 -18
  25. package/dist/agents/cursor.d.ts +6 -1
  26. package/dist/agents/cursor.js +164 -7
  27. package/dist/agents/detect.js +30 -7
  28. package/dist/agents/gemini.d.ts +6 -3
  29. package/dist/agents/gemini.js +57 -20
  30. package/dist/agents/generic-adapter.d.ts +23 -0
  31. package/dist/agents/generic-adapter.js +106 -0
  32. package/dist/agents/intro-skill.d.ts +36 -0
  33. package/dist/agents/intro-skill.js +358 -0
  34. package/dist/agents/registry.d.ts +53 -0
  35. package/dist/agents/registry.js +219 -0
  36. package/dist/agents/types.d.ts +44 -1
  37. package/dist/agents/types.js +11 -4
  38. package/dist/agents/utils/instruction-hint.d.ts +11 -0
  39. package/dist/agents/utils/instruction-hint.js +62 -0
  40. package/dist/agents/utils/json-config.js +3 -3
  41. package/dist/agents/windsurf.d.ts +2 -0
  42. package/dist/agents/windsurf.js +26 -9
  43. package/dist/api/client.d.ts +85 -2
  44. package/dist/api/client.js +40 -10
  45. package/dist/auth/__tests__/login-flow.test.d.ts +1 -0
  46. package/dist/auth/__tests__/login-flow.test.js +125 -0
  47. package/dist/auth/login-flow.d.ts +16 -0
  48. package/dist/auth/login-flow.js +101 -1
  49. package/dist/commands/clone.d.ts +9 -1
  50. package/dist/commands/clone.js +40 -12
  51. package/dist/commands/deploy.js +27 -4
  52. package/dist/commands/dev.js +35 -1
  53. package/dist/commands/doctor.d.ts +2 -0
  54. package/dist/commands/doctor.js +82 -0
  55. package/dist/commands/info.js +6 -2
  56. package/dist/commands/init.d.ts +10 -2
  57. package/dist/commands/init.js +58 -15
  58. package/dist/commands/integrations.js +47 -10
  59. package/dist/commands/login.js +31 -4
  60. package/dist/commands/mcp.js +143 -15
  61. package/dist/commands/setup.js +47 -122
  62. package/dist/commands/skills.js +230 -12
  63. package/dist/commands/sync.d.ts +8 -0
  64. package/dist/commands/sync.js +398 -65
  65. package/dist/commands/welcome.js +6 -3
  66. package/dist/generated/version.d.ts +1 -1
  67. package/dist/generated/version.js +1 -1
  68. package/dist/health/__tests__/checks.test.d.ts +1 -0
  69. package/dist/health/__tests__/checks.test.js +223 -0
  70. package/dist/health/checks.d.ts +20 -0
  71. package/dist/health/checks.js +392 -0
  72. package/dist/health/runner.d.ts +2 -0
  73. package/dist/health/runner.js +26 -0
  74. package/dist/health/types.d.ts +12 -0
  75. package/dist/health/types.js +1 -0
  76. package/dist/index.js +32 -0
  77. package/dist/sync/__tests__/change-detect.test.d.ts +1 -0
  78. package/dist/sync/__tests__/change-detect.test.js +123 -0
  79. package/dist/sync/__tests__/hash.test.d.ts +1 -0
  80. package/dist/sync/__tests__/hash.test.js +18 -0
  81. package/dist/sync/change-detect.d.ts +19 -0
  82. package/dist/sync/change-detect.js +136 -0
  83. package/dist/sync/conflict-ui.d.ts +5 -0
  84. package/dist/sync/conflict-ui.js +78 -0
  85. package/dist/sync/executor.d.ts +21 -0
  86. package/dist/sync/executor.js +98 -0
  87. package/dist/sync/hash.d.ts +1 -0
  88. package/dist/sync/hash.js +4 -0
  89. package/dist/sync/types.d.ts +24 -0
  90. package/dist/sync/types.js +1 -0
  91. package/dist/types.d.ts +12 -0
  92. package/dist/ui/__tests__/banner.test.js +17 -1
  93. package/dist/ui/banner.d.ts +1 -1
  94. package/dist/ui/banner.js +27 -2
  95. package/dist/utils/__tests__/resolve.test.d.ts +1 -0
  96. package/dist/utils/__tests__/resolve.test.js +95 -0
  97. package/dist/utils/__tests__/sqlite.test.d.ts +1 -0
  98. package/dist/utils/__tests__/sqlite.test.js +77 -0
  99. package/dist/utils/agent-guidance.d.ts +32 -0
  100. package/dist/utils/agent-guidance.js +106 -0
  101. package/dist/utils/curl-parser.d.ts +1 -1
  102. package/dist/utils/curl-parser.js +144 -26
  103. package/dist/utils/fs.js +2 -2
  104. package/dist/utils/help-json.d.ts +56 -0
  105. package/dist/utils/help-json.js +85 -0
  106. package/dist/utils/resolve.d.ts +12 -0
  107. package/dist/utils/resolve.js +52 -0
  108. package/dist/utils/setup-state.d.ts +3 -0
  109. package/dist/utils/setup-state.js +19 -0
  110. package/dist/utils/sqlite.d.ts +7 -0
  111. package/dist/utils/sqlite.js +24 -0
  112. package/dist/utils/which.d.ts +6 -0
  113. package/dist/utils/which.js +18 -0
  114. package/package.json +7 -4
  115. package/dist/agents/copilot-cli.d.ts +0 -10
  116. package/dist/agents/copilot-cli.js +0 -46
  117. package/dist/agents/copilot-vscode.d.ts +0 -10
  118. package/dist/agents/copilot-vscode.js +0 -37
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { execFileSync } from 'child_process';
5
+ import path from 'path';
6
+ vi.mock('fs');
7
+ vi.mock('os');
8
+ vi.mock('child_process');
9
+ // Avoid side-effects from other imports in claude-code.ts that use fs/os
10
+ vi.mock('../utils/json-config.js', () => ({
11
+ mergeJsonMcpServers: vi.fn(),
12
+ readJsonConfig: vi.fn(() => ({})),
13
+ writeJsonConfig: vi.fn(),
14
+ }));
15
+ vi.mock('../utils/instruction-hint.js', () => ({
16
+ writeHintToFile: vi.fn(),
17
+ }));
18
+ import { ClaudeCodeAdapter } from '../claude-code.js';
19
+ const FIXTURE_STATS = path.resolve(__dirname, './fixtures/claude-code/stats-cache.json');
20
+ // Read the real fixture once for use in mocks
21
+ // (We read it at import time before mocks interfere, via a direct require)
22
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
23
+ const realFs = require('fs');
24
+ const fixtureContent = realFs.readFileSync(FIXTURE_STATS, 'utf-8');
25
+ let adapter;
26
+ beforeEach(() => {
27
+ vi.restoreAllMocks();
28
+ vi.mocked(homedir).mockReturnValue('/mock-home');
29
+ adapter = new ClaudeCodeAdapter();
30
+ });
31
+ // Helper: make existsSync return true only for specified paths
32
+ function mockExistsPaths(paths) {
33
+ vi.mocked(existsSync).mockImplementation((p) => paths.includes(p));
34
+ }
35
+ describe('ClaudeCodeAdapter.readUsageStats', () => {
36
+ it('parses stats-cache.json and returns correct totals when lastSyncAt is null', async () => {
37
+ mockExistsPaths([
38
+ '/mock-home/.claude',
39
+ '/mock-home/.claude/stats-cache.json',
40
+ ]);
41
+ vi.mocked(readFileSync).mockReturnValue(fixtureContent);
42
+ const stats = await adapter.readUsageStats(null);
43
+ expect(stats).not.toBeNull();
44
+ // All 6 days: 15+22+0+40+10+35 = 122 messages
45
+ expect(stats.messageCount).toBe(122);
46
+ // 2+3+0+4+1+5 = 15 sessions
47
+ expect(stats.sessionCount).toBe(15);
48
+ // 8+12+0+25+5+18 = 68 tool calls
49
+ expect(stats.toolCallCount).toBe(68);
50
+ expect(stats.hasNewActivity).toBe(true);
51
+ });
52
+ it('filters days after lastSyncAt date', async () => {
53
+ mockExistsPaths([
54
+ '/mock-home/.claude',
55
+ '/mock-home/.claude/stats-cache.json',
56
+ ]);
57
+ vi.mocked(readFileSync).mockReturnValue(fixtureContent);
58
+ const stats = await adapter.readUsageStats('2026-03-27T00:00:00Z');
59
+ expect(stats).not.toBeNull();
60
+ // Days after 2026-03-27: 28, 29, 30 => 40+10+35 = 85
61
+ expect(stats.messageCount).toBe(85);
62
+ // 4+1+5 = 10
63
+ expect(stats.sessionCount).toBe(10);
64
+ // 25+5+18 = 48
65
+ expect(stats.toolCallCount).toBe(48);
66
+ });
67
+ it('returns null when all data is before lastSyncAt', async () => {
68
+ mockExistsPaths([
69
+ '/mock-home/.claude',
70
+ '/mock-home/.claude/stats-cache.json',
71
+ ]);
72
+ vi.mocked(readFileSync).mockReturnValue(fixtureContent);
73
+ const stats = await adapter.readUsageStats('2099-01-01T00:00:00Z');
74
+ expect(stats).toBeNull();
75
+ });
76
+ it('returns null when ~/.claude does not exist', async () => {
77
+ mockExistsPaths([]);
78
+ const stats = await adapter.readUsageStats(null);
79
+ expect(stats).toBeNull();
80
+ });
81
+ it('returns null on corrupted stats-cache.json', async () => {
82
+ mockExistsPaths([
83
+ '/mock-home/.claude',
84
+ '/mock-home/.claude/stats-cache.json',
85
+ ]);
86
+ vi.mocked(readFileSync).mockReturnValue('not valid json {{{');
87
+ const stats = await adapter.readUsageStats(null);
88
+ expect(stats).toBeNull();
89
+ });
90
+ it('returns null on unexpected schema (missing dailyActivity)', async () => {
91
+ mockExistsPaths([
92
+ '/mock-home/.claude',
93
+ '/mock-home/.claude/stats-cache.json',
94
+ ]);
95
+ vi.mocked(readFileSync).mockReturnValue(JSON.stringify({ version: 2 }));
96
+ const stats = await adapter.readUsageStats(null);
97
+ expect(stats).toBeNull();
98
+ });
99
+ it('returns null on empty dailyActivity array', async () => {
100
+ mockExistsPaths([
101
+ '/mock-home/.claude',
102
+ '/mock-home/.claude/stats-cache.json',
103
+ ]);
104
+ vi.mocked(readFileSync).mockReturnValue(JSON.stringify({ version: 2, dailyActivity: [] }));
105
+ const stats = await adapter.readUsageStats(null);
106
+ expect(stats).toBeNull();
107
+ });
108
+ it('falls back to session-env directory counting when stats-cache.json missing', async () => {
109
+ mockExistsPaths([
110
+ '/mock-home/.claude',
111
+ '/mock-home/.claude/session-env',
112
+ ]);
113
+ vi.mocked(readdirSync).mockReturnValue(['session-a', 'session-b', 'session-c']);
114
+ vi.mocked(statSync).mockImplementation((p) => {
115
+ const name = p.split('/').pop();
116
+ const mtimes = {
117
+ 'session-a': Date.now() - 1000,
118
+ 'session-b': Date.now() - 2000,
119
+ 'session-c': Date.now() - 3000,
120
+ };
121
+ return { mtimeMs: mtimes[name] ?? 0 };
122
+ });
123
+ const stats = await adapter.readUsageStats(null);
124
+ expect(stats).not.toBeNull();
125
+ expect(stats.sessionCount).toBe(3);
126
+ expect(stats.messageCount).toBe(0);
127
+ expect(stats.hasNewActivity).toBe(true);
128
+ });
129
+ });
130
+ describe('ClaudeCodeAdapter.readVersion', () => {
131
+ it('returns version string from claude --version', async () => {
132
+ vi.mocked(execFileSync).mockReturnValue(Buffer.from('claude-code 1.0.23\n'));
133
+ const version = await adapter.readVersion();
134
+ expect(version).toBe('claude-code 1.0.23');
135
+ expect(execFileSync).toHaveBeenCalledWith('claude', ['--version'], {
136
+ stdio: 'pipe',
137
+ });
138
+ });
139
+ it('returns null when claude is not installed', async () => {
140
+ vi.mocked(execFileSync).mockImplementation(() => {
141
+ throw new Error('ENOENT');
142
+ });
143
+ const version = await adapter.readVersion();
144
+ expect(version).toBeNull();
145
+ });
146
+ });
147
+ describe('ClaudeCodeAdapter.readSkillUsage', () => {
148
+ it('returns null when no skills directory exists', async () => {
149
+ vi.mocked(existsSync).mockReturnValue(false);
150
+ const usage = await adapter.readSkillUsage(null);
151
+ expect(usage).toBeNull();
152
+ });
153
+ });
@@ -0,0 +1,280 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ vi.mock('fs', () => ({
3
+ existsSync: vi.fn(),
4
+ readdirSync: vi.fn(),
5
+ readFileSync: vi.fn(),
6
+ statSync: vi.fn(),
7
+ writeFileSync: vi.fn(),
8
+ mkdirSync: vi.fn(),
9
+ }));
10
+ vi.mock('os', () => ({
11
+ homedir: vi.fn(() => '/mock/home'),
12
+ platform: vi.fn(() => 'darwin'),
13
+ }));
14
+ // Mock the utility imports so the module loads cleanly
15
+ vi.mock('../utils/json-config.js', () => ({
16
+ mergeJsonMcpServers: vi.fn(),
17
+ readJsonConfig: vi.fn(() => ({})),
18
+ writeJsonConfig: vi.fn(),
19
+ }));
20
+ import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
21
+ import { ClaudeDesktopAdapter } from '../claude-desktop.js';
22
+ import * as path from 'path';
23
+ const CLAUDE_APP_DIR = '/mock/home/Library/Application Support/Claude';
24
+ const AGENT_SESSIONS_DIR = `${CLAUDE_APP_DIR}/local-agent-mode-sessions`;
25
+ const CODE_SESSIONS_DIR = `${CLAUDE_APP_DIR}/claude-code-sessions`;
26
+ const SCHEDULED_TASKS_PATH = `${CLAUDE_APP_DIR}/scheduled-tasks.json`;
27
+ const SDK_VERSION_PATH = `${CLAUDE_APP_DIR}/claude-code/sdk-version`;
28
+ // Fixture data
29
+ const SESSION_1 = JSON.stringify({
30
+ sessionId: 'test-session-1',
31
+ title: 'Test coding session',
32
+ model: 'claude-opus-4-6',
33
+ createdAt: 1711929600000,
34
+ lastActivityAt: 1711933200000,
35
+ isArchived: false,
36
+ cwd: '/test/project',
37
+ memoryEnabled: true,
38
+ enabledMcpTools: { 'tool-a': {}, 'tool-b': {}, 'tool-c': {} },
39
+ });
40
+ const SESSION_2 = JSON.stringify({
41
+ sessionId: 'test-session-2',
42
+ title: 'Research session',
43
+ model: 'claude-sonnet-4-6',
44
+ createdAt: 1712000000000,
45
+ lastActivityAt: 1712003600000,
46
+ isArchived: false,
47
+ cwd: '/test/other',
48
+ memoryEnabled: true,
49
+ });
50
+ const SCHEDULED_TASKS = JSON.stringify([
51
+ {
52
+ name: 'daily-research',
53
+ createdAt: '2026-03-01T00:00:00Z',
54
+ lastRunAt: '2026-03-30T06:00:00Z',
55
+ lastScheduledFor: '2026-03-30T06:00:00Z',
56
+ },
57
+ ]);
58
+ // Helper to set up the typical org/user directory structure
59
+ function setupSessionDirs(baseDir, sessions) {
60
+ const orgIds = [...new Set(sessions.map(s => s.orgId))];
61
+ const userIds = [...new Set(sessions.map(s => `${s.orgId}/${s.userId}`))];
62
+ readdirSync.mockImplementation((dir) => {
63
+ if (dir === baseDir)
64
+ return orgIds;
65
+ for (const org of orgIds) {
66
+ if (dir === path.join(baseDir, org)) {
67
+ // Deduplicate userIds within this org
68
+ return [...new Set(sessions.filter(s => s.orgId === org).map(s => s.userId))];
69
+ }
70
+ }
71
+ for (const uid of userIds) {
72
+ if (dir === path.join(baseDir, uid)) {
73
+ return sessions.filter(s => `${s.orgId}/${s.userId}` === uid).map(s => s.fileName);
74
+ }
75
+ }
76
+ return [];
77
+ });
78
+ statSync.mockImplementation(() => ({
79
+ isDirectory: () => true,
80
+ }));
81
+ readFileSync.mockImplementation((filePath) => {
82
+ for (const s of sessions) {
83
+ if (filePath === path.join(baseDir, s.orgId, s.userId, s.fileName)) {
84
+ return s.content;
85
+ }
86
+ }
87
+ if (filePath === SCHEDULED_TASKS_PATH)
88
+ return SCHEDULED_TASKS;
89
+ if (filePath === SDK_VERSION_PATH)
90
+ return '1.2.3\n';
91
+ return '';
92
+ });
93
+ }
94
+ describe('ClaudeDesktopAdapter.readUsageStats', () => {
95
+ let adapter;
96
+ beforeEach(() => {
97
+ vi.restoreAllMocks();
98
+ adapter = new ClaudeDesktopAdapter();
99
+ // Default: Claude app dir exists
100
+ existsSync.mockImplementation((p) => {
101
+ if (p === CLAUDE_APP_DIR)
102
+ return true;
103
+ return false;
104
+ });
105
+ });
106
+ it('reads and counts sessions across org/user directories', async () => {
107
+ existsSync.mockImplementation((p) => {
108
+ if (p === CLAUDE_APP_DIR)
109
+ return true;
110
+ if (p === AGENT_SESSIONS_DIR)
111
+ return true;
112
+ return false;
113
+ });
114
+ setupSessionDirs(AGENT_SESSIONS_DIR, [
115
+ { orgId: 'org1', userId: 'user1', fileName: 'session1.json', content: SESSION_1 },
116
+ { orgId: 'org1', userId: 'user2', fileName: 'session2.json', content: SESSION_2 },
117
+ ]);
118
+ const result = await adapter.readUsageStats(null);
119
+ expect(result).not.toBeNull();
120
+ expect(result.sessionCount).toBe(2);
121
+ expect(result.hasNewActivity).toBe(true);
122
+ });
123
+ it('filters sessions by lastSyncAt', async () => {
124
+ existsSync.mockImplementation((p) => {
125
+ if (p === CLAUDE_APP_DIR)
126
+ return true;
127
+ if (p === AGENT_SESSIONS_DIR)
128
+ return true;
129
+ return false;
130
+ });
131
+ setupSessionDirs(AGENT_SESSIONS_DIR, [
132
+ { orgId: 'org1', userId: 'user1', fileName: 'session1.json', content: SESSION_1 },
133
+ { orgId: 'org1', userId: 'user1', fileName: 'session2.json', content: SESSION_2 },
134
+ ]);
135
+ // lastSyncAt between session1 and session2
136
+ const syncAt = new Date(1711940000000).toISOString();
137
+ const result = await adapter.readUsageStats(syncAt);
138
+ expect(result).not.toBeNull();
139
+ // Only session2 (lastActivityAt 1712003600000) is after syncAt
140
+ expect(result.sessionCount).toBe(1);
141
+ });
142
+ it('collects unique models used', async () => {
143
+ existsSync.mockImplementation((p) => {
144
+ if (p === CLAUDE_APP_DIR)
145
+ return true;
146
+ if (p === AGENT_SESSIONS_DIR)
147
+ return true;
148
+ return false;
149
+ });
150
+ setupSessionDirs(AGENT_SESSIONS_DIR, [
151
+ { orgId: 'org1', userId: 'user1', fileName: 's1.json', content: SESSION_1 },
152
+ { orgId: 'org1', userId: 'user1', fileName: 's2.json', content: SESSION_2 },
153
+ ]);
154
+ const result = await adapter.readUsageStats(null);
155
+ expect(result).not.toBeNull();
156
+ expect(result.modelsUsed).toContain('claude-opus-4-6');
157
+ expect(result.modelsUsed).toContain('claude-sonnet-4-6');
158
+ expect(result.modelsUsed.length).toBe(2);
159
+ });
160
+ it('counts max MCP tools from enabledMcpTools', async () => {
161
+ existsSync.mockImplementation((p) => {
162
+ if (p === CLAUDE_APP_DIR)
163
+ return true;
164
+ if (p === AGENT_SESSIONS_DIR)
165
+ return true;
166
+ return false;
167
+ });
168
+ setupSessionDirs(AGENT_SESSIONS_DIR, [
169
+ { orgId: 'org1', userId: 'user1', fileName: 's1.json', content: SESSION_1 },
170
+ { orgId: 'org1', userId: 'user1', fileName: 's2.json', content: SESSION_2 },
171
+ ]);
172
+ const result = await adapter.readUsageStats(null);
173
+ expect(result).not.toBeNull();
174
+ // session1 has 3 MCP tools, session2 has none
175
+ expect(result.mcpToolCount).toBe(3);
176
+ });
177
+ it('counts scheduled task runs since lastSyncAt', async () => {
178
+ existsSync.mockImplementation((p) => {
179
+ if (p === CLAUDE_APP_DIR)
180
+ return true;
181
+ if (p === SCHEDULED_TASKS_PATH)
182
+ return true;
183
+ return false;
184
+ });
185
+ readFileSync.mockImplementation((filePath) => {
186
+ if (filePath === SCHEDULED_TASKS_PATH)
187
+ return SCHEDULED_TASKS;
188
+ return '';
189
+ });
190
+ // Sync before the task run
191
+ const syncAt = new Date('2026-03-29T00:00:00Z').toISOString();
192
+ const result = await adapter.readUsageStats(syncAt);
193
+ expect(result).not.toBeNull();
194
+ expect(result.extra).toBeDefined();
195
+ expect(result.extra.scheduledTaskRuns).toBe(1);
196
+ });
197
+ it('returns null when Claude Desktop not installed', async () => {
198
+ existsSync.mockReturnValue(false);
199
+ const result = await adapter.readUsageStats(null);
200
+ expect(result).toBeNull();
201
+ });
202
+ it('returns null when local-agent-mode-sessions directory is empty', async () => {
203
+ existsSync.mockImplementation((p) => {
204
+ if (p === CLAUDE_APP_DIR)
205
+ return true;
206
+ if (p === AGENT_SESSIONS_DIR)
207
+ return true;
208
+ return false;
209
+ });
210
+ readdirSync.mockReturnValue([]);
211
+ const result = await adapter.readUsageStats(null);
212
+ expect(result).toBeNull();
213
+ });
214
+ it('handles corrupted session JSON gracefully', async () => {
215
+ existsSync.mockImplementation((p) => {
216
+ if (p === CLAUDE_APP_DIR)
217
+ return true;
218
+ if (p === AGENT_SESSIONS_DIR)
219
+ return true;
220
+ return false;
221
+ });
222
+ setupSessionDirs(AGENT_SESSIONS_DIR, [
223
+ { orgId: 'org1', userId: 'user1', fileName: 'bad.json', content: '{{{invalid json' },
224
+ { orgId: 'org1', userId: 'user1', fileName: 'good.json', content: SESSION_1 },
225
+ ]);
226
+ const result = await adapter.readUsageStats(null);
227
+ expect(result).not.toBeNull();
228
+ // Only the valid session is counted
229
+ expect(result.sessionCount).toBe(1);
230
+ });
231
+ it('handles session with missing fields', async () => {
232
+ const minimalSession = JSON.stringify({
233
+ sessionId: 'minimal',
234
+ createdAt: 1712000000000,
235
+ lastActivityAt: 1712003600000,
236
+ });
237
+ existsSync.mockImplementation((p) => {
238
+ if (p === CLAUDE_APP_DIR)
239
+ return true;
240
+ if (p === AGENT_SESSIONS_DIR)
241
+ return true;
242
+ return false;
243
+ });
244
+ setupSessionDirs(AGENT_SESSIONS_DIR, [
245
+ { orgId: 'org1', userId: 'user1', fileName: 's.json', content: minimalSession },
246
+ ]);
247
+ const result = await adapter.readUsageStats(null);
248
+ expect(result).not.toBeNull();
249
+ expect(result.sessionCount).toBe(1);
250
+ // No model, no enabledMcpTools
251
+ expect(result.modelsUsed).toEqual([]);
252
+ expect(result.mcpToolCount).toBeUndefined();
253
+ });
254
+ });
255
+ describe('ClaudeDesktopAdapter.readVersion', () => {
256
+ let adapter;
257
+ beforeEach(() => {
258
+ vi.restoreAllMocks();
259
+ adapter = new ClaudeDesktopAdapter();
260
+ });
261
+ it('returns version from sdk-version file', async () => {
262
+ existsSync.mockImplementation((p) => {
263
+ if (p === SDK_VERSION_PATH)
264
+ return true;
265
+ return false;
266
+ });
267
+ readFileSync.mockImplementation((filePath) => {
268
+ if (filePath === SDK_VERSION_PATH)
269
+ return '1.2.3\n';
270
+ return '';
271
+ });
272
+ const version = await adapter.readVersion();
273
+ expect(version).toBe('1.2.3');
274
+ });
275
+ it('returns null when file does not exist', async () => {
276
+ existsSync.mockReturnValue(false);
277
+ const version = await adapter.readVersion();
278
+ expect(version).toBeNull();
279
+ });
280
+ });
@@ -0,0 +1 @@
1
+ export {};