@hsafa/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,853 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import api from '../utils/api.js';
4
+ import { graphqlRequest } from '../utils/graphql.js';
5
+ import ora from 'ora';
6
+ import { randomUUID } from 'crypto';
7
+ import { startChatSession } from './chat.js';
8
+ import fs from 'fs';
9
+ export function registerAgentCommands(program) {
10
+ const agent = program.command('agent').description('Agent management');
11
+ const isJson = () => Boolean(program.opts()?.json);
12
+ const output = (data) => {
13
+ console.log(JSON.stringify(data, null, 2));
14
+ };
15
+ const outputError = (error) => {
16
+ const message = error?.message || String(error);
17
+ if (isJson()) {
18
+ console.error(JSON.stringify({ error: message }, null, 2));
19
+ process.exitCode = 1;
20
+ return;
21
+ }
22
+ console.error(chalk.red(message));
23
+ process.exitCode = 1;
24
+ };
25
+ const readStdin = async () => {
26
+ const chunks = [];
27
+ for await (const chunk of process.stdin) {
28
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
29
+ }
30
+ return Buffer.concat(chunks).toString('utf8');
31
+ };
32
+ const readJsonInput = async (options) => {
33
+ if (options.input !== undefined) {
34
+ return JSON.parse(options.input);
35
+ }
36
+ if (options.inputFile) {
37
+ return JSON.parse(fs.readFileSync(options.inputFile, 'utf8'));
38
+ }
39
+ if (options.inputStdin) {
40
+ const text = await readStdin();
41
+ return JSON.parse(text);
42
+ }
43
+ throw new Error('No input provided. Use --input, --input-file, or --input-stdin');
44
+ };
45
+ const getAgentWithFlow = async (id) => {
46
+ const query = `
47
+ query($id: ID!) {
48
+ agentWithFlow(id: $id) {
49
+ id
50
+ name
51
+ description
52
+ type
53
+ color
54
+ icon
55
+ status
56
+ configNodesCount
57
+ toolNodesCount
58
+ nodePositionX
59
+ nodePositionY
60
+ createdAt
61
+ updatedAt
62
+ flowNodes {
63
+ nodeId
64
+ nodeType
65
+ label
66
+ positionX
67
+ positionY
68
+ data
69
+ createdAt
70
+ updatedAt
71
+ }
72
+ flowEdges {
73
+ edgeId
74
+ sourceId
75
+ targetId
76
+ edgeType
77
+ data
78
+ createdAt
79
+ updatedAt
80
+ }
81
+ toolLinks {
82
+ id
83
+ agentId
84
+ toolId
85
+ isEnabled
86
+ order
87
+ tool {
88
+ id
89
+ name
90
+ description
91
+ inputSchema
92
+ outputSchema
93
+ executionType
94
+ executionConfig
95
+ isSystemTool
96
+ createdAt
97
+ updatedAt
98
+ }
99
+ }
100
+ knowledgeBaseLinks {
101
+ id
102
+ agentId
103
+ knowledgeBaseId
104
+ isEnabled
105
+ knowledgeBase {
106
+ id
107
+ name
108
+ description
109
+ createdAt
110
+ updatedAt
111
+ }
112
+ }
113
+ }
114
+ }
115
+ `;
116
+ const data = await graphqlRequest(query, { id });
117
+ return data.agentWithFlow;
118
+ };
119
+ const saveAgentFlow = async (agentId, flow) => {
120
+ const mutation = `
121
+ mutation($agentId: ID!, $input: SaveAgentFlowInput!) {
122
+ saveAgentFlow(agentId: $agentId, input: $input)
123
+ }
124
+ `;
125
+ return graphqlRequest(mutation, {
126
+ agentId,
127
+ input: {
128
+ nodes: flow.nodes,
129
+ edges: flow.edges,
130
+ },
131
+ });
132
+ };
133
+ const normalizeFlow = (input) => {
134
+ const nodes = input?.nodes || input?.flowNodes;
135
+ const edges = input?.edges || input?.flowEdges;
136
+ if (!Array.isArray(nodes) || !Array.isArray(edges)) {
137
+ throw new Error('Invalid flow input. Expected { nodes, edges } or { flowNodes, flowEdges }.');
138
+ }
139
+ return {
140
+ nodes: nodes.map((n) => ({
141
+ nodeId: n.nodeId,
142
+ nodeType: n.nodeType,
143
+ label: n.label,
144
+ positionX: n.positionX,
145
+ positionY: n.positionY,
146
+ data: n.data ?? undefined,
147
+ })),
148
+ edges: edges.map((e) => ({
149
+ edgeId: e.edgeId,
150
+ sourceId: e.sourceId,
151
+ targetId: e.targetId,
152
+ edgeType: e.edgeType ?? null,
153
+ data: e.data ?? undefined,
154
+ })),
155
+ };
156
+ };
157
+ agent
158
+ .command('list')
159
+ .description('List all available agents')
160
+ .option('-p, --project <id>', 'Project ID')
161
+ .action(async (options) => {
162
+ const globalOptions = program.opts();
163
+ const spinner = !globalOptions.json ? ora('Fetching agents...').start() : null;
164
+ try {
165
+ const projectId = options.project;
166
+ if (!projectId) {
167
+ if (spinner)
168
+ spinner.fail('Project ID is required. Use -p <id>');
169
+ else
170
+ console.error(JSON.stringify({ error: 'Project ID is required' }));
171
+ return;
172
+ }
173
+ const response = await api.get(`/api/projects/${projectId}/agents`);
174
+ const agents = response.data;
175
+ if (spinner)
176
+ spinner.stop();
177
+ if (globalOptions.json) {
178
+ console.log(JSON.stringify(agents, null, 2));
179
+ return;
180
+ }
181
+ if (agents.length === 0) {
182
+ console.log(chalk.yellow('No agents found for this project.'));
183
+ return;
184
+ }
185
+ const table = new Table({
186
+ head: [chalk.cyan('ID'), chalk.cyan('Name'), chalk.cyan('Description'), chalk.cyan('Status')],
187
+ colWidths: [36, 20, 40, 10]
188
+ });
189
+ agents.forEach((a) => {
190
+ table.push([a.id, a.name, a.description || 'N/A', a.status]);
191
+ });
192
+ console.log(table.toString());
193
+ }
194
+ catch (error) {
195
+ if (spinner)
196
+ spinner.fail(`Failed to fetch agents: ${error.message}`);
197
+ else
198
+ console.error(JSON.stringify({ error: error.message }));
199
+ }
200
+ });
201
+ agent
202
+ .command('chat <id>')
203
+ .description('Start a persistent chat with an agent')
204
+ .action(async (id) => {
205
+ await startChatSession(id);
206
+ });
207
+ agent
208
+ .command('export <id>')
209
+ .description('Export agent configuration to JSON')
210
+ .action(async (id) => {
211
+ const spinner = ora('Exporting agent...').start();
212
+ try {
213
+ const query = `
214
+ query($id: ID!) {
215
+ agentWithFlow(id: $id) {
216
+ name
217
+ description
218
+ type
219
+ color
220
+ icon
221
+ status
222
+ flowNodes {
223
+ nodeId
224
+ nodeType
225
+ label
226
+ positionX
227
+ positionY
228
+ data
229
+ }
230
+ flowEdges {
231
+ edgeId
232
+ sourceId
233
+ targetId
234
+ edgeType
235
+ data
236
+ }
237
+ }
238
+ }
239
+ `;
240
+ const data = await graphqlRequest(query, { id });
241
+ const agentData = data.agentWithFlow;
242
+ spinner.stop();
243
+ console.log(JSON.stringify(agentData, null, 2));
244
+ }
245
+ catch (error) {
246
+ spinner.fail(`Export failed: ${error.message}`);
247
+ }
248
+ });
249
+ agent
250
+ .command('import <file>')
251
+ .description('Import agent configuration from a JSON file')
252
+ .option('-w, --workspace <id>', 'Target workspace ID')
253
+ .action(async (file, options) => {
254
+ const spinner = ora('Importing agent...').start();
255
+ try {
256
+ if (!fs.existsSync(file)) {
257
+ spinner.fail(`File not found: ${file}`);
258
+ return;
259
+ }
260
+ const agentData = JSON.parse(fs.readFileSync(file, 'utf8'));
261
+ // 1. Get Workspace ID
262
+ let workspaceId = options.workspace;
263
+ if (!workspaceId) {
264
+ const wsQuery = `query { workspaces { id } }`;
265
+ const wsData = await graphqlRequest(wsQuery);
266
+ workspaceId = wsData.workspaces[0]?.id;
267
+ if (!workspaceId) {
268
+ spinner.fail('No workspace found.');
269
+ return;
270
+ }
271
+ }
272
+ // 2. Create Agent
273
+ const createMutation = `
274
+ mutation($workspaceId: ID!, $input: CreateAgentInput!) {
275
+ createAgent(workspaceId: $workspaceId, input: $input) {
276
+ id
277
+ }
278
+ }
279
+ `;
280
+ const created = await graphqlRequest(createMutation, {
281
+ workspaceId,
282
+ input: {
283
+ name: agentData.name + ' (Imported)',
284
+ description: agentData.description,
285
+ type: agentData.type,
286
+ color: agentData.color,
287
+ icon: agentData.icon
288
+ }
289
+ });
290
+ const newId = created.createAgent.id;
291
+ // 3. Save Flow
292
+ const saveFlowMutation = `
293
+ mutation($agentId: ID!, $input: SaveAgentFlowInput!) {
294
+ saveAgentFlow(agentId: $agentId, input: $input)
295
+ }
296
+ `;
297
+ await graphqlRequest(saveFlowMutation, {
298
+ agentId: newId,
299
+ input: {
300
+ nodes: agentData.flowNodes,
301
+ edges: agentData.flowEdges
302
+ }
303
+ });
304
+ spinner.succeed(chalk.green(`Agent imported successfully with ID: ${newId}`));
305
+ }
306
+ catch (error) {
307
+ spinner.fail(`Import failed: ${error.message}`);
308
+ }
309
+ });
310
+ agent
311
+ .command('run <id> <message>')
312
+ .description('Run a single command with an agent')
313
+ .action(async (id, message) => {
314
+ const spinner = ora('Executing...').start();
315
+ try {
316
+ const response = await api.post(`/api/run/${id}`, {
317
+ messages: [{ role: 'user', content: message }],
318
+ }, {
319
+ responseType: 'stream',
320
+ });
321
+ spinner.stop();
322
+ process.stdout.write(chalk.cyan('Agent: '));
323
+ const stream = response.data;
324
+ await new Promise((resolve, reject) => {
325
+ let buffer = '';
326
+ stream.on('data', (chunk) => {
327
+ buffer += chunk.toString();
328
+ const lines = buffer.split('\n');
329
+ buffer = lines.pop() || '';
330
+ for (const line of lines) {
331
+ if (!line.trim())
332
+ continue;
333
+ try {
334
+ const data = JSON.parse(line);
335
+ if (data.type === 'text-delta' || data.type === 'text') {
336
+ process.stdout.write(data.textDelta || data.text);
337
+ }
338
+ }
339
+ catch (e) { }
340
+ }
341
+ });
342
+ stream.on('end', () => {
343
+ process.stdout.write('\n');
344
+ resolve();
345
+ });
346
+ stream.on('error', reject);
347
+ });
348
+ }
349
+ catch (error) {
350
+ spinner.fail(`Error: ${error.message}`);
351
+ }
352
+ });
353
+ agent
354
+ .command('set-model <id> <model>')
355
+ .description('Set the LLM model for an agent')
356
+ .action(async (id, model) => {
357
+ const spinner = ora('Updating model...').start();
358
+ try {
359
+ // Update the agent's main model type
360
+ const mutation = `
361
+ mutation($id: ID!, $input: UpdateAgentInput!) {
362
+ updateAgent(id: $id, input: $input) {
363
+ id
364
+ type
365
+ }
366
+ }
367
+ `;
368
+ await graphqlRequest(mutation, { id, input: { type: model } });
369
+ // Also try to find a 'model' node in the flow and update it
370
+ const flowData = await getAgentWithFlow(id);
371
+ const modelNode = (flowData?.flowNodes || []).find((n) => n.nodeType === 'model');
372
+ if (modelNode) {
373
+ const updateNodeMutation = `
374
+ mutation($agentId: ID!, $nodeId: String!, $input: UpdateFlowNodeInput!) {
375
+ updateFlowNode(agentId: $agentId, nodeId: $nodeId, input: $input) {
376
+ nodeId
377
+ }
378
+ }
379
+ `;
380
+ await graphqlRequest(updateNodeMutation, {
381
+ agentId: id,
382
+ nodeId: modelNode.nodeId,
383
+ input: { data: { ...(modelNode.data || {}), modelName: model } }
384
+ });
385
+ }
386
+ spinner.succeed(chalk.green(`Agent model set to ${model}`));
387
+ }
388
+ catch (error) {
389
+ spinner.fail(`Failed to update model: ${error.message}`);
390
+ }
391
+ });
392
+ agent
393
+ .command('set-prompt <id> <prompt>')
394
+ .description('Set the system prompt for an agent')
395
+ .action(async (id, prompt) => {
396
+ const spinner = ora('Updating system prompt...').start();
397
+ try {
398
+ const flowData = await getAgentWithFlow(id);
399
+ const systemPromptNode = (flowData?.flowNodes || []).find((n) => n.nodeType === 'system_prompt');
400
+ if (!systemPromptNode) {
401
+ spinner.fail('No system_prompt node found for this agent.');
402
+ return;
403
+ }
404
+ const updateNodeMutation = `
405
+ mutation($agentId: ID!, $nodeId: String!, $input: UpdateFlowNodeInput!) {
406
+ updateFlowNode(agentId: $agentId, nodeId: $nodeId, input: $input) {
407
+ nodeId
408
+ }
409
+ }
410
+ `;
411
+ await graphqlRequest(updateNodeMutation, {
412
+ agentId: id,
413
+ nodeId: systemPromptNode.nodeId,
414
+ input: { data: { ...(systemPromptNode.data || {}), systemPrompt: prompt } }
415
+ });
416
+ spinner.succeed(chalk.green('System prompt updated successfully.'));
417
+ }
418
+ catch (error) {
419
+ spinner.fail(`Failed to update prompt: ${error.message}`);
420
+ }
421
+ });
422
+ agent
423
+ .command('get <id>')
424
+ .description('Get full agent data (including flow nodes/edges)')
425
+ .option('--nodes', 'Return only flowNodes')
426
+ .option('--edges', 'Return only flowEdges')
427
+ .option('--tools', 'Return only toolLinks')
428
+ .option('--kbs', 'Return only knowledgeBaseLinks')
429
+ .action(async (id, options) => {
430
+ const spinner = !isJson() ? ora('Fetching agent...').start() : null;
431
+ try {
432
+ const data = await getAgentWithFlow(id);
433
+ if (spinner)
434
+ spinner.stop();
435
+ if (options.nodes)
436
+ return output(data.flowNodes);
437
+ if (options.edges)
438
+ return output(data.flowEdges);
439
+ if (options.tools)
440
+ return output(data.toolLinks);
441
+ if (options.kbs)
442
+ return output(data.knowledgeBaseLinks);
443
+ return output(data);
444
+ }
445
+ catch (error) {
446
+ if (spinner)
447
+ spinner.fail(`Failed to fetch agent: ${error.message}`);
448
+ else
449
+ outputError(error);
450
+ }
451
+ });
452
+ agent
453
+ .command('update <id>')
454
+ .description('Update agent metadata (name/description/type/color/icon/mode)')
455
+ .option('--input <json>', 'Inline JSON object with UpdateAgentInput')
456
+ .option('--input-file <path>', 'Read UpdateAgentInput JSON from a file')
457
+ .option('--input-stdin', 'Read UpdateAgentInput JSON from stdin')
458
+ .action(async (id, options) => {
459
+ const spinner = !isJson() ? ora('Updating agent...').start() : null;
460
+ try {
461
+ const input = await readJsonInput({
462
+ input: options.input,
463
+ inputFile: options.inputFile,
464
+ inputStdin: options.inputStdin,
465
+ });
466
+ const mutation = `
467
+ mutation($id: ID!, $input: UpdateAgentInput!) {
468
+ updateAgent(id: $id, input: $input) {
469
+ id
470
+ name
471
+ description
472
+ type
473
+ color
474
+ icon
475
+ status
476
+ createdAt
477
+ updatedAt
478
+ }
479
+ }
480
+ `;
481
+ const data = await graphqlRequest(mutation, { id, input });
482
+ if (spinner)
483
+ spinner.stop();
484
+ output(data.updateAgent);
485
+ }
486
+ catch (error) {
487
+ if (spinner)
488
+ spinner.fail(`Failed to update agent: ${error.message}`);
489
+ else
490
+ outputError(error);
491
+ }
492
+ });
493
+ agent
494
+ .command('replace <id>')
495
+ .description('Replace agent metadata and flow from JSON export')
496
+ .option('--input <json>', 'Inline JSON object (agent export format)')
497
+ .option('--input-file <path>', 'Read agent export JSON from file')
498
+ .option('--input-stdin', 'Read agent export JSON from stdin')
499
+ .action(async (id, options) => {
500
+ const spinner = !isJson() ? ora('Replacing agent...').start() : null;
501
+ try {
502
+ const exported = await readJsonInput({
503
+ input: options.input,
504
+ inputFile: options.inputFile,
505
+ inputStdin: options.inputStdin,
506
+ });
507
+ const updateInput = {};
508
+ if (exported.name !== undefined)
509
+ updateInput.name = exported.name;
510
+ if (exported.description !== undefined)
511
+ updateInput.description = exported.description;
512
+ if (exported.type !== undefined)
513
+ updateInput.type = exported.type;
514
+ if (exported.color !== undefined)
515
+ updateInput.color = exported.color;
516
+ if (exported.icon !== undefined)
517
+ updateInput.icon = exported.icon;
518
+ if (exported.mode !== undefined)
519
+ updateInput.mode = exported.mode;
520
+ const updateMutation = `
521
+ mutation($id: ID!, $input: UpdateAgentInput!) {
522
+ updateAgent(id: $id, input: $input) { id }
523
+ }
524
+ `;
525
+ await graphqlRequest(updateMutation, { id, input: updateInput });
526
+ const flow = normalizeFlow(exported);
527
+ await saveAgentFlow(id, flow);
528
+ if (spinner)
529
+ spinner.stop();
530
+ output({ ok: true });
531
+ }
532
+ catch (error) {
533
+ if (spinner)
534
+ spinner.fail(`Failed to replace agent: ${error.message}`);
535
+ else
536
+ outputError(error);
537
+ }
538
+ });
539
+ const flow = agent.command('flow').description('Agent flow operations (nodes + edges)');
540
+ flow
541
+ .command('apply <agentId>')
542
+ .description('Apply flow JSON (overwrites nodes and edges)')
543
+ .option('--input <json>', 'Inline JSON for {nodes,edges} or {flowNodes,flowEdges}')
544
+ .option('--input-file <path>', 'Read flow JSON from file')
545
+ .option('--input-stdin', 'Read flow JSON from stdin')
546
+ .action(async (agentId, options) => {
547
+ const spinner = !isJson() ? ora('Saving agent flow...').start() : null;
548
+ try {
549
+ const data = await readJsonInput({
550
+ input: options.input,
551
+ inputFile: options.inputFile,
552
+ inputStdin: options.inputStdin,
553
+ });
554
+ const normalized = normalizeFlow(data);
555
+ await saveAgentFlow(agentId, normalized);
556
+ if (spinner)
557
+ spinner.stop();
558
+ output({ ok: true });
559
+ }
560
+ catch (error) {
561
+ if (spinner)
562
+ spinner.fail(`Failed to save flow: ${error.message}`);
563
+ else
564
+ outputError(error);
565
+ }
566
+ });
567
+ const node = agent.command('node').description('Flow node operations');
568
+ node
569
+ .command('get <agentId> <nodeId>')
570
+ .description('Get a specific flow node by nodeId')
571
+ .action(async (agentId, nodeId) => {
572
+ const spinner = !isJson() ? ora('Fetching node...').start() : null;
573
+ try {
574
+ const query = `
575
+ query($agentId: ID!) {
576
+ agentFlowNodes(agentId: $agentId) {
577
+ nodeId
578
+ nodeType
579
+ label
580
+ positionX
581
+ positionY
582
+ data
583
+ createdAt
584
+ updatedAt
585
+ }
586
+ }
587
+ `;
588
+ const data = await graphqlRequest(query, { agentId });
589
+ const found = (data.agentFlowNodes || []).find((n) => n.nodeId === nodeId);
590
+ if (!found)
591
+ throw new Error(`Node not found: ${nodeId}`);
592
+ if (spinner)
593
+ spinner.stop();
594
+ output(found);
595
+ }
596
+ catch (error) {
597
+ if (spinner)
598
+ spinner.fail(`Failed to fetch node: ${error.message}`);
599
+ else
600
+ outputError(error);
601
+ }
602
+ });
603
+ node
604
+ .command('create <agentId>')
605
+ .description('Create a flow node')
606
+ .requiredOption('--type <nodeType>', 'nodeType')
607
+ .option('--nodeId <nodeId>', 'nodeId (defaults to random UUID)')
608
+ .option('--label <label>', 'label (defaults to nodeType)')
609
+ .option('--x <x>', 'positionX', '0')
610
+ .option('--y <y>', 'positionY', '0')
611
+ .option('--data <json>', 'Inline JSON for node data')
612
+ .option('--data-file <path>', 'Read node data JSON from file')
613
+ .option('--data-stdin', 'Read node data JSON from stdin')
614
+ .action(async (agentId, options) => {
615
+ const spinner = !isJson() ? ora('Creating node...').start() : null;
616
+ try {
617
+ const nodeId = options.nodeId || randomUUID();
618
+ const data = options.data !== undefined || options.dataFile || options.dataStdin
619
+ ? await readJsonInput({
620
+ input: options.data,
621
+ inputFile: options.dataFile,
622
+ inputStdin: options.dataStdin,
623
+ })
624
+ : undefined;
625
+ const mutation = `
626
+ mutation($agentId: ID!, $input: CreateFlowNodeInput!) {
627
+ createFlowNode(agentId: $agentId, input: $input) {
628
+ nodeId
629
+ nodeType
630
+ label
631
+ positionX
632
+ positionY
633
+ data
634
+ }
635
+ }
636
+ `;
637
+ const result = await graphqlRequest(mutation, {
638
+ agentId,
639
+ input: {
640
+ nodeId,
641
+ nodeType: options.type,
642
+ label: options.label || options.type,
643
+ positionX: Number(options.x),
644
+ positionY: Number(options.y),
645
+ data,
646
+ },
647
+ });
648
+ if (spinner)
649
+ spinner.stop();
650
+ output(result.createFlowNode);
651
+ }
652
+ catch (error) {
653
+ if (spinner)
654
+ spinner.fail(`Failed to create node: ${error.message}`);
655
+ else
656
+ outputError(error);
657
+ }
658
+ });
659
+ node
660
+ .command('update <agentId> <nodeId>')
661
+ .description('Update a flow node (label/position/data)')
662
+ .option('--label <label>', 'Update label')
663
+ .option('--x <x>', 'Update positionX')
664
+ .option('--y <y>', 'Update positionY')
665
+ .option('--data <json>', 'Inline JSON for node data')
666
+ .option('--data-file <path>', 'Read node data JSON from file')
667
+ .option('--data-stdin', 'Read node data JSON from stdin')
668
+ .option('--merge', 'Merge provided data into existing node data instead of replacing')
669
+ .action(async (agentId, nodeId, options) => {
670
+ const spinner = !isJson() ? ora('Updating node...').start() : null;
671
+ try {
672
+ let dataUpdate = undefined;
673
+ const hasDataInput = options.data !== undefined || options.dataFile || options.dataStdin;
674
+ if (hasDataInput) {
675
+ dataUpdate = await readJsonInput({
676
+ input: options.data,
677
+ inputFile: options.dataFile,
678
+ inputStdin: options.dataStdin,
679
+ });
680
+ }
681
+ if (options.merge && hasDataInput) {
682
+ const query = `
683
+ query($agentId: ID!) {
684
+ agentFlowNodes(agentId: $agentId) {
685
+ nodeId
686
+ data
687
+ }
688
+ }
689
+ `;
690
+ const current = await graphqlRequest(query, { agentId });
691
+ const found = (current.agentFlowNodes || []).find((n) => n.nodeId === nodeId);
692
+ dataUpdate = { ...(found?.data || {}), ...(dataUpdate || {}) };
693
+ }
694
+ const mutation = `
695
+ mutation($agentId: ID!, $nodeId: String!, $input: UpdateFlowNodeInput!) {
696
+ updateFlowNode(agentId: $agentId, nodeId: $nodeId, input: $input) {
697
+ nodeId
698
+ nodeType
699
+ label
700
+ positionX
701
+ positionY
702
+ data
703
+ updatedAt
704
+ }
705
+ }
706
+ `;
707
+ const input = {};
708
+ if (options.label !== undefined)
709
+ input.label = options.label;
710
+ if (options.x !== undefined)
711
+ input.positionX = Number(options.x);
712
+ if (options.y !== undefined)
713
+ input.positionY = Number(options.y);
714
+ if (hasDataInput)
715
+ input.data = dataUpdate;
716
+ const result = await graphqlRequest(mutation, { agentId, nodeId, input });
717
+ if (spinner)
718
+ spinner.stop();
719
+ output(result.updateFlowNode);
720
+ }
721
+ catch (error) {
722
+ if (spinner)
723
+ spinner.fail(`Failed to update node: ${error.message}`);
724
+ else
725
+ outputError(error);
726
+ }
727
+ });
728
+ node
729
+ .command('delete <agentId> <nodeId>')
730
+ .description('Delete a flow node')
731
+ .action(async (agentId, nodeId) => {
732
+ const spinner = !isJson() ? ora('Deleting node...').start() : null;
733
+ try {
734
+ const mutation = `
735
+ mutation($agentId: ID!, $nodeId: String!) {
736
+ deleteFlowNode(agentId: $agentId, nodeId: $nodeId)
737
+ }
738
+ `;
739
+ const result = await graphqlRequest(mutation, { agentId, nodeId });
740
+ if (spinner)
741
+ spinner.stop();
742
+ output({ ok: Boolean(result.deleteFlowNode) });
743
+ }
744
+ catch (error) {
745
+ if (spinner)
746
+ spinner.fail(`Failed to delete node: ${error.message}`);
747
+ else
748
+ outputError(error);
749
+ }
750
+ });
751
+ const edge = agent.command('edge').description('Flow edge operations');
752
+ edge
753
+ .command('get <agentId> <edgeId>')
754
+ .description('Get a specific flow edge by edgeId')
755
+ .action(async (agentId, edgeId) => {
756
+ const spinner = !isJson() ? ora('Fetching edge...').start() : null;
757
+ try {
758
+ const query = `
759
+ query($agentId: ID!) {
760
+ agentFlowEdges(agentId: $agentId) {
761
+ edgeId
762
+ sourceId
763
+ targetId
764
+ edgeType
765
+ data
766
+ createdAt
767
+ updatedAt
768
+ }
769
+ }
770
+ `;
771
+ const data = await graphqlRequest(query, { agentId });
772
+ const found = (data.agentFlowEdges || []).find((e) => e.edgeId === edgeId);
773
+ if (!found)
774
+ throw new Error(`Edge not found: ${edgeId}`);
775
+ if (spinner)
776
+ spinner.stop();
777
+ output(found);
778
+ }
779
+ catch (error) {
780
+ if (spinner)
781
+ spinner.fail(`Failed to fetch edge: ${error.message}`);
782
+ else
783
+ outputError(error);
784
+ }
785
+ });
786
+ edge
787
+ .command('add <agentId>')
788
+ .description('Add or replace a flow edge (link nodes)')
789
+ .requiredOption('--source <nodeId>', 'source nodeId')
790
+ .requiredOption('--target <nodeId>', 'target nodeId')
791
+ .option('--edgeId <edgeId>', 'edgeId (defaults to random UUID)')
792
+ .option('--type <edgeType>', 'edgeType')
793
+ .option('--data <json>', 'Inline JSON for edge data')
794
+ .option('--data-file <path>', 'Read edge data JSON from file')
795
+ .option('--data-stdin', 'Read edge data JSON from stdin')
796
+ .action(async (agentId, options) => {
797
+ const spinner = !isJson() ? ora('Saving edge...').start() : null;
798
+ try {
799
+ const edgeId = options.edgeId || randomUUID();
800
+ const edgeData = options.data !== undefined || options.dataFile || options.dataStdin
801
+ ? await readJsonInput({
802
+ input: options.data,
803
+ inputFile: options.dataFile,
804
+ inputStdin: options.dataStdin,
805
+ })
806
+ : undefined;
807
+ const agentData = await getAgentWithFlow(agentId);
808
+ const flow = normalizeFlow(agentData);
809
+ const nextEdges = (flow.edges || []).filter((e) => e.edgeId !== edgeId);
810
+ nextEdges.push({
811
+ edgeId,
812
+ sourceId: options.source,
813
+ targetId: options.target,
814
+ edgeType: options.type ?? null,
815
+ data: edgeData ?? undefined,
816
+ });
817
+ await saveAgentFlow(agentId, { nodes: flow.nodes, edges: nextEdges });
818
+ if (spinner)
819
+ spinner.stop();
820
+ output({ ok: true, edgeId });
821
+ }
822
+ catch (error) {
823
+ if (spinner)
824
+ spinner.fail(`Failed to save edge: ${error.message}`);
825
+ else
826
+ outputError(error);
827
+ }
828
+ });
829
+ edge
830
+ .command('delete <agentId> <edgeId>')
831
+ .description('Delete a flow edge by edgeId (unlink nodes)')
832
+ .action(async (agentId, edgeId) => {
833
+ const spinner = !isJson() ? ora('Deleting edge...').start() : null;
834
+ try {
835
+ const agentData = await getAgentWithFlow(agentId);
836
+ const flow = normalizeFlow(agentData);
837
+ const nextEdges = (flow.edges || []).filter((e) => e.edgeId !== edgeId);
838
+ if (nextEdges.length === (flow.edges || []).length) {
839
+ throw new Error(`Edge not found: ${edgeId}`);
840
+ }
841
+ await saveAgentFlow(agentId, { nodes: flow.nodes, edges: nextEdges });
842
+ if (spinner)
843
+ spinner.stop();
844
+ output({ ok: true });
845
+ }
846
+ catch (error) {
847
+ if (spinner)
848
+ spinner.fail(`Failed to delete edge: ${error.message}`);
849
+ else
850
+ outputError(error);
851
+ }
852
+ });
853
+ }