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.
- package/dist/agents/__tests__/claude-code-stats.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-code-stats.test.js +153 -0
- package/dist/agents/__tests__/claude-desktop-stats.test.d.ts +1 -0
- package/dist/agents/__tests__/claude-desktop-stats.test.js +280 -0
- package/dist/agents/__tests__/codex-stats.test.d.ts +1 -0
- package/dist/agents/__tests__/codex-stats.test.js +262 -0
- package/dist/agents/__tests__/cursor-stats.test.d.ts +1 -0
- package/dist/agents/__tests__/cursor-stats.test.js +183 -0
- package/dist/agents/__tests__/graceful-degradation.test.d.ts +1 -0
- package/dist/agents/__tests__/graceful-degradation.test.js +123 -0
- package/dist/agents/__tests__/instruction-hint.test.d.ts +1 -0
- package/dist/agents/__tests__/instruction-hint.test.js +69 -0
- package/dist/agents/__tests__/intro-skill.test.d.ts +1 -0
- package/dist/agents/__tests__/intro-skill.test.js +148 -0
- package/dist/agents/__tests__/schema-validation.test.d.ts +1 -0
- package/dist/agents/__tests__/schema-validation.test.js +212 -0
- package/dist/agents/claude-code.d.ts +16 -1
- package/dist/agents/claude-code.js +344 -21
- package/dist/agents/claude-desktop.d.ts +10 -1
- package/dist/agents/claude-desktop.js +250 -8
- package/dist/agents/cline.d.ts +13 -0
- package/dist/agents/cline.js +88 -0
- package/dist/agents/codex.d.ts +7 -2
- package/dist/agents/codex.js +130 -18
- package/dist/agents/cursor.d.ts +6 -1
- package/dist/agents/cursor.js +164 -7
- package/dist/agents/detect.js +30 -7
- package/dist/agents/gemini.d.ts +6 -3
- package/dist/agents/gemini.js +57 -20
- package/dist/agents/generic-adapter.d.ts +23 -0
- package/dist/agents/generic-adapter.js +106 -0
- package/dist/agents/intro-skill.d.ts +36 -0
- package/dist/agents/intro-skill.js +358 -0
- package/dist/agents/registry.d.ts +53 -0
- package/dist/agents/registry.js +219 -0
- package/dist/agents/types.d.ts +44 -1
- package/dist/agents/types.js +11 -4
- package/dist/agents/utils/instruction-hint.d.ts +11 -0
- package/dist/agents/utils/instruction-hint.js +62 -0
- package/dist/agents/utils/json-config.js +3 -3
- package/dist/agents/windsurf.d.ts +2 -0
- package/dist/agents/windsurf.js +26 -9
- package/dist/api/client.d.ts +85 -2
- package/dist/api/client.js +40 -10
- package/dist/auth/__tests__/login-flow.test.d.ts +1 -0
- package/dist/auth/__tests__/login-flow.test.js +125 -0
- package/dist/auth/login-flow.d.ts +16 -0
- package/dist/auth/login-flow.js +101 -1
- package/dist/commands/clone.d.ts +9 -1
- package/dist/commands/clone.js +40 -12
- package/dist/commands/deploy.js +27 -4
- package/dist/commands/dev.js +35 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +82 -0
- package/dist/commands/info.js +6 -2
- package/dist/commands/init.d.ts +10 -2
- package/dist/commands/init.js +58 -15
- package/dist/commands/integrations.js +47 -10
- package/dist/commands/login.js +31 -4
- package/dist/commands/mcp.js +143 -15
- package/dist/commands/setup.js +47 -122
- package/dist/commands/skills.js +230 -12
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.js +398 -65
- package/dist/commands/welcome.js +6 -3
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/health/__tests__/checks.test.d.ts +1 -0
- package/dist/health/__tests__/checks.test.js +223 -0
- package/dist/health/checks.d.ts +20 -0
- package/dist/health/checks.js +392 -0
- package/dist/health/runner.d.ts +2 -0
- package/dist/health/runner.js +26 -0
- package/dist/health/types.d.ts +12 -0
- package/dist/health/types.js +1 -0
- package/dist/index.js +32 -0
- package/dist/sync/__tests__/change-detect.test.d.ts +1 -0
- package/dist/sync/__tests__/change-detect.test.js +123 -0
- package/dist/sync/__tests__/hash.test.d.ts +1 -0
- package/dist/sync/__tests__/hash.test.js +18 -0
- package/dist/sync/change-detect.d.ts +19 -0
- package/dist/sync/change-detect.js +136 -0
- package/dist/sync/conflict-ui.d.ts +5 -0
- package/dist/sync/conflict-ui.js +78 -0
- package/dist/sync/executor.d.ts +21 -0
- package/dist/sync/executor.js +98 -0
- package/dist/sync/hash.d.ts +1 -0
- package/dist/sync/hash.js +4 -0
- package/dist/sync/types.d.ts +24 -0
- package/dist/sync/types.js +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/ui/__tests__/banner.test.js +17 -1
- package/dist/ui/banner.d.ts +1 -1
- package/dist/ui/banner.js +27 -2
- package/dist/utils/__tests__/resolve.test.d.ts +1 -0
- package/dist/utils/__tests__/resolve.test.js +95 -0
- package/dist/utils/__tests__/sqlite.test.d.ts +1 -0
- package/dist/utils/__tests__/sqlite.test.js +77 -0
- package/dist/utils/agent-guidance.d.ts +32 -0
- package/dist/utils/agent-guidance.js +106 -0
- package/dist/utils/curl-parser.d.ts +1 -1
- package/dist/utils/curl-parser.js +144 -26
- package/dist/utils/fs.js +2 -2
- package/dist/utils/help-json.d.ts +56 -0
- package/dist/utils/help-json.js +85 -0
- package/dist/utils/resolve.d.ts +12 -0
- package/dist/utils/resolve.js +52 -0
- package/dist/utils/setup-state.d.ts +3 -0
- package/dist/utils/setup-state.js +19 -0
- package/dist/utils/sqlite.d.ts +7 -0
- package/dist/utils/sqlite.js +24 -0
- package/dist/utils/which.d.ts +6 -0
- package/dist/utils/which.js +18 -0
- package/package.json +7 -4
- package/dist/agents/copilot-cli.d.ts +0 -10
- package/dist/agents/copilot-cli.js +0 -46
- package/dist/agents/copilot-vscode.d.ts +0 -10
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 {};
|