Skip to content

Commit 48920d0

Browse files
committed
Add SSE streaming support, improve project name handling, and fix linting errors
1 parent cdde203 commit 48920d0

2 files changed

Lines changed: 2243 additions & 48 deletions

File tree

nodes/CloudCli/CloudCli.node.ts

Lines changed: 170 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class CloudCli implements INodeType {
2424
inputs: [NodeConnectionTypes.Main],
2525
outputs: [NodeConnectionTypes.Main],
2626
usableAsTool: true,
27+
documentationUrl: 'https://developer.cloudcli.ai',
2728
credentials: [
2829
{
2930
name: 'cloudCliApi',
@@ -63,37 +64,37 @@ export class CloudCli implements INodeType {
6364
{
6465
name: 'Create',
6566
value: 'create',
66-
description: 'Create a new development environment',
67+
description: 'Create a new development environment. <a href="https://developer.cloudcli.ai/create-environment-3998768e0" target="_blank">Documentation</a>.',
6768
action: 'Create an environment',
6869
},
6970
{
7071
name: 'Delete',
7172
value: 'delete',
72-
description: 'Delete an environment (must be stopped first)',
73+
description: 'Delete an environment (must be stopped first). <a href="https://developer.cloudcli.ai/delete-environment-3998770e0" target="_blank">Documentation</a>.',
7374
action: 'Delete an environment',
7475
},
7576
{
7677
name: 'Get',
7778
value: 'get',
78-
description: 'Get details of a specific environment',
79+
description: 'Get details of a specific environment. <a href="https://developer.cloudcli.ai/get-environment-3998769e0" target="_blank">Documentation</a>.',
7980
action: 'Get an environment',
8081
},
8182
{
8283
name: 'Get Many',
8384
value: 'list',
84-
description: 'Retrieve a list of environments',
85+
description: 'Retrieve a list of environments. <a href="https://developer.cloudcli.ai/list-environments-3998767e0" target="_blank">Documentation</a>.',
8586
action: 'Get many environments',
8687
},
8788
{
8889
name: 'Start',
8990
value: 'start',
90-
description: 'Start a stopped environment',
91+
description: 'Start a stopped environment. <a href="https://developer.cloudcli.ai/start-environment-3998771e0" target="_blank">Documentation</a>.',
9192
action: 'Start an environment',
9293
},
9394
{
9495
name: 'Stop',
9596
value: 'stop',
96-
description: 'Stop a running environment',
97+
description: 'Stop a running environment. <a href="https://developer.cloudcli.ai/stop-environment-3998772e0" target="_blank">Documentation</a>.',
9798
action: 'Stop an environment',
9899
},
99100
],
@@ -305,21 +306,6 @@ export class CloudCli implements INodeType {
305306
},
306307
],
307308
},
308-
{
309-
displayName: 'Project Name',
310-
name: 'projectName',
311-
type: 'string',
312-
required: true,
313-
displayOptions: {
314-
show: {
315-
resource: ['agent'],
316-
operation: ['execute'],
317-
},
318-
},
319-
default: '={{$parameter["agentEnvironmentId"].cachedResultName?.split(" ")[0] || ""}}',
320-
placeholder: 'e.g. backend',
321-
description: 'Name of the project inside /workspace/ directory. Defaults to the environment subdomain. Only change this if you know what you are doing.',
322-
},
323309
{
324310
displayName: 'Message',
325311
name: 'message',
@@ -403,6 +389,58 @@ export class CloudCli implements INodeType {
403389
default: '',
404390
description: 'GitHub token for private repos or PR creation',
405391
},
392+
{
393+
displayName: 'Project Name (Path)',
394+
name: 'projectName',
395+
type: 'resourceLocator',
396+
default: { mode: 'list', value: '' },
397+
description: 'Name of the project inside /workspace/ directory. This is usually the environment name without spaces. Leave empty to auto-select from environment.',
398+
modes: [
399+
{
400+
displayName: 'From List',
401+
name: 'list',
402+
type: 'list',
403+
placeholder: 'Select a project...',
404+
typeOptions: {
405+
searchListMethod: 'searchProjectName',
406+
},
407+
},
408+
{
409+
displayName: 'By Path',
410+
name: 'path',
411+
type: 'string',
412+
placeholder: 'e.g. MyProject',
413+
validation: [
414+
{
415+
type: 'regex',
416+
properties: {
417+
regex: '^[a-zA-Z0-9_-]+$',
418+
errorMessage: 'Project path must not contain spaces',
419+
},
420+
},
421+
],
422+
},
423+
],
424+
},
425+
{
426+
displayName: 'Return Format',
427+
name: 'returnFormat',
428+
type: 'options',
429+
options: [
430+
{
431+
name: 'Single Item with Events Array',
432+
value: 'single',
433+
description: 'Return all events in a single item as {"events": [...]}',
434+
},
435+
{
436+
name: 'Multiple Items (One Per Event)',
437+
value: 'multiple',
438+
description: 'Return each event as a separate item for easier filtering and processing',
439+
},
440+
],
441+
default: 'single',
442+
description: 'Choose how to return the streaming events in the workflow',
443+
},
406444
],
407445
},
408446
],
@@ -445,6 +483,44 @@ export class CloudCli implements INodeType {
445483

446484
return { results };
447485
},
486+
async searchProjectName(
487+
this: ILoadOptionsFunctions,
488+
): Promise<INodeListSearchResult> {
489+
const environmentId = this.getNodeParameter('agentEnvironmentId.value') as string;
490+
491+
if (!environmentId) {
492+
return { results: [] };
493+
}
494+
495+
const credentials = await this.getCredentials('cloudCliApi');
496+
const baseUrl = credentials.host as string;
497+
498+
try {
499+
const response = await this.helpers.httpRequest({
500+
method: 'GET',
501+
url: `${baseUrl}/environments/${environmentId}`,
502+
headers: {
503+
'X-API-KEY': credentials.apiKey as string,
504+
},
505+
json: true,
506+
});
507+
508+
const environmentName = (response.name as string) || '';
509+
// Remove spaces from the project name
510+
const projectName = environmentName.replace(/\s+/g, '');
511+
512+
const results = [
513+
{
514+
name: projectName,
515+
value: projectName,
516+
},
517+
];
518+
519+
return { results };
520+
} catch {
521+
return { results: [] };
522+
}
523+
},
448524
},
449525
};
450526

