@hasna/connectors 1.3.45 → 1.4.0

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/README.md +25 -10
  2. package/bin/index.js +1362 -11796
  3. package/bin/mcp.js +751 -10328
  4. package/bin/serve.js +2252 -11829
  5. package/connectors/cloudflare-workers/.env.example +5 -0
  6. package/connectors/cloudflare-workers/AGENTS.md +11 -0
  7. package/connectors/cloudflare-workers/CLAUDE.md +11 -0
  8. package/connectors/cloudflare-workers/README.md +45 -0
  9. package/connectors/cloudflare-workers/package.json +47 -0
  10. package/connectors/cloudflare-workers/src/api/client.test.ts +144 -0
  11. package/connectors/cloudflare-workers/src/api/client.ts +386 -0
  12. package/connectors/cloudflare-workers/src/api/index.ts +1 -0
  13. package/connectors/cloudflare-workers/src/cli/index.ts +206 -0
  14. package/connectors/cloudflare-workers/src/index.ts +16 -0
  15. package/connectors/cloudflare-workers/src/types/index.ts +53 -0
  16. package/connectors/cloudflare-workers/src/utils/config.ts +61 -0
  17. package/connectors/cloudflare-workers/src/utils/output.ts +26 -0
  18. package/connectors/cloudflare-workers/tsconfig.build.json +4 -0
  19. package/connectors/cloudflare-workers/tsconfig.json +14 -0
  20. package/connectors/tidio/.env.example +3 -2
  21. package/connectors/tidio/CLAUDE.md +21 -96
  22. package/connectors/tidio/README.md +22 -18
  23. package/connectors/tidio/package.json +4 -2
  24. package/connectors/tidio/src/api/client.ts +26 -15
  25. package/connectors/tidio/src/api/index.ts +185 -128
  26. package/connectors/tidio/src/api/tidio.test.ts +175 -44
  27. package/connectors/tidio/src/cli/index.ts +154 -388
  28. package/connectors/tidio/src/index.ts +5 -3
  29. package/connectors/tidio/src/types/index.ts +135 -82
  30. package/connectors/tidio/src/utils/config.ts +90 -44
  31. package/connectors/wandb/.env.example +7 -0
  32. package/connectors/wandb/AGENTS.md +43 -0
  33. package/connectors/wandb/CLAUDE.md +67 -0
  34. package/connectors/wandb/README.md +65 -0
  35. package/connectors/wandb/package.json +52 -0
  36. package/connectors/wandb/src/api/client.test.ts +116 -0
  37. package/connectors/wandb/src/api/client.ts +83 -0
  38. package/connectors/wandb/src/api/graphql.ts +9 -0
  39. package/connectors/wandb/src/api/index.ts +36 -0
  40. package/connectors/wandb/src/api/projects.ts +36 -0
  41. package/connectors/wandb/src/api/viewer.ts +20 -0
  42. package/connectors/wandb/src/cli/index.ts +265 -0
  43. package/connectors/wandb/src/index.ts +22 -0
  44. package/connectors/wandb/src/types/index.ts +75 -0
  45. package/connectors/wandb/src/utils/config.ts +164 -0
  46. package/connectors/wandb/src/utils/output.ts +119 -0
  47. package/connectors/wandb/tsconfig.json +16 -0
  48. package/connectors/weights-biases/.env.example +7 -0
  49. package/connectors/weights-biases/CLAUDE.md +65 -0
  50. package/connectors/weights-biases/README.md +62 -0
  51. package/connectors/weights-biases/package.json +53 -0
  52. package/connectors/weights-biases/src/api/client.test.ts +46 -0
  53. package/connectors/weights-biases/src/api/client.ts +119 -0
  54. package/connectors/weights-biases/src/api/events.ts +18 -0
  55. package/connectors/weights-biases/src/api/index.ts +56 -0
  56. package/connectors/weights-biases/src/api/runs.ts +28 -0
  57. package/connectors/weights-biases/src/api/search.ts +10 -0
  58. package/connectors/weights-biases/src/cli/index.ts +289 -0
  59. package/connectors/weights-biases/src/index.ts +19 -0
  60. package/connectors/weights-biases/src/types/index.ts +92 -0
  61. package/connectors/weights-biases/src/utils/config.ts +168 -0
  62. package/connectors/weights-biases/src/utils/output.ts +116 -0
  63. package/connectors/weights-biases/tsconfig.json +16 -0
  64. package/connectors/x/src/api/oauth.test.ts +205 -0
  65. package/connectors/zoominfo/.env.example +4 -0
  66. package/connectors/zoominfo/AGENTS.md +26 -0
  67. package/connectors/zoominfo/CLAUDE.md +20 -0
  68. package/connectors/zoominfo/README.md +69 -0
  69. package/connectors/zoominfo/package.json +52 -0
  70. package/connectors/zoominfo/src/api/client.test.ts +140 -0
  71. package/connectors/zoominfo/src/api/client.ts +228 -0
  72. package/connectors/zoominfo/src/api/index.ts +1 -0
  73. package/connectors/zoominfo/src/cli/index.ts +179 -0
  74. package/connectors/zoominfo/src/index.ts +3 -0
  75. package/connectors/zoominfo/src/types/index.ts +39 -0
  76. package/connectors/zoominfo/src/utils/config.ts +61 -0
  77. package/connectors/zoominfo/src/utils/output.ts +27 -0
  78. package/connectors/zoominfo/tsconfig.json +20 -0
  79. package/connectors/zotero/.env.example +7 -0
  80. package/connectors/zotero/.npmrc.example +2 -0
  81. package/connectors/zotero/AGENTS.md +40 -0
  82. package/connectors/zotero/CLAUDE.md +69 -0
  83. package/connectors/zotero/README.md +91 -0
  84. package/connectors/zotero/package.json +43 -0
  85. package/connectors/zotero/src/api/attachments.ts +130 -0
  86. package/connectors/zotero/src/api/client.test.ts +166 -0
  87. package/connectors/zotero/src/api/client.ts +169 -0
  88. package/connectors/zotero/src/api/collections.ts +26 -0
  89. package/connectors/zotero/src/api/index.ts +45 -0
  90. package/connectors/zotero/src/api/items.ts +64 -0
  91. package/connectors/zotero/src/cli/index.ts +386 -0
  92. package/connectors/zotero/src/index.ts +28 -0
  93. package/connectors/zotero/src/types/index.ts +129 -0
  94. package/connectors/zotero/src/utils/config.ts +245 -0
  95. package/connectors/zotero/src/utils/output.ts +119 -0
  96. package/connectors/zotero/tsconfig.json +16 -0
  97. package/connectors/zymbly/.env.example +2 -0
  98. package/connectors/zymbly/AGENTS.md +11 -0
  99. package/connectors/zymbly/CLAUDE.md +5 -0
  100. package/connectors/zymbly/README.md +35 -0
  101. package/connectors/zymbly/package.json +52 -0
  102. package/connectors/zymbly/src/api/client.test.ts +110 -0
  103. package/connectors/zymbly/src/api/client.ts +166 -0
  104. package/connectors/zymbly/src/api/index.ts +1 -0
  105. package/connectors/zymbly/src/cli/index.ts +156 -0
  106. package/connectors/zymbly/src/index.ts +12 -0
  107. package/connectors/zymbly/src/types/index.ts +34 -0
  108. package/connectors/zymbly/src/utils/config.ts +52 -0
  109. package/connectors/zymbly/src/utils/output.ts +17 -0
  110. package/connectors/zymbly/tsconfig.json +13 -0
  111. package/dist/db/database.d.ts +2 -1
  112. package/dist/db/sqlite-adapter.d.ts +43 -0
  113. package/dist/index.js +172 -9653
  114. package/dist/lib/compact-output.d.ts +40 -0
  115. package/dist/mcp/tools/jobs.d.ts +32 -0
  116. package/dist/mcp/tools/jobs.test.d.ts +1 -0
  117. package/dist/no-cloud-boundary.test.d.ts +1 -0
  118. package/package.json +3 -3
