cisco-axl 2.0.0 → 2.1.2

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,134 @@
1
+ "use strict";
2
+
3
+ const { loadConfig, getConfigPath, getConfigDir } = require("../utils/config.js");
4
+ const { resolveConfig } = require("../utils/connection.js");
5
+
6
+ module.exports = function (program) {
7
+ program.command("doctor")
8
+ .description("Check AXL connectivity and configuration health")
9
+ .action(async (opts, command) => {
10
+ const globalOpts = command.optsWithGlobals();
11
+ let passed = 0;
12
+ let warned = 0;
13
+ let failed = 0;
14
+
15
+ const ok = (msg) => { console.log(` ✓ ${msg}`); passed++; };
16
+ const warn = (msg) => { console.log(` ⚠ ${msg}`); warned++; };
17
+ const fail = (msg) => { console.log(` ✗ ${msg}`); failed++; };
18
+
19
+ console.log("\n cisco-axl doctor");
20
+ console.log(" " + "─".repeat(50));
21
+
22
+ // 1. Configuration
23
+ console.log("\n Configuration");
24
+ let conn;
25
+ try {
26
+ const data = loadConfig();
27
+ if (!data.activeCluster) {
28
+ fail("No active cluster configured");
29
+ console.log(" Run: cisco-axl config add <name> --host <host> --username <user> --password <pass> --cucm-version <ver>");
30
+ printSummary(passed, warned, failed);
31
+ return;
32
+ }
33
+ ok(`Active cluster: ${data.activeCluster}`);
34
+ const cluster = data.clusters[data.activeCluster];
35
+ ok(`Host: ${cluster.host}`);
36
+ ok(`Username: ${cluster.username}`);
37
+ ok(`CUCM version: ${cluster.version}`);
38
+
39
+ if (cluster.insecure) warn("TLS verification: disabled (--insecure)");
40
+ else ok("TLS verification: enabled");
41
+
42
+ conn = await resolveConfig(globalOpts);
43
+ } catch (err) {
44
+ fail(`Config error: ${err.message}`);
45
+ printSummary(passed, warned, failed);
46
+ return;
47
+ }
48
+
49
+ // 2. AXL API connectivity
50
+ console.log("\n AXL API");
51
+ try {
52
+ if (conn.insecure) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; }
53
+ const axlService = require("../../dist/index.js");
54
+ const service = new axlService(conn.host, conn.username, conn.password, conn.version);
55
+
56
+ await service.testAuthentication();
57
+ ok(`AXL API: connected`);
58
+
59
+ // Try a simple SQL query to verify read access
60
+ try {
61
+ const sqlResult = await service.executeSqlQuery("SELECT COUNT(*) AS cnt FROM device");
62
+ const rows = Array.isArray(sqlResult) ? sqlResult : sqlResult?.row || [];
63
+ const count = rows[0]?.cnt || "?";
64
+ ok(`SQL query: ${count} device(s) in database`);
65
+ } catch {
66
+ warn("SQL query: could not query device table — may lack permissions");
67
+ }
68
+
69
+ // Check operation count
70
+ try {
71
+ const ops = await service.returnOperations();
72
+ const count = Array.isArray(ops) ? ops.length : 0;
73
+ ok(`AXL operations: ${count} available`);
74
+ } catch {
75
+ warn("Could not list AXL operations");
76
+ }
77
+ } catch (err) {
78
+ const msg = err.message || String(err);
79
+ if (msg.includes("401") || msg.includes("Authentication") || msg.includes("Unauthorized")) {
80
+ fail("AXL API: authentication failed — check username/password");
81
+ } else if (msg.includes("ECONNREFUSED")) {
82
+ fail("AXL API: connection refused — check host and port");
83
+ } else if (msg.includes("ENOTFOUND")) {
84
+ fail("AXL API: hostname not found — check host");
85
+ } else if (msg.includes("certificate")) {
86
+ fail("AXL API: TLS certificate error — try adding --insecure to the cluster config");
87
+ } else {
88
+ fail(`AXL API: ${msg}`);
89
+ }
90
+ }
91
+
92
+ // 3. Security
93
+ console.log("\n Security");
94
+ try {
95
+ const fs = require("node:fs");
96
+ const configPath = getConfigPath();
97
+ const stats = fs.statSync(configPath);
98
+ const mode = (stats.mode & 0o777).toString(8);
99
+ if (mode === "600") ok(`Config file permissions: ${mode} (secure)`);
100
+ else warn(`Config file permissions: ${mode} — should be 600. Run: chmod 600 ${configPath}`);
101
+ } catch { /* config file may not exist yet */ }
102
+
103
+ // 4. Audit trail
104
+ try {
105
+ const fs = require("node:fs");
106
+ const path = require("node:path");
107
+ const auditPath = path.join(getConfigDir(), "audit.jsonl");
108
+ if (fs.existsSync(auditPath)) {
109
+ const stats = fs.statSync(auditPath);
110
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(1);
111
+ ok(`Audit trail: ${sizeMB}MB`);
112
+ if (stats.size > 8 * 1024 * 1024) warn("Audit trail approaching 10MB rotation limit");
113
+ } else {
114
+ ok("Audit trail: empty (no operations logged yet)");
115
+ }
116
+ } catch { /* ignore */ }
117
+
118
+ printSummary(passed, warned, failed);
119
+ });
120
+
121
+ function printSummary(passed, warned, failed) {
122
+ console.log("\n " + "─".repeat(50));
123
+ console.log(` Results: ${passed} passed, ${warned} warning${warned !== 1 ? "s" : ""}, ${failed} failed`);
124
+ if (failed > 0) {
125
+ process.exitCode = 1;
126
+ console.log(" Status: issues found — review failures above");
127
+ } else if (warned > 0) {
128
+ console.log(" Status: healthy with warnings");
129
+ } else {
130
+ console.log(" Status: all systems healthy");
131
+ }
132
+ console.log("");
133
+ }
134
+ };
@@ -10,6 +10,7 @@
10
10
  const { createService } = require("../utils/connection.js");
