@assistkick/create 1.19.0 → 1.22.0

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 (26) hide show
  1. package/package.json +1 -1
  2. package/templates/assistkick-product-system/packages/backend/src/routes/mcp_config.ts +87 -0
  3. package/templates/assistkick-product-system/packages/backend/src/server.ts +8 -1
  4. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +7 -0
  5. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +26 -1
  6. package/templates/assistkick-product-system/packages/backend/src/services/mcp_config_service.ts +157 -0
  7. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +37 -0
  8. package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +14 -1
  9. package/templates/assistkick-product-system/packages/frontend/src/components/McpConfigModal.tsx +307 -0
  10. package/templates/assistkick-product-system/packages/shared/db/migrations/0001_superb_roxanne_simpson.sql +8 -0
  11. package/templates/assistkick-product-system/packages/shared/db/migrations/0002_noisy_maelstrom.sql +1 -0
  12. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +1019 -23
  13. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0002_snapshot.json +997 -22
  14. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +14 -0
  15. package/templates/assistkick-product-system/packages/shared/db/schema.ts +11 -0
  16. package/templates/assistkick-product-system/packages/shared/lib/app_use_flow.ts +484 -0
  17. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  18. package/templates/assistkick-product-system/packages/shared/tools/agent_builder.ts +341 -0
  19. package/templates/assistkick-product-system/packages/shared/tools/app_use_record.ts +268 -0
  20. package/templates/assistkick-product-system/packages/shared/tools/app_use_run.ts +348 -0
  21. package/templates/assistkick-product-system/packages/shared/tools/app_use_validate.ts +67 -0
  22. package/templates/assistkick-product-system/packages/shared/tools/workflow_builder.ts +754 -0
  23. package/templates/skills/assistkick-agent-builder/SKILL.md +168 -0
  24. package/templates/skills/assistkick-app-use/SKILL.md +296 -0
  25. package/templates/skills/assistkick-app-use/references/agent-browser.md +1156 -0
  26. package/templates/skills/assistkick-workflow-builder/SKILL.md +234 -0
