digital-workers 0.1.1 → 2.0.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.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +290 -106
  4. package/dist/actions.d.ts +95 -0
  5. package/dist/actions.d.ts.map +1 -0
  6. package/dist/actions.js +437 -0
  7. package/dist/actions.js.map +1 -0
  8. package/dist/approve.d.ts +49 -0
  9. package/dist/approve.d.ts.map +1 -0
  10. package/dist/approve.js +235 -0
  11. package/dist/approve.js.map +1 -0
  12. package/dist/ask.d.ts +42 -0
  13. package/dist/ask.d.ts.map +1 -0
  14. package/dist/ask.js +227 -0
  15. package/dist/ask.js.map +1 -0
  16. package/dist/decide.d.ts +62 -0
  17. package/dist/decide.d.ts.map +1 -0
  18. package/dist/decide.js +245 -0
  19. package/dist/decide.js.map +1 -0
  20. package/dist/do.d.ts +63 -0
  21. package/dist/do.d.ts.map +1 -0
  22. package/dist/do.js +228 -0
  23. package/dist/do.js.map +1 -0
  24. package/dist/generate.d.ts +61 -0
  25. package/dist/generate.d.ts.map +1 -0
  26. package/dist/generate.js +299 -0
  27. package/dist/generate.js.map +1 -0
  28. package/dist/goals.d.ts +89 -0
  29. package/dist/goals.d.ts.map +1 -0
  30. package/dist/goals.js +206 -0
  31. package/dist/goals.js.map +1 -0
  32. package/dist/index.d.ts +68 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +69 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/is.d.ts +54 -0
  37. package/dist/is.d.ts.map +1 -0
  38. package/dist/is.js +318 -0
  39. package/dist/is.js.map +1 -0
  40. package/dist/kpis.d.ts +103 -0
  41. package/dist/kpis.d.ts.map +1 -0
  42. package/dist/kpis.js +271 -0
  43. package/dist/kpis.js.map +1 -0
  44. package/dist/notify.d.ts +47 -0
  45. package/dist/notify.d.ts.map +1 -0
  46. package/dist/notify.js +220 -0
  47. package/dist/notify.js.map +1 -0
  48. package/dist/role.d.ts +53 -0
  49. package/dist/role.d.ts.map +1 -0
  50. package/dist/role.js +111 -0
  51. package/dist/role.js.map +1 -0
  52. package/dist/team.d.ts +61 -0
  53. package/dist/team.d.ts.map +1 -0
  54. package/dist/team.js +131 -0
  55. package/dist/team.js.map +1 -0
  56. package/dist/transports.d.ts +164 -0
  57. package/dist/transports.d.ts.map +1 -0
  58. package/dist/transports.js +358 -0
  59. package/dist/transports.js.map +1 -0
  60. package/dist/types.d.ts +693 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +72 -0
  63. package/dist/types.js.map +1 -0
  64. package/package.json +27 -61
  65. package/src/actions.ts +615 -0
  66. package/src/approve.ts +317 -0
  67. package/src/ask.ts +304 -0
  68. package/src/decide.ts +295 -0
  69. package/src/do.ts +275 -0
  70. package/src/generate.ts +364 -0
  71. package/src/goals.ts +220 -0
  72. package/src/index.ts +118 -0
  73. package/src/is.ts +372 -0
  74. package/src/kpis.ts +348 -0
  75. package/src/notify.ts +303 -0
  76. package/src/role.ts +116 -0
  77. package/src/team.ts +142 -0
  78. package/src/transports.ts +504 -0
  79. package/src/types.ts +843 -0
  80. package/test/actions.test.ts +546 -0
  81. package/test/standalone.test.ts +299 -0
  82. package/test/types.test.ts +460 -0
  83. package/tsconfig.json +9 -0