11
11
  const { printResult, printError } = require("../utils/output.js");
12
12
  const { enforceReadOnly } = require("../utils/readonly.js");
13
+ const { readStdin } = require("../utils/stdin.js");
13
14
 
14
15
  /**
15
16
  * Registers the execute command on the given Commander program.
@@ -20,6 +21,7 @@ module.exports = function registerExecuteCommand(program) {
20
21
  .command("execute <operation>")
21
22
  .description("Execute a raw AXL operation with JSON tags")
22
23
  .option("--tags <json>", "JSON object of operation tags")
24
+ .option("--stdin", "read JSON tags from stdin (for piping)")
23
25
  .option("--template <file>", "JSON template file with %%var%% placeholders")
24
26
  .option("--vars <json>", "variables to resolve in template (JSON)")
25
27
  .option("--csv <file>", "CSV file for bulk operations (use with --template)")
@@ -32,14 +34,22 @@ module.exports = function registerExecuteCommand(program) {
32
34
  let errorMsg;
33
35
 
34
36
  try {
35
- enforceReadOnly(globalOpts, "execute");
37
+ await enforceReadOnly(globalOpts, "execute");
38
+
39
+ // Read from stdin if --stdin flag is set
40
+ if (cmdOpts.stdin) {
41
+ const stdinData = await readStdin();
42
+ if (!stdinData) throw new Error("--stdin specified but no data piped. Pipe JSON via: echo '{...}' | cisco-axl execute <op> --stdin");
43
+ cmdOpts.tags = stdinData.trim();
44
+ }
36
45
 
37
46
  // Validate mutual exclusivity
38
- if (cmdOpts.tags && cmdOpts.template) {
39
- throw new Error("--tags and --template are mutually exclusive");
47
+ const inputCount = [cmdOpts.tags, cmdOpts.template].filter(Boolean).length;
48
+ if (inputCount > 1) {
49
+ throw new Error("--tags, --stdin, and --template are mutually exclusive");
40
50
  }
41
- if (!cmdOpts.tags && !cmdOpts.template) {
42
- throw new Error("Either --tags or --template must be provided");
51
+ if (inputCount === 0) {
52
+ throw new Error("Provide input via --tags, --stdin, or --template");
43
53
  }
44
54
 
45
55
  const opts = {
@@ -24,7 +24,7 @@ module.exports = function registerRemoveCommand(program) {
24
24
  let errorMsg;
25
25
 
26
26
  try {
27
- enforceReadOnly(globalOpts, "remove");
27
+ await enforceReadOnly(globalOpts, "remove");
28
28
 
29
29
  const service = await createService(globalOpts);
30
30
  const opts = {
@@ -75,7 +75,7 @@ module.exports = function registerSqlCommand(program) {
75
75
  let errorMsg;
76
76
 
77
77
  try {
78
- enforceReadOnly(globalOpts, "sql update");
78
+ await enforceReadOnly(globalOpts, "sql update");
79
79
 
80
80
  const service = await createService(globalOpts);
81
81
  const opts = {
@@ -10,6 +10,7 @@
10
10
  const { createService } = require("../utils/connection.js");
11
11
  const { printResult, printError } = require("../utils/output.js");
12
12
  const { enforceReadOnly } = require("../utils/readonly.js");
13
+ const { readStdin } = require("../utils/stdin.js");
13
14
 
14
15
  /**
15
16
  * Registers the update command on the given Commander program.
@@ -20,6 +21,7 @@ module.exports = function registerUpdateCommand(program) {
20
21
  .command("update <type> [identifier]")
21
22
  .description("Update an existing AXL item by type and name or UUID")
22
23
  .option("--data <json>", "JSON object of fields to update")
24
+ .option("--stdin", "read JSON data from stdin (for piping)")
23
25
  .option("--template <file>", "JSON template file with %%var%% placeholders")
24
26
  .option("--vars <json>", "variables to resolve in template (JSON)")
25
27
  .option("--csv <file>", "CSV file for bulk operations (use with --template)")
@@ -32,14 +34,22 @@ module.exports = function registerUpdateCommand(program) {
32
34
  let errorMsg;
33
35
 
34
36
  try {
35
- enforceReadOnly(globalOpts, "update");
37
+ await enforceReadOnly(globalOpts, "update");
38
+
39
+ // Read from stdin if --stdin flag is set
40
+ if (cmdOpts.stdin) {
41
+ const stdinData = await readStdin();
42
+ if (!stdinData) throw new Error("--stdin specified but no data piped. Pipe JSON via: echo '{...}' | cisco-axl update <type> <id> --stdin");
43
+ cmdOpts.data = stdinData.trim();
44
+ }
36
45
 
37
46
  // Validate mutual exclusivity
38
- if (cmdOpts.data && cmdOpts.template) {
39
- throw new Error("--data and --template are mutually exclusive");
47
+ const inputCount = [cmdOpts.data, cmdOpts.template].filter(Boolean).length;
48
+ if (inputCount > 1) {
49
+ throw new Error("--data, --stdin, and --template are mutually exclusive");
40
50
  }
41
- if (!cmdOpts.data && !cmdOpts.template) {
42
- throw new Error("Either --data or --template must be provided");
51
+ if (inputCount === 0) {
52
+ throw new Error("Provide input via --data, --stdin, or --template");
43
53
  }
44
54
 
45
55
  const opts = {
package/cli/index.js CHANGED
@@ -1,14 +1,26 @@
1
1
  "use strict";
2
2
 
3
3
  const { Command } = require("commander");
4
- const { version } = require("../package.json");
4
+ const pkg = require("../package.json");
5
+
6
+ // Suppress Node.js TLS warning when --insecure is used
7
+ const originalEmitWarning = process.emitWarning;
8
+ process.emitWarning = (warning, ...args) => {
9
+ if (typeof warning === "string" && warning.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
10
+ originalEmitWarning.call(process, warning, ...args);
11
+ };
12
+
13
+ try {
14
+ const updateNotifier = require("update-notifier").default || require("update-notifier");
15
+ updateNotifier({ pkg }).notify();
16
+ } catch {};
5
17
 
6
18
  const program = new Command();
7
19
 
8
20
  program
9
21
  .name("cisco-axl")
10
22
  .description("CLI for Cisco CUCM AXL operations")
11
- .version(version)
23
+ .version(pkg.version)
12
24
  .option("--format <type>", "output format: table, json, toon, csv", "table")
13
25
  .option("--host <host>", "CUCM hostname (overrides config/env)")
14
26
  .option("--username <user>", "CUCM username (overrides config/env)")
@@ -33,6 +45,7 @@ require("./commands/sql.js")(program);
33
45
  require("./commands/execute.js")(program);
34
46
  require("./commands/operations.js")(program);
35
47
  require("./commands/describe.js")(program);
48
+ require("./commands/doctor.js")(program);
36
49
  // require("./commands/phone.js")(program);
37
50
  // require("./commands/line.js")(program);
38
51
  // require("./commands/user.js")(program);
@@ -263,15 +263,30 @@ function resolveSsValue(id, field) {
263
263
 
264
264
  try {
265
265
  const data = JSON.parse(stdout);
266
- // ss-cli returns an object; find the field case-insensitively
267
266
  const fieldLower = field.toLowerCase();
267
+
268
+ // First: check top-level keys (simple key-value secrets)
268
269
  const foundKey = Object.keys(data).find(
269
270
  (k) => k.toLowerCase() === fieldLower
270
271
  );
271
- if (foundKey === undefined) {
272
- return reject(new Error(`Field "${field}" not found in ss-cli response for secret ${id}`));
272
+ if (foundKey !== undefined) {
273
+ return resolve(data[foundKey]);
273
274
  }
274
- resolve(data[foundKey]);
275
+
276
+ // Second: check items array (Secret Server template secrets)
277
+ // Items have fieldName, slug, and itemValue properties
278
+ if (Array.isArray(data.items)) {
279
+ const item = data.items.find(
280
+ (i) =>
281
+ (i.slug && i.slug.toLowerCase() === fieldLower) ||
282
+ (i.fieldName && i.fieldName.toLowerCase() === fieldLower)
283
+ );
284
+ if (item) {
285
+ return resolve(item.itemValue);
286
+ }
287
+ }
288
+
289
+ return reject(new Error(`Field "${field}" not found in secret ${id}`));
275
290
  } catch (parseErr) {
276
291
  reject(new Error(`Failed to parse ss-cli output for secret ${id}: ${parseErr.message}`));
277
292
  }
@@ -0,0 +1,34 @@
1
+ const readline = require("readline");
2
+ const { getRandomWord } = require("./wordlist.js");
3
+
4
+ function checkWriteAllowed(clusterConfig, globalOpts = {}) {
5
+ const readOnly = clusterConfig?.readOnly || globalOpts.readOnly;
6
+ if (!readOnly) return Promise.resolve(true);
7
+
8
+ if (!process.stdin.isTTY) {
9
+ throw new Error(
10
+ "This cluster is configured as read-only. " +
11
+ "Interactive TTY required for write confirmation. " +
12
+ "Change config with: cisco-axl config update <name> --no-read-only"
13
+ );
14
+ }
15
+
16
+ const word = getRandomWord();
17
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
18
+
19
+ return new Promise((resolve, reject) => {
20
+ rl.question(
21
+ `\n⚠ This cluster is configured as read-only.\nTo proceed, type "${word}" to confirm: `,
22
+ (answer) => {
23
+ rl.close();
24
+ if (answer.trim().toLowerCase() === word.toLowerCase()) {
25
+ resolve({ confirmed: true, word });
26
+ } else {
27
+ reject(new Error("Confirmation failed. Write operation cancelled."));
28
+ }
29
+ }
30
+ );
31
+ });
32
+ }
33
+
34
+ module.exports = { checkWriteAllowed };
@@ -119,9 +119,13 @@ async function resolveConfig(flags = {}) {
119
119
  async function createService(flags = {}) {
120
120
  const config = await resolveConfig(flags);
121
121
 
122
- // Handle --insecure flag
122
+ // Handle --insecure flag — suppress Node's TLS warning since user opted in
123
123
  if (config.insecure || flags.insecure) {
124
124
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
125
+ process.emitWarning = ((orig) => function (warning, ...args) {
126
+ if (typeof warning === "string" && warning.includes("NODE_TLS_REJECT_UNAUTHORIZED")) return;
127
+ return orig.call(process, warning, ...args);
128
+ })(process.emitWarning);
125
129
  }
126
130
 
127
131
  // Build axlService options
@@ -1,31 +1,26 @@
1
1
  "use strict";
2
2
 
3
+ const { checkWriteAllowed } = require("./confirm.js");
4
+
3
5
  /**
4
6
  * Check if read-only mode is active (via --read-only flag or cluster config).
5
- * Throws an error if a write operation is attempted in read-only mode.
7
+ * If read-only, requires interactive TTY confirmation with a random word.
8
+ * Non-interactive sessions (AI agents without TTY) fail automatically.
6
9
  *
7
10
  * @param {object} globalOpts - Commander global options
8
11
  * @param {string} operation - The operation being attempted (for error message)
9
12
  */
