@hasna/connectors 1.3.45 → 1.3.46

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 (114) hide show
  1. package/README.md +25 -0
  2. package/bin/index.js +792 -130
  3. package/bin/mcp.js +438 -52
  4. package/bin/serve.js +438 -52
  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/zoominfo/.env.example +4 -0
  65. package/connectors/zoominfo/AGENTS.md +26 -0
  66. package/connectors/zoominfo/CLAUDE.md +20 -0
  67. package/connectors/zoominfo/README.md +69 -0
  68. package/connectors/zoominfo/package.json +52 -0
  69. package/connectors/zoominfo/src/api/client.test.ts +140 -0
  70. package/connectors/zoominfo/src/api/client.ts +228 -0
  71. package/connectors/zoominfo/src/api/index.ts +1 -0
  72. package/connectors/zoominfo/src/cli/index.ts +179 -0
  73. package/connectors/zoominfo/src/index.ts +3 -0
  74. package/connectors/zoominfo/src/types/index.ts +39 -0
  75. package/connectors/zoominfo/src/utils/config.ts +61 -0
  76. package/connectors/zoominfo/src/utils/output.ts +27 -0
  77. package/connectors/zoominfo/tsconfig.json +20 -0
  78. package/connectors/zotero/.env.example +7 -0
  79. package/connectors/zotero/.npmrc.example +2 -0
  80. package/connectors/zotero/AGENTS.md +40 -0
  81. package/connectors/zotero/CLAUDE.md +69 -0
  82. package/connectors/zotero/README.md +91 -0
  83. package/connectors/zotero/package.json +43 -0
  84. package/connectors/zotero/src/api/attachments.ts +130 -0
  85. package/connectors/zotero/src/api/client.test.ts +166 -0
  86. package/connectors/zotero/src/api/client.ts +169 -0
  87. package/connectors/zotero/src/api/collections.ts +26 -0
  88. package/connectors/zotero/src/api/index.ts +45 -0
  89. package/connectors/zotero/src/api/items.ts +64 -0
  90. package/connectors/zotero/src/cli/index.ts +386 -0
  91. package/connectors/zotero/src/index.ts +28 -0
  92. package/connectors/zotero/src/types/index.ts +129 -0
  93. package/connectors/zotero/src/utils/config.ts +245 -0
  94. package/connectors/zotero/src/utils/output.ts +119 -0
  95. package/connectors/zotero/tsconfig.json +16 -0
  96. package/connectors/zymbly/.env.example +2 -0
  97. package/connectors/zymbly/AGENTS.md +11 -0
  98. package/connectors/zymbly/CLAUDE.md +5 -0
  99. package/connectors/zymbly/README.md +35 -0
  100. package/connectors/zymbly/package.json +52 -0
  101. package/connectors/zymbly/src/api/client.test.ts +110 -0
  102. package/connectors/zymbly/src/api/client.ts +166 -0
  103. package/connectors/zymbly/src/api/index.ts +1 -0
  104. package/connectors/zymbly/src/cli/index.ts +156 -0
  105. package/connectors/zymbly/src/index.ts +12 -0
  106. package/connectors/zymbly/src/types/index.ts +34 -0
  107. package/connectors/zymbly/src/utils/config.ts +52 -0
  108. package/connectors/zymbly/src/utils/output.ts +17 -0
  109. package/connectors/zymbly/tsconfig.json +13 -0
  110. package/dist/index.js +45 -2
  111. package/dist/lib/compact-output.d.ts +40 -0
  112. package/dist/mcp/tools/jobs.d.ts +32 -0
  113. package/dist/mcp/tools/jobs.test.d.ts +1 -0
  114. package/package.json +3 -2
@@ -1,12 +1,12 @@
1
1
  // Tidio Connector
2
- // Live chat support, contacts, conversations, and operators
2
+ // Contacts, contact messages, departments, operators, project, tickets, products, and Lyro.
3
3
 
