ultraclaude-agent 0.0.3

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 (62) hide show
  1. package/__tests__/config-windows.test.ts +93 -0
  2. package/__tests__/daemon.test.ts +166 -0
  3. package/__tests__/service-windows.test.ts +123 -0
  4. package/__tests__/sync-bugs.test.ts +246 -0
  5. package/__tests__/sync.test.ts +169 -0
  6. package/__tests__/usage-sync.test.ts +291 -0
  7. package/__tests__/version-check.test.ts +128 -0
  8. package/dist/auth.d.ts +10 -0
  9. package/dist/auth.d.ts.map +1 -0
  10. package/dist/auth.js +105 -0
  11. package/dist/auth.js.map +1 -0
  12. package/dist/cli.d.ts +3 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +196 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/config.d.ts +26 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +181 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/daemon.d.ts +27 -0
  21. package/dist/daemon.d.ts.map +1 -0
  22. package/dist/daemon.js +214 -0
  23. package/dist/daemon.js.map +1 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +7 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/logger.d.ts +3 -0
  29. package/dist/logger.d.ts.map +1 -0
  30. package/dist/logger.js +37 -0
  31. package/dist/logger.js.map +1 -0
  32. package/dist/service.d.ts +4 -0
  33. package/dist/service.d.ts.map +1 -0
  34. package/dist/service.js +223 -0
  35. package/dist/service.js.map +1 -0
  36. package/dist/sync.d.ts +25 -0
  37. package/dist/sync.d.ts.map +1 -0
  38. package/dist/sync.js +344 -0
  39. package/dist/sync.js.map +1 -0
  40. package/dist/usage-sync.d.ts +7 -0
  41. package/dist/usage-sync.d.ts.map +1 -0
  42. package/dist/usage-sync.js +208 -0
  43. package/dist/usage-sync.js.map +1 -0
  44. package/dist/watcher.d.ts +16 -0
  45. package/dist/watcher.d.ts.map +1 -0
  46. package/dist/watcher.js +90 -0
  47. package/dist/watcher.js.map +1 -0
  48. package/package.json +31 -0
  49. package/run.sh +28 -0
  50. package/src/auth.ts +127 -0
  51. package/src/cli.ts +235 -0
  52. package/src/config.ts +207 -0
  53. package/src/daemon.ts +264 -0
  54. package/src/index.ts +7 -0
  55. package/src/logger.ts +42 -0
  56. package/src/service.ts +237 -0
  57. package/src/sync.ts +473 -0
  58. package/src/usage-sync.ts +275 -0
  59. package/src/watcher.ts +106 -0
  60. package/tsconfig.build.json +6 -0
  61. package/tsconfig.json +8 -0
  62. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { join } from 'node:path';
