@nlxai/cli 1.2.4 → 1.2.6

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 CHANGED
@@ -41,6 +41,10 @@ Commands:
41
41
  data-requests Data Requests
42
42
  data-requests sync [opts] <input> Sync Data Requests from an OpenAPI Specification or Swagger
43
43
  test [options] <applicationId> Run conversation tests for a given application ID
44
+ code Work with code nodes in NLX flows
45
+ code init Initialize local folders for flows with code nodes
46
+ code pull [options] Pull code nodes from a flow into local files
47
+ code push [options] Push local code node files to NLX flow
44
48
  http [options] <method> <path> [body] Perform an authenticated request to the management API
45
49
  help [command] display help for command
46
50
 
@@ -94,6 +98,22 @@ Transcript:
94
98
  └───────┘
95
99
 
96
100
  Debug at: https://dev.platform.nlx.ai/flows/SimpleCarousel
101
+
102
+
103
+ > nlx code init
104
+ ℹ Fetching available flows...
105
+ ✔ Select flows to initialize: ChattyKathy
106
+ ✔ Created folder ChattyKathy
107
+ ℹ Fetching flow from conversationTrees/ChattyKathy-Omni
108
+ ✔ Created/Updated 74af8e42-4c30-4ce0-aaec-bb2b155f2f95.js
109
+
110
+ > cd ChattyKathy
111
+
112
+ > echo "export default async (input, system) => { console.log('Hello world'); }" > 74af8e42-4c30-4ce0-aaec-bb2b155f2f95.js
113
+
114
+ > nlx code push
115
+ ✔ Updated code for node 74af8e42-4c30-4ce0-aaec-bb2b155f2f95
116
+ ✔ Pushed 1 code node(s) to flow.
97
117
  ```
98
118
 
99
119
  ## Running in CI
@@ -0,0 +1,9 @@
1
+ import { Command } from "commander";
2
+ import { pullCommand } from "./pull.js";
3
+ import { pushCommand } from "./push.js";
4
+ import { initCommand } from "./init.js";
5
+ export const codeCommand = new Command("code")
6
+ .description("Work with code nodes in NLX flows")
7
+ .addCommand(initCommand)
8
+ .addCommand(pullCommand)
9
+ .addCommand(pushCommand);
@@ -0,0 +1,42 @@
1
+ import { fetchManagementApiPaginated, } from "../../utils/index.js";
2
+ import { consola } from "consola";
3
+ import { checkbox } from "@inquirer/prompts";
4
+ import * as fs from "fs";
5
+ import { pull } from "./pull.js";
6
+ import { Command } from "commander";
7
+ export default async function init() {
8
+ consola.info("Fetching available flows...");
9
+ const flows = await fetchManagementApiPaginated("conversationTrees");
10
+ // Filter flows with code nodes
11
+ const codeFlows = new Set();
12
+ for (const flow of flows) {
13
+ if (flow.nodes &&
14
+ Object.values(flow.nodes).some((node) => node.type === "code")) {
15
+ codeFlows.add(flow.intentId);
16
+ }
17
+ }
18
+ if (codeFlows.size === 0) {
19
+ consola.info("No flows with code nodes found.");
20
+ return;
21
+ }
22
+ // Interactive selection
23
+ const selected = await checkbox({
24
+ message: "Select flows to initialize:",
25
+ choices: Array.from(codeFlows).map((f) => ({ value: f, name: f })),
26
+ });
27
+ for (const flowId of selected) {
28
+ if (fs.existsSync(flowId)) {
29
+ consola.info(`Folder ${flowId} already exists. Skipping.`);
30
+ continue;
31
+ }
32
+ fs.mkdirSync(flowId);
33
+ consola.success(`Created folder ${flowId}`);
34
+ // Run pull in new folder
35
+ process.chdir(flowId);
36
+ await pull({ flowId });
37
+ process.chdir("..");
38
+ }
39
+ }
40
+ export const initCommand = new Command("init")
41
+ .description("Initialize local folders for flows with code nodes")
42
+ .action(init);
@@ -0,0 +1,86 @@
1
+ import { Command } from "commander";
2
+ import { fetchManagementApi } from "../../utils/index.js";
3
+ import { consola } from "consola";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import { tmpdir } from "os";
7
+ import { execSync } from "child_process";
8
+ // Helper: get current folder name
9
+ function getCurrentFolderName() {
10
+ return path.basename(process.cwd());
11
+ }
12
+ export async function pull({ flowId, force, interactive } = {}) {
13
+ const id = flowId || getCurrentFolderName();
14
+ const endpoint = `conversationTrees/${id}-Omni`;
15
+ consola.info(`Fetching flow from ${endpoint}`);
16
+ const flow = await fetchManagementApi(endpoint);
17
+ if (!flow.nodes) {
18
+ consola.error("No nodes found in flow.");
19
+ process.exit(1);
20
+ }
21
+ let codeNodeCount = 0;
22
+ for (const nodeId in flow.nodes) {
23
+ const node = flow.nodes[nodeId];
24
+ if (node.type === "code" && node.metadata?.code?.code) {
25
+ codeNodeCount++;
26
+ const fileName = `${nodeId}.js`;
27
+ if (fs.existsSync(fileName)) {
28
+ const existing = fs.readFileSync(fileName, "utf8");
29
+ if (existing.trim() === node.metadata.code.code.trim()) {
30
+ consola.info(`File ${fileName} already up to date.`);
31
+ continue;
32
+ }
33
+ if (interactive) {
34
+ // Write remote code to temp file
35
+ const tempFile = path.join(tmpdir(), `${nodeId}-remote.js`);
36
+ let content = "";
37
+ if (node.metadata?.name) {
38
+ content += `// ${node.metadata.name}\n`;
39
+ }
40
+ content += node.metadata.code.code;
41
+ if (existing.trim() === content.trim()) {
42
+ consola.info(`File ${fileName} already up to date.`);
43
+ continue;
44
+ }
45
+ fs.writeFileSync(tempFile, content);
46
+ const mergeTool = process.env.NLX_CODE_MERGE_TOOL || "vimdiff";
47
+ consola.info(`Launching merge tool (${mergeTool}) for ${fileName}...`);
48
+ try {
49
+ execSync(`${mergeTool} ${fileName} ${tempFile}`, {
50
+ stdio: "inherit",
51
+ });
52
+ consola.success(`Merge completed for ${fileName}`);
53
+ }
54
+ catch (err) {
55
+ consola.error(`Merge tool failed for ${fileName}:`, err);
56
+ }
57
+ fs.unlinkSync(tempFile);
58
+ continue;
59
+ }
60
+ else if (force) {
61
+ consola.warn(`Overwriting ${fileName} without prompting.`);
62
+ }
63
+ else {
64
+ consola.error(`File ${fileName} already exists. Skipping.`);
65
+ continue;
66
+ }
67
+ }
68
+ let content = "";
69
+ if (node.metadata?.name) {
70
+ content += `// ${node.metadata.name}\n`;
71
+ }
72
+ content += node.metadata.code.code;
73
+ fs.writeFileSync(fileName, content);
74
+ consola.success(`Created/Updated ${fileName}`);
75
+ }
76
+ }
77
+ if (codeNodeCount === 0) {
78
+ consola.info("No code nodes found.");
79
+ }
80
+ }
81
+ export const pullCommand = new Command("pull")
82
+ .description("Pull code nodes from a flow")
83
+ .option("--flow <flowId>", "Specify flow ID (default: current folder name)")
84
+ .option("-f, --force", "Overwrite existing files without prompting", false)
85
+ .option("-i, --interactive", "Use git mergetool for interactive merge", false)
86
+ .action(pull);
@@ -0,0 +1,55 @@
1
+ import { fetchManagementApi } from "../../utils/index.js";
2
+ import { consola } from "consola";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ function getCurrentFolderName() {
6
+ return path.basename(process.cwd());
7
+ }
8
+ export async function push({ flowId } = {}) {
9
+ const id = flowId || getCurrentFolderName();
10
+ const endpoint = `conversationTrees/${id}-Omni`;
11
+ const flow = await fetchManagementApi(endpoint);
12
+ if (!flow.nodes) {
13
+ consola.error("No nodes found in flow.");
14
+ process.exit(1);
15
+ }
16
+ let updated = 0;
17
+ for (const nodeId in flow.nodes) {
18
+ const node = flow.nodes[nodeId];
19
+ if (node.type === "code" && node.metadata?.code?.code) {
20
+ const fileName = `${nodeId}.js`;
21
+ if (!fs.existsSync(fileName)) {
22
+ consola.warn(`File ${fileName} does not exist. Skipping.`);
23
+ continue;
24
+ }
25
+ let localCode = fs.readFileSync(fileName, "utf8");
26
+ // Remove leading comment if present and matches node name
27
+ const lines = localCode.split("\n");
28
+ if (node.metadata?.name &&
29
+ lines[0].trim() === `// ${node.metadata.name}`) {
30
+ localCode = lines.slice(1).join("\n");
31
+ }
32
+ if (localCode.trim() === node.metadata.code.code.trim()) {
33
+ consola.info(`Code for ${nodeId} is already up to date.`);
34
+ continue;
35
+ }
36
+ // Update node code
37
+ node.metadata.code.code = localCode;
38
+ updated++;
39
+ consola.success(`Updated code for node ${nodeId}`);
40
+ }
41
+ }
42
+ if (updated > 0) {
43
+ // Push updated flow
44
+ await fetchManagementApi(endpoint, "POST", flow);
45
+ consola.success(`Pushed ${updated} code node(s) to flow.`);
46
+ }
47
+ else {
48
+ consola.info("No code changes to push.");
49
+ }
50
+ }
51
+ import { Command } from "commander";
52
+ export const pushCommand = new Command("push")
53
+ .description("Push local code node files to NLX flow")
54
+ .option("--flow <flowId>", "Specify flow ID (default: current folder name)")
55
+ .action(push);
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import * as fs from "fs";
3
- import { fetchManagementApi } from "../utils/index.js";
3
+ import { fetchManagementApi, fetchManagementApiPaginated, } from "../utils/index.js";
4
4
  import { consola } from "consola";