@@ -0,0 +1,754 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * workflow_builder — Create and edit workflow graphs stored in the database.
5
+ *
6
+ * Actions:
7
+ * list — List all workflows
8
+ * get <id> — Get a workflow with its graph
9
+ * create — Create a new workflow
10
+ * update <id> — Update workflow metadata and/or graph
11
+ * delete <id> — Delete a workflow
12
+ * set-default <id> — Mark workflow as default for its scope
13
+ * add-node <workflow-id> — Add a node to a workflow graph
14
+ * remove-node <workflow-id> — Remove a node and its edges
15
+ * update-node <workflow-id> — Update a node's data/config
16
+ * add-edge <workflow-id> — Add an edge between two nodes
17
+ * remove-edge <workflow-id> — Remove an edge
18
+ * auto-layout <workflow-id> — Auto-layout the graph nodes
19
+ * validate <workflow-id> — Validate graph structure
20
+ * node-types — List available node types with config
21
+ * list-agents — List available agents for runAgent nodes
22
+ * list-groups — List available reusable groups
23
+ * get-group <id> — Get a group's internal graph
24
+ */
25
+
26
+ import { program } from 'commander';
27
+ import chalk from 'chalk';
28
+ import { eq, and, isNull, or } from 'drizzle-orm';
29
+ import { workflows, workflowExecutions, workflowGroups, agents } from '../db/schema.js';
30
+ import { getDb } from '../lib/db.js';
31
+ import { randomUUID } from 'node:crypto';
32
+
33
+ // ── Constants ──────────────────────────────────────────────────────────────────
34
+
35
+ const WORKFLOW_NODE_TYPES = [
36
+ 'start', 'transitionCard', 'runAgent', 'checkCardPosition',
37
+ 'checkCycleCount', 'setCardMetadata', 'end', 'group',
38
+ 'rebuildBundle', 'generateTTS', 'renderVideo', 'programmable',
39
+ ] as const;
40
+
41
+ const KANBAN_COLUMNS = ['backlog', 'todo', 'in_progress', 'in_review', 'qa', 'done'] as const;
42
+
43
+ const NODE_DEFAULTS: Record<string, () => Record<string, unknown>> = {
44
+ start: () => ({}),
45
+ transitionCard: () => ({ sourceColumn: 'todo', targetColumn: 'in_progress' }),
46
+ runAgent: () => ({ agentId: '', agentName: '' }),
47
+ checkCardPosition: () => ({}),
48
+ checkCycleCount: () => ({ maxCount: 3 }),
49
+ setCardMetadata: () => ({ key: '', value: '' }),
50
+ end: () => ({ statusType: 'success' }),
51
+ group: () => ({ groupId: '', groupName: '', internalNodes: [], internalEdges: [], inputHandles: [], outputHandles: [] }),
52
+ rebuildBundle: () => ({}),
53
+ generateTTS: () => ({ scriptPath: '', force: false, voiceId: '' }),
54
+ renderVideo: () => ({ compositionId: '', resolution: '1920x1080', aspectRatio: '', fileOutputPrefix: '' }),
55
+ programmable: () => ({
56
+ label: '', code: 'async function run(context) {\n return {};\n}',
57
+ timeout: 30000, memory: 128, maxRequests: 20, maxResponseSize: 5242880,
58
+ }),
59
+ };
60
+
61
+ const NODE_DESCRIPTIONS: Record<string, { label: string; description: string; config: string }> = {
62
+ start: { label: 'Start', description: 'Workflow entry point', config: 'No config needed' },
63
+ transitionCard: { label: 'Transition Card', description: 'Move kanban card between columns', config: 'sourceColumn, targetColumn (backlog|todo|in_progress|in_review|qa|done)' },
64
+ runAgent: { label: 'Run Agent', description: 'Execute an AI agent', config: 'agentId, agentName' },
65
+ checkCardPosition: { label: 'Check Card Position', description: 'Branch by current kanban column', config: 'No config. Outputs: backlog, todo, in_progress, in_review, qa, done' },
66
+ checkCycleCount: { label: 'Check Cycle Count', description: 'Branch by iteration count', config: 'maxCount (number). Outputs: under_limit, at_limit' },
67
+ setCardMetadata: { label: 'Set Card Metadata', description: 'Set key/value on kanban card', config: 'key, value' },
68
+ end: { label: 'End', description: 'Workflow exit point', config: 'statusType (success|blocked|failed)' },
69
+ group: { label: 'Group', description: 'Reusable nested node group', config: 'groupId, groupName, internalNodes, internalEdges, inputHandles, outputHandles' },
70
+ rebuildBundle: { label: 'Rebuild Bundle', description: 'Build Remotion webpack bundle', config: 'No config needed' },
71
+ generateTTS: { label: 'Generate TTS', description: 'Generate text-to-speech audio', config: 'scriptPath, force, voiceId' },
72
+ renderVideo: { label: 'Render Video', description: 'Render composition to MP4', config: 'compositionId, resolution, aspectRatio, fileOutputPrefix' },
73
+ programmable: { label: 'Programmable', description: 'Run custom JavaScript code in sandbox', config: 'label, code, timeout, memory, maxRequests, maxResponseSize. Outputs: success, error' },
74
+ };
75
+
76
+ // ── Auto-layout (port of frontend autoLayout.ts) ──────────────────────────────
77
+
78
+ const NODE_WIDTH = 200;
79
+ const NODE_HEIGHT = 160;
80
+ const HORIZONTAL_GAP = 60;
81
+ const VERTICAL_GAP = 80;
82
+
83
+ interface GraphNode {
84
+ id: string;
85
+ type: string;
86
+ position: { x: number; y: number };
87
+ data: Record<string, unknown>;
88
+ [key: string]: unknown;
89
+ }
90
+
91
+ interface GraphEdge {
92
+ id: string;
93
+ source: string;
94
+ target: string;
95
+ sourceHandle?: string | null;
96
+ targetHandle?: string | null;
97
+ type?: string;
98
+ animated?: boolean;
99
+ data?: Record<string, unknown>;
100
+ }
101
+
102
+ interface GraphData {
103
+ nodes: GraphNode[];
104
+ edges: GraphEdge[];
105
+ }
106
+
107
+ function autoLayoutNodes(nodes: GraphNode[], edges: GraphEdge[]): GraphNode[] {
108
+ if (nodes.length === 0) return nodes;
109
+
110
+ const children = new Map<string, string[]>();
111
+ const parentCount = new Map<string, number>();
112
+ for (const node of nodes) {
113
+ children.set(node.id, []);
114
+ parentCount.set(node.id, 0);
115
+ }
116
+ for (const edge of edges) {
117
+ if (children.has(edge.source) && parentCount.has(edge.target)) {
118
+ children.get(edge.source)!.push(edge.target);
119
+ parentCount.set(edge.target, (parentCount.get(edge.target) || 0) + 1);
120
+ }
121
+ }
122
+
123
+ const roots = nodes.filter(n => (parentCount.get(n.id) || 0) === 0);
124
+ if (roots.length === 0) {
125
+ const startNodes = nodes.filter(n => n.type === 'start');
126
+ roots.push(...(startNodes.length > 0 ? startNodes : [nodes[0]]));
127
+ }
128
+
129
+ const depth = new Map<string, number>();
130
+ const maxDepth = nodes.length;
131
+ const queue: Array<{ id: string; d: number }> = [];
132
+ for (const root of roots) {
133
+ depth.set(root.id, 0);
134
+ queue.push({ id: root.id, d: 0 });
135
+ }
136
+
137
+ while (queue.length > 0) {
138
+ const { id, d } = queue.shift()!;
139
+ for (const childId of children.get(id) || []) {
140
+ const newDepth = d + 1;
141
+ if (newDepth > maxDepth) continue;
142
+ const currentDepth = depth.get(childId);
143
+ if (currentDepth === undefined || newDepth > currentDepth) {
144
+ depth.set(childId, newDepth);
145
+ queue.push({ id: childId, d: newDepth });
146
+ }
147
+ }
148
+ }
149
+
150
+ for (const node of nodes) {
151
+ if (!depth.has(node.id)) depth.set(node.id, 0);
152
+ }
153
+
154
+ const levels = new Map<number, GraphNode[]>();
155
+ for (const node of nodes) {
156
+ const d = depth.get(node.id)!;
157
+ if (!levels.has(d)) levels.set(d, []);
158
+ levels.get(d)!.push(node);
159
+ }
160
+
161
+ const sortedDepths = [...levels.keys()].sort((a, b) => a - b);
162
+ const positioned = new Map<string, { x: number; y: number }>();
163
+
164
+ for (const d of sortedDepths) {
165
+ const levelNodes = levels.get(d)!;
166
+ const rowWidth = levelNodes.length * NODE_WIDTH + (levelNodes.length - 1) * HORIZONTAL_GAP;
167
+ const startX = -rowWidth / 2;
168
+ for (let i = 0; i < levelNodes.length; i++) {
169
+ positioned.set(levelNodes[i].id, {
170
+ x: startX + i * (NODE_WIDTH + HORIZONTAL_GAP),
171
+ y: d * (NODE_HEIGHT + VERTICAL_GAP),
172
+ });
173
+ }
174
+ }
175
+
176
+ return nodes.map(node => ({
177
+ ...node,
178
+ position: positioned.get(node.id) || node.position,
179
+ }));
180
+ }
181
+
182
+ // ── Helpers ───────────────────────────────────────────────────────────────────
183
+
184
+ function generateNodeId(): string {
185
+ return `node_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
186
+ }
187
+
188
+ function generateEdgeId(source: string, target: string, sourceHandle?: string): string {
189
+ const handleSuffix = sourceHandle ? `_${sourceHandle}` : '';
190
+ return `edge_${source}_${target}${handleSuffix}`;
191
+ }
192
+
193
+ function parseGraphData(raw: string): GraphData {
194
+ try {
195
+ const data = JSON.parse(raw);
196
+ return {
197
+ nodes: Array.isArray(data.nodes) ? data.nodes : [],
198
+ edges: Array.isArray(data.edges) ? data.edges : [],
199
+ };
200
+ } catch {
201
+ return { nodes: [], edges: [] };
202
+ }
203
+ }
204
+
205
+ async function loadWorkflow(db: any, id: string) {
206
+ const [row] = await db.select().from(workflows).where(eq(workflows.id, id));
207
+ if (!row) throw new Error(`Workflow not found: ${id}`);
208
+ return row;
209
+ }
210
+
211
+ async function saveGraphData(db: any, id: string, graph: GraphData) {
212
+ const now = new Date().toISOString();
213
+ await db.update(workflows)
214
+ .set({ graphData: JSON.stringify(graph), updatedAt: now })
215
+ .where(eq(workflows.id, id));
216
+ }
217
+
218
+ function validateNodeType(type: string): void {
219
+ if (!WORKFLOW_NODE_TYPES.includes(type as any)) {
220
+ throw new Error(`Invalid node type: ${type}. Valid types: ${WORKFLOW_NODE_TYPES.join(', ')}`);
221
+ }
222
+ }
223
+
224
+ function validateGraph(graph: GraphData): string[] {
225
+ const issues: string[] = [];
226
+ const nodeIds = new Set(graph.nodes.map(n => n.id));
227
+
228
+ // Must have exactly one start node
229
+ const startNodes = graph.nodes.filter(n => n.type === 'start');
230
+ if (startNodes.length === 0) issues.push('Missing start node');
231
+ if (startNodes.length > 1) issues.push(`Multiple start nodes: ${startNodes.map(n => n.id).join(', ')}`);
232
+
233
+ // Must have at least one end node
234
+ const endNodes = graph.nodes.filter(n => n.type === 'end');
235
+ if (endNodes.length === 0) issues.push('Missing end node');
236
+
237
+ // Edges must reference existing nodes
238
+ for (const edge of graph.edges) {
239
+ if (!nodeIds.has(edge.source)) issues.push(`Edge ${edge.id}: source "${edge.source}" not found`);
240
+ if (!nodeIds.has(edge.target)) issues.push(`Edge ${edge.id}: target "${edge.target}" not found`);
241
+ }
242
+
243
+ // Start node should have no incoming edges
244
+ for (const start of startNodes) {
245
+ const incoming = graph.edges.filter(e => e.target === start.id);
246
+ if (incoming.length > 0) issues.push(`Start node "${start.id}" has incoming edges`);
247
+ }
248
+
249
+ // End nodes should have no outgoing edges
250
+ for (const end of endNodes) {
251
+ const outgoing = graph.edges.filter(e => e.source === end.id);
252
+ if (outgoing.length > 0) issues.push(`End node "${end.id}" has outgoing edges`);
253
+ }
254
+
255
+ // Non-start, non-end nodes should have both incoming and outgoing edges
256
+ for (const node of graph.nodes) {
257
+ if (node.type === 'start' || node.type === 'end') continue;
258
+ const incoming = graph.edges.filter(e => e.target === node.id);
259
+ const outgoing = graph.edges.filter(e => e.source === node.id);
260
+ if (incoming.length === 0) issues.push(`Node "${node.id}" (${node.type}) has no incoming edges`);
261
+ if (outgoing.length === 0) issues.push(`Node "${node.id}" (${node.type}) has no outgoing edges`);
262
+ }
263
+
264
+ // Branching nodes must have correct output handles
265
+ for (const node of graph.nodes) {
266
+ if (node.type === 'checkCardPosition') {
267
+ const outgoing = graph.edges.filter(e => e.source === node.id);
268
+ const handles = new Set(outgoing.map(e => e.sourceHandle).filter(Boolean));
269
+ for (const h of handles) {
270
+ if (!KANBAN_COLUMNS.includes(h as any)) {
271
+ issues.push(`checkCardPosition node "${node.id}": invalid handle "${h}"`);
272
+ }
273
+ }
274
+ }
275
+ if (node.type === 'checkCycleCount') {
276
+ const outgoing = graph.edges.filter(e => e.source === node.id);
277
+ const handles = new Set(outgoing.map(e => e.sourceHandle).filter(Boolean));
278
+ for (const h of handles) {
279
+ if (!['under_limit', 'at_limit'].includes(h as string)) {
280
+ issues.push(`checkCycleCount node "${node.id}": invalid handle "${h}"`);
281
+ }
282
+ }
283
+ }
284
+ if (node.type === 'programmable') {
285
+ const outgoing = graph.edges.filter(e => e.source === node.id);
286
+ const handles = new Set(outgoing.map(e => e.sourceHandle).filter(Boolean));
287
+ for (const h of handles) {
288
+ if (!['success', 'error'].includes(h as string)) {
289
+ issues.push(`programmable node "${node.id}": invalid handle "${h}"`);
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ // runAgent nodes must have agentId set
296
+ for (const node of graph.nodes) {
297
+ if (node.type === 'runAgent' && !node.data?.agentId) {
298
+ issues.push(`runAgent node "${node.id}": missing agentId`);
299
+ }
300
+ }
301
+
302
+ return issues;
303
+ }
304
+
305
+ // ── CLI ───────────────────────────────────────────────────────────────────────
306
+
307
+ program
308
+ .argument('<action>', 'Action to perform')
309
+ .argument('[target]', 'Workflow ID, group ID, or other target')
310
+ .option('--project-id <id>', 'Project ID (scope filter)')
311
+ .option('--name <name>', 'Workflow or group name')
312
+ .option('--description <desc>', 'Workflow description')
313
+ .option('--feature-type <type>', 'Feature type filter')
314
+ .option('--trigger-column <col>', 'Trigger column filter')
315
+ .option('--graph-data <json>', 'Full graph data as JSON')
316
+ .option('--node-type <type>', 'Node type for add-node')
317
+ .option('--node-id <id>', 'Node ID for update-node/remove-node')
318
+ .option('--node-data <json>', 'Node data/config as JSON')
319
+ .option('--position <xy>', 'Node position as "x,y"')
320
+ .option('--source <id>', 'Source node ID for add-edge')
321
+ .option('--target-node <id>', 'Target node ID for add-edge')
322
+ .option('--source-handle <handle>', 'Source handle for add-edge')
323
+ .option('--target-handle <handle>', 'Target handle for add-edge')
324
+ .option('--edge-label <label>', 'Edge label')
325
+ .option('--edge-id <id>', 'Edge ID for remove-edge')
326
+ .parse();
327
+
328
+ const [action, target] = program.args;
329
+ const opts = program.opts();
330
+
331
+ (async () => {
332
+ const db = getDb();
333
+
334
+ try {
335
+ switch (action) {
336
+
337
+ // ── List workflows ──────────────────────────────────────────────────────
338
+ case 'list': {
339
+ const conditions = [];
340
+ if (opts.projectId) {
341
+ conditions.push(or(isNull(workflows.projectId), eq(workflows.projectId, opts.projectId)));
342
+ }
343
+ if (opts.featureType) conditions.push(eq(workflows.featureType, opts.featureType));
344
+ if (opts.triggerColumn) conditions.push(eq(workflows.triggerColumn, opts.triggerColumn));
345
+
346
+ const rows = conditions.length > 0
347
+ ? await db.select({
348
+ id: workflows.id, name: workflows.name, description: workflows.description,
349
+ projectId: workflows.projectId, featureType: workflows.featureType,
350
+ triggerColumn: workflows.triggerColumn, isDefault: workflows.isDefault,
351
+ createdAt: workflows.createdAt, updatedAt: workflows.updatedAt,
352
+ }).from(workflows).where(and(...conditions))
353
+ : await db.select({
354
+ id: workflows.id, name: workflows.name, description: workflows.description,
355
+ projectId: workflows.projectId, featureType: workflows.featureType,
356
+ triggerColumn: workflows.triggerColumn, isDefault: workflows.isDefault,
357
+ createdAt: workflows.createdAt, updatedAt: workflows.updatedAt,
358
+ }).from(workflows);
359
+
360
+ console.log(chalk.cyan.bold('Workflows:\n'));
361
+ for (const row of rows) {
362
+ const def = row.isDefault ? chalk.yellow(' [DEFAULT]') : '';
363
+ const scope = row.projectId ? chalk.gray(` (project: ${row.projectId})`) : chalk.gray(' (global)');
364
+ console.log(` ${chalk.bold(row.name)}${def}${scope}`);
365
+ console.log(` ID: ${row.id}`);
366
+ if (row.description) console.log(` ${row.description}`);
367
+ if (row.featureType) console.log(` Feature type: ${row.featureType}`);
368
+ if (row.triggerColumn) console.log(` Trigger column: ${row.triggerColumn}`);
369
+ }
370
+ console.log('\n' + JSON.stringify({ workflows: rows }));
371
+ break;
372
+ }
373
+
374
+ // ── Get workflow ────────────────────────────────────────────────────────
375
+ case 'get': {
376
+ if (!target) throw new Error('Usage: workflow_builder get <workflow-id>');
377
+ const row = await loadWorkflow(db, target);
378
+ const graph = parseGraphData(row.graphData);
379
+
380
+ console.log(chalk.cyan.bold(`\nWorkflow: ${row.name}\n`));
381
+ console.log(` ID: ${row.id}`);
382
+ if (row.description) console.log(` Description: ${row.description}`);
383
+ console.log(` Nodes: ${graph.nodes.length}`);
384
+ console.log(` Edges: ${graph.edges.length}`);
385
+ if (row.isDefault) console.log(` ${chalk.yellow('DEFAULT')}`);
386
+ console.log();
387
+
388
+ console.log(chalk.cyan(' Nodes:'));
389
+ for (const node of graph.nodes) {
390
+ const data = Object.entries(node.data || {})
391
+ .filter(([k]) => k !== 'type')
392
+ .map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`)
393
+ .join(', ');
394
+ console.log(` ${chalk.bold(node.id)} [${node.type}]${data ? ` — ${data}` : ''}`);
395
+ }
396
+ console.log();
397
+
398
+ console.log(chalk.cyan(' Edges:'));
399
+ for (const edge of graph.edges) {
400
+ const handle = edge.sourceHandle ? ` (${edge.sourceHandle})` : '';
401
+ const label = edge.data?.label ? ` "${edge.data.label}"` : '';
402
+ console.log(` ${edge.source}${handle} → ${edge.target}${label}`);
403
+ }
404
+
405
+ console.log('\n' + JSON.stringify({
406
+ id: row.id, name: row.name, description: row.description,
407
+ projectId: row.projectId, featureType: row.featureType,
408
+ triggerColumn: row.triggerColumn, isDefault: row.isDefault,
409
+ graph,
410
+ }));
411
+ break;
412
+ }
413
+
414
+ // ── Create workflow ─────────────────────────────────────────────────────
415
+ case 'create': {
416
+ if (!opts.name) throw new Error('--name is required for create');
417
+ const now = new Date().toISOString();
418
+ const id = randomUUID();
419
+ const graphData = opts.graphData || JSON.stringify({ nodes: [], edges: [] });
420
+
421
+ const record = {
422
+ id, name: opts.name,
423
+ description: opts.description || null,
424
+ projectId: opts.projectId || null,
425
+ featureType: opts.featureType || null,
426
+ triggerColumn: opts.triggerColumn || null,
427
+ isDefault: 0,
428
+ graphData,
429
+ createdAt: now, updatedAt: now,
430
+ };
431
+
432
+ await db.insert(workflows).values(record);
433
+ console.log(chalk.green(`Created workflow: ${opts.name} (${id})`));
434
+ console.log(JSON.stringify({ id, name: opts.name }));
435
+ break;
436
+ }
437
+
438
+ // ── Update workflow metadata ────────────────────────────────────────────
439
+ case 'update': {
440
+ if (!target) throw new Error('Usage: workflow_builder update <workflow-id> [options]');
441
+ const existing = await loadWorkflow(db, target);
442
+ const now = new Date().toISOString();
443
+ const updates: Record<string, any> = { updatedAt: now };
444
+
445
+ if (opts.name) updates.name = opts.name;
446
+ if (opts.description) updates.description = opts.description;
447
+ if (opts.featureType !== undefined) updates.featureType = opts.featureType || null;
448
+ if (opts.triggerColumn !== undefined) updates.triggerColumn = opts.triggerColumn || null;
449
+ if (opts.graphData) updates.graphData = opts.graphData;
450
+
451
+ await db.update(workflows).set(updates).where(eq(workflows.id, target));
452
+ console.log(chalk.green(`Updated workflow: ${existing.name} (${target})`));
453
+ console.log(JSON.stringify({ id: target, ...updates }));
454
+ break;
455
+ }
456
+
457
+ // ── Delete workflow ─────────────────────────────────────────────────────
458
+ case 'delete': {
459
+ if (!target) throw new Error('Usage: workflow_builder delete <workflow-id>');
460
+ const existing = await loadWorkflow(db, target);
461
+
462
+ const active = await db.select().from(workflowExecutions)
463
+ .where(and(
464
+ eq(workflowExecutions.workflowId, target),
465
+ or(eq(workflowExecutions.status, 'running'), eq(workflowExecutions.status, 'pending')),
466
+ ));
467
+ if (active.length > 0) throw new Error('Cannot delete workflow with active executions');
468
+
469
+ await db.delete(workflows).where(eq(workflows.id, target));
470
+ console.log(chalk.green(`Deleted workflow: ${existing.name} (${target})`));
471
+ console.log(JSON.stringify({ deleted: target }));
472
+ break;
473
+ }
474
+
475
+ // ── Set default ─────────────────────────────────────────────────────────
476
+ case 'set-default': {
477
+ if (!target) throw new Error('Usage: workflow_builder set-default <workflow-id>');
478
+ const existing = await loadWorkflow(db, target);
479
+ const now = new Date().toISOString();
480
+
481
+ const scopeCondition = existing.projectId
482
+ ? eq(workflows.projectId, existing.projectId)
483
+ : isNull(workflows.projectId);
484
+
485
+ await db.update(workflows)
486
+ .set({ isDefault: 0, updatedAt: now })
487
+ .where(and(scopeCondition, eq(workflows.isDefault, 1)));
488
+
489
+ await db.update(workflows)
490
+ .set({ isDefault: 1, updatedAt: now })
491
+ .where(eq(workflows.id, target));
492
+
493
+ console.log(chalk.green(`Set default: ${existing.name} (${target})`));
494
+ console.log(JSON.stringify({ id: target, isDefault: 1 }));
495
+ break;
496
+ }
497
+
498
+ // ── Add node ────────────────────────────────────────────────────────────
499
+ case 'add-node': {
500
+ if (!target) throw new Error('Usage: workflow_builder add-node <workflow-id> --node-type <type> [--node-data <json>] [--position <x,y>]');
501
+ if (!opts.nodeType) throw new Error('--node-type is required');
502
+ validateNodeType(opts.nodeType);
503
+
504
+ const row = await loadWorkflow(db, target);
505
+ const graph = parseGraphData(row.graphData);
506
+ const nodeId = generateNodeId();
507
+
508
+ let position = { x: 0, y: 0 };
509
+ if (opts.position) {
510
+ const [x, y] = opts.position.split(',').map(Number);
511
+ position = { x: x || 0, y: y || 0 };
512
+ }
513
+
514
+ const defaults = NODE_DEFAULTS[opts.nodeType]?.() || {};
515
+ const customData = opts.nodeData ? JSON.parse(opts.nodeData) : {};
516
+
517
+ const newNode: GraphNode = {
518
+ id: nodeId,
519
+ type: opts.nodeType,
520
+ position,
521
+ data: { ...defaults, ...customData },
522
+ };
523
+
524
+ graph.nodes.push(newNode);
525
+ await saveGraphData(db, target, graph);
526
+
527
+ console.log(chalk.green(`Added node: ${nodeId} [${opts.nodeType}]`));
528
+ console.log(JSON.stringify({ nodeId, type: opts.nodeType, data: newNode.data }));
529
+ break;
530
+ }
531
+
532
+ // ── Remove node ─────────────────────────────────────────────────────────
533
+ case 'remove-node': {
534
+ if (!target) throw new Error('Usage: workflow_builder remove-node <workflow-id> --node-id <id>');
535
+ if (!opts.nodeId) throw new Error('--node-id is required');
536
+
537
+ const row = await loadWorkflow(db, target);
538
+ const graph = parseGraphData(row.graphData);
539
+
540
+ const nodeIdx = graph.nodes.findIndex(n => n.id === opts.nodeId);
541
+ if (nodeIdx === -1) throw new Error(`Node not found: ${opts.nodeId}`);
542
+
543
+ graph.nodes.splice(nodeIdx, 1);
544
+ graph.edges = graph.edges.filter(e => e.source !== opts.nodeId && e.target !== opts.nodeId);
545
+ await saveGraphData(db, target, graph);
546
+
547
+ console.log(chalk.green(`Removed node: ${opts.nodeId}`));
548
+ console.log(JSON.stringify({ removed: opts.nodeId }));
549
+ break;
550
+ }
551
+
552
+ // ── Update node ─────────────────────────────────────────────────────────
553
+ case 'update-node': {
554
+ if (!target) throw new Error('Usage: workflow_builder update-node <workflow-id> --node-id <id> --node-data <json>');
555
+ if (!opts.nodeId) throw new Error('--node-id is required');
556
+ if (!opts.nodeData) throw new Error('--node-data is required');
557
+
558
+ const row = await loadWorkflow(db, target);
559
+ const graph = parseGraphData(row.graphData);
560
+
561
+ const node = graph.nodes.find(n => n.id === opts.nodeId);
562
+ if (!node) throw new Error(`Node not found: ${opts.nodeId}`);
563
+
564
+ const updates = JSON.parse(opts.nodeData);
565
+ node.data = { ...node.data, ...updates };
566
+
567
+ if (opts.position) {
568
+ const [x, y] = opts.position.split(',').map(Number);
569
+ node.position = { x: x || 0, y: y || 0 };
570
+ }
571
+
572
+ await saveGraphData(db, target, graph);
573
+
574
+ console.log(chalk.green(`Updated node: ${opts.nodeId}`));
575
+ console.log(JSON.stringify({ nodeId: opts.nodeId, data: node.data }));
576
+ break;
577
+ }
578
+
579
+ // ── Add edge ────────────────────────────────────────────────────────────
580
+ case 'add-edge': {
581
+ if (!target) throw new Error('Usage: workflow_builder add-edge <workflow-id> --source <id> --target-node <id>');
582
+ if (!opts.source) throw new Error('--source is required');
583
+ if (!opts.targetNode) throw new Error('--target-node is required');
584
+
585
+ const row = await loadWorkflow(db, target);
586
+ const graph = parseGraphData(row.graphData);
587
+ const nodeIds = new Set(graph.nodes.map(n => n.id));
588
+
589
+ if (!nodeIds.has(opts.source)) throw new Error(`Source node not found: ${opts.source}`);
590
+ if (!nodeIds.has(opts.targetNode)) throw new Error(`Target node not found: ${opts.targetNode}`);
591
+
592
+ const edgeId = generateEdgeId(opts.source, opts.targetNode, opts.sourceHandle);
593
+
594
+ // Check for duplicate
595
+ const exists = graph.edges.some(e =>
596
+ e.source === opts.source && e.target === opts.targetNode &&
597
+ (e.sourceHandle || null) === (opts.sourceHandle || null)
598
+ );
599
+ if (exists) throw new Error('Edge already exists between these nodes with this handle');
600
+
601
+ const newEdge: GraphEdge = {
602
+ id: edgeId,
603
+ source: opts.source,
604
+ target: opts.targetNode,
605
+ sourceHandle: opts.sourceHandle || null,
606
+ targetHandle: opts.targetHandle || null,
607
+ type: 'smoothstep',
608
+ animated: true,
609
+ };
610
+ if (opts.edgeLabel) {
611
+ newEdge.data = { label: opts.edgeLabel };
612
+ }
613
+
614
+ graph.edges.push(newEdge);
615
+ await saveGraphData(db, target, graph);
616
+
617
+ console.log(chalk.green(`Added edge: ${opts.source} → ${opts.targetNode}`));
618
+ console.log(JSON.stringify({ edgeId, source: opts.source, target: opts.targetNode }));
619
+ break;
620
+ }
621
+
622
+ // ── Remove edge ─────────────────────────────────────────────────────────
623
+ case 'remove-edge': {
624
+ if (!target) throw new Error('Usage: workflow_builder remove-edge <workflow-id> --edge-id <id>');
625
+ if (!opts.edgeId) throw new Error('--edge-id is required');
626
+
627
+ const row = await loadWorkflow(db, target);
628
+ const graph = parseGraphData(row.graphData);
629
+
630
+ const edgeIdx = graph.edges.findIndex(e => e.id === opts.edgeId);
631
+ if (edgeIdx === -1) throw new Error(`Edge not found: ${opts.edgeId}`);
632
+
633
+ graph.edges.splice(edgeIdx, 1);
634
+ await saveGraphData(db, target, graph);
635
+
636
+ console.log(chalk.green(`Removed edge: ${opts.edgeId}`));
637
+ console.log(JSON.stringify({ removed: opts.edgeId }));
638
+ break;
639
+ }
640
+
641
+ // ── Auto-layout ─────────────────────────────────────────────────────────
642
+ case 'auto-layout': {
643
+ if (!target) throw new Error('Usage: workflow_builder auto-layout <workflow-id>');
644
+
645
+ const row = await loadWorkflow(db, target);
646
+ const graph = parseGraphData(row.graphData);
647
+
648
+ graph.nodes = autoLayoutNodes(graph.nodes, graph.edges);
649
+ await saveGraphData(db, target, graph);
650
+
651
+ console.log(chalk.green(`Auto-layout applied to ${graph.nodes.length} nodes`));
652
+ console.log(JSON.stringify({ nodes: graph.nodes.length, edges: graph.edges.length }));
653
+ break;
654
+ }
655
+
656
+ // ── Validate ────────────────────────────────────────────────────────────
657
+ case 'validate': {
658
+ if (!target) throw new Error('Usage: workflow_builder validate <workflow-id>');
659
+
660
+ const row = await loadWorkflow(db, target);
661
+ const graph = parseGraphData(row.graphData);
662
+ const issues = validateGraph(graph);
663
+
664
+ if (issues.length === 0) {
665
+ console.log(chalk.green('Workflow graph is valid'));
666
+ } else {
667
+ console.log(chalk.yellow(`Found ${issues.length} issue(s):`));
668
+ for (const issue of issues) {
669
+ console.log(chalk.yellow(` - ${issue}`));
670
+ }
671
+ }
672
+ console.log(JSON.stringify({ valid: issues.length === 0, issues }));
673
+ break;
674
+ }
675
+
676
+ // ── Node types reference ────────────────────────────────────────────────
677
+ case 'node-types': {
678
+ console.log(chalk.cyan.bold('Available Node Types:\n'));
679
+ for (const [type, info] of Object.entries(NODE_DESCRIPTIONS)) {
680
+ console.log(` ${chalk.bold(info.label)} (${type})`);
681
+ console.log(` ${info.description}`);
682
+ console.log(` Config: ${info.config}`);
683
+ console.log();
684
+ }
685
+ console.log(JSON.stringify({ nodeTypes: NODE_DESCRIPTIONS }));
686
+ break;
687
+ }
688
+
689
+ // ── List agents ─────────────────────────────────────────────────────────
690
+ case 'list-agents': {
691
+ const conditions = opts.projectId
692
+ ? or(isNull(agents.projectId), eq(agents.projectId, opts.projectId))
693
+ : undefined;
694
+
695
+ const rows = conditions
696
+ ? await db.select({ id: agents.id, name: agents.name, model: agents.model, projectId: agents.projectId })
697
+ .from(agents).where(conditions)
698
+ : await db.select({ id: agents.id, name: agents.name, model: agents.model, projectId: agents.projectId })
699
+ .from(agents);
700
+
701
+ console.log(chalk.cyan.bold('Available Agents:\n'));
702
+ for (const row of rows) {
703
+ const scope = row.projectId ? chalk.gray(` (project: ${row.projectId})`) : chalk.gray(' (global)');
704
+ console.log(` ${chalk.bold(row.name)}${scope}`);
705
+ console.log(` ID: ${row.id} Model: ${row.model}`);
706
+ }
707
+ console.log('\n' + JSON.stringify({ agents: rows }));
708
+ break;
709
+ }
710
+
711
+ // ── List groups ─────────────────────────────────────────────────────────
712
+ case 'list-groups': {
713
+ const conditions = opts.projectId
714
+ ? or(isNull(workflowGroups.projectId), eq(workflowGroups.projectId, opts.projectId))
715
+ : undefined;
716
+
717
+ const rows = conditions
718
+ ? await db.select().from(workflowGroups).where(conditions)
719
+ : await db.select().from(workflowGroups);
720
+
721
+ console.log(chalk.cyan.bold('Reusable Groups:\n'));
722
+ for (const row of rows) {
723
+ const scope = row.projectId ? chalk.gray(` (project: ${row.projectId})`) : chalk.gray(' (global)');
724
+ const graph = parseGraphData(row.graphData);
725
+ console.log(` ${chalk.bold(row.name)}${scope}`);
726
+ console.log(` ID: ${row.id} Nodes: ${graph.nodes.length} Edges: ${graph.edges.length}`);
727
+ }
728
+ console.log('\n' + JSON.stringify({ groups: rows }));
729
+ break;
730
+ }
731
+
732
+ // ── Get group ───────────────────────────────────────────────────────────
733
+ case 'get-group': {
734
+ if (!target) throw new Error('Usage: workflow_builder get-group <group-id>');
735
+ const [row] = await db.select().from(workflowGroups).where(eq(workflowGroups.id, target));
736
+ if (!row) throw new Error(`Group not found: ${target}`);
737
+
738
+ const graph = parseGraphData(row.graphData);
739
+ console.log(chalk.cyan.bold(`\nGroup: ${row.name}\n`));
740
+ console.log(` ID: ${row.id}`);
741
+ console.log(` Nodes: ${graph.nodes.length}`);
742
+ console.log(` Edges: ${graph.edges.length}`);
743
+ console.log('\n' + JSON.stringify({ id: row.id, name: row.name, graph }));
744
+ break;
745
+ }
746
+
747
+ default:
748
+ throw new Error(`Unknown action "${action}". Valid actions: list, get, create, update, delete, set-default, add-node, remove-node, update-node, add-edge, remove-edge, auto-layout, validate, node-types, list-agents, list-groups, get-group`);
749
+ }
750
+ } catch (err) {
751
+ console.error(chalk.red(`Error: ${(err as Error).message}`));
752
+ process.exit(1);
753
+ }
754
+ })();