3
+ import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+
6
+ // Mock fetch globally
7
+ const mockFetch = vi.fn();
8
+ vi.stubGlobal('fetch', mockFetch);
9
+
10
+ // Mock config module
11
+ vi.mock('../src/config.js', () => ({
12
+ loadCredentials: vi.fn().mockResolvedValue({
13
+ apiKey: 'test-api-key',
14
+ userId: 'test-user',
15
+ serverUrl: 'http://localhost:3000',
16
+ }),
17
+ getServerUrl: vi.fn().mockReturnValue('http://localhost:3000'),
18
+ getProjectId: vi.fn().mockResolvedValue('test-project-id'),
19
+ writeProjectId: vi.fn().mockResolvedValue(undefined),
20
+ paths: {
21
+ configDir: '/tmp/test-config',
22
+ credentials: '/tmp/test-config/credentials.json',
23
+ pid: '/tmp/test-config/daemon.pid',
24
+ logDir: '/tmp/test-config/logs',
25
+ registry: '/tmp/test-registry.json',
26
+ projectIdFile: '.claude/ultra/project-id',
27
+ },
28
+ }));
29
+
30
+ // Mock logger
31
+ vi.mock('../src/logger.js', () => ({
32
+ logger: {
33
+ child: () => ({
34
+ info: vi.fn(),
35
+ warn: vi.fn(),
36
+ error: vi.fn(),
37
+ debug: vi.fn(),
38
+ }),
39
+ info: vi.fn(),
40
+ warn: vi.fn(),
41
+ error: vi.fn(),
42
+ debug: vi.fn(),
43
+ },
44
+ }));
45
+
46
+ describe('sync — syncMarkdownFile', () => {
47
+ let tempDir: string;
48
+
49
+ beforeEach(async () => {
50
+ mockFetch.mockReset();
51
+ tempDir = await mkdtemp(join(tmpdir(), 'agent-sync-test-'));
52
+ await mkdir(join(tempDir, 'documentation'), { recursive: true });
53
+ });
54
+
55
+ afterEach(async () => {
56
+ await rm(tempDir, { recursive: true, force: true });
57
+ });
58
+
59
+ it('pushes changed sections to the server', async () => {
60
+ const { syncMarkdownFile } = await import('../src/sync.js');
61
+
62
+ const mdContent = '# Title\n\nSome content.\n\n## Section\n\nMore content.';
63
+ const filePath = join(tempDir, 'documentation', 'test.md');
64
+ await writeFile(filePath, mdContent);
65
+
66
+ mockFetch.mockResolvedValueOnce({
67
+ ok: true,
68
+ json: async () => ({ data: [] }),
69
+ });
70
+
71
+ // First push — all sections are new
72
+ mockFetch.mockResolvedValueOnce({
73
+ ok: true,
74
+ json: async () => ({ data: { upserted: 2 } }),
75
+ });
76
+
77
+ const count = await syncMarkdownFile('test-project-id', tempDir, filePath);
78
+ expect(count).toBe(2);
79
+
80
+ // Verify the POST request
81
+ const postCall = mockFetch.mock.calls.find(
82
+ (call: unknown[]) =>
83
+ (call[1] as { method: string }).method === 'POST' &&
84
+ (call[0] as string).includes('/api/sync/sections'),
85
+ );
86
+ expect(postCall).toBeDefined();
87
+
88
+ const body = JSON.parse((postCall![1] as { body: string }).body);
89
+ expect(body.file).toBe('test.md');
90
+ expect(body.sections).toHaveLength(2);
91
+ expect(body.sections[0].slug).toBe('title');
92
+ expect(body.sections[1].slug).toBe('title/section');
93
+ });
94
+
95
+ it('queues changes when server is unreachable', async () => {
96
+ // Re-import to get fresh module state
97
+ const sync = await import('../src/sync.js');
98
+
99
+ const mdContent = '# Title\n\nContent.';
100
+ const filePath = join(tempDir, 'documentation', 'test2.md');
101
+ await writeFile(filePath, mdContent);
102
+
103
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
104
+
105
+ const count = await sync.syncMarkdownFile('test-project-id', tempDir, filePath);
106
+ expect(count).toBe(0); // Nothing pushed
107
+ expect(sync.getQueueSize()).toBeGreaterThan(0);
108
+ });
109
+ });
110
+
111
+ describe('sync — syncJsonFile', () => {
112
+ let tempDir: string;
113
+
114
+ beforeEach(async () => {
115
+ mockFetch.mockReset();
116
+ tempDir = await mkdtemp(join(tmpdir(), 'agent-sync-json-test-'));
117
+ await mkdir(join(tempDir, 'documentation'), { recursive: true });
118
+ });
119
+
120
+ afterEach(async () => {
121
+ await rm(tempDir, { recursive: true, force: true });
122
+ });
123
+
124
+ it('pushes JSON files with full content', async () => {
125
+ const { syncJsonFile } = await import('../src/sync.js');
126
+
127
+ const jsonContent = JSON.stringify({ status: 'active', tasks: 5 });
128
+ const filePath = join(tempDir, 'documentation', 'status.json');
129
+ await writeFile(filePath, jsonContent);
130
+
131
+ mockFetch.mockResolvedValueOnce({
132
+ ok: true,
133
+ json: async () => ({ data: { stored: 1 } }),
134
+ });
135
+
136
+ const pushed = await syncJsonFile('test-project-id', tempDir, filePath);
137
+ expect(pushed).toBe(true);
138
+ });
139
+ });
140
+
141
+ describe('sync — createProjectOnServer', () => {
142
+ beforeEach(() => {
143
+ mockFetch.mockReset();
144
+ });
145
+
146
+ it('creates a project and returns the ID', async () => {
147
+ const { createProjectOnServer } = await import('../src/sync.js');
148
+
149
+ mockFetch.mockResolvedValueOnce({
150
+ ok: true,
151
+ json: async () => ({ data: { id: 'new-project-uuid' } }),
152
+ });
153
+
154
+ const result = await createProjectOnServer('My Project', 'my-project');
155
+ expect(result).toEqual({ id: 'new-project-uuid' });
156
+ });
157
+
158
+ it('returns null on server error', async () => {
159
+ const { createProjectOnServer } = await import('../src/sync.js');
160
+
161
+ mockFetch.mockResolvedValueOnce({
162
+ ok: false,
163
+ status: 500,
164
+ });
165
+
166
+ const result = await createProjectOnServer('Bad Project', 'bad-project');
167
+ expect(result).toBeNull();
168
+ });
169
+ });
@@ -0,0 +1,291 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { join } from 'node:path';
3
+ import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+
6
+ // Mock fetch globally
7
+ const mockFetch = vi.fn();
8
+ vi.stubGlobal('fetch', mockFetch);
9
+
10
+ // Mock config module
11
+ vi.mock('../src/config.js', () => ({
12
+ loadCredentials: vi.fn().mockResolvedValue({
13
+ apiKey: 'test-api-key',
14
+ userId: 'test-user',
15
+ serverUrl: 'http://localhost:3000',
16
+ }),
17
+ getServerUrl: vi.fn().mockReturnValue('http://localhost:3000'),
18
+ paths: {
19
+ configDir: '/tmp/test-config',
20
+ credentials: '/tmp/test-config/credentials.json',
21
+ pid: '/tmp/test-config/daemon.pid',
22
+ logDir: '/tmp/test-config/logs',
23
+ claudeProjects: '/tmp/test-projects',
24
+ projectIdFile: '.claude/ultra/project-id',
25
+ },
26
+ }));
27
+
28
+ // Mock logger
29
+ vi.mock('../src/logger.js', () => ({
30
+ logger: {
31
+ child: () => ({
32
+ info: vi.fn(),
33
+ warn: vi.fn(),
34
+ error: vi.fn(),
35
+ debug: vi.fn(),
36
+ }),
37
+ info: vi.fn(),
38
+ warn: vi.fn(),
39
+ error: vi.fn(),
40
+ debug: vi.fn(),
41
+ },
42
+ }));
43
+
44
+ // We need to mock the paths used by usage-sync.ts to point to our temp dir
45
+ let tempDir: string;
46
+ let usageStatusPath: string;
47
+ let accountsDir: string;
48
+
49
+ // Dynamic mock for the homedir-based paths
50
+ vi.mock('node:os', async (importOriginal) => {
51
+ const original = await importOriginal<typeof import('node:os')>();
52
+ return {
53
+ ...original,
54
+ homedir: () => tempDir,
55
+ };
56
+ });
57
+
58
+ describe('usage-sync — syncUsageData', () => {
59
+ beforeEach(async () => {
60
+ mockFetch.mockReset();
61
+ tempDir = await mkdtemp(join(tmpdir(), 'usage-sync-test-'));
62
+ usageStatusPath = join(tempDir, '.claude', 'ultra', 'usage-status.json');
63
+ accountsDir = join(tempDir, '.claude', 'ultra', 'accounts');
64
+ await mkdir(join(tempDir, '.claude', 'ultra'), { recursive: true });
65
+ });
66
+
67
+ afterEach(async () => {
68
+ // Reset module state between tests
69
+ vi.resetModules();
70
+ await rm(tempDir, { recursive: true, force: true });
71
+ });
72
+
73
+ it('reads and merges usage-status.json with account registry', async () => {
74
+ // Write usage status
75
+ await writeFile(usageStatusPath, JSON.stringify({
76
+ accounts: {
77
+ 'dawid-duniec-at-axb-co': {
78
+ account_id: 'dawid-duniec-at-axb-co',
79
+ rate_limits: {
80
+ five_hour: { used_percentage: 42, resets_at: 1775329200 },
81
+ seven_day: { used_percentage: 18, resets_at: 1775894400 },
82
+ },
83
+ updated_at: '2026-04-04T16:13:01Z',
84
+ },
85
+ },
86
+ updated_at: '2026-04-04T16:13:01Z',
87
+ }));
88
+
89
+ // Write account registry
90
+ await mkdir(accountsDir, { recursive: true });
91
+ await writeFile(join(accountsDir, 'dawid-duniec-at-axb-co.json'), JSON.stringify({
92
+ account_id: 'dawid-duniec-at-axb-co',
93
+ email: 'dawid.duniec@axb.co',
94
+ orgName: 'AXB',
95
+ subscriptionType: 'team',
96
+ }));
97
+
98
+ // Mock successful response
99
+ mockFetch.mockResolvedValueOnce({
100
+ ok: true,
101
+ status: 200,
102
+ json: () => Promise.resolve({ data: { ok: true, upserted: 1 } }),
103
+ });
104
+
105
+ const { syncUsageData } = await import('../src/usage-sync.js');
106
+ await syncUsageData();
107
+
108
+ expect(mockFetch).toHaveBeenCalledOnce();
109
+ const [url, options] = mockFetch.mock.calls[0]!;
110
+ expect(url).toBe('http://localhost:3000/api/sync/usage');
111
+ expect(options.method).toBe('POST');
112
+
113
+ const body = JSON.parse(options.body);
114
+ expect(body.accounts['dawid-duniec-at-axb-co']).toMatchObject({
115
+ account_id: 'dawid-duniec-at-axb-co',
116
+ email: 'dawid.duniec@axb.co',
117
+ org_name: 'AXB',
118
+ subscription_type: 'team',
119
+ rate_limits: {
120
+ five_hour: { used_percentage: 42, resets_at: 1775329200 },
121
+ seven_day: { used_percentage: 18, resets_at: 1775894400 },
122
+ },
123
+ updated_at: '2026-04-04T16:13:01Z',
124
+ });
125
+ });
126
+
127
+ it('handles missing usage-status.json gracefully', async () => {
128
+ // Don't create the file
129
+ const { syncUsageData } = await import('../src/usage-sync.js');
130
+ await syncUsageData();
131
+
132
+ expect(mockFetch).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it('handles empty usage-status.json gracefully', async () => {
136
+ await writeFile(usageStatusPath, '');
137
+
138
+ const { syncUsageData } = await import('../src/usage-sync.js');
139
+ await syncUsageData();
140
+
141
+ expect(mockFetch).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it('handles malformed JSON in usage-status.json gracefully', async () => {
145
+ await writeFile(usageStatusPath, '{not valid json!!!');
146
+
147
+ const { syncUsageData } = await import('../src/usage-sync.js');
148
+ await syncUsageData();
149
+
150
+ expect(mockFetch).not.toHaveBeenCalled();
151
+ });
152
+
153
+ it('handles missing accounts/ directory — sends data without account details', async () => {
154
+ // Write usage status but NO accounts/ directory
155
+ await writeFile(usageStatusPath, JSON.stringify({
156
+ accounts: {
157
+ 'test-user-at-example-com': {
158
+ account_id: 'test-user-at-example-com',
159
+ rate_limits: {
160
+ five_hour: { used_percentage: 80, resets_at: 1775329200 },
161
+ seven_day: { used_percentage: 50, resets_at: 1775894400 },
162
+ },
163
+ updated_at: '2026-04-04T16:13:01Z',
164
+ },
165
+ },
166
+ updated_at: '2026-04-04T16:13:01Z',
167
+ }));
168
+
169
+ mockFetch.mockResolvedValueOnce({
170
+ ok: true,
171
+ status: 200,
172
+ json: () => Promise.resolve({ data: { ok: true, upserted: 1 } }),
173
+ });
174
+
175
+ const { syncUsageData } = await import('../src/usage-sync.js');
176
+ await syncUsageData();
177
+
178
+ expect(mockFetch).toHaveBeenCalledOnce();
179
+ const body = JSON.parse(mockFetch.mock.calls[0]![1].body);
180
+ const account = body.accounts['test-user-at-example-com'];
181
+ expect(account.account_id).toBe('test-user-at-example-com');
182
+ // No account registry → optional fields should be undefined
183
+ expect(account.email).toBeUndefined();
184
+ expect(account.org_name).toBeUndefined();
185
+ expect(account.subscription_type).toBeUndefined();
186
+ // Rate limits should still be present
187
+ expect(account.rate_limits.five_hour.used_percentage).toBe(80);
188
+ });
189
+
190
+ it('handles missing individual account file — other accounts still sent', async () => {
191
+ await writeFile(usageStatusPath, JSON.stringify({
192
+ accounts: {
193
+ 'user-a-at-example-com': {
194
+ account_id: 'user-a-at-example-com',
195
+ rate_limits: {
196
+ five_hour: { used_percentage: 10, resets_at: 1775329200 },
197
+ seven_day: { used_percentage: 5, resets_at: 1775894400 },
198
+ },
199
+ updated_at: '2026-04-04T16:13:01Z',
200
+ },
201
+ 'user-b-at-example-com': {
202
+ account_id: 'user-b-at-example-com',
203
+ rate_limits: {
204
+ five_hour: { used_percentage: 90, resets_at: 1775329200 },
205
+ seven_day: { used_percentage: 70, resets_at: 1775894400 },
206
+ },
207
+ updated_at: '2026-04-04T16:13:01Z',
208
+ },
209
+ },
210
+ updated_at: '2026-04-04T16:13:01Z',
211
+ }));
212
+
213
+ // Only create account file for user-a
214
+ await mkdir(accountsDir, { recursive: true });
215
+ await writeFile(join(accountsDir, 'user-a-at-example-com.json'), JSON.stringify({
216
+ account_id: 'user-a-at-example-com',
217
+ email: 'user-a@example.com',
218
+ orgName: 'OrgA',
219
+ subscriptionType: 'pro',
220
+ }));
221
+
222
+ mockFetch.mockResolvedValueOnce({
223
+ ok: true,
224
+ status: 200,
225
+ json: () => Promise.resolve({ data: { ok: true, upserted: 2 } }),
226
+ });
227
+
228
+ const { syncUsageData } = await import('../src/usage-sync.js');
229
+ await syncUsageData();
230
+
231
+ expect(mockFetch).toHaveBeenCalledOnce();
232
+ const body = JSON.parse(mockFetch.mock.calls[0]![1].body);
233
+
234
+ // user-a has registry data
235
+ expect(body.accounts['user-a-at-example-com'].email).toBe('user-a@example.com');
236
+ expect(body.accounts['user-a-at-example-com'].org_name).toBe('OrgA');
237
+
238
+ // user-b has no registry data but is still included
239
+ expect(body.accounts['user-b-at-example-com'].account_id).toBe('user-b-at-example-com');
240
+ expect(body.accounts['user-b-at-example-com'].email).toBeUndefined();
241
+ expect(body.accounts['user-b-at-example-com'].rate_limits.five_hour.used_percentage).toBe(90);
242
+ });
243
+
244
+ it('stores retry payload on network failure', async () => {
245
+ await writeFile(usageStatusPath, JSON.stringify({
246
+ accounts: {
247
+ 'test-at-example-com': {
248
+ account_id: 'test-at-example-com',
249
+ rate_limits: {
250
+ five_hour: { used_percentage: 50, resets_at: 1775329200 },
251
+ seven_day: { used_percentage: 25, resets_at: 1775894400 },
252
+ },
253
+ updated_at: '2026-04-04T16:13:01Z',
254
+ },
255
+ },
256
+ updated_at: '2026-04-04T16:13:01Z',
257
+ }));
258
+
259
+ // Simulate network failure
260
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
261
+
262
+ const { syncUsageData } = await import('../src/usage-sync.js');
263
+ await syncUsageData();
264
+
265
+ // fetch was called (and failed)
266
+ expect(mockFetch).toHaveBeenCalledOnce();
267
+
268
+ // On second call with success, retry should work
269
+ mockFetch.mockResolvedValueOnce({
270
+ ok: true,
271
+ status: 200,
272
+ json: () => Promise.resolve({ data: { ok: true, upserted: 1 } }),
273
+ });
274
+
275
+ // Call sync again (simulating a new file change that supersedes retry)
276
+ await syncUsageData();
277
+ expect(mockFetch).toHaveBeenCalledTimes(2);
278
+ });
279
+
280
+ it('handles empty accounts object in usage-status.json', async () => {
281
+ await writeFile(usageStatusPath, JSON.stringify({
282
+ accounts: {},
283
+ updated_at: '2026-04-04T16:13:01Z',
284
+ }));
285
+
286
+ const { syncUsageData } = await import('../src/usage-sync.js');
287
+ await syncUsageData();
288
+
289
+ expect(mockFetch).not.toHaveBeenCalled();
290
+ });
291
+ });
@@ -0,0 +1,128 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ // Mock fetch globally
4
+ const mockFetch = vi.fn();
5
+ vi.stubGlobal('fetch', mockFetch);
6
+
7
+ // Mock node:fs/promises for reading package.json
8
+ const mockReadFile = vi.fn();
9
+ vi.mock('node:fs/promises', () => ({
10
+ readFile: (...args: unknown[]) => mockReadFile(...args),
11
+ }));
12
+
13
+ // Capture log calls
14
+ const mockLogInfo = vi.fn();
15
+ vi.mock('../src/logger.js', () => ({
16
+ logger: {
17
+ child: () => ({
18
+ info: mockLogInfo,
19
+ warn: vi.fn(),
20
+ error: vi.fn(),
21
+ debug: vi.fn(),
22
+ }),
23
+ info: vi.fn(),
24
+ warn: vi.fn(),
25
+ error: vi.fn(),
26
+ debug: vi.fn(),
27
+ },
28
+ }));
29
+
30
+ // Mock other daemon dependencies to avoid side effects
31
+ vi.mock('chokidar', () => ({
32
+ default: { watch: vi.fn().mockReturnValue({ on: vi.fn().mockReturnThis(), close: vi.fn() }) },
33
+ }));
34
+ vi.mock('../src/config.js', () => ({
35
+ paths: { configDir: '/tmp', credentials: '/tmp/creds', pid: '/tmp/pid', logDir: '/tmp/logs', claudeProjects: '/tmp/projects', projectIdFile: '.claude/ultra/project-id' },
36
+ loadCredentials: vi.fn().mockResolvedValue(null),
37
+ loadRegistry: vi.fn().mockResolvedValue({ projects: [] }),
38
+ getProjectId: vi.fn(),
39
+ writeProjectId: vi.fn(),
40
+ writePid: vi.fn(),
41
+ removePid: vi.fn(),
42
+ }));
43
+ vi.mock('../src/watcher.js', () => ({ startProjectWatcher: vi.fn() }));
44
+ vi.mock('../src/sync.js', () => ({ createProjectOnServer: vi.fn(), initialSync: vi.fn(), stopSync: vi.fn() }));
45
+ vi.mock('../src/usage-sync.js', () => ({ startUsageWatcher: vi.fn().mockReturnValue({ close: vi.fn() }) }));
46
+
47
+ describe('checkForUpdate', () => {
48
+ beforeEach(() => {
49
+ vi.clearAllMocks();
50
+ mockReadFile.mockResolvedValue(JSON.stringify({ version: '0.0.1' }));
51
+ });
52
+
53
+ afterEach(() => {
54
+ vi.restoreAllMocks();
55
+ });
56
+
57
+ it('logs notice when local version is older than npm latest', async () => {
58
+ mockFetch.mockResolvedValue({
59
+ ok: true,
60
+ json: async () => ({ version: '0.1.0' }),
61
+ });
62
+
63
+ const { checkForUpdate } = await import('../src/daemon.js');
64
+ await checkForUpdate();
65
+
66
+ expect(mockLogInfo).toHaveBeenCalledWith(
67
+ { current: '0.0.1', latest: '0.1.0' },
68
+ expect.stringContaining('Update available'),
69
+ );
70
+ });
71
+
72
+ it('does not log when local version equals npm latest', async () => {
73
+ mockFetch.mockResolvedValue({
74
+ ok: true,
75
+ json: async () => ({ version: '0.0.1' }),
76
+ });
77
+
78
+ vi.resetModules();
79
+ const { checkForUpdate } = await import('../src/daemon.js');
80
+ await checkForUpdate();
81
+
82
+ expect(mockLogInfo).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('does not log when local version is newer than npm latest', async () => {
86
+ mockFetch.mockResolvedValue({
87
+ ok: true,
88
+ json: async () => ({ version: '0.0.1' }),
89
+ });
90
+ mockReadFile.mockResolvedValue(JSON.stringify({ version: '0.1.0' }));
91
+
92
+ vi.resetModules();
93
+ const { checkForUpdate } = await import('../src/daemon.js');
94
+ await checkForUpdate();
95
+
96
+ expect(mockLogInfo).not.toHaveBeenCalled();
97
+ });
98
+
99
+ it('silently handles network errors', async () => {
100
+ mockFetch.mockRejectedValue(new Error('network error'));
101
+
102
+ vi.resetModules();
103
+ const { checkForUpdate } = await import('../src/daemon.js');
104
+
105
+ await expect(checkForUpdate()).resolves.toBeUndefined();
106
+ expect(mockLogInfo).not.toHaveBeenCalled();
107
+ });
108
+
109
+ it('silently handles non-ok responses', async () => {
110
+ mockFetch.mockResolvedValue({ ok: false, status: 404 });
111
+
112
+ vi.resetModules();
113
+ const { checkForUpdate } = await import('../src/daemon.js');
114
+
115
+ await expect(checkForUpdate()).resolves.toBeUndefined();
116
+ expect(mockLogInfo).not.toHaveBeenCalled();
117
+ });
118
+
119
+ it('silently handles fetch timeout (abort)', async () => {
120
+ mockFetch.mockRejectedValue(new DOMException('Aborted', 'AbortError'));
121
+
122
+ vi.resetModules();
123
+ const { checkForUpdate } = await import('../src/daemon.js');
124
+
125
+ await expect(checkForUpdate()).resolves.toBeUndefined();
126
+ expect(mockLogInfo).not.toHaveBeenCalled();
127
+ });
128
+ });
package/dist/auth.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { AgentCredentials } from '@ultra-claude/shared';
2
+ /**
3
+ * Run the browser-based login flow.
4
+ * 1. Start a localhost HTTP server on a random port
5
+ * 2. Open browser to the server's daemon-login page with callback URL
6
+ * 3. Wait for the server to redirect back with the API key
7
+ * 4. Save credentials and close the server
8
+ */
9
+ export declare function login(serverUrl?: string): Promise<AgentCredentials>;
10
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAI7D;;;;;;GAMG;AACH,wBAAsB,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA6GzE"}