5
5
  export const httpCommand = new Command("http")
6
6
  .description("Perform an authenticated request to the management API")
@@ -38,23 +38,12 @@ export const httpCommand = new Command("http")
38
38
  process.exit(1);
39
39
  }
40
40
  }
41
- let result = await fetchManagementApi(apiPath +
42
- (opts.paginate
43
- ? apiPath.includes("?")
44
- ? "&size=1000"
45
- : "?size=1000"
46
- : ""), method.toUpperCase(), body);
47
- let agg;
41
+ let result;
48
42
  if (opts.paginate) {
49
- const key = Object.keys(result).filter((k) => k !== "nextPageId")[0];
50
- agg = result[key];
51
- while (result.nextPageId) {
52
- result = await fetchManagementApi(apiPath + `?pageId=${result.nextPageId}`, method.toUpperCase(), body);
53
- agg.push(...result[key]);
54
- }
43
+ result = await fetchManagementApiPaginated(apiPath);
55
44
  }
56
45
  else {
57
- agg = result;
46
+ result = await fetchManagementApi(apiPath, method.toUpperCase(), body);
58
47
  }
59
- console.log(JSON.stringify(agg, null, 2));
48
+ console.log(JSON.stringify(result, null, 2));
60
49
  });
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ import { modalitiesCommand } from "./commands/modalities/index.js";
4
4
  import { dataRequestsCommand } from "./commands/data-requests/index.js";
