busy-cli 0.1.2

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 (128) hide show
  1. package/README.md +129 -0
  2. package/dist/builders/context.d.ts +50 -0
  3. package/dist/builders/context.d.ts.map +1 -0
  4. package/dist/builders/context.js +190 -0
  5. package/dist/cache/index.d.ts +100 -0
  6. package/dist/cache/index.d.ts.map +1 -0
  7. package/dist/cache/index.js +270 -0
  8. package/dist/cli/index.d.ts +3 -0
  9. package/dist/cli/index.d.ts.map +1 -0
  10. package/dist/cli/index.js +463 -0
  11. package/dist/commands/package.d.ts +96 -0
  12. package/dist/commands/package.d.ts.map +1 -0
  13. package/dist/commands/package.js +285 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +7 -0
  17. package/dist/loader.d.ts +6 -0
  18. package/dist/loader.d.ts.map +1 -0
  19. package/dist/loader.js +361 -0
  20. package/dist/merge.d.ts +16 -0
  21. package/dist/merge.d.ts.map +1 -0
  22. package/dist/merge.js +102 -0
  23. package/dist/package/manifest.d.ts +59 -0
  24. package/dist/package/manifest.d.ts.map +1 -0
  25. package/dist/package/manifest.js +265 -0
  26. package/dist/parser.d.ts +28 -0
  27. package/dist/parser.d.ts.map +1 -0
  28. package/dist/parser.js +220 -0
  29. package/dist/parsers/frontmatter.d.ts +14 -0
  30. package/dist/parsers/frontmatter.d.ts.map +1 -0
  31. package/dist/parsers/frontmatter.js +110 -0
  32. package/dist/parsers/imports.d.ts +48 -0
  33. package/dist/parsers/imports.d.ts.map +1 -0
  34. package/dist/parsers/imports.js +147 -0
  35. package/dist/parsers/links.d.ts +12 -0
  36. package/dist/parsers/links.d.ts.map +1 -0
  37. package/dist/parsers/links.js +79 -0
  38. package/dist/parsers/localdefs.d.ts +6 -0
  39. package/dist/parsers/localdefs.d.ts.map +1 -0
  40. package/dist/parsers/localdefs.js +132 -0
  41. package/dist/parsers/operations.d.ts +32 -0
  42. package/dist/parsers/operations.d.ts.map +1 -0
  43. package/dist/parsers/operations.js +313 -0
  44. package/dist/parsers/sections.d.ts +15 -0
  45. package/dist/parsers/sections.d.ts.map +1 -0
  46. package/dist/parsers/sections.js +173 -0
  47. package/dist/parsers/tools.d.ts +30 -0
  48. package/dist/parsers/tools.d.ts.map +1 -0
  49. package/dist/parsers/tools.js +178 -0
  50. package/dist/parsers/triggers.d.ts +35 -0
  51. package/dist/parsers/triggers.d.ts.map +1 -0
  52. package/dist/parsers/triggers.js +219 -0
  53. package/dist/providers/base.d.ts +60 -0
  54. package/dist/providers/base.d.ts.map +1 -0
  55. package/dist/providers/base.js +34 -0
  56. package/dist/providers/github.d.ts +18 -0
  57. package/dist/providers/github.d.ts.map +1 -0
  58. package/dist/providers/github.js +109 -0
  59. package/dist/providers/gitlab.d.ts +18 -0
  60. package/dist/providers/gitlab.d.ts.map +1 -0
  61. package/dist/providers/gitlab.js +101 -0
  62. package/dist/providers/index.d.ts +13 -0
  63. package/dist/providers/index.d.ts.map +1 -0
  64. package/dist/providers/index.js +17 -0
  65. package/dist/providers/local.d.ts +31 -0
  66. package/dist/providers/local.d.ts.map +1 -0
  67. package/dist/providers/local.js +116 -0
  68. package/dist/providers/url.d.ts +16 -0
  69. package/dist/providers/url.d.ts.map +1 -0
  70. package/dist/providers/url.js +45 -0
  71. package/dist/registry/index.d.ts +99 -0
  72. package/dist/registry/index.d.ts.map +1 -0
  73. package/dist/registry/index.js +320 -0
  74. package/dist/types/schema.d.ts +3259 -0
  75. package/dist/types/schema.d.ts.map +1 -0
  76. package/dist/types/schema.js +258 -0
  77. package/dist/utils/logger.d.ts +19 -0
  78. package/dist/utils/logger.d.ts.map +1 -0
  79. package/dist/utils/logger.js +23 -0
  80. package/dist/utils/slugify.d.ts +14 -0
  81. package/dist/utils/slugify.d.ts.map +1 -0
  82. package/dist/utils/slugify.js +28 -0
  83. package/package.json +61 -0
  84. package/src/__tests__/cache.test.ts +393 -0
  85. package/src/__tests__/cli-package.test.ts +667 -0
  86. package/src/__tests__/fixtures/automated-workflow.busy.md +84 -0
  87. package/src/__tests__/fixtures/concept.busy.md +30 -0
  88. package/src/__tests__/fixtures/document.busy.md +44 -0
  89. package/src/__tests__/fixtures/simple-operation.busy.md +45 -0
  90. package/src/__tests__/fixtures/tool-document.busy.md +71 -0
  91. package/src/__tests__/fixtures/tool.busy.md +54 -0
  92. package/src/__tests__/imports.test.ts +244 -0
  93. package/src/__tests__/integration.test.ts +432 -0
  94. package/src/__tests__/operations.test.ts +408 -0
  95. package/src/__tests__/package-manifest.test.ts +455 -0
  96. package/src/__tests__/providers.test.ts +672 -0
  97. package/src/__tests__/registry.test.ts +402 -0
  98. package/src/__tests__/schema.test.ts +467 -0
  99. package/src/__tests__/tools.test.ts +376 -0
  100. package/src/__tests__/triggers.test.ts +312 -0
  101. package/src/builders/context.ts +294 -0
  102. package/src/cache/index.ts +312 -0
  103. package/src/cli/index.ts +514 -0
  104. package/src/commands/package.ts +392 -0
  105. package/src/index.ts +46 -0
  106. package/src/loader.ts +474 -0
  107. package/src/merge.ts +126 -0
  108. package/src/package/manifest.ts +349 -0
  109. package/src/parser.ts +278 -0
  110. package/src/parsers/frontmatter.ts +135 -0
  111. package/src/parsers/imports.ts +196 -0
  112. package/src/parsers/links.ts +108 -0
  113. package/src/parsers/localdefs.ts +166 -0
  114. package/src/parsers/operations.ts +404 -0
  115. package/src/parsers/sections.ts +230 -0
  116. package/src/parsers/tools.ts +215 -0
  117. package/src/parsers/triggers.ts +252 -0
  118. package/src/providers/base.ts +77 -0
  119. package/src/providers/github.ts +129 -0
  120. package/src/providers/gitlab.ts +121 -0
  121. package/src/providers/index.ts +25 -0
  122. package/src/providers/local.ts +129 -0
  123. package/src/providers/url.ts +56 -0
  124. package/src/registry/index.ts +408 -0
  125. package/src/types/schema.ts +369 -0
  126. package/src/utils/logger.ts +25 -0
  127. package/src/utils/slugify.ts +31 -0
  128. package/tsconfig.json +21 -0
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Tool Parsing Tests - Match busy-python Tool and ToolDocument models
3
+ *
4
+ * busy-python Tool model:
5
+ * - name: str
6
+ * - description: str
7
+ * - inputs: list[str]
8
+ * - outputs: list[str]
9
+ * - examples: Optional[list[str]]
10
+ * - providers: Optional[dict[str, dict[str, Any]]] (provider_name -> {action, parameters})
11
+ */
12
+
13
+ import { describe, it, expect } from 'vitest';
14
+ import { parseTools, parseToolProviders } from '../parsers/tools';
15
+ import type { Tool, ToolDocument } from '../types/schema';
16
+
17
+ describe('parseTools', () => {
18
+ it('should parse tool with all fields', () => {
19
+ const content = `
20
+ # [Tools]
21
+
22
+ ## send_email
23
+
24
+ Send an email to one or more recipients.
25
+
26
+ ### [Inputs]
27
+ - to: Recipient email address(es)
28
+ - subject: Email subject line
29
+ - body: Email body content
30
+ - cc: (Optional) CC recipients
31
+
32
+ ### [Outputs]
33
+ - message_id: The sent message ID
34
+ - thread_id: The thread ID
35
+
36
+ ### [Examples]
37
+ - send_email(to="user@example.com", subject="Hello", body="Hi there")
38
+ - send_email(to="team@company.com", subject="Update", body="Status report", cc="manager@company.com")
39
+
40
+ ### [Providers]
41
+
42
+ #### composio
43
+ Action: GMAIL_SEND_EMAIL
44
+ Parameters:
45
+ to: to
46
+ subject: subject
47
+ body: body
48
+
49
+ #### mcp
50
+ Action: gmail/sendEmail
51
+ Parameters:
52
+ recipient: to
53
+ title: subject
54
+ content: body
55
+ `;
56
+
57
+ const tools = parseTools(content);
58
+ expect(tools).toHaveLength(1);
59
+
60
+ const tool = tools[0];
61
+ expect(tool.name).toBe('send_email');
62
+ expect(tool.description).toBe('Send an email to one or more recipients.');
63
+ expect(tool.inputs).toHaveLength(4);
64
+ expect(tool.outputs).toHaveLength(2);
65
+ expect(tool.examples).toHaveLength(2);
66
+ expect(tool.providers).toHaveProperty('composio');
67
+ expect(tool.providers).toHaveProperty('mcp');
68
+ });
69
+
70
+ it('should parse multiple tools', () => {
71
+ const content = `
72
+ # [Tools]
73
+
74
+ ## list_emails
75
+
76
+ List emails from inbox.
77
+
78
+ ### [Inputs]
79
+ - limit: Maximum number to return
80
+
81
+ ### [Outputs]
82
+ - emails: List of email objects
83
+
84
+ ## get_email
85
+
86
+ Get a single email by ID.
87
+
88
+ ### [Inputs]
89
+ - message_id: The email message ID
90
+
91
+ ### [Outputs]
92
+ - email: The email object
93
+ `;
94
+
95
+ const tools = parseTools(content);
96
+ expect(tools).toHaveLength(2);
97
+ expect(tools.map((t) => t.name)).toEqual(['list_emails', 'get_email']);
98
+ });
99
+
100
+ it('should handle tool without providers', () => {
101
+ const content = `
102
+ # [Tools]
103
+
104
+ ## local_tool
105
+
106
+ A tool without external providers.
107
+
108
+ ### [Inputs]
109
+ - data: Input data
110
+
111
+ ### [Outputs]
112
+ - result: Output result
113
+ `;
114
+
115
+ const tools = parseTools(content);
116
+ expect(tools).toHaveLength(1);
117
+ expect(tools[0].providers).toBeUndefined();
118
+ });
119
+
120
+ it('should handle tool without examples', () => {
121
+ const content = `
122
+ # [Tools]
123
+
124
+ ## simple_tool
125
+
126
+ Simple tool description.
127
+
128
+ ### [Inputs]
129
+ - input: The input
130
+
131
+ ### [Outputs]
132
+ - output: The output
133
+ `;
134
+
135
+ const tools = parseTools(content);
136
+ expect(tools).toHaveLength(1);
137
+ expect(tools[0].examples).toBeUndefined();
138
+ });
139
+
140
+ it('should parse tool inputs as string array', () => {
141
+ const content = `
142
+ # [Tools]
143
+
144
+ ## process
145
+
146
+ ### [Inputs]
147
+ - data: The raw data to process
148
+ - format: Output format (json, xml, csv)
149
+ - validate: Whether to validate (default: true)
150
+ `;
151
+
152
+ const tools = parseTools(content);
153
+ expect(tools[0].inputs).toEqual([
154
+ 'data: The raw data to process',
155
+ 'format: Output format (json, xml, csv)',
156
+ 'validate: Whether to validate (default: true)',
157
+ ]);
158
+ });
159
+
160
+ it('should handle Tools section without bracket notation', () => {
161
+ const content = `
162
+ # Tools
163
+
164
+ ## my_tool
165
+
166
+ Tool description.
167
+
168
+ ### Inputs
169
+ - input: Data
170
+
171
+ ### Outputs
172
+ - output: Result
173
+ `;
174
+
175
+ const tools = parseTools(content);
176
+ expect(tools).toHaveLength(1);
177
+ });
178
+
179
+ it('should return empty array for document without Tools section', () => {
180
+ const content = `
181
+ # [Operations]
182
+
183
+ ## SomeOp
184
+
185
+ Not a tool document.
186
+ `;
187
+
188
+ const tools = parseTools(content);
189
+ expect(tools).toHaveLength(0);
190
+ });
191
+ });
192
+
193
+ describe('parseToolProviders', () => {
194
+ it('should parse provider with action and parameters', () => {
195
+ const content = `
196
+ ### [Providers]
197
+
198
+ #### composio
199
+ Action: GMAIL_SEND_EMAIL
200
+ Parameters:
201
+ to: recipient
202
+ subject: title
203
+ body: content
204
+ `;
205
+
206
+ const providers = parseToolProviders(content);
207
+ expect(providers).toHaveProperty('composio');
208
+ expect(providers.composio.action).toBe('GMAIL_SEND_EMAIL');
209
+ expect(providers.composio.parameters).toEqual({
210
+ to: 'recipient',
211
+ subject: 'title',
212
+ body: 'content',
213
+ });
214
+ });
215
+
216
+ it('should parse multiple providers', () => {
217
+ const content = `
218
+ ### [Providers]
219
+
220
+ #### composio
221
+ Action: COMPOSIO_ACTION
222
+
223
+ #### mcp
224
+ Action: mcp/action
225
+
226
+ #### custom
227
+ Action: custom.action
228
+ `;
229
+
230
+ const providers = parseToolProviders(content);
231
+ expect(Object.keys(providers)).toHaveLength(3);
232
+ expect(providers.composio.action).toBe('COMPOSIO_ACTION');
233
+ expect(providers.mcp.action).toBe('mcp/action');
234
+ expect(providers.custom.action).toBe('custom.action');
235
+ });
236
+
237
+ it('should handle provider without parameters', () => {
238
+ const content = `
239
+ ### [Providers]
240
+
241
+ #### simple
242
+ Action: SIMPLE_ACTION
243
+ `;
244
+
245
+ const providers = parseToolProviders(content);
246
+ expect(providers.simple.action).toBe('SIMPLE_ACTION');
247
+ expect(providers.simple.parameters).toBeUndefined();
248
+ });
249
+
250
+ it('should return empty object for content without providers', () => {
251
+ const content = `
252
+ ### [Inputs]
253
+ - data: Input
254
+ `;
255
+
256
+ const providers = parseToolProviders(content);
257
+ expect(providers).toEqual({});
258
+ });
259
+
260
+ it('should handle Providers section without bracket notation', () => {
261
+ const content = `
262
+ ### Providers
263
+
264
+ #### test
265
+ Action: TEST_ACTION
266
+ `;
267
+
268
+ const providers = parseToolProviders(content);
269
+ expect(providers).toHaveProperty('test');
270
+ });
271
+ });
272
+
273
+ describe('ToolDocument Integration', () => {
274
+ it('should parse complete tool document', () => {
275
+ const content = `
276
+ ---
277
+ Name: GmailTools
278
+ Type: [Tool]
279
+ Description: Gmail integration tools for email management.
280
+ Provider: composio
281
+ ---
282
+
283
+ # [Imports]
284
+
285
+ [Tool]: ../core/tool.busy.md
286
+
287
+ # [Tools]
288
+
289
+ ## send_email
290
+
291
+ Send an email message.
292
+
293
+ ### [Inputs]
294
+ - to: Recipient
295
+ - subject: Subject
296
+ - body: Body
297
+
298
+ ### [Outputs]
299
+ - message_id: Sent message ID
300
+
301
+ ### [Providers]
302
+
303
+ #### composio
304
+ Action: GMAIL_SEND_EMAIL
305
+ Parameters:
306
+ to: to
307
+ subject: subject
308
+ body: body
309
+
310
+ ## list_emails
311
+
312
+ List inbox emails.
313
+
314
+ ### [Inputs]
315
+ - limit: Max count
316
+
317
+ ### [Outputs]
318
+ - emails: Email list
319
+
320
+ ### [Providers]
321
+
322
+ #### composio
323
+ Action: GMAIL_LIST_EMAILS
324
+ Parameters:
325
+ max_results: limit
326
+ `;
327
+
328
+ // This would be parsed by parseDocument and return a ToolDocument
329
+ const tools = parseTools(content);
330
+ expect(tools).toHaveLength(2);
331
+
332
+ const sendEmail = tools.find((t) => t.name === 'send_email');
333
+ expect(sendEmail?.providers?.composio?.action).toBe('GMAIL_SEND_EMAIL');
334
+
335
+ const listEmails = tools.find((t) => t.name === 'list_emails');
336
+ expect(listEmails?.providers?.composio?.action).toBe('GMAIL_LIST_EMAILS');
337
+ });
338
+ });
339
+
340
+ describe('Tool Provider Parameter Mapping', () => {
341
+ it('should support direct parameter mapping', () => {
342
+ const content = `
343
+ #### provider
344
+ Action: ACTION
345
+ Parameters:
346
+ api_to: to
347
+ api_subject: subject
348
+ `;
349
+
350
+ const providers = parseToolProviders(content);
351
+ expect(providers.provider.parameters).toEqual({
352
+ api_to: 'to',
353
+ api_subject: 'subject',
354
+ });
355
+ });
356
+
357
+ it('should support nested parameter values', () => {
358
+ const content = `
359
+ #### provider
360
+ Action: ACTION
361
+ Parameters:
362
+ recipient:
363
+ email: to
364
+ name: sender_name
365
+ content:
366
+ subject: subject
367
+ body: body
368
+ `;
369
+
370
+ const providers = parseToolProviders(content);
371
+ expect(providers.provider.parameters).toEqual({
372
+ recipient: { email: 'to', name: 'sender_name' },
373
+ content: { subject: 'subject', body: 'body' },
374
+ });
375
+ });
376
+ });
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Trigger Parsing Tests - Match busy-python Trigger model
3
+ *
4
+ * busy-python supports two trigger formats:
5
+ * 1. Time-based (alarm): "Set alarm for <time> to run <Operation>"
6
+ * 2. Event-based: "When <event> [from <filter>], run <Operation>"
7
+ *
8
+ * Triggers can appear in:
9
+ * - # [Triggers] section as bullet points
10
+ * - Frontmatter as a Triggers array
11
+ */
12
+
13
+ import { describe, it, expect } from 'vitest';
14
+ import { parseTriggers, parseTriggerDeclaration, parseTimeSpec } from '../parsers/triggers';
15
+ import type { Trigger } from '../types/schema';
16
+
17
+ describe('parseTriggerDeclaration', () => {
18
+ describe('Alarm Triggers (Time-based)', () => {
19
+ it('should parse simple alarm trigger', () => {
20
+ const text = 'Set alarm for 6am to run DailyReview';
21
+
22
+ const trigger = parseTriggerDeclaration(text);
23
+ expect(trigger.triggerType).toBe('alarm');
24
+ expect(trigger.operation).toBe('DailyReview');
25
+ expect(trigger.rawText).toBe(text);
26
+ });
27
+
28
+ it('should convert time to cron expression', () => {
29
+ const text = 'Set alarm for 6am each morning to run DailyReview';
30
+
31
+ const trigger = parseTriggerDeclaration(text);
32
+ expect(trigger.schedule).toBe('0 6 * * *');
33
+ });
34
+
35
+ it('should handle PM times', () => {
36
+ const text = 'Set alarm for 3pm to run AfternoonCheck';
37
+
38
+ const trigger = parseTriggerDeclaration(text);
39
+ expect(trigger.schedule).toBe('0 15 * * *');
40
+ });
41
+
42
+ it('should handle 12-hour format edge cases', () => {
43
+ const trigger12am = parseTriggerDeclaration('Set alarm for 12am to run Midnight');
44
+ expect(trigger12am.schedule).toBe('0 0 * * *');
45
+
46
+ const trigger12pm = parseTriggerDeclaration('Set alarm for 12pm to run Noon');
47
+ expect(trigger12pm.schedule).toBe('0 12 * * *');
48
+ });
49
+
50
+ it('should handle weekly schedules', () => {
51
+ const text = 'Set alarm for 9am on Monday to run WeeklySync';
52
+
53
+ const trigger = parseTriggerDeclaration(text);
54
+ expect(trigger.schedule).toBe('0 9 * * 1');
55
+ });
56
+
57
+ it('should handle multiple days', () => {
58
+ const text = 'Set alarm for 8am on Monday, Wednesday, Friday to run StandupReminder';
59
+
60
+ const trigger = parseTriggerDeclaration(text);
61
+ expect(trigger.schedule).toBe('0 8 * * 1,3,5');
62
+ });
63
+ });
64
+
65
+ describe('Event Triggers', () => {
66
+ it('should parse simple event trigger', () => {
67
+ const text = 'When gmail.message.received, run ProcessEmail';
68
+
69
+ const trigger = parseTriggerDeclaration(text);
70
+ expect(trigger.triggerType).toBe('event');
71
+ expect(trigger.eventType).toBe('gmail.message.received');
72
+ expect(trigger.operation).toBe('ProcessEmail');
73
+ });
74
+
75
+ it('should parse event trigger with filter', () => {
76
+ const text = 'When gmail.message.received from *@lead.com, run ProcessLead';
77
+
78
+ const trigger = parseTriggerDeclaration(text);
79
+ expect(trigger.triggerType).toBe('event');
80
+ expect(trigger.eventType).toBe('gmail.message.received');
81
+ expect(trigger.filter).toEqual({ from: '*@lead.com' });
82
+ expect(trigger.operation).toBe('ProcessLead');
83
+ });
84
+
85
+ it('should handle various event types', () => {
86
+ const events = [
87
+ 'When slack.message.posted, run NotifyTeam',
88
+ 'When github.pr.opened, run ReviewPR',
89
+ 'When calendar.event.created, run ScheduleReminder',
90
+ ];
91
+
92
+ for (const text of events) {
93
+ const trigger = parseTriggerDeclaration(text);
94
+ expect(trigger.triggerType).toBe('event');
95
+ expect(trigger.eventType).not.toBeUndefined();
96
+ expect(trigger.operation).not.toBeUndefined();
97
+ }
98
+ });
99
+
100
+ it('should handle complex filter patterns', () => {
101
+ const text = 'When email.received from *@company.com, run InternalEmail';
102
+
103
+ const trigger = parseTriggerDeclaration(text);
104
+ expect(trigger.filter?.from).toBe('*@company.com');
105
+ });
106
+ });
107
+
108
+ describe('queueWhenPaused', () => {
109
+ it('should default to true', () => {
110
+ const trigger = parseTriggerDeclaration('When event.test, run TestOp');
111
+ expect(trigger.queueWhenPaused).toBe(true);
112
+ });
113
+ });
114
+ });
115
+
116
+ describe('parseTimeSpec', () => {
117
+ it('should parse simple hour AM', () => {
118
+ expect(parseTimeSpec('6am')).toBe('0 6 * * *');
119
+ expect(parseTimeSpec('9am')).toBe('0 9 * * *');
120
+ expect(parseTimeSpec('11am')).toBe('0 11 * * *');
121
+ });
122
+
123
+ it('should parse simple hour PM', () => {
124
+ expect(parseTimeSpec('1pm')).toBe('0 13 * * *');
125
+ expect(parseTimeSpec('6pm')).toBe('0 18 * * *');
126
+ expect(parseTimeSpec('11pm')).toBe('0 23 * * *');
127
+ });
128
+
129
+ it('should handle 12am and 12pm', () => {
130
+ expect(parseTimeSpec('12am')).toBe('0 0 * * *');
131
+ expect(parseTimeSpec('12pm')).toBe('0 12 * * *');
132
+ });
133
+
134
+ it('should handle times with "each morning"', () => {
135
+ expect(parseTimeSpec('6am each morning')).toBe('0 6 * * *');
136
+ });
137
+
138
+ it('should handle times with "every day"', () => {
139
+ expect(parseTimeSpec('3pm every day')).toBe('0 15 * * *');
140
+ });
141
+
142
+ it('should handle weekday specifications', () => {
143
+ expect(parseTimeSpec('9am on Monday')).toBe('0 9 * * 1');
144
+ expect(parseTimeSpec('9am on Tuesday')).toBe('0 9 * * 2');
145
+ expect(parseTimeSpec('9am on Wednesday')).toBe('0 9 * * 3');
146
+ expect(parseTimeSpec('9am on Thursday')).toBe('0 9 * * 4');
147
+ expect(parseTimeSpec('9am on Friday')).toBe('0 9 * * 5');
148
+ expect(parseTimeSpec('9am on Saturday')).toBe('0 9 * * 6');
149
+ expect(parseTimeSpec('9am on Sunday')).toBe('0 9 * * 0');
150
+ });
151
+
152
+ it('should handle multiple weekdays', () => {
153
+ expect(parseTimeSpec('8am on Monday, Wednesday, Friday')).toBe('0 8 * * 1,3,5');
154
+ expect(parseTimeSpec('10am on Tuesday, Thursday')).toBe('0 10 * * 2,4');
155
+ });
156
+
157
+ it('should handle weekdays modifier', () => {
158
+ expect(parseTimeSpec('9am on weekdays')).toBe('0 9 * * 1-5');
159
+ });
160
+
161
+ it('should handle weekends modifier', () => {
162
+ expect(parseTimeSpec('10am on weekends')).toBe('0 10 * * 0,6');
163
+ });
164
+ });
165
+
166
+ describe('parseTriggers', () => {
167
+ it('should parse triggers from Triggers section', () => {
168
+ const content = `
169
+ # [Triggers]
170
+
171
+ - Set alarm for 6am to run DailyReview
172
+ - When gmail.message.received from *@lead.com, run ProcessLead
173
+ `;
174
+
175
+ const triggers = parseTriggers(content);
176
+ expect(triggers).toHaveLength(2);
177
+ expect(triggers[0].triggerType).toBe('alarm');
178
+ expect(triggers[1].triggerType).toBe('event');
179
+ });
180
+
181
+ it('should parse triggers from frontmatter', () => {
182
+ const content = `
183
+ ---
184
+ Name: AutomatedDoc
185
+ Type: [Document]
186
+ Description: Document with triggers
187
+ Triggers:
188
+ - event_type: gmail.message.received
189
+ filter:
190
+ from: "*@important.com"
191
+ operation: ProcessImportant
192
+ queue_when_paused: false
193
+ ---
194
+
195
+ # Content
196
+ `;
197
+
198
+ const triggers = parseTriggers(content);
199
+ expect(triggers).toHaveLength(1);
200
+ expect(triggers[0].eventType).toBe('gmail.message.received');
201
+ expect(triggers[0].filter).toEqual({ from: '*@important.com' });
202
+ expect(triggers[0].queueWhenPaused).toBe(false);
203
+ });
204
+
205
+ it('should combine frontmatter and section triggers', () => {
206
+ const content = `
207
+ ---
208
+ Name: Mixed
209
+ Type: [Document]
210
+ Description: Mixed triggers
211
+ Triggers:
212
+ - event_type: webhook.received
213
+ operation: HandleWebhook
214
+ ---
215
+
216
+ # [Triggers]
217
+
218
+ - Set alarm for 9am to run MorningTask
219
+ `;
220
+
221
+ const triggers = parseTriggers(content);
222
+ expect(triggers).toHaveLength(2);
223
+ });
224
+
225
+ it('should return empty array for document without triggers', () => {
226
+ const content = `
227
+ ---
228
+ Name: NoTriggers
229
+ Type: [Document]
230
+ Description: No triggers here
231
+ ---
232
+
233
+ # [Operations]
234
+
235
+ ## SomeOp
236
+
237
+ ### [Steps]
238
+ 1. Do something
239
+ `;
240
+
241
+ const triggers = parseTriggers(content);
242
+ expect(triggers).toHaveLength(0);
243
+ });
244
+
245
+ it('should handle Triggers section without bracket notation', () => {
246
+ const content = `
247
+ # Triggers
248
+
249
+ - When test.event, run TestOp
250
+ `;
251
+
252
+ const triggers = parseTriggers(content);
253
+ expect(triggers).toHaveLength(1);
254
+ });
255
+
256
+ it('should parse frontmatter alarm triggers', () => {
257
+ const content = `
258
+ ---
259
+ Name: Scheduled
260
+ Type: [Document]
261
+ Description: Scheduled document
262
+ Triggers:
263
+ - schedule: "0 6 * * *"
264
+ operation: DailyTask
265
+ ---
266
+ `;
267
+
268
+ const triggers = parseTriggers(content);
269
+ expect(triggers).toHaveLength(1);
270
+ expect(triggers[0].triggerType).toBe('alarm');
271
+ expect(triggers[0].schedule).toBe('0 6 * * *');
272
+ });
273
+ });
274
+
275
+ describe('Trigger Format Validation', () => {
276
+ it('should match busy-python alarm pattern', () => {
277
+ // busy-python pattern: r"(?i)set\s+alarm\s+for\s+(.+?)\s+to\s+run\s+(\w+)"
278
+ const pattern = /(?:set\s+alarm\s+for\s+)(.+?)\s+to\s+run\s+(\w+)/i;
279
+
280
+ const testCases = [
281
+ { input: 'Set alarm for 6am to run DailyReview', time: '6am', op: 'DailyReview' },
282
+ { input: 'set alarm for 3pm each day to run Check', time: '3pm each day', op: 'Check' },
283
+ { input: 'SET ALARM FOR 9am on Monday to run Weekly', time: '9am on Monday', op: 'Weekly' },
284
+ ];
285
+
286
+ for (const { input, time, op } of testCases) {
287
+ const match = pattern.exec(input);
288
+ expect(match).not.toBeNull();
289
+ expect(match?.[1]).toBe(time);
290
+ expect(match?.[2]).toBe(op);
291
+ }
292
+ });
293
+
294
+ it('should match busy-python event pattern', () => {
295
+ // busy-python pattern: r"(?i)when\s+([\w.]+)(?:\s+from\s+(.+?))?,\s*run\s+(\w+)"
296
+ const pattern = /(?:when\s+)([\w.]+)(?:\s+from\s+(.+?))?,\s*run\s+(\w+)/i;
297
+
298
+ const testCases = [
299
+ { input: 'When gmail.message.received, run Process', event: 'gmail.message.received', filter: undefined, op: 'Process' },
300
+ { input: 'When email.sent from *@test.com, run Log', event: 'email.sent', filter: '*@test.com', op: 'Log' },
301
+ { input: 'WHEN slack.posted, RUN Notify', event: 'slack.posted', filter: undefined, op: 'Notify' },
302
+ ];
303
+
304
+ for (const { input, event, filter, op } of testCases) {
305
+ const match = pattern.exec(input);
306
+ expect(match).not.toBeNull();
307
+ expect(match?.[1]).toBe(event);
308
+ expect(match?.[2]).toBe(filter);
309
+ expect(match?.[3]).toBe(op);
310
+ }
311
+ });
312
+ });