4
4
  export { Tidio, TidioClient } from './api';
5
5
  export * from './types';
6
6
 
7
7
  export {
8
- getApiKey,
9
- setApiKey,
8
+ getClientCredentials,
9
+ setClientCredentials,
10
10
  getCurrentProfile,
11
11
  setCurrentProfile,
12
12
  listProfiles,
@@ -16,5 +16,7 @@ export {
16
16
  saveProfile,
17
17
  clearConfig,
18
18
  getConfigDir,
19
+ getProfilesDir,
19
20
  getActiveProfileName,
21
+ validateProfileName,
20
22
  } from './utils/config';
@@ -1,20 +1,16 @@
1
1
  // Tidio Connector Types
2
2
 
3
3
  export interface TidioConfig {
4
- apiKey: string;
4
+ clientId: string;
5
+ clientSecret: string;
5
6
  baseUrl?: string;
6
7
  }
7
8
 
8
9
  export type OutputFormat = 'json' | 'pretty';
9
10
 
10
- export type ConversationStatus = 'open' | 'closed' | 'snoozed';
11
- export type MessageType = 'text' | 'image' | 'file' | 'note';
12
- export type WebhookEvent =
13
- | 'contact.created'
14
- | 'contact.updated'
15
- | 'conversation.created'
16
- | 'conversation.updated'
17
- | 'message.created';
11
+ export type TicketStatus = 'open' | 'pending' | 'closed';
12
+ export type TicketPriority = 'low' | 'medium' | 'high' | 'urgent';
13
+ export type LyroDataSourceKind = 'folder' | 'qa';
18
14
 
19
15
  export interface PaginatedMeta {
20
16
  cursor?: string;
@@ -23,7 +19,7 @@ export interface PaginatedMeta {
23
19
  }
24
20
 
25
21
  export interface PaginatedResponse<T> {
26
- data?: T[];
22
+ resources?: T[];
27
23
  meta?: PaginatedMeta;
28
24
  [key: string]: unknown;
29
25
  }
@@ -32,40 +28,39 @@ export interface Contact {
32
28
  id: string;
33
29
  email?: string;
34
30
  phone?: string;
35
- name?: string;
36
- external_id?: string;
37
- tags?: string[];
38
- properties?: Record<string, unknown>;
39
- subscriber?: boolean;
40
- consent?: boolean;
31
+ first_name?: string | null;
32
+ last_name?: string | null;
33
+ distinct_id?: string;
34
+ properties?: ContactProperty[];
35
+ email_consent?: 'subscribed' | 'unsubscribed' | null;
41
36
  created_at?: string;
42
37
  updated_at?: string;
43
38
  [key: string]: unknown;
44
39
  }
45
40
 
46
- export interface Conversation {
41
+ export interface ContactProperty {
42
+ name: string;
43
+ value: string | number | boolean | null;
44
+ }
45
+
46
+ export interface ContactMessage {
47
47
  id: string;
48
- status?: ConversationStatus;
49
- channel?: string;
50
48
  contact_id?: string;
51
- operator_id?: string;
52
- department_id?: string;
53
- created_at?: string;
54
- updated_at?: string;
49
+ message?: string;
50
+ author?: string;
51
+ timestamp?: string;
55
52
  [key: string]: unknown;
56
53
  }
57
54
 
58
- export interface Message {
59
- id: string;
60
- type?: MessageType;
61
- content?: string;
62
- media_url?: string;
63
- private?: boolean;
64
- operator_id?: string;
65
- created_at?: string;
55
+ export interface MessageAccepted {
56
+ status?: string;
66
57
  [key: string]: unknown;
67
58
  }
68
59
 
60
+ export interface UuidResponse {
61
+ id: string;
62
+ }
63
+
69
64
  export interface Operator {
70
65
  id: string;
71
66
  name?: string;
@@ -79,32 +74,59 @@ export interface Department {
79
74
  [key: string]: unknown;
80
75
  }
81
76
 
82
- export interface Tag {
77
+ export interface TicketTag {
83
78
  id: string;
84
79
  name: string;
85
- color?: string;
86
80
  [key: string]: unknown;
87
81
  }
88
82
 
89
- export interface Automation {
83
+ export interface TicketCustomField {
90
84
  id: string;
91
85
  name?: string;
86
+ type?: string;
92
87
  [key: string]: unknown;
93
88
  }
94
89
 
95
- export interface CannedResponse {
90
+ export interface Ticket {
96
91
  id: string;
97
- shortcut: string;
98
- content: string;
99
- department_id?: string;
92
+ status?: TicketStatus;
93
+ priority?: TicketPriority;
94
+ subject?: string;
95
+ contact_id?: string;
96
+ assignee_id?: string | null;
97
+ department_id?: string | null;
98
+ messages?: unknown[];
99
+ created_at?: string;
100
+ updated_at?: string;
100
101
  [key: string]: unknown;
101
102
  }
102
103
 
103
- export interface Webhook {
104
- id: string;
104
+ export interface Product {
105
+ id: number;
106
+ title: string;
105
107
  url: string;
106
- events: WebhookEvent[];
107
- secret?: string;
108
+ image_url?: string;
109
+ description?: string;
110
+ price?: number;
111
+ default_currency?: string;
112
+ vendor?: string;
113
+ product_type?: string;
114
+ sku?: string;
115
+ barcode?: string;
116
+ availability?: string;
117
+ updated_at: string;
118
+ features?: Record<string, string>;
119
+ [key: string]: unknown;
120
+ }
121
+
122
+ export interface LyroDataSource {
123
+ id: string;
124
+ kind?: LyroDataSourceKind;
125
+ type?: string;
126
+ title?: string;
127
+ content?: string | null;
128
+ source_url?: string | null;
129
+ parent_id?: string | null;
108
130
  [key: string]: unknown;
109
131
  }
110
132
 
@@ -124,76 +146,106 @@ export interface ListContactsParams {
124
146
  export interface CreateContactParams {
125
147
  email?: string;
126
148
  phone?: string;
127
- name?: string;
128
- externalId?: string;
129
- tags?: string[];
130
- properties?: Record<string, unknown>;
131
- subscriber?: boolean;
132
- consent?: boolean;
149
+ firstName?: string | null;
150
+ lastName?: string | null;
151
+ distinctId?: string;
152
+ properties?: ContactProperty[];
153
+ emailConsent?: 'subscribed' | 'unsubscribed' | null;
133
154
  }
134
155
 
135
156
  export interface UpdateContactParams {
136
157
  email?: string;
137
158
  phone?: string;
138
- name?: string;
139
- externalId?: string;
140
- tags?: string[];
141
- properties?: Record<string, unknown>;
142
- subscriber?: boolean;
143
- consent?: boolean;
159
+ firstName?: string | null;
160
+ lastName?: string | null;
161
+ distinctId?: string;
162
+ properties?: ContactProperty[];
163
+ emailConsent?: 'subscribed' | 'unsubscribed' | null;
144
164
  }
145
165
 
146
- export interface ListConversationsParams {
147
- limit?: number;
166
+ export interface BatchContactsParams<T extends CreateContactParams | UpdateContactParams> {
167
+ contacts: T[];
168
+ }
169
+
170
+ export interface ListContactMessagesParams {
148
171
  cursor?: string;
149
- status?: ConversationStatus;
150
- channel?: string;
151
- updatedAfter?: string;
152
172
  }
153
173
 
154
- export interface ListMessagesParams {
174
+ export interface SendContactMessageParams {
175
+ message: string;
176
+ }
177
+
178
+ export interface ListTicketsParams {
155
179
  limit?: number;
156
180
  cursor?: string;
181
+ status?: TicketStatus;
182
+ priority?: TicketPriority;
157
183
  }
158
184
 
159
- export interface SendMessageParams {
160
- type: MessageType;
161
- content: string;
162
- mediaUrl?: string;
163
- private?: boolean;
164
- operatorId?: string;
185
+ export interface CreateTicketAsContactParams {
186
+ contactId: string;
187
+ message: string;
188
+ subject?: string;
189
+ priority?: TicketPriority;
165
190
  }
166
191
 
167
- export interface SetConversationStatusParams {
168
- status: ConversationStatus;
169
- snoozedUntil?: string;
192
+ export interface UpdateTicketParams {
193
+ status?: TicketStatus;
194
+ priority?: TicketPriority;
195
+ assigneeId?: string | null;
196
+ departmentId?: string | null;
170
197
  }
171
198
 
172
- export interface AssignConversationParams {
173
- operatorId: string | null;
174
- departmentId?: string;
199
+ export interface ReplyTicketParams {
200
+ message: string;
201
+ authorType?: 'operator' | 'contact';
175
202
  }
176
203
 
177
- export interface CreateTagParams {
178
- name: string;
179
- color?: string;
204
+ export interface ListLyroDataSourcesParams {
205
+ cursor?: string;
206
+ limit?: number;
207
+ kind?: LyroDataSourceKind;
208
+ parentId?: string | null;
209
+ }
210
+
211
+ export interface CreateLyroQaDataSourceParams {
212
+ question: string;
213
+ answer: string;
214
+ parentId?: string | null;
180
215
  }
181
216
 
182
- export interface CreateCannedResponseParams {
183
- shortcut: string;
217
+ export interface UpsertLyroWebsiteDataSourceParams {
218
+ url: string;
219
+ title?: string;
184
220
  content: string;
185
- departmentId?: string;
186
221
  }
187
222
 
188
- export interface CreateWebhookParams {
223
+ export interface ScrapeLyroWebsiteParams {
189
224
  url: string;
190
- events: WebhookEvent[];
191
- secret?: string;
225
+ }
226
+
227
+ export interface LyroTicketMessage {
228
+ created_at: string;
229
+ message_id: string;
230
+ author_type: 'contact';
231
+ message_type: 'public';
232
+ message_content: string;
233
+ attachments?: string[];
234
+ }
235
+
236
+ export interface AskLyroTicketParams {
237
+ ticketId: string;
238
+ subject: string;
239
+ contactEmail: string;
240
+ contactName: string;
241
+ recipientEmail: string;
242
+ messages: LyroTicketMessage[];
192
243
  }
193
244
 
194
245
  export interface TidioErrorResponse {
195
246
  message?: string;
196
247
  error?: string;
248
+ errors?: Array<{ code?: string; message?: string }>;
197
249
  [key: string]: unknown;
198
250
  }
199
251
 
@@ -211,6 +263,7 @@ export class TidioApiError extends Error {
211
263
 
212
264
  export function parseTidioError(data: unknown, statusCode: number): TidioApiError {
213
265
  const record = data && typeof data === 'object' ? (data as TidioErrorResponse) : {};
214
- const message = record.message ?? record.error ?? `Request failed (${statusCode})`;
266
+ const nestedMessage = Array.isArray(record.errors) ? record.errors[0]?.message : undefined;
267
+ const message = nestedMessage ?? record.message ?? record.error ?? `Request failed (${statusCode})`;
215
268
  return new TidioApiError(String(message), statusCode, record);
216
269
  }
@@ -1,35 +1,73 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync } from 'fs';
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ readdirSync,
6
+ rmSync,
7
+ chmodSync,
8
+ writeFileSync,
9
+ } from 'fs';
2
10
  import { homedir } from 'os';
3
- import { join } from 'path';
11
+ import { join, resolve, sep } from 'path';
4
12
 
5
13
  const CONNECTOR_NAME = 'connect-tidio';
6
14
  const DEFAULT_PROFILE = 'default';
15
+ const PROFILE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
7
16
 
8
17
  export interface ProfileConfig {
9
- apiKey?: string;
18
+ clientId?: string;
19
+ clientSecret?: string;
10
20
  }
11
21
 
12
22
  let profileOverride: string | undefined;
13
23
 
14
- const CONFIG_DIR = join(homedir(), '.hasna', 'connectors', CONNECTOR_NAME);
15
- const PROFILES_DIR = join(CONFIG_DIR, 'profiles');
16
- const CURRENT_PROFILE_FILE = join(CONFIG_DIR, 'current_profile');
24
+ function rootConfigDir(): string {
25
+ return process.env.TIDIO_CONFIG_HOME || join(homedir(), '.hasna', 'connectors', CONNECTOR_NAME);
26
+ }
17
27
 
18
- export function setProfileOverride(profile: string | undefined): void {
19
- profileOverride = profile;
28
+ function profilesDir(): string {
29
+ return join(rootConfigDir(), 'profiles');
20
30
  }
21
31
 
22
- export function ensureConfigDir(): void {
23
- if (!existsSync(CONFIG_DIR)) {
24
- mkdirSync(CONFIG_DIR, { recursive: true });
32
+ function currentProfileFile(): string {
33
+ return join(rootConfigDir(), 'current_profile');
34
+ }
35
+
36
+ export function validateProfileName(profile: string): string {
37
+ if (!PROFILE_NAME_PATTERN.test(profile)) {
38
+ throw new Error('Profile name can only contain letters, numbers, hyphens, and underscores');
25
39
  }
26
- if (!existsSync(PROFILES_DIR)) {
27
- mkdirSync(PROFILES_DIR, { recursive: true });
40
+ return profile;
41
+ }
42
+
43
+ function ensureInsideProfilesDir(path: string): string {
44
+ const root = resolve(profilesDir());
45
+ const candidate = resolve(path);
46
+ if (candidate !== root && !candidate.startsWith(`${root}${sep}`)) {
47
+ throw new Error('Profile path escapes the profiles directory');
28
48
  }
49
+ return candidate;
50
+ }
51
+
52
+ export function setProfileOverride(profile: string | undefined): void {
53
+ profileOverride = profile ? validateProfileName(profile) : undefined;
54
+ }
55
+
56
+ export function ensureConfigDir(): void {
57
+ mkdirSync(rootConfigDir(), { recursive: true, mode: 0o700 });
58
+ chmodSync(rootConfigDir(), 0o700);
59
+ mkdirSync(profilesDir(), { recursive: true, mode: 0o700 });
60
+ chmodSync(profilesDir(), 0o700);
29
61
  }
30
62
 
31
63
  function getProfilePath(profile: string): string {
32
- return join(PROFILES_DIR, `${profile}.json`);
64
+ const validProfile = validateProfileName(profile);
65
+ return ensureInsideProfilesDir(join(profilesDir(), `${validProfile}.json`));
66
+ }
67
+
68
+ function writePrivateFile(path: string, value: string): void {
69
+ writeFileSync(path, value, { mode: 0o600 });
70
+ chmodSync(path, 0o600);
33
71
  }
34
72
 
35
73
  export function getCurrentProfile(): string {
@@ -39,14 +77,14 @@ export function getCurrentProfile(): string {
39
77
 
40
78
  ensureConfigDir();
41
79
 
42
- if (existsSync(CURRENT_PROFILE_FILE)) {
80
+ if (existsSync(currentProfileFile())) {
43
81
  try {
44
- const profile = readFileSync(CURRENT_PROFILE_FILE, 'utf-8').trim();
45
- if (profile && profileExists(profile)) {
82
+ const profile = readFileSync(currentProfileFile(), 'utf-8').trim();
83
+ if (profile && validateProfileName(profile) && profileExists(profile)) {
46
84
  return profile;
47
85
  }
48
86
  } catch {
49
- // Fall through to default
87
+ // Fall through to default.
50
88
  }
51
89
  }
52
90
 
@@ -54,13 +92,14 @@ export function getCurrentProfile(): string {
54
92
  }
55
93
 
56
94
  export function setCurrentProfile(profile: string): void {
95
+ const validProfile = validateProfileName(profile);
57
96
  ensureConfigDir();
58
97
 
59
- if (!profileExists(profile) && profile !== DEFAULT_PROFILE) {
60
- throw new Error(`Profile "${profile}" does not exist`);
98
+ if (!profileExists(validProfile) && validProfile !== DEFAULT_PROFILE) {
99
+ throw new Error(`Profile "${validProfile}" does not exist`);
61
100
  }
62
101
 
63
- writeFileSync(CURRENT_PROFILE_FILE, profile);
102
+ writePrivateFile(currentProfileFile(), validProfile);
64
103
  }
65
104
 
66
105
  export function profileExists(profile: string): boolean {
@@ -70,51 +109,50 @@ export function profileExists(profile: string): boolean {
70
109
  export function listProfiles(): string[] {
71
110
  ensureConfigDir();
72
111
 
73
- if (!existsSync(PROFILES_DIR)) {
112
+ if (!existsSync(profilesDir())) {
74
113
  return [];
75
114
  }
76
115
 
77
- return readdirSync(PROFILES_DIR)
78
- .filter(f => f.endsWith('.json'))
79
- .map(f => f.replace('.json', ''))
116
+ return readdirSync(profilesDir())
117
+ .filter(file => file.endsWith('.json'))
118
+ .map(file => file.replace(/\.json$/, ''))
119
+ .filter(profile => PROFILE_NAME_PATTERN.test(profile))
80
120
  .sort();
81
121
  }
82
122
 
83
123
  export function createProfile(profile: string, config: ProfileConfig = {}): boolean {
124
+ const validProfile = validateProfileName(profile);
84
125
  ensureConfigDir();
85
126
 
86
- if (profileExists(profile)) {
127
+ if (profileExists(validProfile)) {
87
128
  return false;
88
129
  }
89
130
 
90
- if (!/^[a-zA-Z0-9_-]+$/.test(profile)) {
91
- throw new Error('Profile name can only contain letters, numbers, hyphens, and underscores');
92
- }
93
-
94
- writeFileSync(getProfilePath(profile), JSON.stringify(config, null, 2));
131
+ writePrivateFile(getProfilePath(validProfile), `${JSON.stringify(config, null, 2)}\n`);
95
132
  return true;
96
133
  }
97
134
 
98
135
  export function deleteProfile(profile: string): boolean {
99
- if (profile === DEFAULT_PROFILE) {
136
+ const validProfile = validateProfileName(profile);
137
+ if (validProfile === DEFAULT_PROFILE) {
100
138
  return false;
101
139
  }
102
140
 
103
- if (!profileExists(profile)) {
141
+ if (!profileExists(validProfile)) {
104
142
  return false;
105
143
  }
106
144
 
107
- if (getCurrentProfile() === profile) {
145
+ if (getCurrentProfile() === validProfile) {
108
146
  setCurrentProfile(DEFAULT_PROFILE);
109
147
  }
110
148
 
111
- rmSync(getProfilePath(profile));
149
+ rmSync(getProfilePath(validProfile));
112
150
  return true;
113
151
  }
114
152
 
115
153
  export function loadProfile(profile?: string): ProfileConfig {
116
154
  ensureConfigDir();
117
- const profileName = profile || getCurrentProfile();
155
+ const profileName = profile ? validateProfileName(profile) : getCurrentProfile();
118
156
  const profilePath = getProfilePath(profileName);
119
157
 
120
158
  if (!existsSync(profilePath)) {
@@ -122,7 +160,7 @@ export function loadProfile(profile?: string): ProfileConfig {
122
160
  }
123
161
 
124
162
  try {
125
- return JSON.parse(readFileSync(profilePath, 'utf-8'));
163
+ return JSON.parse(readFileSync(profilePath, 'utf-8')) as ProfileConfig;
126
164
  } catch {
127
165
  return {};
128
166
  }
@@ -130,17 +168,21 @@ export function loadProfile(profile?: string): ProfileConfig {
130
168
 
131
169
  export function saveProfile(config: ProfileConfig, profile?: string): void {
132
170
  ensureConfigDir();
133
- const profileName = profile || getCurrentProfile();
134
- writeFileSync(getProfilePath(profileName), JSON.stringify(config, null, 2));
171
+ const profileName = profile ? validateProfileName(profile) : getCurrentProfile();
172
+ writePrivateFile(getProfilePath(profileName), `${JSON.stringify(config, null, 2)}\n`);
135
173
  }
136
174
 
137
- export function getApiKey(): string | undefined {
138
- return process.env.TIDIO_API_KEY || loadProfile().apiKey;
175
+ export function getClientCredentials(): ProfileConfig {
176
+ return {
177
+ clientId: process.env.TIDIO_CLIENT_ID || loadProfile().clientId,
178
+ clientSecret: process.env.TIDIO_CLIENT_SECRET || loadProfile().clientSecret,
179
+ };
139
180
  }
140
181
 
141
- export function setApiKey(apiKey: string): void {
182
+ export function setClientCredentials(clientId: string, clientSecret: string): void {
142
183
  const config = loadProfile();
143
- config.apiKey = apiKey;
184
+ config.clientId = clientId;
185
+ config.clientSecret = clientSecret;
144
186
  saveProfile(config);
145
187
  }
146
188
 
@@ -149,7 +191,11 @@ export function clearConfig(): void {
149
191
  }
150
192
 
151
193
  export function getConfigDir(): string {
152
- return CONFIG_DIR;
194
+ return rootConfigDir();
195
+ }
196
+
197
+ export function getProfilesDir(): string {
198
+ return profilesDir();
153
199
  }
154
200
 
155
201
  export function getActiveProfileName(): string {
@@ -0,0 +1,7 @@
1
+ # Weights & Biases API credentials
2
+ # Get your API key from: https://wandb.ai/authorize
3
+
4
+ WANDB_API_KEY=your-api-key-here
5
+
6
+ # Optional: Custom GraphQL base URL (default: https://api.wandb.ai/graphql)
7
+ # WANDB_BASE_URL=https://api.wandb.ai/graphql
@@ -0,0 +1,43 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI coding agents working with the Weights & Biases connector.
4
+
5
+ ## Project Overview
6
+
7
+ connect-wandb is a TypeScript connector for the W&B GraphQL API with multi-profile configuration, Bearer token authentication, and Commander.js CLI.
8
+
9
+ ## Build & Run Commands
10
+
11
+ ```bash
12
+ bun install
13
+ bun run dev
14
+ bun run build
15
+ bun run typecheck
16
+ bun test
17
+ ```
18
+
19
+ ## Authentication
20
+
21
+ Bearer Token authentication via `WANDB_API_KEY` or `wandb config set-key <key>`.
22
+
23
+ ## Environment Variables
24
+
25
+ | Variable | Description |
26
+ |----------|-------------|
27
+ | `WANDB_API_KEY` | API key (overrides profile) |
28
+ | `WANDB_BASE_URL` | Override GraphQL base URL |
29
+
30
+ ## Data Storage
31
+
32
+ ```
33
+ ~/.hasna/connectors/wandb/
34
+ ├── current_profile
35
+ └── profiles/
36
+ └── default.json
37
+ ```
38
+
39
+ ## Key Patterns
40
+
41
+ - `WandbClient` handles GraphQL POST with Bearer auth and error handling
42
+ - `Wandb` facade exposes `viewer`, `projects`, and `graphql` APIs
43
+ - Run filters use JSON-stringified MongoDB-style syntax per W&B API