5
5
  import { httpCommand } from "./commands/http.js";
6
6
  import { testCommand } from "./commands/test.js";
7
+ import { codeCommand } from "./commands/code/index.js";
7
8
  import { consola } from "consola";
8
9
  import { readFileSync } from "fs";
9
10
  import { resolve } from "path";
@@ -22,5 +23,6 @@ program.addCommand(authCommand);
22
23
  program.addCommand(modalitiesCommand);
23
24
  program.addCommand(dataRequestsCommand);
24
25
  program.addCommand(testCommand);
26
+ program.addCommand(codeCommand);
25
27
  program.addCommand(httpCommand);
26
28
  program.parse(process.argv);
@@ -20,6 +20,17 @@ export const fetchManagementApi = async (path, method = "GET", body) => {
20
20
  consola.debug("Response:", JSON.stringify(result));
21
21
  return result;
22
22
  };
23
+ export const fetchManagementApiPaginated = async (path) => {
24
+ let result = await fetchManagementApi(path + (path.includes("?") ? "&size=1000" : "?size=1000"));
25
+ let agg;
26
+ const key = Object.keys(result).filter((k) => k !== "nextPageId")[0];
27
+ agg = result[key];
28
+ while (result.nextPageId) {
29
+ result = await fetchManagementApi(path + `?page=${result.nextPageId}`);
30
+ agg.push(...result[key]);
31
+ }
32
+ return agg;
33
+ };
23
34
  export const singleton = (fn) => {
24
35
  let running = null;
25
36
  return async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nlxai/cli",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "description": "Tools for integrating with NLX apps",
5
5
  "keywords": [
6
6
  "NLX",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@inquirer/prompts": "^7.8.4",
37
- "@nlxai/core": "^1.2.4",
37
+ "@nlxai/core": "^1.2.6",
38
38
  "boxen": "^8.0.1",
39
39
  "chalk": "^4.1.0",
40
40
  "commander": "^14.0",
@@ -58,5 +58,5 @@
58
58
  "@vitest/ui": "^3.2.4",
59
59
  "vitest": "^3.2.4"
60
60
  },
61
- "gitHead": "bbfa1fad3f009a0f509903d6db4f51bec17b1f6c"
61
+ "gitHead": "fb9a0b00ca2184fb69ffd1e11e8f55d30fe1d85e"
62
62
  }