@hsafa/cli 1.0.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.
- package/README.md +72 -0
- package/dist/commands/agent.js +853 -0
- package/dist/commands/auth.js +168 -0
- package/dist/commands/chat.js +233 -0
- package/dist/commands/doc.js +37 -0
- package/dist/commands/gql.js +106 -0
- package/dist/commands/invite.js +147 -0
- package/dist/commands/kb.js +155 -0
- package/dist/commands/key.js +87 -0
- package/dist/commands/mcp.js +56 -0
- package/dist/commands/member.js +103 -0
- package/dist/commands/profile.js +65 -0
- package/dist/commands/project.js +90 -0
- package/dist/commands/system.js +27 -0
- package/dist/commands/user.js +48 -0
- package/dist/commands/workspace.js +77 -0
- package/dist/index.js +56 -0
- package/dist/templates/basic-assistant.json +57 -0
- package/dist/templates/researcher.json +58 -0
- package/dist/utils/api.js +13 -0
- package/dist/utils/config.js +22 -0
- package/dist/utils/graphql.js +17 -0
- package/package.json +43 -0
|
@@ -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
|
+
}
|