@@ -0,0 +1,504 @@
1
+ /**
2
+ * Communication Transports Bridge
3
+ *
4
+ * Connects digital-workers contact channels to digital-tools
5
+ * communication providers (Message, Call).
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type {
11
+ Worker,
12
+ Team,
13
+ WorkerRef,
14
+ Contacts,
15
+ ContactChannel,
16
+ NotifyActionData,
17
+ AskActionData,
18
+ ApproveActionData,
19
+ NotifyResult,
20
+ AskResult,
21
+ ApprovalResult,
22
+ } from './types.js'
23
+
24
+ // =============================================================================
25
+ // Transport Types
26
+ // =============================================================================
27
+
28
+ /**
29
+ * Communication transport - maps contact channels to delivery mechanisms
30
+ */
31
+ export type Transport =
32
+ | 'email' // Email transport (SendGrid, Resend, etc.)
33
+ | 'sms' // SMS transport (Twilio, etc.)
34
+ | 'voice' // Voice call transport (Vapi, Twilio, etc.)
35
+ | 'slack' // Slack API
36
+ | 'teams' // Microsoft Teams API
37
+ | 'discord' // Discord API
38
+ | 'whatsapp' // WhatsApp Business API
39
+ | 'telegram' // Telegram Bot API
40
+ | 'web' // Web push/in-app
41
+ | 'webhook' // Generic webhook
42
+
43
+ /**
44
+ * Transport configuration
45
+ */
46
+ export interface TransportConfig {
47
+ transport: Transport
48
+ provider?: string // Specific provider (e.g., 'sendgrid', 'resend')
49
+ apiKey?: string
50
+ apiUrl?: string
51
+ options?: Record<string, unknown>
52
+ }
53
+
54
+ /**
55
+ * Message payload for transport
56
+ */
57
+ export interface MessagePayload {
58
+ // Addressing
59
+ to: string | string[]
60
+ from?: string
61
+ replyTo?: string
62
+
63
+ // Content
64
+ subject?: string
65
+ body: string
66
+ html?: string
67
+
68
+ // Metadata
69
+ type: 'notification' | 'question' | 'approval'
70
+ priority?: 'low' | 'normal' | 'high' | 'urgent'
71
+ threadId?: string
72
+ metadata?: Record<string, unknown>
73
+
74
+ // Interactive (for questions/approvals)
75
+ actions?: MessageAction[]
76
+ schema?: unknown // SimpleSchema from ai-functions
77
+ timeout?: number
78
+ }
79
+
80
+ /**
81
+ * Interactive message action
82
+ */
83
+ export interface MessageAction {
84
+ id: string
85
+ label: string
86
+ style?: 'primary' | 'secondary' | 'danger'
87
+ value?: unknown
88
+ }
89
+
90
+ /**
91
+ * Transport delivery result
92
+ */
93
+ export interface DeliveryResult {
94
+ success: boolean
95
+ transport: Transport
96
+ messageId?: string
97
+ error?: string
98
+ metadata?: Record<string, unknown>
99
+ }
100
+
101
+ // =============================================================================
102
+ // Channel to Transport Mapping
103
+ // =============================================================================
104
+
105
+ /**
106
+ * Map contact channel to transport
107
+ */
108
+ export function channelToTransport(channel: ContactChannel): Transport {
109
+ const mapping: Record<ContactChannel, Transport> = {
110
+ email: 'email',
111
+ slack: 'slack',
112
+ teams: 'teams',
113
+ discord: 'discord',
114
+ phone: 'voice',
115
+ sms: 'sms',
116
+ whatsapp: 'whatsapp',
117
+ telegram: 'telegram',
118
+ web: 'web',
119
+ api: 'webhook',
120
+ webhook: 'webhook',
121
+ }
122
+ return mapping[channel] || 'webhook'
123
+ }
124
+
125
+ /**
126
+ * Get available transports for a worker
127
+ */
128
+ export function getWorkerTransports(worker: Worker): Transport[] {
129
+ const transports: Transport[] = []
130
+ const contacts = worker.contacts
131
+
132
+ if (contacts.email) transports.push('email')
133
+ if (contacts.slack) transports.push('slack')
134
+ if (contacts.teams) transports.push('teams')
135
+ if (contacts.discord) transports.push('discord')
136
+ if (contacts.phone) transports.push('voice')
137
+ if (contacts.sms) transports.push('sms')
138
+ if (contacts.whatsapp) transports.push('whatsapp')
139
+ if (contacts.telegram) transports.push('telegram')
140
+ if (contacts.web) transports.push('web')
141
+ if (contacts.webhook) transports.push('webhook')
142
+
143
+ return transports
144
+ }
145
+
146
+ /**
147
+ * Get team transports (union of all member transports + team contacts)
148
+ */
149
+ export function getTeamTransports(team: Team): Transport[] {
150
+ const transports = new Set<Transport>()
151
+
152
+ // Add team-level contacts
153
+ const contacts = team.contacts
154
+ if (contacts.email) transports.add('email')
155
+ if (contacts.slack) transports.add('slack')
156
+ if (contacts.teams) transports.add('teams')
157
+ if (contacts.discord) transports.add('discord')
158
+ if (contacts.phone) transports.add('voice')
159
+ if (contacts.sms) transports.add('sms')
160
+ if (contacts.whatsapp) transports.add('whatsapp')
161
+ if (contacts.telegram) transports.add('telegram')
162
+ if (contacts.web) transports.add('web')
163
+ if (contacts.webhook) transports.add('webhook')
164
+
165
+ return Array.from(transports)
166
+ }
167
+
168
+ // =============================================================================
169
+ // Address Resolution
170
+ // =============================================================================
171
+
172
+ /**
173
+ * Identifier for a communication address
174
+ */
175
+ export interface Address {
176
+ transport: Transport
177
+ value: string
178
+ name?: string
179
+ metadata?: Record<string, unknown>
180
+ }
181
+
182
+ /**
183
+ * Resolve contact to address
184
+ */
185
+ export function resolveAddress(contacts: Contacts, channel: ContactChannel): Address | null {
186
+ const contact = contacts[channel]
187
+ if (!contact) return null
188
+
189
+ const transport = channelToTransport(channel)
190
+
191
+ if (typeof contact === 'string') {
192
+ return { transport, value: contact }
193
+ }
194
+
195
+ // Handle structured contact types
196
+ switch (channel) {
197
+ case 'email':
198
+ const emailContact = contact as { address: string; name?: string }
199
+ return { transport, value: emailContact.address, name: emailContact.name }
200
+ case 'phone':
201
+ case 'sms':
202
+ case 'whatsapp':
203
+ const phoneContact = contact as { number: string }
204
+ return { transport, value: phoneContact.number }
205
+ case 'slack':
206
+ const slackContact = contact as { user?: string; channel?: string; workspace?: string }
207
+ return {
208
+ transport,
209
+ value: slackContact.user || slackContact.channel || '',
210
+ metadata: { workspace: slackContact.workspace },
211
+ }
212
+ case 'teams':
213
+ const teamsContact = contact as { user?: string; channel?: string; team?: string }
214
+ return {
215
+ transport,
216
+ value: teamsContact.user || teamsContact.channel || '',
217
+ metadata: { team: teamsContact.team },
218
+ }
219
+ case 'discord':
220
+ const discordContact = contact as { user?: string; channel?: string; server?: string }
221
+ return {
222
+ transport,
223
+ value: discordContact.user || discordContact.channel || '',
224
+ metadata: { server: discordContact.server },
225
+ }
226
+ case 'telegram':
227
+ const telegramContact = contact as { user?: string; chat?: string }
228
+ return { transport, value: telegramContact.user || telegramContact.chat || '' }
229
+ case 'web':
230
+ const webContact = contact as { userId?: string; url?: string }
231
+ return { transport, value: webContact.userId || '', metadata: { url: webContact.url } }
232
+ case 'webhook':
233
+ const webhookContact = contact as { url: string; secret?: string }
234
+ return { transport, value: webhookContact.url, metadata: { secret: webhookContact.secret } }
235
+ case 'api':
236
+ const apiContact = contact as { endpoint: string; auth?: string }
237
+ return { transport, value: apiContact.endpoint, metadata: { auth: apiContact.auth } }
238
+ default:
239
+ return null
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Resolve all addresses for a worker
245
+ */
246
+ export function resolveWorkerAddresses(worker: Worker): Address[] {
247
+ const addresses: Address[] = []
248
+ const channels: ContactChannel[] = [
249
+ 'email', 'slack', 'teams', 'discord', 'phone', 'sms',
250
+ 'whatsapp', 'telegram', 'web', 'api', 'webhook',
251
+ ]
252
+
253
+ for (const channel of channels) {
254
+ const address = resolveAddress(worker.contacts, channel)
255
+ if (address) addresses.push(address)
256
+ }
257
+
258
+ return addresses
259
+ }
260
+
261
+ /**
262
+ * Get primary address for a worker based on preferences
263
+ */
264
+ export function getPrimaryAddress(worker: Worker): Address | null {
265
+ const preferences = worker.preferences
266
+ if (preferences?.primary) {
267
+ return resolveAddress(worker.contacts, preferences.primary)
268
+ }
269
+
270
+ // Default priority: slack > email > teams > sms > phone
271
+ const defaultPriority: ContactChannel[] = ['slack', 'email', 'teams', 'sms', 'phone']
272
+ for (const channel of defaultPriority) {
273
+ const address = resolveAddress(worker.contacts, channel)
274
+ if (address) return address
275
+ }
276
+
277
+ // Fall back to any available
278
+ const addresses = resolveWorkerAddresses(worker)
279
+ return addresses[0] || null
280
+ }
281
+
282
+ // =============================================================================
283
+ // Transport Registry
284
+ // =============================================================================
285
+
286
+ /**
287
+ * Transport handler function type
288
+ */
289
+ export type TransportHandler = (
290
+ payload: MessagePayload,
291
+ config: TransportConfig
292
+ ) => Promise<DeliveryResult>
293
+
294
+ /**
295
+ * Transport registry
296
+ */
297
+ const transportRegistry = new Map<Transport, TransportHandler>()
298
+
299
+ /**
300
+ * Register a transport handler
301
+ */
302
+ export function registerTransport(transport: Transport, handler: TransportHandler): void {
303
+ transportRegistry.set(transport, handler)
304
+ }
305
+
306
+ /**
307
+ * Get transport handler
308
+ */
309
+ export function getTransportHandler(transport: Transport): TransportHandler | undefined {
310
+ return transportRegistry.get(transport)
311
+ }
312
+
313
+ /**
314
+ * Check if transport is registered
315
+ */
316
+ export function hasTransport(transport: Transport): boolean {
317
+ return transportRegistry.has(transport)
318
+ }
319
+
320
+ /**
321
+ * List registered transports
322
+ */
323
+ export function listTransports(): Transport[] {
324
+ return Array.from(transportRegistry.keys())
325
+ }
326
+
327
+ // =============================================================================
328
+ // Default Transport Handlers (Stubs - implemented by providers)
329
+ // =============================================================================
330
+
331
+ /**
332
+ * Send via transport
333
+ */
334
+ export async function sendViaTransport(
335
+ transport: Transport,
336
+ payload: MessagePayload,
337
+ config?: TransportConfig
338
+ ): Promise<DeliveryResult> {
339
+ const handler = transportRegistry.get(transport)
340
+ if (!handler) {
341
+ return {
342
+ success: false,
343
+ transport,
344
+ error: `Transport '${transport}' not registered`,
345
+ }
346
+ }
347
+
348
+ try {
349
+ return await handler(payload, config || { transport })
350
+ } catch (error) {
351
+ return {
352
+ success: false,
353
+ transport,
354
+ error: error instanceof Error ? error.message : String(error),
355
+ }
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Send to multiple transports (fan-out)
361
+ */
362
+ export async function sendToMultipleTransports(
363
+ transports: Transport[],
364
+ payload: MessagePayload,
365
+ configs?: Record<Transport, TransportConfig>
366
+ ): Promise<DeliveryResult[]> {
367
+ const results = await Promise.all(
368
+ transports.map(transport =>
369
+ sendViaTransport(transport, payload, configs?.[transport])
370
+ )
371
+ )
372
+ return results
373
+ }
374
+
375
+ // =============================================================================
376
+ // Worker Action to Transport
377
+ // =============================================================================
378
+
379
+ /**
380
+ * Build message payload from notify action
381
+ */
382
+ export function buildNotifyPayload(action: NotifyActionData): MessagePayload {
383
+ return {
384
+ to: resolveActionTarget(action.object),
385
+ body: action.message,
386
+ type: 'notification',
387
+ priority: action.priority || 'normal',
388
+ metadata: action.metadata,
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Build message payload from ask action
394
+ */
395
+ export function buildAskPayload(action: AskActionData): MessagePayload {
396
+ return {
397
+ to: resolveActionTarget(action.object),
398
+ body: action.question,
399
+ type: 'question',
400
+ schema: action.schema,
401
+ timeout: action.timeout,
402
+ metadata: action.metadata,
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Build message payload from approve action
408
+ */
409
+ export function buildApprovePayload(action: ApproveActionData): MessagePayload {
410
+ return {
411
+ to: resolveActionTarget(action.object),
412
+ body: action.request,
413
+ type: 'approval',
414
+ timeout: action.timeout,
415
+ actions: [
416
+ { id: 'approve', label: 'Approve', style: 'primary', value: true },
417
+ { id: 'reject', label: 'Reject', style: 'danger', value: false },
418
+ ],
419
+ metadata: {
420
+ ...action.metadata,
421
+ context: action.context,
422
+ },
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Resolve action target to address string
428
+ */
429
+ function resolveActionTarget(target: Worker | Team | WorkerRef | string): string {
430
+ if (typeof target === 'string') return target
431
+ if ('contacts' in target) {
432
+ const address = getPrimaryAddress(target as Worker)
433
+ return address?.value || target.id
434
+ }
435
+ return target.id
436
+ }
437
+
438
+ // =============================================================================
439
+ // Integration with digital-tools Message/Call types
440
+ // =============================================================================
441
+
442
+ /**
443
+ * Message type mapping (from digital-tools)
444
+ */
445
+ export const MessageTypeMapping = {
446
+ email: 'email',
447
+ sms: 'text',
448
+ slack: 'chat',
449
+ teams: 'chat',
450
+ discord: 'chat',
451
+ whatsapp: 'text',
452
+ telegram: 'text',
453
+ voice: 'voicemail', // For voicemail messages
454
+ } as const
455
+
456
+ /**
457
+ * Call type mapping (from digital-tools)
458
+ */
459
+ export const CallTypeMapping = {
460
+ phone: 'phone',
461
+ voice: 'phone',
462
+ web: 'web',
463
+ video: 'video',
464
+ } as const
465
+
466
+ /**
467
+ * Convert worker notification to digital-tools Message format
468
+ */
469
+ export function toDigitalToolsMessage(
470
+ payload: MessagePayload,
471
+ transport: Transport
472
+ ): Record<string, unknown> {
473
+ const messageType = MessageTypeMapping[transport as keyof typeof MessageTypeMapping] || 'chat'
474
+
475
+ return {
476
+ type: messageType,
477
+ to: Array.isArray(payload.to) ? payload.to : [payload.to],
478
+ from: payload.from,
479
+ subject: payload.subject,
480
+ body: payload.body,
481
+ html: payload.html,
482
+ metadata: {
483
+ ...payload.metadata,
484
+ workerActionType: payload.type,
485
+ priority: payload.priority,
486
+ },
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Convert digital-tools Message to worker notification format
492
+ */
493
+ export function fromDigitalToolsMessage(
494
+ message: Record<string, unknown>
495
+ ): Partial<MessagePayload> {
496
+ return {
497
+ to: message.to as string | string[],
498
+ from: message.from as string | undefined,
499
+ subject: message.subject as string | undefined,
500
+ body: message.body as string,
501
+ html: message.html as string | undefined,
502
+ metadata: message.metadata as Record<string, unknown> | undefined,
503
+ }
504
+ }