@@ -556,10 +632,28 @@ export class CloudCli implements INodeType {
556632
if (operation === 'execute') {
557633
const environmentIdValue = this.getNodeParameter('agentEnvironmentId', itemIndex) as { value: string };
558634
const environmentId = environmentIdValue.value;
559-
const projectName = this.getNodeParameter('projectName', itemIndex) as string;
635+
const additionalOptions = this.getNodeParameter('additionalOptions', itemIndex, {}) as IDataObject;
636+
637+
// Get project name, default to environment name without spaces if not provided
638+
let projectName = '';
639+
const projectNameValue = additionalOptions.projectName as { value: string } | undefined;
640+
641+
if (projectNameValue && projectNameValue.value) {
642+
projectName = projectNameValue.value;
643+
} else {
644+
// Fetch environment details to get the name
645+
const envResponse = await this.helpers.httpRequest({
646+
method: 'GET',
647+
url: `${baseUrl}/environments/${environmentId}`,
648+
headers,
649+
json: true,
650+
});
651+
const environmentName = (envResponse.name as string) || '';
652+
projectName = environmentName.replace(/\s+/g, '');
653+
}
654+
560655
const message = this.getNodeParameter('message', itemIndex) as string;
561656
const provider = this.getNodeParameter('provider', itemIndex) as string;
562-
const additionalOptions = this.getNodeParameter('additionalOptions', itemIndex, {}) as IDataObject;
563657

564658
const body: IDataObject = {
565659
environmentId,
@@ -578,14 +672,64 @@ export class CloudCli implements INodeType {
578672
body.githubToken = additionalOptions.githubToken;
579673
}
580674

581-
responseData = await this.helpers.httpRequest({
675+
// Get the raw SSE stream response
676+
const rawResponse = await this.helpers.httpRequest({
582677
method: 'POST',
583678
url: `${baseUrl}/agent/execute`,
584-
headers,
679+
headers: {
680+
...headers,
681+
'Content-Type': 'application/json',
682+
},
585683
body,
586-
json: true,
684+
json: false, // Get raw text response
587685
timeout: 600000, // 10 minutes timeout for long-running agent tasks
588686
});
687+
688+
// Parse SSE stream into events array
689+
try {
690+
const events: IDataObject[] = [];
691+
const lines = (rawResponse as string).split('\n');
692+
693+
for (const line of lines) {
694+
// SSE format: each event starts with "data: "
695+
if (line.startsWith('data: ')) {
696+
const jsonStr = line.substring(6).trim(); // Remove 'data: ' prefix
697+
if (jsonStr) {
698+
try {
699+
const eventData = JSON.parse(jsonStr);
700+
events.push(eventData);
701+
} catch {
702+
// Skip lines that aren't valid JSON
703+
continue;
704+
}
705+
}
706+
}
707+
// Skip empty lines, comments (starting with ':'), and other SSE fields
708+
}
709+
710+
// Check return format preference
711+
const returnFormat = additionalOptions.returnFormat || 'single';
712+
713+
if (returnFormat === 'multiple') {
714+
// Return each event as a separate item
715+
for (const event of events) {
716+
returnData.push({
717+
json: event,
718+
pairedItem: itemIndex,
719+
});
720+
}
721+
continue; // Skip the default push at the end
722+
} else {
723+
// Return all events in a single item
724+
responseData = { events };
725+
}
726+
} catch (error) {
727+
// If parsing fails completely, return the raw response for debugging
728+
responseData = {
729+
raw_response: rawResponse,
730+
parse_error: error.message,
731+
};
732+
}
589733
} else {
590734
throw new NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, {
591735
itemIndex,

0 commit comments

Comments
 (0)