@@ -1,7 +1,21 @@
1
- import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { mkdtempSync, statSync } from 'fs';
3
+ import { rm } from 'fs/promises';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
2
6
  import { Tidio, TidioClient } from './index';
7
+ import {
8
+ createProfile,
9
+ getProfilesDir,
10
+ loadProfile,
11
+ saveProfile,
12
+ setClientCredentials,
13
+ setProfileOverride,
14
+ validateProfileName,
15
+ } from '../utils/config';
3
16
 
4
17
  const originalFetch = globalThis.fetch;
18
+ const originalConfigHome = process.env.TIDIO_CONFIG_HOME;
5
19
 
6
20
  type CapturedRequest = {
7
21
  url: URL;
@@ -18,95 +32,212 @@ function parseBody(body: BodyInit | null | undefined): unknown {
18
32
  function installFetchMock(response?: Response) {
19
33
  const captured: CapturedRequest[] = [];
20
34
  globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
21
- const url = new URL(
22
- typeof input === 'string'
23
- ? input
24
- : input instanceof URL
25
- ? input.toString()
26
- : input.url,
27
- );
35
+ const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url);
28
36
  captured.push({
29
37
  url,
30
38
  method: init?.method ?? 'GET',
31
39
  headers: new Headers(init?.headers),
32
40
  body: parseBody(init?.body),
33
41
  });
34
- if (response) return response.clone();
35
- return Response.json({ id: 'test-id' });
42
+ return response?.clone() ?? Response.json({ id: 'test-id' });
36
43
  }) as typeof fetch;
