@qikdev/mcp 6.7.5 → 6.7.7

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,1047 @@
1
+ import { ConfigManager } from "../config.js";
2
+ import { getUserSessionData, getAvailableScopes, getScopeTitle } from "./user.js";
3
+ // ============================================================================
4
+ // CONSTANTS
5
+ // ============================================================================
6
+ const STEP_COLORS = [
7
+ '#3B82F6', // Blue
8
+ '#10B981', // Green
9
+ '#F59E0B', // Amber
10
+ '#EF4444', // Red
11
+ '#8B5CF6', // Purple
12
+ '#EC4899', // Pink
13
+ '#06B6D4', // Cyan
14
+ '#F97316', // Orange
15
+ ];
16
+ // ============================================================================
17
+ // TOOL DEFINITIONS
18
+ // ============================================================================
19
+ export const listWorkflowsTool = {
20
+ name: "list_workflows",
21
+ description: `List workflow definitions in the platform.
22
+
23
+ Workflows are kanban-style boards where cards move through steps with automation.
24
+
25
+ **Example:**
26
+ \`\`\`json
27
+ { "search": "order" }
28
+ \`\`\`
29
+
30
+ Returns workflows matching "order" with their IDs and structure summary.`,
31
+ inputSchema: {
32
+ type: "object",
33
+ properties: {
34
+ search: {
35
+ type: "string",
36
+ description: "Search term to filter workflows by title"
37
+ },
38
+ category: {
39
+ type: "string",
40
+ description: "Filter by category"
41
+ },
42
+ page: {
43
+ type: "object",
44
+ properties: {
45
+ size: { type: "number", description: "Items per page (default: 20)" },
46
+ index: { type: "number", description: "Page number, 1-based (default: 1)" }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ };
52
+ export const getWorkflowTool = {
53
+ name: "get_workflow",
54
+ description: `Get full workflow details including all columns, steps, and automation.
55
+
56
+ **IMPORTANT:** Use this before updating a workflow to understand its current structure.
57
+
58
+ Returns:
59
+ - Columns with their steps
60
+ - Step configuration (assignees, due dates, requirements)
61
+ - Automation functions (entry, exit, success, fail)
62
+ - Navigation rules (success/failure actions)`,
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ workflowId: {
67
+ type: "string",
68
+ description: "The workflow definition ID"
69
+ },
70
+ workflowKey: {
71
+ type: "string",
72
+ description: "The workflow key (e.g., 'orderWorkflow')"
73
+ },
74
+ workflowTitle: {
75
+ type: "string",
76
+ description: "Search by workflow title"
77
+ }
78
+ }
79
+ }
80
+ };
81
+ export const createWorkflowTool = {
82
+ name: "create_workflow",
83
+ description: `Create a new workflow definition.
84
+
85
+ A workflow is a kanban board with columns and steps. Cards move through steps based on completion criteria and automated actions.
86
+
87
+ ## Workflow Structure
88
+
89
+ \`\`\`json
90
+ {
91
+ "title": "Order Processing",
92
+ "category": "Operations",
93
+ "workflow": [
94
+ {
95
+ "title": "Column Name",
96
+ "description": "Optional description",
97
+ "steps": [
98
+ {
99
+ "title": "Step Name",
100
+ "key": "uniqueStepKey",
101
+ "color": "#3B82F6",
102
+ "description": "Help text for users",
103
+
104
+ "assignees": [],
105
+ "assigneeBehaviour": "",
106
+
107
+ "dueDateBehaviour": "period",
108
+ "dueDateQuantity": 3,
109
+ "dueDateUnit": "days",
110
+
111
+ "hasRequiredTasks": true,
112
+ "requireTasks": [{ "title": "Review application" }],
113
+
114
+ "completionWhen": "immediate",
115
+
116
+ "successFunctions": [
117
+ { "action": "addTag", "title": "Mark processed", "tag": "processed" }
118
+ ],
119
+
120
+ "successAction": "next",
121
+ "failureAction": "step",
122
+ "failureTargetStep": "rejected"
123
+ }
124
+ ]
125
+ }
126
+ ]
127
+ }
128
+ \`\`\`
129
+
130
+ ## Step Properties
131
+
132
+ **Assignees:**
133
+ - \`assignees\`: Array of profile IDs to auto-assign
134
+ - \`assigneeBehaviour\`: "" (replace), "leave" (keep existing), "remove" (clear all)
135
+
136
+ **Due Dates:**
137
+ - \`dueDateBehaviour\`: "" (none), "period" (relative), "point" (fixed time)
138
+ - \`dueDateQuantity\`: Number (e.g., 3)
139
+ - \`dueDateUnit\`: "minutes", "hours", "days", "weeks"
140
+
141
+ **Requirements:**
142
+ - \`hasRequiredTasks\`: true/false
143
+ - \`requireTasks\`: [{ title, description }]
144
+ - \`hasRequiredForms\`: true/false (requires form submission)
145
+ - \`hasRequiredCriteria\`: true/false (requires filter match)
146
+
147
+ **Automation Timing:**
148
+ - \`completionWhen\`: "" (manual), "immediate" (auto when done), "wait" (at due date)
149
+
150
+ **Automation Functions:**
151
+ - \`entryFunctions\`: Run when card enters step
152
+ - \`exitFunctions\`: Run when card leaves step
153
+ - \`successFunctions\`: Run on successful completion
154
+ - \`failFunctions\`: Run on failure
155
+
156
+ **Navigation:**
157
+ - \`successAction\`: "next", "previous", "step", "archive"
158
+ - \`successTargetStep\`: Step key if successAction="step"
159
+ - \`failureAction\`: Same options
160
+ - \`failureTargetStep\`: Step key if failureAction="step"
161
+
162
+ ## Function Types
163
+
164
+ **addTag** - Add a tag to the card
165
+ \`\`\`json
166
+ { "action": "addTag", "title": "Description", "tag": "tagName" }
167
+ \`\`\`
168
+
169
+ **removeTag** - Remove a tag
170
+ \`\`\`json
171
+ { "action": "removeTag", "title": "Description", "tag": "tagName" }
172
+ \`\`\`
173
+
174
+ **email** - Send email notification
175
+ \`\`\`json
176
+ { "action": "email", "title": "Description", "to": "assignees" }
177
+ \`\`\`
178
+
179
+ **sms** - Send SMS
180
+ \`\`\`json
181
+ { "action": "sms", "title": "Description", "to": "assignees", "message": "Text" }
182
+ \`\`\`
183
+
184
+ **update** - Update card/profile fields
185
+ \`\`\`json
186
+ { "action": "update", "title": "Description", "target": "card", "data": { "fieldKey": "value" } }
187
+ \`\`\`
188
+
189
+ ## Common Patterns
190
+
191
+ **Simple Order Flow:**
192
+ \`\`\`json
193
+ {
194
+ "workflow": [
195
+ { "title": "New", "steps": [{ "title": "Order Received" }] },
196
+ { "title": "Processing", "steps": [{ "title": "Preparing" }] },
197
+ { "title": "Complete", "steps": [{ "title": "Ready", "successAction": "archive" }] }
198
+ ]
199
+ }
200
+ \`\`\`
201
+
202
+ **Approval Flow:**
203
+ \`\`\`json
204
+ {
205
+ "workflow": [
206
+ { "title": "Submitted", "steps": [{ "title": "New Application" }] },
207
+ { "title": "Review", "steps": [{ "title": "Under Review", "hasRequiredTasks": true, "requireTasks": [{ "title": "Review documents" }] }] },
208
+ { "title": "Decision", "steps": [{ "title": "Approved", "successFunctions": [{ "action": "addTag", "tag": "approved" }] }, { "title": "Rejected" }] }
209
+ ]
210
+ }
211
+ \`\`\``,
212
+ inputSchema: {
213
+ type: "object",
214
+ properties: {
215
+ title: {
216
+ type: "string",
217
+ description: "Workflow name (e.g., 'Food Order Workflow')"
218
+ },
219
+ plural: {
220
+ type: "string",
221
+ description: "Plural name (auto-generated if not provided)"
222
+ },
223
+ category: {
224
+ type: "string",
225
+ description: "Category for organization (e.g., 'Operations')"
226
+ },
227
+ workflow: {
228
+ type: "array",
229
+ description: "Array of columns, each containing steps",
230
+ items: {
231
+ type: "object",
232
+ properties: {
233
+ title: { type: "string" },
234
+ description: { type: "string" },
235
+ steps: { type: "array" }
236
+ },
237
+ required: ["title", "steps"]
238
+ }
239
+ },
240
+ scope: {
241
+ type: "string",
242
+ description: "Scope ID to create the workflow in"
243
+ }
244
+ },
245
+ required: ["title", "workflow"]
246
+ }
247
+ };
248
+ export const updateWorkflowTool = {
249
+ name: "update_workflow",
250
+ description: `Update an existing workflow definition.
251
+
252
+ **IMPORTANT:** Use get_workflow first to see the current structure before making changes.
253
+
254
+ ## Update Operations
255
+
256
+ **Add a column:**
257
+ \`\`\`json
258
+ {
259
+ "workflowId": "...",
260
+ "addColumns": [{ "title": "Archive", "steps": [{ "title": "Archived" }] }]
261
+ }
262
+ \`\`\`
263
+
264
+ **Add a step to existing column:**
265
+ \`\`\`json
266
+ {
267
+ "workflowId": "...",
268
+ "addSteps": [{ "columnTitle": "Processing", "step": { "title": "Quality Check" } }]
269
+ }
270
+ \`\`\`
271
+
272
+ **Modify a step:**
273
+ \`\`\`json
274
+ {
275
+ "workflowId": "...",
276
+ "updateSteps": [{
277
+ "stepKey": "newOrder",
278
+ "changes": {
279
+ "title": "New Order Received",
280
+ "successFunctions": [{ "action": "addTag", "tag": "received" }]
281
+ }
282
+ }]
283
+ }
284
+ \`\`\`
285
+
286
+ **Remove a step:**
287
+ \`\`\`json
288
+ {
289
+ "workflowId": "...",
290
+ "removeSteps": ["oldStepKey"]
291
+ }
292
+ \`\`\`
293
+
294
+ **Add automation to a step:**
295
+ \`\`\`json
296
+ {
297
+ "workflowId": "...",
298
+ "updateSteps": [{
299
+ "stepKey": "approved",
300
+ "changes": {
301
+ "successFunctions": [
302
+ { "action": "addTag", "title": "Add confirmed tag", "tag": "confirmed" }
303
+ ]
304
+ }
305
+ }]
306
+ }
307
+ \`\`\`
308
+
309
+ **Change navigation:**
310
+ \`\`\`json
311
+ {
312
+ "workflowId": "...",
313
+ "updateSteps": [{
314
+ "stepKey": "review",
315
+ "changes": {
316
+ "successAction": "step",
317
+ "successTargetStep": "approved",
318
+ "failureAction": "step",
319
+ "failureTargetStep": "rejected"
320
+ }
321
+ }]
322
+ }
323
+ \`\`\``,
324
+ inputSchema: {
325
+ type: "object",
326
+ properties: {
327
+ workflowId: {
328
+ type: "string",
329
+ description: "The workflow definition ID"
330
+ },
331
+ workflowKey: {
332
+ type: "string",
333
+ description: "The workflow key"
334
+ },
335
+ workflowTitle: {
336
+ type: "string",
337
+ description: "Search by workflow title"
338
+ },
339
+ title: {
340
+ type: "string",
341
+ description: "New title for the workflow"
342
+ },
343
+ category: {
344
+ type: "string",
345
+ description: "New category"
346
+ },
347
+ addColumns: {
348
+ type: "array",
349
+ description: "New columns to add",
350
+ items: {
351
+ type: "object",
352
+ properties: {
353
+ title: { type: "string" },
354
+ description: { type: "string" },
355
+ steps: { type: "array" },
356
+ insertAfter: { type: "string", description: "Column title to insert after" }
357
+ }
358
+ }
359
+ },
360
+ removeColumns: {
361
+ type: "array",
362
+ items: { type: "string" },
363
+ description: "Column titles to remove"
364
+ },
365
+ addSteps: {
366
+ type: "array",
367
+ description: "Steps to add to existing columns",
368
+ items: {
369
+ type: "object",
370
+ properties: {
371
+ columnTitle: { type: "string", description: "Column to add step to" },
372
+ step: { type: "object", description: "Step configuration" },
373
+ insertAfter: { type: "string", description: "Step key to insert after" }
374
+ }
375
+ }
376
+ },
377
+ removeSteps: {
378
+ type: "array",
379
+ items: { type: "string" },
380
+ description: "Step keys to remove"
381
+ },
382
+ updateSteps: {
383
+ type: "array",
384
+ description: "Steps to modify",
385
+ items: {
386
+ type: "object",
387
+ properties: {
388
+ stepKey: { type: "string", description: "Key of step to update" },
389
+ changes: { type: "object", description: "Fields to update" }
390
+ }
391
+ }
392
+ }
393
+ }
394
+ }
395
+ };
396
+ // ============================================================================
397
+ // HANDLERS
398
+ // ============================================================================
399
+ export async function handleListWorkflows(args) {
400
+ try {
401
+ const configManager = new ConfigManager();
402
+ const config = await configManager.loadConfig();
403
+ if (!config) {
404
+ throw new Error('Qik MCP server not configured. Run setup first.');
405
+ }
406
+ const filters = [
407
+ { key: 'definesType', comparator: 'equal', value: 'workflowcard' }
408
+ ];
409
+ if (args.search) {
410
+ filters.push({
411
+ operator: 'or',
412
+ filters: [
413
+ { key: 'title', comparator: 'contains', value: args.search },
414
+ { key: 'key', comparator: 'contains', value: args.search }
415
+ ]
416
+ });
417
+ }
418
+ if (args.category) {
419
+ filters.push({ key: 'category', comparator: 'contains', value: args.category });
420
+ }
421
+ const listPayload = {
422
+ filter: { operator: 'and', filters },
423
+ sort: { key: 'title', direction: 'asc', type: 'string' },
424
+ page: {
425
+ size: args.page?.size || 20,
426
+ index: args.page?.index || 1
427
+ },
428
+ select: ['_id', 'title', 'plural', 'key', 'category', 'workflow', 'meta']
429
+ };
430
+ const response = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/content/definition/list`, {
431
+ method: 'POST',
432
+ headers: {
433
+ 'Authorization': `Bearer ${config.accessToken}`,
434
+ 'Content-Type': 'application/json',
435
+ },
436
+ body: JSON.stringify(listPayload)
437
+ });
438
+ if (!response.ok) {
439
+ const errorText = await response.text();
440
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
441
+ }
442
+ const results = await response.json();
443
+ const items = results.items || [];
444
+ const total = results.total || items.length;
445
+ const currentPage = args.page?.index || 1;
446
+ const pageSize = args.page?.size || 20;
447
+ if (items.length === 0) {
448
+ const searchNote = args.search ? ` matching "${args.search}"` : '';
449
+ return {
450
+ content: [{
451
+ type: "text",
452
+ text: `No workflows found${searchNote}.`
453
+ }]
454
+ };
455
+ }
456
+ let resultText = `# Workflows (${total} total)`;
457
+ if (total > pageSize) {
458
+ const totalPages = Math.ceil(total / pageSize);
459
+ resultText += ` - Page ${currentPage} of ${totalPages}`;
460
+ }
461
+ resultText += '\n\n';
462
+ items.forEach((item, index) => {
463
+ const itemNumber = ((currentPage - 1) * pageSize) + index + 1;
464
+ const columnCount = item.workflow?.length || 0;
465
+ const stepCount = item.workflow?.reduce((acc, col) => acc + (col.steps?.length || 0), 0) || 0;
466
+ resultText += `**${itemNumber}. ${item.title}**\n`;
467
+ resultText += ` ID: \`${item._id}\`\n`;
468
+ resultText += ` Key: \`${item.key}\`\n`;
469
+ if (item.category) {
470
+ resultText += ` Category: ${item.category}\n`;
471
+ }
472
+ resultText += ` Structure: ${columnCount} columns, ${stepCount} steps\n\n`;
473
+ });
474
+ if (total > pageSize && currentPage < Math.ceil(total / pageSize)) {
475
+ resultText += `\nTo see more: use page: { index: ${currentPage + 1} }`;
476
+ }
477
+ return {
478
+ content: [{
479
+ type: "text",
480
+ text: resultText
481
+ }]
482
+ };
483
+ }
484
+ catch (error) {
485
+ return {
486
+ content: [{
487
+ type: "text",
488
+ text: `Failed to list workflows: ${error.message}`
489
+ }]
490
+ };
491
+ }
492
+ }
493
+ export async function handleGetWorkflow(args) {
494
+ try {
495
+ const configManager = new ConfigManager();
496
+ const config = await configManager.loadConfig();
497
+ if (!config) {
498
+ throw new Error('Qik MCP server not configured. Run setup first.');
499
+ }
500
+ let workflowId = args.workflowId;
501
+ // Resolve ID from key or title
502
+ if (!workflowId && (args.workflowKey || args.workflowTitle)) {
503
+ const searchFilters = [
504
+ { key: 'definesType', comparator: 'equal', value: 'workflowcard' }
505
+ ];
506
+ if (args.workflowKey) {
507
+ searchFilters.push({ key: 'key', comparator: 'equal', value: args.workflowKey });
508
+ }
509
+ else if (args.workflowTitle) {
510
+ searchFilters.push({ key: 'title', comparator: 'contains', value: args.workflowTitle });
511
+ }
512
+ const searchResponse = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/content/definition/list`, {
513
+ method: 'POST',
514
+ headers: {
515
+ 'Authorization': `Bearer ${config.accessToken}`,
516
+ 'Content-Type': 'application/json',
517
+ },
518
+ body: JSON.stringify({
519
+ filter: { operator: 'and', filters: searchFilters },
520
+ page: { size: 1, index: 1 },
521
+ select: ['_id', 'title']
522
+ })
523
+ });
524
+ if (!searchResponse.ok) {
525
+ throw new Error(`HTTP ${searchResponse.status}: ${await searchResponse.text()}`);
526
+ }
527
+ const searchResults = await searchResponse.json();
528
+ if (!searchResults.items?.length) {
529
+ const searchTerm = args.workflowKey || args.workflowTitle;
530
+ return {
531
+ content: [{
532
+ type: "text",
533
+ text: `No workflow found matching "${searchTerm}"`
534
+ }]
535
+ };
536
+ }
537
+ workflowId = searchResults.items[0]._id;
538
+ }
539
+ if (!workflowId) {
540
+ return {
541
+ content: [{
542
+ type: "text",
543
+ text: `Please provide workflowId, workflowKey, or workflowTitle`
544
+ }]
545
+ };
546
+ }
547
+ const response = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/content/${workflowId}`, {
548
+ headers: {
549
+ 'Authorization': `Bearer ${config.accessToken}`,
550
+ 'Content-Type': 'application/json',
551
+ }
552
+ });
553
+ if (!response.ok) {
554
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
555
+ }
556
+ const workflow = await response.json();
557
+ let resultText = `# Workflow: ${workflow.title}\n\n`;
558
+ resultText += `**ID:** \`${workflow._id}\`\n`;
559
+ resultText += `**Key:** \`${workflow.key}\`\n`;
560
+ if (workflow.category) {
561
+ resultText += `**Category:** ${workflow.category}\n`;
562
+ }
563
+ resultText += '\n---\n';
564
+ resultText += formatWorkflowForDisplay(workflow.workflow || []);
565
+ resultText += '\n---\n\n## Raw Structure\n\n';
566
+ resultText += '```json\n' + JSON.stringify(workflow.workflow, null, 2) + '\n```';
567
+ return {
568
+ content: [{
569
+ type: "text",
570
+ text: resultText
571
+ }]
572
+ };
573
+ }
574
+ catch (error) {
575
+ return {
576
+ content: [{
577
+ type: "text",
578
+ text: `Failed to get workflow: ${error.message}`
579
+ }]
580
+ };
581
+ }
582
+ }
583
+ export async function handleCreateWorkflow(args) {
584
+ try {
585
+ const configManager = new ConfigManager();
586
+ const config = await configManager.loadConfig();
587
+ if (!config) {
588
+ throw new Error('Qik MCP server not configured. Run setup first.');
589
+ }
590
+ if (!args.title) {
591
+ return createErrorResponse('Title is required');
592
+ }
593
+ if (!args.workflow || args.workflow.length === 0) {
594
+ return createErrorResponse('Workflow must have at least one column');
595
+ }
596
+ // Get user session for permissions
597
+ const userSession = await getUserSessionData();
598
+ const createPermission = 'definition.create';
599
+ const availableScopes = getAvailableScopes(userSession, createPermission);
600
+ if (availableScopes.length === 0) {
601
+ return createErrorResponse(`You don't have permission to create definitions.`);
602
+ }
603
+ const selectedScopeId = args.scope;
604
+ if (!selectedScopeId && availableScopes.length > 1) {
605
+ return createScopeSelectionPrompt('workflow', availableScopes, userSession);
606
+ }
607
+ const targetScopeId = selectedScopeId || availableScopes[0];
608
+ if (!availableScopes.includes(targetScopeId)) {
609
+ return createErrorResponse(`Invalid scope "${targetScopeId}".`);
610
+ }
611
+ // Normalize and validate workflow
612
+ const normalizedWorkflow = normalizeWorkflow(args.workflow);
613
+ const validation = validateWorkflow(normalizedWorkflow);
614
+ if (!validation.valid) {
615
+ return createErrorResponse(`Invalid workflow structure:\n- ${validation.errors.join('\n- ')}`);
616
+ }
617
+ // Generate key from title
618
+ const key = generateKey(args.title);
619
+ const definitionPayload = {
620
+ title: args.title,
621
+ plural: args.plural || `${args.title}s`,
622
+ key: key,
623
+ definesType: 'workflowcard',
624
+ category: args.category || '',
625
+ workflow: normalizedWorkflow,
626
+ fields: [],
627
+ defaultScopes: [],
628
+ restrictScopes: [],
629
+ weight: 0,
630
+ meta: {
631
+ scopes: [targetScopeId],
632
+ status: 'active'
633
+ }
634
+ };
635
+ const response = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/content/definition/create`, {
636
+ method: 'POST',
637
+ headers: {
638
+ 'Authorization': `Bearer ${config.accessToken}`,
639
+ 'Content-Type': 'application/json',
640
+ },
641
+ body: JSON.stringify(definitionPayload)
642
+ });
643
+ if (!response.ok) {
644
+ const errorText = await response.text();
645
+ throw new Error(`HTTP ${response.status} - ${errorText}`);
646
+ }
647
+ const created = await response.json();
648
+ const columnCount = normalizedWorkflow.length;
649
+ const stepCount = normalizedWorkflow.reduce((acc, col) => acc + col.steps.length, 0);
650
+ return {
651
+ content: [{
652
+ type: "text",
653
+ text: `# Workflow Created Successfully!
654
+
655
+ **Title:** ${args.title}
656
+ **ID:** \`${created._id}\`
657
+ **Key:** \`${key}\`
658
+ **Category:** ${args.category || 'None'}
659
+ **Structure:** ${columnCount} columns, ${stepCount} steps
660
+ **Scope:** ${getScopeTitle(userSession, targetScopeId)}
661
+
662
+ ## Structure Overview
663
+ ${formatWorkflowForDisplay(normalizedWorkflow)}`
664
+ }]
665
+ };
666
+ }
667
+ catch (error) {
668
+ return createErrorResponse(`Failed to create workflow: ${error.message}`);
669
+ }
670
+ }
671
+ export async function handleUpdateWorkflow(args) {
672
+ try {
673
+ const configManager = new ConfigManager();
674
+ const config = await configManager.loadConfig();
675
+ if (!config) {
676
+ throw new Error('Qik MCP server not configured. Run setup first.');
677
+ }
678
+ // Resolve workflow ID
679
+ let workflowId = args.workflowId;
680
+ if (!workflowId && (args.workflowKey || args.workflowTitle)) {
681
+ const searchFilters = [
682
+ { key: 'definesType', comparator: 'equal', value: 'workflowcard' }
683
+ ];
684
+ if (args.workflowKey) {
685
+ searchFilters.push({ key: 'key', comparator: 'equal', value: args.workflowKey });
686
+ }
687
+ else if (args.workflowTitle) {
688
+ searchFilters.push({ key: 'title', comparator: 'contains', value: args.workflowTitle });
689
+ }
690
+ const searchResponse = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/content/definition/list`, {
691
+ method: 'POST',
692
+ headers: {
693
+ 'Authorization': `Bearer ${config.accessToken}`,
694
+ 'Content-Type': 'application/json',
695
+ },
696
+ body: JSON.stringify({
697
+ filter: { operator: 'and', filters: searchFilters },
698
+ page: { size: 1, index: 1 }
699
+ })
700
+ });
701
+ if (!searchResponse.ok) {
702
+ throw new Error(`HTTP ${searchResponse.status}`);
703
+ }
704
+ const searchResults = await searchResponse.json();
705
+ if (!searchResults.items?.length) {
706
+ return createErrorResponse(`No workflow found matching "${args.workflowKey || args.workflowTitle}"`);
707
+ }
708
+ workflowId = searchResults.items[0]._id;
709
+ }
710
+ if (!workflowId) {
711
+ return createErrorResponse('Please provide workflowId, workflowKey, or workflowTitle');
712
+ }
713
+ // Fetch current workflow
714
+ const getResponse = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/content/${workflowId}`, {
715
+ headers: {
716
+ 'Authorization': `Bearer ${config.accessToken}`,
717
+ 'Content-Type': 'application/json',
718
+ }
719
+ });
720
+ if (!getResponse.ok) {
721
+ throw new Error(`HTTP ${getResponse.status}`);
722
+ }
723
+ const current = await getResponse.json();
724
+ let workflow = [...(current.workflow || [])];
725
+ const changes = [];
726
+ // Update title/category
727
+ if (args.title) {
728
+ current.title = args.title;
729
+ changes.push(`Title changed to "${args.title}"`);
730
+ }
731
+ if (args.category) {
732
+ current.category = args.category;
733
+ changes.push(`Category changed to "${args.category}"`);
734
+ }
735
+ // Remove columns
736
+ if (args.removeColumns?.length) {
737
+ const originalCount = workflow.length;
738
+ workflow = workflow.filter(col => !args.removeColumns.includes(col.title));
739
+ changes.push(`Removed ${originalCount - workflow.length} column(s)`);
740
+ }
741
+ // Remove steps
742
+ if (args.removeSteps?.length) {
743
+ let removedCount = 0;
744
+ workflow = workflow.map(col => ({
745
+ ...col,
746
+ steps: col.steps.filter(step => {
747
+ if (args.removeSteps.includes(step.key)) {
748
+ removedCount++;
749
+ return false;
750
+ }
751
+ return true;
752
+ })
753
+ }));
754
+ changes.push(`Removed ${removedCount} step(s)`);
755
+ }
756
+ // Update steps
757
+ if (args.updateSteps?.length) {
758
+ for (const update of args.updateSteps) {
759
+ for (const col of workflow) {
760
+ const stepIndex = col.steps.findIndex(s => s.key === update.stepKey);
761
+ if (stepIndex !== -1) {
762
+ col.steps[stepIndex] = { ...col.steps[stepIndex], ...update.changes };
763
+ changes.push(`Updated step "${update.stepKey}"`);
764
+ break;
765
+ }
766
+ }
767
+ }
768
+ }
769
+ // Add columns
770
+ if (args.addColumns?.length) {
771
+ const existingKeys = getAllStepKeys(workflow);
772
+ for (const newCol of args.addColumns) {
773
+ const normalizedSteps = newCol.steps.map((step, i) => normalizeStep(step, existingKeys, workflow.length * 10 + i));
774
+ const column = {
775
+ title: newCol.title,
776
+ description: newCol.description || '',
777
+ steps: normalizedSteps
778
+ };
779
+ if (newCol.insertAfter) {
780
+ const insertIndex = workflow.findIndex(c => c.title === newCol.insertAfter);
781
+ if (insertIndex !== -1) {
782
+ workflow.splice(insertIndex + 1, 0, column);
783
+ }
784
+ else {
785
+ workflow.push(column);
786
+ }
787
+ }
788
+ else {
789
+ workflow.push(column);
790
+ }
791
+ changes.push(`Added column "${newCol.title}"`);
792
+ }
793
+ }
794
+ // Add steps
795
+ if (args.addSteps?.length) {
796
+ const existingKeys = getAllStepKeys(workflow);
797
+ for (const addStep of args.addSteps) {
798
+ const column = workflow.find(c => c.title === addStep.columnTitle);
799
+ if (column) {
800
+ const normalizedStep = normalizeStep(addStep.step, existingKeys, column.steps.length);
801
+ if (addStep.insertAfter) {
802
+ const insertIndex = column.steps.findIndex(s => s.key === addStep.insertAfter);
803
+ if (insertIndex !== -1) {
804
+ column.steps.splice(insertIndex + 1, 0, normalizedStep);
805
+ }
806
+ else {
807
+ column.steps.push(normalizedStep);
808
+ }
809
+ }
810
+ else {
811
+ column.steps.push(normalizedStep);
812
+ }
813
+ changes.push(`Added step "${normalizedStep.title}" to "${column.title}"`);
814
+ }
815
+ else {
816
+ changes.push(`Warning: Column "${addStep.columnTitle}" not found`);
817
+ }
818
+ }
819
+ }
820
+ // Validate final structure
821
+ const validation = validateWorkflow(workflow);
822
+ if (!validation.valid) {
823
+ return createErrorResponse(`Invalid workflow structure after changes:\n- ${validation.errors.join('\n- ')}`);
824
+ }
825
+ // Update
826
+ current.workflow = workflow;
827
+ const updateResponse = await fetch(`${config.apiUrl || 'https://api.qik.dev'}/content/${workflowId}`, {
828
+ method: 'PUT',
829
+ headers: {
830
+ 'Authorization': `Bearer ${config.accessToken}`,
831
+ 'Content-Type': 'application/json',
832
+ },
833
+ body: JSON.stringify(current)
834
+ });
835
+ if (!updateResponse.ok) {
836
+ const errorText = await updateResponse.text();
837
+ throw new Error(`HTTP ${updateResponse.status} - ${errorText}`);
838
+ }
839
+ const columnCount = workflow.length;
840
+ const stepCount = workflow.reduce((acc, col) => acc + col.steps.length, 0);
841
+ return {
842
+ content: [{
843
+ type: "text",
844
+ text: `# Workflow Updated Successfully!
845
+
846
+ **Title:** ${current.title}
847
+ **ID:** \`${workflowId}\`
848
+ **Structure:** ${columnCount} columns, ${stepCount} steps
849
+
850
+ ## Changes Made
851
+ ${changes.map(c => `- ${c}`).join('\n')}
852
+
853
+ ## Current Structure
854
+ ${formatWorkflowForDisplay(workflow)}`
855
+ }]
856
+ };
857
+ }
858
+ catch (error) {
859
+ return createErrorResponse(`Failed to update workflow: ${error.message}`);
860
+ }
861
+ }
862
+ // ============================================================================
863
+ // HELPER FUNCTIONS
864
+ // ============================================================================
865
+ function createErrorResponse(message) {
866
+ return {
867
+ content: [{
868
+ type: "text",
869
+ text: message
870
+ }]
871
+ };
872
+ }
873
+ function createScopeSelectionPrompt(contentType, availableScopes, userSession) {
874
+ const scopeOptions = availableScopes.map(scopeId => {
875
+ const title = getScopeTitle(userSession, scopeId);
876
+ return `- **${title}** (ID: \`${scopeId}\`)`;
877
+ }).join('\n');
878
+ return {
879
+ content: [{
880
+ type: "text",
881
+ text: `You can create this ${contentType} in multiple scopes. Please select one:
882
+
883
+ ${scopeOptions}
884
+
885
+ Call create_workflow again with the scope parameter.`
886
+ }]
887
+ };
888
+ }
889
+ function generateKey(title) {
890
+ return title
891
+ .toLowerCase()
892
+ .replace(/[^a-z0-9\s]/g, '')
893
+ .split(/\s+/)
894
+ .map((word, i) => i === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
895
+ .join('') + 'Workflow';
896
+ }
897
+ function generateStepKey(title, existingKeys) {
898
+ let key = title
899
+ .toLowerCase()
900
+ .replace(/[^a-z0-9\s]/g, '')
901
+ .split(/\s+/)
902
+ .map((word, i) => i === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1))
903
+ .join('');
904
+ let finalKey = key;
905
+ let counter = 1;
906
+ while (existingKeys.includes(finalKey)) {
907
+ finalKey = `${key}${counter}`;
908
+ counter++;
909
+ }
910
+ existingKeys.push(finalKey);
911
+ return finalKey;
912
+ }
913
+ function normalizeStep(step, existingKeys, colorIndex) {
914
+ const key = step.key || generateStepKey(step.title, existingKeys);
915
+ return {
916
+ title: step.title,
917
+ key,
918
+ color: step.color || STEP_COLORS[colorIndex % STEP_COLORS.length],
919
+ description: step.description || '',
920
+ assignees: step.assignees || [],
921
+ assigneeBehaviour: step.assigneeBehaviour || '',
922
+ dueDateBehaviour: step.dueDateBehaviour || '',
923
+ dueDateQuantity: step.dueDateQuantity || 0,
924
+ dueDateUnit: step.dueDateUnit || 'days',
925
+ dueDateReplacement: step.dueDateReplacement || '',
926
+ hasRequiredTasks: step.hasRequiredTasks || false,
927
+ requireTasks: step.requireTasks || [],
928
+ hasRequiredForms: step.hasRequiredForms || false,
929
+ requireForms: step.requireForms || [],
930
+ hasRequiredCriteria: step.hasRequiredCriteria || false,
931
+ completionCriteria: step.completionCriteria || [],
932
+ completionWhen: step.completionWhen || '',
933
+ entryFunctions: step.entryFunctions || [],
934
+ exitFunctions: step.exitFunctions || [],
935
+ successFunctions: step.successFunctions || [],
936
+ failFunctions: step.failFunctions || [],
937
+ successAction: step.successAction || 'next',
938
+ successTargetStep: step.successTargetStep || '',
939
+ failureAction: step.failureAction || '',
940
+ failureTargetStep: step.failureTargetStep || ''
941
+ };
942
+ }
943
+ function normalizeWorkflow(workflow) {
944
+ const existingKeys = [];
945
+ let colorIndex = 0;
946
+ return workflow.map(column => ({
947
+ title: column.title,
948
+ description: column.description || '',
949
+ steps: (column.steps || []).map((step) => {
950
+ const normalized = normalizeStep(step, existingKeys, colorIndex);
951
+ colorIndex++;
952
+ return normalized;
953
+ })
954
+ }));
955
+ }
956
+ function getAllStepKeys(workflow) {
957
+ const keys = [];
958
+ for (const col of workflow) {
959
+ for (const step of col.steps) {
960
+ if (step.key)
961
+ keys.push(step.key);
962
+ }
963
+ }
964
+ return keys;
965
+ }
966
+ function formatWorkflowForDisplay(workflow) {
967
+ let output = '';
968
+ workflow.forEach((column, colIndex) => {
969
+ output += `\n## Column ${colIndex + 1}: ${column.title}\n`;
970
+ if (column.description) {
971
+ output += `_${column.description}_\n`;
972
+ }
973
+ column.steps.forEach((step, stepIndex) => {
974
+ output += `\n### ${colIndex + 1}.${stepIndex + 1} ${step.title}\n`;
975
+ output += `- Key: \`${step.key}\`\n`;
976
+ if (step.dueDateBehaviour === 'period' && step.dueDateQuantity) {
977
+ output += `- Due: ${step.dueDateQuantity} ${step.dueDateUnit} after entry\n`;
978
+ }
979
+ if (step.hasRequiredTasks && step.requireTasks?.length) {
980
+ output += `- Required tasks: ${step.requireTasks.map(t => t.title).join(', ')}\n`;
981
+ }
982
+ const functions = [
983
+ step.entryFunctions?.length ? `entry: ${step.entryFunctions.length}` : null,
984
+ step.exitFunctions?.length ? `exit: ${step.exitFunctions.length}` : null,
985
+ step.successFunctions?.length ? `success: ${step.successFunctions.length}` : null,
986
+ step.failFunctions?.length ? `fail: ${step.failFunctions.length}` : null
987
+ ].filter(Boolean);
988
+ if (functions.length) {
989
+ output += `- Automation: ${functions.join(', ')}\n`;
990
+ }
991
+ output += `- On success: ${step.successAction || 'next'}`;
992
+ if (step.successAction === 'step' && step.successTargetStep) {
993
+ output += ` → \`${step.successTargetStep}\``;
994
+ }
995
+ output += '\n';
996
+ if (step.failureAction) {
997
+ output += `- On failure: ${step.failureAction}`;
998
+ if (step.failureAction === 'step' && step.failureTargetStep) {
999
+ output += ` → \`${step.failureTargetStep}\``;
1000
+ }
1001
+ output += '\n';
1002
+ }
1003
+ });
1004
+ });
1005
+ return output;
1006
+ }
1007
+ function validateWorkflow(workflow) {
1008
+ const errors = [];
1009
+ const allStepKeys = new Set();
1010
+ workflow.forEach((column, colIndex) => {
1011
+ if (!column.title) {
1012
+ errors.push(`Column ${colIndex + 1} is missing a title`);
1013
+ }
1014
+ if (!column.steps || column.steps.length === 0) {
1015
+ errors.push(`Column "${column.title}" has no steps`);
1016
+ }
1017
+ column.steps?.forEach((step, stepIndex) => {
1018
+ if (!step.title) {
1019
+ errors.push(`Step ${colIndex + 1}.${stepIndex + 1} is missing a title`);
1020
+ }
1021
+ if (!step.key) {
1022
+ errors.push(`Step "${step.title}" is missing a key`);
1023
+ }
1024
+ if (allStepKeys.has(step.key)) {
1025
+ errors.push(`Duplicate step key: "${step.key}"`);
1026
+ }
1027
+ allStepKeys.add(step.key);
1028
+ });
1029
+ });
1030
+ // Validate navigation targets
1031
+ workflow.forEach(column => {
1032
+ column.steps?.forEach((step) => {
1033
+ if (step.successAction === 'step' && step.successTargetStep) {
1034
+ if (!allStepKeys.has(step.successTargetStep)) {
1035
+ errors.push(`Step "${step.key}" has invalid successTargetStep: "${step.successTargetStep}"`);
1036
+ }
1037
+ }
1038
+ if (step.failureAction === 'step' && step.failureTargetStep) {
1039
+ if (!allStepKeys.has(step.failureTargetStep)) {
1040
+ errors.push(`Step "${step.key}" has invalid failureTargetStep: "${step.failureTargetStep}"`);
1041
+ }
1042
+ }
1043
+ });
1044
+ });
1045
+ return { valid: errors.length === 0, errors };
1046
+ }
1047
+ //# sourceMappingURL=workflow.js.map