@plosson/agentio 0.4.2 → 0.4.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.
@@ -0,0 +1,402 @@
1
+ import { CliError } from '../utils/errors';
2
+ import { loadConfig } from '../config/config-manager';
3
+ import { getEnv } from '../config/config-manager';
4
+ import type { ServiceName } from '../types/config';
5
+ import type {
6
+ GatewayConfig,
7
+ InboxPullRequest,
8
+ InboxPullResponse,
9
+ InboxGetRequest,
10
+ InboxGetResponse,
11
+ InboxAckRequest,
12
+ InboxAckResponse,
13
+ InboxReplyRequest,
14
+ InboxReplyResponse,
15
+ InboxStatsRequest,
16
+ InboxStatsResponse,
17
+ OutboxSendRequest,
18
+ OutboxSendResponse,
19
+ OutboxStatusRequest,
20
+ OutboxStatusResponse,
21
+ OutboxListRequest,
22
+ OutboxListResponse,
23
+ GatewayStatusResponse,
24
+ HealthResponse,
25
+ WhatsAppPairResponse,
26
+ InboundMessage,
27
+ OutboundMessage,
28
+ MediaType,
29
+ InboxStatus,
30
+ OutboxStatus,
31
+ WhatsAppGroupListRequest,
32
+ WhatsAppGroupListResponse,
33
+ WhatsAppGroupGetRequest,
34
+ WhatsAppGroupGetResponse,
35
+ WhatsAppGroupCreateRequest,
36
+ WhatsAppGroupCreateResponse,
37
+ WhatsAppGroupUpdateRequest,
38
+ WhatsAppGroupUpdateResponse,
39
+ WhatsAppGroupParticipantsRequest,
40
+ WhatsAppGroupParticipantsResponse,
41
+ WhatsAppGroupLeaveRequest,
42
+ WhatsAppGroupLeaveResponse,
43
+ WhatsAppGroupInviteRequest,
44
+ WhatsAppGroupInviteResponse,
45
+ WhatsAppGroupJoinRequest,
46
+ WhatsAppGroupJoinResponse,
47
+ WhatsAppGroupResolveRequest,
48
+ WhatsAppGroupResolveResponse,
49
+ } from './types';
50
+ import type { WhatsAppGroup, WhatsAppParticipantAction } from '../types/whatsapp';
51
+
52
+ let cachedConfig: { url: string; secret: string } | null = null;
53
+
54
+ /**
55
+ * Get gateway URL and secret from config or environment
56
+ */
57
+ async function getGatewayConnection(): Promise<{ url: string; secret: string }> {
58
+ if (cachedConfig) return cachedConfig;
59
+
60
+ // Check environment variables first
61
+ const envUrl = process.env.AGENTIO_GATEWAY_URL || await getEnv('AGENTIO_GATEWAY_URL');
62
+ const envSecret = process.env.AGENTIO_GATEWAY_SECRET || await getEnv('AGENTIO_GATEWAY_SECRET');
63
+
64
+ if (envUrl) {
65
+ cachedConfig = { url: envUrl, secret: envSecret || '' };
66
+ return cachedConfig;
67
+ }
68
+
69
+ // Load from config
70
+ const config = await loadConfig() as unknown as { gateway?: GatewayConfig };
71
+ const gatewayConfig = config.gateway;
72
+
73
+ if (!gatewayConfig?.api) {
74
+ throw new CliError('CONFIG_ERROR', 'Gateway not configured', 'Set AGENTIO_GATEWAY_URL or configure gateway in config');
75
+ }
76
+
77
+ const host = gatewayConfig.api.host ?? '127.0.0.1';
78
+ const port = gatewayConfig.api.port ?? 7890;
79
+ const url = `http://${host}:${port}`;
80
+ const secret = gatewayConfig.api.secret ?? '';
81
+
82
+ cachedConfig = { url, secret };
83
+ return cachedConfig;
84
+ }
85
+
86
+ /**
87
+ * Make a request to the gateway API
88
+ */
89
+ async function request<T>(method: string, endpoint: string, body?: unknown): Promise<T> {
90
+ const { url, secret } = await getGatewayConnection();
91
+
92
+ const headers: Record<string, string> = {
93
+ 'Content-Type': 'application/json',
94
+ };
95
+
96
+ if (secret) {
97
+ headers['Authorization'] = `Bearer ${secret}`;
98
+ }
99
+
100
+ try {
101
+ const response = await fetch(`${url}${endpoint}`, {
102
+ method,
103
+ headers,
104
+ body: body ? JSON.stringify(body) : undefined,
105
+ });
106
+
107
+ if (!response.ok) {
108
+ if (response.status === 401) {
109
+ throw new CliError('AUTH_FAILED', 'Gateway authentication failed', 'Check AGENTIO_GATEWAY_SECRET');
110
+ }
111
+
112
+ const errorData = await response.json().catch(() => ({})) as { error?: string };
113
+ throw new CliError('API_ERROR', errorData.error || `Gateway error: ${response.status}`);
114
+ }
115
+
116
+ return await response.json() as T;
117
+ } catch (error) {
118
+ if (error instanceof CliError) throw error;
119
+
120
+ if (error instanceof TypeError && error.message.includes('fetch')) {
121
+ throw new CliError('NETWORK_ERROR', 'Cannot connect to gateway', 'Is the gateway running? Try: agentio gateway status');
122
+ }
123
+
124
+ throw new CliError('NETWORK_ERROR', error instanceof Error ? error.message : 'Unknown error');
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Gateway client for CLI commands
130
+ */
131
+ export class GatewayClient {
132
+ /**
133
+ * Check gateway health
134
+ */
135
+ async health(): Promise<HealthResponse> {
136
+ return request<HealthResponse>('GET', '/health');
137
+ }
138
+
139
+ /**
140
+ * Get gateway status
141
+ */
142
+ async status(): Promise<GatewayStatusResponse> {
143
+ return request<GatewayStatusResponse>('GET', '/status');
144
+ }
145
+
146
+ /**
147
+ * Pull messages from inbox
148
+ */
149
+ async inboxPull(options: {
150
+ service?: ServiceName;
151
+ profile?: string;
152
+ conversationId?: string;
153
+ limit?: number;
154
+ status?: InboxStatus;
155
+ }): Promise<InboundMessage[]> {
156
+ const body: InboxPullRequest = {
157
+ service: options.service,
158
+ profile: options.profile,
159
+ conversationId: options.conversationId,
160
+ limit: options.limit,
161
+ status: options.status,
162
+ };
163
+ const response = await request<InboxPullResponse>('POST', '/inbox/pull', body);
164
+ return response.messages;
165
+ }
166
+
167
+ /**
168
+ * Get a specific inbox message
169
+ */
170
+ async inboxGet(id: string): Promise<InboundMessage | null> {
171
+ const body: InboxGetRequest = { id };
172
+ const response = await request<InboxGetResponse>('POST', '/inbox/get', body);
173
+ return response.message;
174
+ }
175
+
176
+ /**
177
+ * Acknowledge (mark as done) an inbox message
178
+ */
179
+ async inboxAck(id: string): Promise<boolean> {
180
+ const body: InboxAckRequest = { id };
181
+ const response = await request<InboxAckResponse>('POST', '/inbox/ack', body);
182
+ return response.success;
183
+ }
184
+
185
+ /**
186
+ * Reply to an inbox message
187
+ */
188
+ async inboxReply(id: string, content: string, options?: {
189
+ mediaPath?: string;
190
+ mediaType?: MediaType;
191
+ }): Promise<{ outboxId: string; status: OutboxStatus }> {
192
+ const body: InboxReplyRequest = {
193
+ id,
194
+ content,
195
+ mediaPath: options?.mediaPath,
196
+ mediaType: options?.mediaType,
197
+ };
198
+ const response = await request<InboxReplyResponse>('POST', '/inbox/reply', body);
199
+ return { outboxId: response.outboxId, status: response.status };
200
+ }
201
+
202
+ /**
203
+ * Get inbox statistics
204
+ */
205
+ async inboxStats(options?: {
206
+ service?: ServiceName;
207
+ profile?: string;
208
+ }): Promise<{ pending: number; done: number; total: number }> {
209
+ const body: InboxStatsRequest = {
210
+ service: options?.service,
211
+ profile: options?.profile,
212
+ };
213
+ return request<InboxStatsResponse>('POST', '/inbox/stats', body);
214
+ }
215
+
216
+ /**
217
+ * Send a message via outbox
218
+ */
219
+ async outboxSend(options: {
220
+ service: ServiceName;
221
+ profile: string;
222
+ conversationId: string;
223
+ content?: string;
224
+ mediaPath?: string;
225
+ mediaType?: MediaType;
226
+ replyToPlatformId?: string;
227
+ metadata?: Record<string, unknown>;
228
+ }): Promise<{ id: string; status: OutboxStatus }> {
229
+ const body: OutboxSendRequest = {
230
+ service: options.service,
231
+ profile: options.profile,
232
+ conversationId: options.conversationId,
233
+ content: options.content,
234
+ mediaPath: options.mediaPath,
235
+ mediaType: options.mediaType,
236
+ replyToPlatformId: options.replyToPlatformId,
237
+ metadata: options.metadata,
238
+ };
239
+ const response = await request<OutboxSendResponse>('POST', '/outbox/send', body);
240
+ return { id: response.id, status: response.status };
241
+ }
242
+
243
+ /**
244
+ * Get outbox message status
245
+ */
246
+ async outboxStatus(id: string): Promise<OutboundMessage | null> {
247
+ const body: OutboxStatusRequest = { id };
248
+ const response = await request<OutboxStatusResponse>('POST', '/outbox/status', body);
249
+ return response.message;
250
+ }
251
+
252
+ /**
253
+ * List outbox messages
254
+ */
255
+ async outboxList(options?: {
256
+ service?: ServiceName;
257
+ profile?: string;
258
+ status?: OutboxStatus;
259
+ limit?: number;
260
+ }): Promise<OutboundMessage[]> {
261
+ const body: OutboxListRequest = {
262
+ service: options?.service,
263
+ profile: options?.profile,
264
+ status: options?.status,
265
+ limit: options?.limit,
266
+ };
267
+ const response = await request<OutboxListResponse>('POST', '/outbox/list', body);
268
+ return response.messages;
269
+ }
270
+
271
+ /**
272
+ * Get media URL for an inbox message
273
+ */
274
+ getMediaUrl(messageId: string): string {
275
+ // This needs the URL synchronously, so we use cached config
276
+ if (!cachedConfig) {
277
+ throw new CliError('CONFIG_ERROR', 'Gateway not configured');
278
+ }
279
+ return `${cachedConfig.url}/media/${messageId}`;
280
+ }
281
+
282
+ /**
283
+ * Get WhatsApp pairing status and QR code
284
+ */
285
+ async whatsappPair(profile: string): Promise<WhatsAppPairResponse> {
286
+ return request<WhatsAppPairResponse>('GET', `/whatsapp/pair/${encodeURIComponent(profile)}`);
287
+ }
288
+
289
+ // ============ WHATSAPP GROUP METHODS ============
290
+
291
+ /**
292
+ * List all WhatsApp groups
293
+ */
294
+ async whatsappGroupList(profile: string): Promise<WhatsAppGroup[]> {
295
+ const body: WhatsAppGroupListRequest = { profile };
296
+ const response = await request<WhatsAppGroupListResponse>('POST', '/whatsapp/groups/list', body);
297
+ return response.groups;
298
+ }
299
+
300
+ /**
301
+ * Get WhatsApp group details
302
+ */
303
+ async whatsappGroupGet(profile: string, groupId: string): Promise<WhatsAppGroup> {
304
+ const body: WhatsAppGroupGetRequest = { profile, groupId };
305
+ const response = await request<WhatsAppGroupGetResponse>('POST', '/whatsapp/groups/get', body);
306
+ return response.group;
307
+ }
308
+
309
+ /**
310
+ * Create a WhatsApp group
311
+ */
312
+ async whatsappGroupCreate(profile: string, name: string, participants: string[], picture?: string): Promise<WhatsAppGroup> {
313
+ const body: WhatsAppGroupCreateRequest = { profile, name, participants, picture };
314
+ const response = await request<WhatsAppGroupCreateResponse>('POST', '/whatsapp/groups/create', body);
315
+ return response.group;
316
+ }
317
+
318
+ /**
319
+ * Update WhatsApp group info
320
+ */
321
+ async whatsappGroupUpdate(
322
+ profile: string,
323
+ groupId: string,
324
+ options: { subject?: string; description?: string; picture?: string }
325
+ ): Promise<boolean> {
326
+ const body: WhatsAppGroupUpdateRequest = { profile, groupId, ...options };
327
+ const response = await request<WhatsAppGroupUpdateResponse>('POST', '/whatsapp/groups/update', body);
328
+ return response.success;
329
+ }
330
+
331
+ /**
332
+ * Update WhatsApp group participants
333
+ */
334
+ async whatsappGroupParticipants(
335
+ profile: string,
336
+ groupId: string,
337
+ participants: string[],
338
+ action: WhatsAppParticipantAction
339
+ ): Promise<{ success: boolean; results?: { participant: string; status: string }[] }> {
340
+ const body: WhatsAppGroupParticipantsRequest = { profile, groupId, participants, action };
341
+ const response = await request<WhatsAppGroupParticipantsResponse>('POST', '/whatsapp/groups/participants', body);
342
+ return response;
343
+ }
344
+
345
+ /**
346
+ * Leave a WhatsApp group
347
+ */
348
+ async whatsappGroupLeave(profile: string, groupId: string): Promise<boolean> {
349
+ const body: WhatsAppGroupLeaveRequest = { profile, groupId };
350
+ const response = await request<WhatsAppGroupLeaveResponse>('POST', '/whatsapp/groups/leave', body);
351
+ return response.success;
352
+ }
353
+
354
+ /**
355
+ * Get WhatsApp group invite link
356
+ */
357
+ async whatsappGroupInvite(profile: string, groupId: string): Promise<{ inviteCode: string; inviteLink: string }> {
358
+ const body: WhatsAppGroupInviteRequest = { profile, groupId };
359
+ const response = await request<WhatsAppGroupInviteResponse>('POST', '/whatsapp/groups/invite', body);
360
+ return response;
361
+ }
362
+
363
+ /**
364
+ * Join WhatsApp group via invite code
365
+ */
366
+ async whatsappGroupJoin(profile: string, inviteCode: string): Promise<string> {
367
+ const body: WhatsAppGroupJoinRequest = { profile, inviteCode };
368
+ const response = await request<WhatsAppGroupJoinResponse>('POST', '/whatsapp/groups/join', body);
369
+ return response.groupId;
370
+ }
371
+
372
+ /**
373
+ * Resolve group name to JID or vice versa
374
+ */
375
+ async whatsappGroupResolve(profile: string, nameOrId: string): Promise<{ groupId: string | null; groupName: string | null }> {
376
+ const body: WhatsAppGroupResolveRequest = { profile, nameOrId };
377
+ const response = await request<WhatsAppGroupResolveResponse>('POST', '/whatsapp/groups/resolve', body);
378
+ return response;
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Get a gateway client instance
384
+ */
385
+ export async function getGatewayClient(): Promise<GatewayClient> {
386
+ // Ensure config is loaded
387
+ await getGatewayConnection();
388
+ return new GatewayClient();
389
+ }
390
+
391
+ /**
392
+ * Check if gateway is available
393
+ */
394
+ export async function isGatewayAvailable(): Promise<boolean> {
395
+ try {
396
+ const client = await getGatewayClient();
397
+ await client.health();
398
+ return true;
399
+ } catch {
400
+ return false;
401
+ }
402
+ }