10
- function enforceReadOnly(globalOpts, operation) {
11
- if (globalOpts.readOnly) {
12
- throw new Error(
13
- `Operation "${operation}" blocked — read-only mode is active.\n` +
14
- "Read-only mode only allows: get, list, describe, operations, sql query.\n" +
15
- "Remove --read-only flag to perform write operations."
16
- );
17
- }
18
-
19
- // Also check cluster config for readOnly setting
13
+ async function enforceReadOnly(globalOpts, operation) {
20
14
  const { getActiveCluster } = require("./config.js");
21
15
  const cluster = getActiveCluster(globalOpts.cluster);
22
- if (cluster && cluster.readOnly) {
23
- throw new Error(
24
- `Operation "${operation}" blocked cluster "${cluster.name}" is configured as read-only.\n` +
25
- "Update the cluster config to allow write operations:\n" +
26
- ` cisco-axl config add ${cluster.name} ... (without read-only)`
27
- );
28
- }
16
+ const clusterConfig = cluster || {};
17
+
18
+ // Build a combined config for checkWriteAllowed
19
+ const config = {
20
+ readOnly: globalOpts.readOnly || clusterConfig.readOnly || false,
21
+ };
22
+
23
+ await checkWriteAllowed(config, globalOpts);
29
24
  }
30
25
 
31
26
  module.exports = { enforceReadOnly };
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Read all data from stdin as a string.
5
+ * Returns null if stdin is a TTY (no piped input).
6
+ */
7
+ function readStdin() {
8
+ return new Promise((resolve, reject) => {
9
+ if (process.stdin.isTTY) {
10
+ return resolve(null);
11
+ }
12
+ const chunks = [];
13
+ process.stdin.setEncoding("utf-8");
14
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
15
+ process.stdin.on("end", () => resolve(chunks.join("")));
16
+ process.stdin.on("error", reject);
17
+ });
18
+ }
19
+
20
+ module.exports = { readStdin };
@@ -0,0 +1,9 @@
1
+ const crypto = require("crypto");
2
+
3
+ function getRandomWord() {
4
+ // Generate a random 8-char string that doesn't exist in the codebase
5
+ // Not guessable, not brute-forceable from a known word list
6
+ return crypto.randomBytes(4).toString("hex");
7
+ }
8
+
9
+ module.exports = { getRandomWord };
package/docs/api.md ADDED
@@ -0,0 +1,191 @@
1
+ # Library API Reference
2
+
3
+ ## Setup
4
+
5
+ ```javascript
6
+ const axlService = require("cisco-axl");
7
+
8
+ let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
9
+
10
+ // With options
11
+ let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0", {
12
+ logging: { level: "info" },
13
+ retry: { retries: 3, retryDelay: 1000 }
14
+ });
15
+ ```
16
+
17
+ ## ESM / TypeScript
18
+
19
+ ```javascript
20
+ // CommonJS
21
+ const axlService = require("cisco-axl");
22
+
23
+ // ESM
24
+ import axlService from "cisco-axl";
25
+ import { AXLAuthError, AXLOperationError } from "cisco-axl";
26
+ ```
27
+
28
+ ```typescript
29
+ import axlService from 'cisco-axl';
30
+
31
+ const service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
32
+
33
+ const tags = await service.getOperationTags("listRoutePartition");
34
+ tags.searchCriteria.name = "%%";
35
+ const result = await service.executeOperation("listRoutePartition", tags);
36
+ ```
37
+
38
+ See the `examples/typescript` directory for more examples.
39
+
40
+ ## Logging
41
+
42
+ ```javascript
43
+ // Via environment variable
44
+ // DEBUG=true
45
+
46
+ // Via constructor
47
+ let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0", {
48
+ logging: {
49
+ level: "info", // "error" | "warn" | "info" | "debug"
50
+ handler: (level, message, data) => {
51
+ myLogger[level](message, data);
52
+ }
53
+ }
54
+ });
55
+
56
+ // Change at runtime
57
+ service.setLogLevel("debug");
58
+ ```
59
+
60
+ ## Convenience Methods
61
+
62
+ ```javascript
63
+ // Get a single item by name or UUID
64
+ await service.getItem("Phone", "SEP001122334455");
65
+ await service.getItem("Phone", { uuid: "abc-123" });
66
+
67
+ // List items with search criteria and returned tags
68
+ await service.listItems("RoutePartition"); // all partitions
69
+ await service.listItems("Phone", { name: "SEP%" }, { name: "", model: "" });
70
+
71
+ // Add, update, remove
72
+ await service.addItem("RoutePartition", { name: "NEW-PT", description: "New" });
73
+ await service.updateItem("Phone", "SEP001122334455", { description: "Updated" });
74
+ await service.removeItem("RoutePartition", "NEW-PT");
75
+
76
+ // SQL
77
+ const rows = await service.executeSqlQuery("SELECT name FROM routepartition");
78
+ await service.executeSqlUpdate("UPDATE routepartition SET description='test' WHERE name='NEW-PT'");
79
+ ```
80
+
81
+ ## Operation Discovery
82
+
83
+ ```javascript
84
+ // List all operations
85
+ const ops = await service.returnOperations();
86
+ const phoneOps = await service.returnOperations("phone");
87
+
88
+ // Get tag schema
89
+ const tags = await service.getOperationTags("addRoutePartition");
90
+
91
+ // Get detailed metadata (required, nillable, type)
92
+ const detailed = await service.getOperationTagsDetailed("addRoutePartition");
93
+ console.log(detailed.routePartition.required); // true
94
+ console.log(detailed.routePartition.children.name.type); // "string"
95
+ ```
96
+
97
+ ## Execute Any Operation
98
+
99
+ ```javascript
100
+ const tags = await service.getOperationTags("addRoutePartition");
101
+ tags.routePartition.name = "INTERNAL-PT";
102
+ tags.routePartition.description = "Internal directory numbers";
103
+
104
+ const result = await service.executeOperation("addRoutePartition", tags);
105
+ console.log("UUID:", result);
106
+ ```
107
+
108
+ ## Batch Operations
109
+
110
+ ```javascript
111
+ const results = await service.executeBatch([
112
+ { operation: "getPhone", tags: { name: "SEP001122334455" } },
113
+ { operation: "getPhone", tags: { name: "SEP556677889900" } },
114
+ ], 5); // concurrency limit
115
+
116
+ results.forEach((r) => {
117
+ console.log(r.success ? `${r.operation}: OK` : `${r.operation}: ${r.error.message}`);
118
+ });
119
+ ```
120
+
121
+ ## Error Handling
122
+
123
+ ```javascript
124
+ const { AXLAuthError, AXLNotFoundError, AXLOperationError, AXLValidationError } = require("cisco-axl");
125
+
126
+ try {
127
+ await service.executeOperation("getPhone", { name: "INVALID" });
128
+ } catch (error) {
129
+ if (error instanceof AXLAuthError) console.log("Bad credentials");
130
+ else if (error instanceof AXLNotFoundError) console.log("Operation not found:", error.operation);
131
+ else if (error instanceof AXLOperationError) console.log("SOAP fault:", error.message);
132
+ else if (error instanceof AXLValidationError) console.log("Invalid input:", error.message);
133
+ }
134
+ ```
135
+
136
+ ## Retry Configuration
137
+
138
+ ```javascript
139
+ let service = new axlService("10.10.20.1", "admin", "pass", "14.0", {
140
+ retry: {
141
+ retries: 3,
142
+ retryDelay: 1000,
143
+ retryOn: (error) => error.message.includes("ECONNRESET")
144
+ }
145
+ });
146
+ ```
147
+
148
+ ## json-variables Support
149
+
150
+ ```javascript
151
+ var lineTemplate = {
152
+ pattern: "%%_extension_%%",
153
+ alertingName: "%%_firstName_%% %%_lastName_%%",
154
+ description: "%%_firstName_%% %%_lastName_%%",
155
+ _data: {
156
+ extension: "1001",
157
+ firstName: "Tom",
158
+ lastName: "Smith",
159
+ },
160
+ };
161
+
162
+ const lineTags = jVar(lineTemplate);
163
+ await service.executeOperation("updateLine", lineTags);
164
+ ```
165
+
166
+ ## Methods Reference
167
+
168
+ ### Core
169
+
170
+ | Method | Description |
171
+ |--------|-------------|
172
+ | `new axlService(host, user, pass, version, opts?)` | Constructor |
173
+ | `testAuthentication()` | Test credentials against AXL endpoint |
174
+ | `returnOperations(filter?)` | List available operations |
175
+ | `getOperationTags(operation)` | Get tag schema for an operation |
176
+ | `getOperationTagsDetailed(operation)` | Get detailed tag metadata (required/nillable/type) |
177
+ | `executeOperation(operation, tags, opts?)` | Execute any AXL operation |
178
+ | `executeBatch(operations[], concurrency?)` | Parallel batch execution |
179
+ | `setLogLevel(level)` | Change log level at runtime |
180
+
181
+ ### Convenience
182
+
183
+ | Method | Description |
184
+ |--------|-------------|
185
+ | `getItem(type, identifier, opts?)` | Get single item by name or UUID |
186
+ | `listItems(type, search?, returnedTags?, opts?)` | List items with filtering |
187
+ | `addItem(type, data, opts?)` | Add a new item |
188
+ | `updateItem(type, identifier, updates, opts?)` | Update an existing item |
189
+ | `removeItem(type, identifier, opts?)` | Remove an item |
190
+ | `executeSqlQuery(sql)` | Run a SQL SELECT query |
191
+ | `executeSqlUpdate(sql)` | Run a SQL INSERT/UPDATE/DELETE |