37
44
  return captured;
38
45
  }
39
46
 
40
47
  beforeEach(() => {
41
48
  globalThis.fetch = originalFetch;
49
+ delete process.env.TIDIO_CLIENT_ID;
50
+ delete process.env.TIDIO_CLIENT_SECRET;
51
+ delete process.env.TIDIO_CONFIG_HOME;
52
+ setProfileOverride(undefined);
42
53
  });
43
54
 
44
- afterEach(() => {
55
+ afterEach(async () => {
45
56
  globalThis.fetch = originalFetch;
57
+ if (originalConfigHome) {
58
+ process.env.TIDIO_CONFIG_HOME = originalConfigHome;
59
+ } else {
60
+ delete process.env.TIDIO_CONFIG_HOME;
61
+ }
62
+ setProfileOverride(undefined);
46
63
  });
47
64
 
48
65
  describe('TidioClient', () => {
49
- test('requires API key', () => {
50
- expect(() => new TidioClient({ apiKey: '' })).toThrow('API key is required');
66
+ test('requires client id and client secret', () => {
67
+ expect(() => new TidioClient({ clientId: '', clientSecret: 'cs_test' })).toThrow('Client ID is required');
68
+ expect(() => new TidioClient({ clientId: 'ci_test', clientSecret: '' })).toThrow('Client secret is required');
51
69
  });
52
70
 
53
- test('listContacts sends X-Tidio-Openapi-Key header and query params', async () => {
71
+ test('sends current Tidio base URL, versioned Accept header, auth headers, and query params', async () => {
54
72
  const captured = installFetchMock();
55
- const client = new TidioClient({ apiKey: 'test-key' });
73
+ const client = new TidioClient({ clientId: 'ci_test', clientSecret: 'cs_test' });
56
74
  await client.get('/contacts', { limit: 25, updated_after: '2026-01-01T00:00:00Z' });
57
75
 
58
76
  const req = captured[0]!;
59
- expect(req.url.origin).toBe('https://api.tidio.co');
60
- expect(req.url.pathname).toBe('/v1/contacts');
77
+ expect(req.url.origin).toBe('https://api.tidio.com');
78
+ expect(req.url.pathname).toBe('/contacts');
61
79
  expect(req.method).toBe('GET');
62
- expect(req.headers.get('X-Tidio-Openapi-Key')).toBe('test-key');
63
- expect(req.headers.get('Accept')).toBe('application/json');
80
+ expect(req.headers.get('Accept')).toBe('application/json; version=1');
81
+ expect(req.headers.get('X-Tidio-Openapi-Client-Id')).toBe('ci_test');
82
+ expect(req.headers.get('X-Tidio-Openapi-Client-Secret')).toBe('cs_test');
64
83
  expect(Object.fromEntries(req.url.searchParams.entries())).toEqual({
65
84
  limit: '25',
66
85
  updated_after: '2026-01-01T00:00:00Z',
67
86
  });
68
87
  });
69
88
 
70
- test('sendConversationMessage POSTs body with correct path', async () => {
89
+ test('surfaces nested Tidio API error messages', async () => {
90
+ installFetchMock(Response.json({ errors: [{ message: 'invalid credentials' }] }, { status: 401 }));
91
+ const tidio = new Tidio({ clientId: 'ci_test', clientSecret: 'cs_test' });
92
+ await expect(tidio.getProject()).rejects.toThrow('invalid credentials');
93
+ });
94
+ });
95
+
96
+ describe('Tidio documented API facade', () => {
97
+ test('createContact uses documented contact payload shape', async () => {
71
98
  const captured = installFetchMock();
72
- const tidio = new Tidio({ apiKey: 'test-key' });
73
- await tidio.sendConversationMessage('conv-1', { type: 'text', content: 'hello' });
99
+ const tidio = new Tidio({ clientId: 'ci_test', clientSecret: 'cs_test' });
100
+ await tidio.createContact({
101
+ email: 'ada@example.com',
102
+ firstName: 'Ada',
103
+ lastName: 'Lovelace',
104
+ distinctId: 'crm-123',
105
+ properties: [{ name: 'plan', value: 'pro' }],
106
+ emailConsent: 'subscribed',
107
+ });
74
108
 
75
109
  const req = captured[0]!;
76
- expect(req.url.pathname).toBe('/v1/conversations/conv-1/messages');
110
+ expect(req.url.pathname).toBe('/contacts');
77
111
  expect(req.method).toBe('POST');
78
112
  expect(req.headers.get('Content-Type')).toBe('application/json');
79
- expect(req.body).toEqual({ type: 'text', content: 'hello' });
113
+ expect(req.body).toEqual({
114
+ email: 'ada@example.com',
115
+ first_name: 'Ada',
116
+ last_name: 'Lovelace',
117
+ distinct_id: 'crm-123',
118
+ properties: [{ name: 'plan', value: 'pro' }],
119
+ email_consent: 'subscribed',
120
+ });
121
+ });
122
+
123
+ test('sendContactMessage uses /contacts/{contactId}/messages with message body', async () => {
124
+ const captured = installFetchMock(Response.json({ status: 'accepted' }, { status: 202 }));
125
+ const tidio = new Tidio({ clientId: 'ci_test', clientSecret: 'cs_test' });
126
+ await tidio.sendContactMessage('a1b4ca4c-1108-4432-b256-1e4cf2bf6f9e', { message: 'hello' });
127
+
128
+ const req = captured[0]!;
129
+ expect(req.url.pathname).toBe('/contacts/a1b4ca4c-1108-4432-b256-1e4cf2bf6f9e/messages');
130
+ expect(req.method).toBe('POST');
131
+ expect(req.body).toEqual({ message: 'hello' });
132
+ });
133
+
134
+ test('updateContact treats documented 204 response as no content', async () => {
135
+ const captured = installFetchMock(new Response(null, { status: 204 }));
136
+ const tidio = new Tidio({ clientId: 'ci_test', clientSecret: 'cs_test' });
137
+ await expect(tidio.updateContact('a1b4ca4c-1108-4432-b256-1e4cf2bf6f9e', { firstName: 'Ada' })).resolves.toBeUndefined();
138
+
139
+ const req = captured[0]!;
140
+ expect(req.url.pathname).toBe('/contacts/a1b4ca4c-1108-4432-b256-1e4cf2bf6f9e');
141
+ expect(req.method).toBe('PATCH');
142
+ expect(req.body).toEqual({ first_name: 'Ada' });
80
143
  });
81
144
 
82
- test('createWebhook maps events in request body', async () => {
145
+ test('listContactMessages uses documented cursor pagination only', async () => {
83
146
  const captured = installFetchMock();
84
- const tidio = new Tidio({ apiKey: 'test-key' });
85
- await tidio.createWebhook({
86
- url: 'https://example.com/hook',
87
- events: ['contact.created', 'message.created'],
88
- });
147
+ const tidio = new Tidio({ clientId: 'ci_test', clientSecret: 'cs_test' });
148
+ await tidio.listContactMessages('a1b4ca4c-1108-4432-b256-1e4cf2bf6f9e', { cursor: 'next' });
89
149
 
90
150
  const req = captured[0]!;
91
- expect(req.url.pathname).toBe('/v1/webhooks');
92
- expect(req.body).toEqual({
93
- url: 'https://example.com/hook',
94
- events: ['contact.created', 'message.created'],
95
- });
151
+ expect(req.url.pathname).toBe('/contacts/a1b4ca4c-1108-4432-b256-1e4cf2bf6f9e/messages');
152
+ expect(Object.fromEntries(req.url.searchParams.entries())).toEqual({ cursor: 'next' });
96
153
  });
97
154
 
98
- test('surfaces API error message on non-2xx', async () => {
99
- installFetchMock(Response.json({ message: 'invalid key' }, { status: 401 }));
100
- const tidio = new Tidio({ apiKey: 'test-key' });
101
- await expect(tidio.getProject()).rejects.toThrow('invalid key');
155
+ test('project, tickets, products, and Lyro helpers use documented paths', async () => {
156
+ const captured = installFetchMock();
157
+ const tidio = new Tidio({ clientId: 'ci_test', clientSecret: 'cs_test' });
158
+ await tidio.getProject();
159
+ await tidio.getTicket('ticket-1');
160
+ await tidio.upsertProducts([
161
+ {
162
+ id: 1,
163
+ title: 'Red shirt',
164
+ url: 'https://example.com/red-shirt',
165
+ default_currency: 'USD',
166
+ updated_at: '2026-01-01T00:00:00+00:00',
167
+ },
168
+ ]);
169
+ await tidio.listLyroDataSources({ kind: 'qa', cursor: 'next' });
170
+ await tidio.askLyroToAnswerTicket({
171
+ ticketId: '01H6XZJ88T5PMRRQC3C54WY1EJ',
172
+ subject: 'Question about delivery',
173
+ contactEmail: 'user@example.com',
174
+ contactName: 'Ada Lovelace',
175
+ recipientEmail: 'support@example.com',
176
+ messages: [
177
+ {
178
+ created_at: '2026-01-01T00:00:00+00:00',
179
+ message_id: '01JVRXTTZ4ACV3JZTQS8ZVKASK',
180
+ author_type: 'contact',
181
+ message_type: 'public',
182
+ message_content: 'Can you ship to the USA?',
183
+ },
184
+ ],
185
+ });
186
+
187
+ expect(captured.map(req => `${req.method} ${req.url.pathname}`)).toEqual([
188
+ 'GET /project',
189
+ 'GET /tickets/ticket-1',
190
+ 'PUT /products/batch',
191
+ 'GET /lyro/data-sources',
192
+ 'POST /lyro/tickets',
193
+ ]);
194
+ expect(captured[3]!.url.searchParams.get('kind')).toBe('qa');
195
+ expect(captured[3]!.url.searchParams.get('cursor')).toBe('next');
196
+ expect(captured[4]!.body).toEqual({
197
+ ticket_id: '01H6XZJ88T5PMRRQC3C54WY1EJ',
198
+ subject: 'Question about delivery',
199
+ contact_email: 'user@example.com',
200
+ contact_name: 'Ada Lovelace',
201
+ recipient_email: 'support@example.com',
202
+ messages: [
203
+ {
204
+ created_at: '2026-01-01T00:00:00+00:00',
205
+ message_id: '01JVRXTTZ4ACV3JZTQS8ZVKASK',
206
+ author_type: 'contact',
207
+ message_type: 'public',
208
+ message_content: 'Can you ship to the USA?',
209
+ },
210
+ ],
211
+ });
102
212
  });
103
213
  });
104
214
 
105
215
  describe('Tidio.fromEnv', () => {
106
- test('requires TIDIO_API_KEY', () => {
107
- const prev = process.env.TIDIO_API_KEY;
108
- delete process.env.TIDIO_API_KEY;
109
- expect(() => Tidio.fromEnv()).toThrow('TIDIO_API_KEY environment variable is required');
110
- if (prev) process.env.TIDIO_API_KEY = prev;
216
+ test('requires both TIDIO_CLIENT_ID and TIDIO_CLIENT_SECRET', () => {
217
+ process.env.TIDIO_CLIENT_ID = 'ci_test';
218
+ expect(() => Tidio.fromEnv()).toThrow('TIDIO_CLIENT_ID and TIDIO_CLIENT_SECRET environment variables are required');
219
+ });
220
+ });
221
+
222
+ describe('secure profile storage', () => {
223
+ test('validates profile names before path joins', () => {
224
+ expect(() => validateProfileName('../escape')).toThrow('Profile name can only contain');
225
+ expect(() => createProfile('../escape')).toThrow('Profile name can only contain');
226
+ });
227
+
228
+ test('writes config directories as 0700 and profile files as 0600', async () => {
229
+ const home = mkdtempSync(join(tmpdir(), 'tidio-config-'));
230
+ process.env.TIDIO_CONFIG_HOME = home;
231
+ createProfile('default');
232
+ setClientCredentials('ci_test', 'cs_test');
233
+ saveProfile({ clientId: 'ci_other', clientSecret: 'cs_other' }, 'work');
234
+
235
+ expect((statSync(home).mode & 0o777).toString(8)).toBe('700');
236
+ expect((statSync(getProfilesDir()).mode & 0o777).toString(8)).toBe('700');
237
+ expect((statSync(join(getProfilesDir(), 'default.json')).mode & 0o777).toString(8)).toBe('600');
238
+ expect((statSync(join(getProfilesDir(), 'work.json')).mode & 0o777).toString(8)).toBe('600');
239
+ expect(loadProfile('work')).toEqual({ clientId: 'ci_other', clientSecret: 'cs_other' });
240
+
241
+ await rm(home, { recursive: true, force: true });
111
242
  });
112
243
  });