cisco-axl 2.1.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.
@@ -3,11 +3,11 @@ name: Release
3
3
  on:
4
4
  push:
5
5
  tags:
6
- - 'v*'
6
+ - "v*"
7
7
 
8
8
  permissions:
9
- id-token: write # Required for OIDC authentication
10
- contents: write # Required for creating GitHub Release
9
+ id-token: write # Required for OIDC authentication
10
+ contents: write # Required for creating GitHub Release
11
11
 
12
12
  jobs:
13
13
  publish:
@@ -19,8 +19,8 @@ jobs:
19
19
  - name: Setup Node.js
20
20
  uses: actions/setup-node@v4
21
21
  with:
22
- node-version: '22'
23
- registry-url: 'https://registry.npmjs.org'
22
+ node-version: "22"
23
+ registry-url: "https://registry.npmjs.org"
24
24
 
25
25
  - name: Update npm
26
26
  run: npm install -g npm@latest
@@ -41,7 +41,7 @@ jobs:
41
41
  run: node -e "const axlService = require('./dist/index.js'); console.log('CJS require OK');"
42
42
 
43
43
  - name: Publish to npm
44
- run: npm publish --access public
44
+ run: npm publish --access public --provenance
45
45
 
46
46
  - name: Create GitHub Release
47
47
  uses: softprops/action-gh-release@v2
package/README.md CHANGED
@@ -42,11 +42,7 @@ If you are using self-signed certificates on Cisco VOS products you may need to
42
42
 
43
43
  Supported CUCM versions: `11.0`, `11.5`, `12.0`, `12.5`, `14.0`, `15.0`
44
44
 
45
- ## CLI
46
-
47
- The CLI provides full AXL access from the command line — CRUD operations, SQL queries, operation discovery, bulk provisioning from CSV, and a raw execute escape hatch for any AXL operation.
48
-
49
- ### Quick Start
45
+ ## Quick Start
50
46
 
51
47
  ```bash
52
48
  # Configure a cluster
@@ -59,27 +55,31 @@ cisco-axl config test
59
55
  cisco-axl list Phone --search "name=SEP%"
60
56
 
61
57
  # Get a specific phone
62
- cisco-axl get Phone SEP001122334455 --returned-tags "name,model,description"
58
+ cisco-axl get Phone SEP001122334455
63
59
 
64
60
  # SQL query
65
61
  cisco-axl sql query "SELECT name, description FROM device WHERE name LIKE 'SEP%'"
62
+ ```
66
63
 
67
- # Discover available operations
68
- cisco-axl operations --filter phone
69
- cisco-axl operations --type action --filter phone
70
-
71
- # Describe what tags an operation needs
72
- cisco-axl describe getPhone --detailed
64
+ ## Configuration
73
65
 
74
- # Execute any AXL operation
75
- cisco-axl execute doLdapSync --tags '{"name":"LDAP_Main"}'
66
+ ```bash
67
+ cisco-axl config add <name> --host <host> --username <user> --password <pass> --cucm-version <ver> --insecure
68
+ cisco-axl config use <name> # switch active cluster
69
+ cisco-axl config list # list all clusters
70
+ cisco-axl config show # show active cluster (masks passwords)
71
+ cisco-axl config remove <name> # remove a cluster
72
+ cisco-axl config test # test connectivity
76
73
  ```
77
74
 
78
- ### Commands
75
+ Auth precedence: CLI flags > env vars (`CUCM_HOST`, `CUCM_USERNAME`, `CUCM_PASSWORD`, `CUCM_VERSION`) > config file.
76
+
77
+ Config stored at `~/.cisco-axl/config.json`. Supports [ss-cli](https://github.com/sieteunoseis/ss-cli) `<ss:ID:field>` placeholders.
78
+
79
+ ## CLI Commands
79
80
 
80
81
  | Command | Description |
81
82
  |---------|-------------|
82
- | `config add/use/list/show/remove/test` | Manage multi-cluster configurations |
83
83
  | `get <type> <identifier>` | Get a single item |
84
84
  | `list <type>` | List items with search, pagination, returned tags |
85
85
  | `add <type>` | Add an item (inline JSON, template, or bulk CSV) |
@@ -87,313 +87,52 @@ cisco-axl execute doLdapSync --tags '{"name":"LDAP_Main"}'
87
87
  | `remove <type> <identifier>` | Remove an item |
88
88
  | `sql query/update` | Execute SQL against CUCM |
89
89
  | `execute <operation>` | Run any raw AXL operation |
90
- | `operations` | List available operations with `--filter` and `--type crud\|action` |
91
- | `describe <operation>` | Show tag schema with `--detailed` for required/optional/type info |
92
-
93
- ### Configuration
94
-
95
- ```bash
96
- # Multiple clusters
97
- cisco-axl config add lab --host 10.0.0.1 --username admin --password secret --cucm-version 14.0 --insecure
98
- cisco-axl config add prod --host 10.0.0.2 --username axladmin --password secret --cucm-version 15.0 --insecure
99
- cisco-axl config use prod
100
- cisco-axl config list
101
-
102
- # Per-command cluster override
103
- cisco-axl list Phone --search "name=SEP%" --cluster lab
104
-
105
- # Environment variables (CI/CD, AI agents)
106
- export CUCM_HOST=10.0.0.1 CUCM_USERNAME=admin CUCM_PASSWORD=secret CUCM_VERSION=14.0
107
- ```
108
-
109
- Config stored at `~/.cisco-axl/config.json`. Supports optional [Secret Server](https://github.com/sieteunoseis/ss-cli) integration via `<ss:ID:field>` placeholders.
110
-
111
- ### Output Formats
112
-
113
- ```bash
114
- cisco-axl list Phone --search "name=SEP%" --format table # default, human-readable
115
- cisco-axl list Phone --search "name=SEP%" --format json # structured JSON
116
- cisco-axl list Phone --search "name=SEP%" --format toon # token-efficient for AI agents
117
- cisco-axl list Phone --search "name=SEP%" --format csv # spreadsheet export
118
- ```
119
-
120
- ### Bulk Operations from CSV
121
-
122
- Requires optional packages: `npm install json-variables csv-parse`
123
-
124
- ```bash
125
- # Bulk add phones from template + CSV
126
- cisco-axl add Phone --template phone-template.json --csv phones.csv
127
- cisco-axl add Phone --template phone-template.json --csv phones.csv --dry-run # preview first
128
-
129
- # Single template with inline vars
130
- cisco-axl add Phone --template phone-template.json --vars '{"mac":"001122334455","dp":"DP_HQ"}'
131
- ```
132
-
133
- Template file (`phone-template.json`):
134
- ```json
135
- {
136
- "name": "SEP%%mac%%",
137
- "devicePoolName": "%%devicePool%%",
138
- "description": "%%description%%",
139
- "protocol": "SIP"
140
- }
141
- ```
142
-
143
- ### Global Flags
144
-
145
- ```
146
- --format table|json|toon|csv Output format (default: table)
147
- --insecure Skip TLS certificate verification
148
- --clean Remove empty/null values from results
149
- --no-attributes Remove XML attributes from results
150
- --read-only Restrict to read-only operations
151
- --no-audit Disable audit logging for this command
152
- --debug Enable debug logging
153
- ```
154
-
155
- ### Command Chaining
156
-
157
- Shell `&&` chains commands sequentially — each waits for the previous to complete, and the chain stops on the first failure:
158
-
159
- ```bash
160
- # Create a partition, CSS, and line in order
161
- cisco-axl add RoutePartition --data '{"name":"PT_INTERNAL","description":"Internal"}' && \
162
- cisco-axl add Css --data '{"name":"CSS_INTERNAL","members":{"member":{"routePartitionName":"PT_INTERNAL","index":"1"}}}' && \
163
- cisco-axl add Line --data '{"pattern":"1000","routePartitionName":"PT_INTERNAL"}'
164
- ```
165
-
166
- ### Piping with --stdin
167
-
168
- Use `--stdin` to pipe JSON between commands or from other tools like `jq`:
169
-
170
- ```bash
171
- # Get a phone's config, modify it with jq, update it
172
- cisco-axl get Phone SEP001122334455 --format json | \
173
- jq '.description = "Updated via pipe"' | \
174
- cisco-axl update Phone SEP001122334455 --stdin
175
-
176
- # Pipe JSON from a file
177
- cat phone-config.json | cisco-axl add Phone --stdin
178
-
179
- # Discover tags, fill them in, execute
180
- cisco-axl describe applyPhone --format json | \
181
- jq '.name = "SEP001122334455"' | \
182
- cisco-axl execute applyPhone --stdin
183
- ```
90
+ | `operations` | List available operations |
91
+ | `describe <operation>` | Show tag schema for an operation |
92
+ | `doctor` | Check AXL connectivity and health |
184
93
 
185
- The `--stdin` flag is available on `add`, `update`, and `execute`. It is mutually exclusive with `--data`/`--tags` and `--template`.
94
+ See [full CLI reference](docs/cli.md) for bulk CSV, command chaining, piping with `--stdin`, and operation discovery.
186
95
 
187
- ### Audit Trail
96
+ ## Global Flags
188
97
 
189
- All operations are logged to `~/.cisco-axl/audit.jsonl` (JSONL format). Credentials are never logged. Use `--no-audit` to skip.
98
+ | Flag | Description |
99
+ |------|-------------|
100
+ | `--format table\|json\|toon\|csv` | Output format (default: table) |
101
+ | `--insecure` | Skip TLS certificate verification |
102
+ | `--clean` | Remove empty/null values from results |
103
+ | `--no-attributes` | Remove XML attributes from results |
104
+ | `--read-only` | Restrict to read-only operations |
105
+ | `--no-audit` | Disable audit logging for this command |
106
+ | `--debug` | Enable debug logging |
190
107
 
191
108
  ## Library API
192
109
 
193
- ### Setup
194
-
195
110
  ```javascript
196
111
  const axlService = require("cisco-axl");
112
+ const service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
197
113
 
198
- let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
199
-
200
- // With options
201
- let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0", {
202
- logging: { level: "info" },
203
- retry: { retries: 3, retryDelay: 1000 }
204
- });
205
- ```
206
-
207
- ### Logging
208
-
209
- ```javascript
210
- // Via environment variable
211
- // DEBUG=true
212
-
213
- // Via constructor
214
- let service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0", {
215
- logging: {
216
- level: "info", // "error" | "warn" | "info" | "debug"
217
- handler: (level, message, data) => {
218
- myLogger[level](message, data);
219
- }
220
- }
221
- });
222
-
223
- // Change at runtime
224
- service.setLogLevel("debug");
225
- ```
226
-
227
- ### Convenience Methods
228
-
229
- ```javascript
230
- // Get a single item by name or UUID
114
+ // Get, list, add, update, remove
231
115
  await service.getItem("Phone", "SEP001122334455");
232
- await service.getItem("Phone", { uuid: "abc-123" });
233
-
234
- // List items with search criteria and returned tags
235
- await service.listItems("RoutePartition"); // all partitions
236
- await service.listItems("Phone", { name: "SEP%" }, { name: "", model: "" });
237
-
238
- // Add, update, remove
116
+ await service.listItems("Phone", { name: "SEP%" });
239
117
  await service.addItem("RoutePartition", { name: "NEW-PT", description: "New" });
240
- await service.updateItem("Phone", "SEP001122334455", { description: "Updated" });
241
- await service.removeItem("RoutePartition", "NEW-PT");
242
118
 
243
119
  // SQL
244
- const rows = await service.executeSqlQuery("SELECT name FROM routepartition");
245
- await service.executeSqlUpdate("UPDATE routepartition SET description='test' WHERE name='NEW-PT'");
246
- ```
247
-
248
- ### Operation Discovery
120
+ await service.executeSqlQuery("SELECT name FROM routepartition");
249
121
 
250
- ```javascript
251
- // List all operations
252
- const ops = await service.returnOperations();
253
- const phoneOps = await service.returnOperations("phone");
254
-
255
- // Get tag schema
256
- const tags = await service.getOperationTags("addRoutePartition");
257
-
258
- // Get detailed metadata (required, nillable, type)
259
- const detailed = await service.getOperationTagsDetailed("addRoutePartition");
260
- console.log(detailed.routePartition.required); // true
261
- console.log(detailed.routePartition.children.name.type); // "string"
262
- ```
263
-
264
- ### Execute Any Operation
265
-
266
- ```javascript
122
+ // Any AXL operation
267
123
  const tags = await service.getOperationTags("addRoutePartition");
268
124
  tags.routePartition.name = "INTERNAL-PT";
269
- tags.routePartition.description = "Internal directory numbers";
270
-
271
- const result = await service.executeOperation("addRoutePartition", tags);
272
- console.log("UUID:", result);
273
- ```
274
-
275
- ### Batch Operations
276
-
277
- ```javascript
278
- const results = await service.executeBatch([
279
- { operation: "getPhone", tags: { name: "SEP001122334455" } },
280
- { operation: "getPhone", tags: { name: "SEP556677889900" } },
281
- ], 5); // concurrency limit
282
-
283
- results.forEach((r) => {
284
- console.log(r.success ? `${r.operation}: OK` : `${r.operation}: ${r.error.message}`);
285
- });
286
- ```
287
-
288
- ### Error Handling
289
-
290
- ```javascript
291
- const { AXLAuthError, AXLNotFoundError, AXLOperationError, AXLValidationError } = require("cisco-axl");
292
-
293
- try {
294
- await service.executeOperation("getPhone", { name: "INVALID" });
295
- } catch (error) {
296
- if (error instanceof AXLAuthError) console.log("Bad credentials");
297
- else if (error instanceof AXLNotFoundError) console.log("Operation not found:", error.operation);
298
- else if (error instanceof AXLOperationError) console.log("SOAP fault:", error.message);
299
- else if (error instanceof AXLValidationError) console.log("Invalid input:", error.message);
300
- }
301
- ```
302
-
303
- ### Retry Configuration
304
-
305
- ```javascript
306
- let service = new axlService("10.10.20.1", "admin", "pass", "14.0", {
307
- retry: {
308
- retries: 3,
309
- retryDelay: 1000,
310
- retryOn: (error) => error.message.includes("ECONNRESET")
311
- }
312
- });
313
- ```
314
-
315
- ### ESM Support
316
-
317
- ```javascript
318
- // CommonJS
319
- const axlService = require("cisco-axl");
320
-
321
- // ESM
322
- import axlService from "cisco-axl";
323
- import { AXLAuthError, AXLOperationError } from "cisco-axl";
324
- ```
325
-
326
- ### json-variables Support
327
-
328
- ```javascript
329
- var lineTemplate = {
330
- pattern: "%%_extension_%%",
331
- alertingName: "%%_firstName_%% %%_lastName_%%",
332
- description: "%%_firstName_%% %%_lastName_%%",
333
- _data: {
334
- extension: "1001",
335
- firstName: "Tom",
336
- lastName: "Smith",
337
- },
338
- };
339
-
340
- const lineTags = jVar(lineTemplate);
341
- await service.executeOperation("updateLine", lineTags);
342
- ```
343
-
344
- ## Methods Reference
345
-
346
- ### Core
347
-
348
- | Method | Description |
349
- |--------|-------------|
350
- | `new axlService(host, user, pass, version, opts?)` | Constructor |
351
- | `testAuthentication()` | Test credentials against AXL endpoint |
352
- | `returnOperations(filter?)` | List available operations |
353
- | `getOperationTags(operation)` | Get tag schema for an operation |
354
- | `getOperationTagsDetailed(operation)` | Get detailed tag metadata (required/nillable/type) |
355
- | `executeOperation(operation, tags, opts?)` | Execute any AXL operation |
356
- | `executeBatch(operations[], concurrency?)` | Parallel batch execution |
357
- | `setLogLevel(level)` | Change log level at runtime |
358
-
359
- ### Convenience
360
-
361
- | Method | Description |
362
- |--------|-------------|
363
- | `getItem(type, identifier, opts?)` | Get single item by name or UUID |
364
- | `listItems(type, search?, returnedTags?, opts?)` | List items with filtering |
365
- | `addItem(type, data, opts?)` | Add a new item |
366
- | `updateItem(type, identifier, updates, opts?)` | Update an existing item |
367
- | `removeItem(type, identifier, opts?)` | Remove an item |
368
- | `executeSqlQuery(sql)` | Run a SQL SELECT query |
369
- | `executeSqlUpdate(sql)` | Run a SQL INSERT/UPDATE/DELETE |
370
-
371
- ## Examples
372
-
373
- Check the **examples** folder for different ways to use this library.
374
-
375
- Run the integration tests against a CUCM cluster:
376
-
377
- ```bash
378
- npm run staging
125
+ await service.executeOperation("addRoutePartition", tags);
379
126
  ```
380
127
 
381
- ## TypeScript Support
128
+ See [full API documentation](docs/api.md) for all methods, error handling, batch operations, TypeScript, retry config, and logging.
382
129
 
383
- ```typescript
384
- import axlService from 'cisco-axl';
385
-
386
- const service = new axlService("10.10.20.1", "administrator", "ciscopsdt", "14.0");
387
-
388
- const tags = await service.getOperationTags("listRoutePartition");
389
- tags.searchCriteria.name = "%%";
390
- const result = await service.executeOperation("listRoutePartition", tags);
391
- ```
130
+ ## Giving Back
392
131
 
393
- See the `examples/typescript` directory for more examples.
132
+ If you found this helpful, consider:
394
133
 
395
- ## Giving Back
134
+ [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://buymeacoffee.com/automatebldrs)
396
135
 
397
- If you would like to support my work and the time I put in creating the code, you can click the image below to get me a coffee. I would really appreciate it (but is not required).
136
+ ## License
398
137
 
399
- [Buy Me a Coffee](https://www.buymeacoffee.com/automatebldrs)
138
+ MIT
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "cisco-axl",
3
+ "description": "Hooks for Cisco CUCM AXL CLI",
4
+ "author": "sieteunoseis",
5
+ "version": "1.0.0",
6
+ "hooks": [
7
+ {
8
+ "name": "Block write operations",
9
+ "description": "Blocks add, update, remove, execute, and sql update commands",
10
+ "default": true,
11
+ "event": "PreToolUse",
12
+ "matcher": "Bash",
13
+ "hook": {
14
+ "type": "command",
15
+ "command": "jq -r '.tool_input.command' | { read -r cmd; if echo \"$cmd\" | grep -qE '^(npx )?cisco-axl (add|update|remove|execute|sql update)'; then echo '{\"decision\":\"block\",\"reason\":\"BLOCKED: cisco-axl write operation. Use --read-only or get explicit user approval.\"}'; fi; }"
16
+ }
17
+ },
18
+ {
19
+ "name": "Enforce --read-only flag",
20
+ "description": "Requires --read-only on every cisco-axl command",
21
+ "default": false,
22
+ "event": "PreToolUse",
23
+ "matcher": "Bash",
24
+ "hook": {
25
+ "type": "command",
26
+ "command": "jq -r '.tool_input.command' | { read -r cmd; if echo \"$cmd\" | grep -qE '^(npx )?cisco-axl ' && ! echo \"$cmd\" | grep -q '\\-\\-read-only'; then echo '{\"decision\":\"block\",\"reason\":\"BLOCKED: cisco-axl must be run with --read-only. Retry with the flag.\"}'; fi; }"
27
+ }
28
+ }
29
+ ]
30
+ }
@@ -34,7 +34,7 @@ module.exports = function registerAddCommand(program) {
34
34
  let errorMsg;
35
35
 
36
36
  try {
37
- enforceReadOnly(globalOpts, "add");
37
+ await enforceReadOnly(globalOpts, "add");
38
38
 
39
39
  // Read from stdin if --stdin flag is set
40
40
  if (cmdOpts.stdin) {
@@ -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
+ };
@@ -34,7 +34,7 @@ module.exports = function registerExecuteCommand(program) {
34
34
  let errorMsg;
35
35
 
36
36
  try {
37
- enforceReadOnly(globalOpts, "execute");
37
+ await enforceReadOnly(globalOpts, "execute");
38
38
 
39
39
  // Read from stdin if --stdin flag is set
40
40
  if (cmdOpts.stdin) {
@@ -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 = {
@@ -34,7 +34,7 @@ module.exports = function registerUpdateCommand(program) {
34
34
  let errorMsg;
35
35
 
36
36
  try {
37
- enforceReadOnly(globalOpts, "update");
37
+ await enforceReadOnly(globalOpts, "update");
38
38
 
39
39
  // Read from stdin if --stdin flag is set
40
40
  if (cmdOpts.stdin) {
package/cli/index.js CHANGED
@@ -3,9 +3,17 @@
3
3
  const { Command } = require("commander");
4
4
  const pkg = require("../package.json");
5
5
 
6
- import("update-notifier").then(({ default: updateNotifier }) => {
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");
7
15
  updateNotifier({ pkg }).notify();
8
- }).catch(() => {});
16
+ } catch {};
9
17
 
10
18
  const program = new Command();
11
19
 
@@ -37,6 +45,7 @@ require("./commands/sql.js")(program);
37
45
  require("./commands/execute.js")(program);
38
46
  require("./commands/operations.js")(program);
39
47
  require("./commands/describe.js")(program);
48
+ require("./commands/doctor.js")(program);
40
49
  // require("./commands/phone.js")(program);
41
50
  // require("./commands/line.js")(program);
42
51
  // require("./commands/user.js")(program);
@@ -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 };
@@ -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,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 |
@@ -0,0 +1,85 @@
1
+ # Claude Code Hooks for cisco-axl
2
+
3
+ [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) let you enforce guardrails when AI agents use the CLI. The examples below block write operations so Claude can only read from CUCM.
4
+
5
+ ## Quick Install
6
+
7
+ Install the write-safety hook with one command using [cc-hooks-install](https://github.com/sieteunoseis/cc-hooks-install):
8
+
9
+ ```bash
10
+ npx cc-hooks-install add sieteunoseis/cisco-axl
11
+ ```
12
+
13
+ This fetches the hook definitions from this repo, shows an interactive prompt to select which hooks to install, and merges them into your `~/.claude/settings.json`.
14
+
15
+ If you prefer to install manually, see below.
16
+
17
+ ## Block Write Operations
18
+
19
+ Add this to your `~/.claude/settings.json` (global) or `.claude/settings.json` (project-level) under `hooks.PreToolUse`:
20
+
21
+ ```json
22
+ {
23
+ "hooks": {
24
+ "PreToolUse": [
25
+ {
26
+ "matcher": "Bash",
27
+ "hooks": [
28
+ {
29
+ "type": "command",
30
+ "command": "jq -r '.tool_input.command' | { read -r cmd; if echo \"$cmd\" | grep -qE '^(npx )?cisco-axl (add|update|remove|execute|sql update)'; then echo '{\"decision\":\"block\",\"reason\":\"BLOCKED: cisco-axl write operation. Use --read-only or get explicit user approval.\"}'; fi; }"
31
+ }
32
+ ]
33
+ }
34
+ ]
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### What it blocks
40
+
41
+ | Command | Blocked | Why |
42
+ | ---------------------------------------------- | ------- | ------------------- |
43
+ | `cisco-axl get Phone SEP...` | No | Read operation |
44
+ | `cisco-axl list Phone --search "name=SEP%"` | No | Read operation |
45
+ | `cisco-axl sql query "SELECT ..."` | No | Read-only SQL |
46
+ | `cisco-axl operations --filter phone` | No | Schema discovery |
47
+ | `cisco-axl describe getPhone` | No | Schema discovery |
48
+ | `cisco-axl add Phone --data '{...}'` | **Yes** | Creates a resource |
49
+ | `cisco-axl update Phone SEP... --data '{...}'` | **Yes** | Modifies a resource |
50
+ | `cisco-axl remove Phone SEP...` | **Yes** | Deletes a resource |
51
+ | `cisco-axl execute applyPhone --tags '{...}'` | **Yes** | Executes an action |
52
+ | `cisco-axl sql update "UPDATE ..."` | **Yes** | Modifies database |
53
+
54
+ ### Alternative: Use the built-in `--read-only` flag
55
+
56
+ The CLI has a native `--read-only` flag that restricts to `get`, `list`, `describe`, `operations`, and `sql query`:
57
+
58
+ ```bash
59
+ cisco-axl --read-only add Phone --data '{...}'
60
+ # Error: Write operations are not allowed in read-only mode
61
+ ```
62
+
63
+ You can enforce this globally by adding a hook that appends `--read-only` to every command:
64
+
65
+ ```json
66
+ {
67
+ "hooks": {
68
+ "PreToolUse": [
69
+ {
70
+ "matcher": "Bash",
71
+ "hooks": [
72
+ {
73
+ "type": "command",
74
+ "command": "jq -r '.tool_input.command' | { read -r cmd; if echo \"$cmd\" | grep -qE '^(npx )?cisco-axl ' && ! echo \"$cmd\" | grep -q '\\-\\-read-only'; then echo '{\"decision\":\"block\",\"reason\":\"BLOCKED: cisco-axl must be run with --read-only. Retry with the flag.\"}'; fi; }"
75
+ }
76
+ ]
77
+ }
78
+ ]
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Audit Logging
84
+
85
+ All cisco-axl operations are logged to `~/.cisco-axl/audit.jsonl` by default. This provides a record of every command run by Claude or any other agent.
package/docs/cli.md ADDED
@@ -0,0 +1,109 @@
1
+ # CLI Reference
2
+
3
+ The CLI provides full AXL access from the command line — CRUD operations, SQL queries, operation discovery, bulk provisioning from CSV, and a raw execute escape hatch for any AXL operation.
4
+
5
+ ## Commands
6
+
7
+ | Command | Description |
8
+ |---------|-------------|
9
+ | `config add/use/list/show/remove/test` | Manage multi-cluster configurations |
10
+ | `get <type> <identifier>` | Get a single item |
11
+ | `list <type>` | List items with search, pagination, returned tags |
12
+ | `add <type>` | Add an item (inline JSON, template, or bulk CSV) |
13
+ | `update <type> <identifier>` | Update an item |
14
+ | `remove <type> <identifier>` | Remove an item |
15
+ | `sql query/update` | Execute SQL against CUCM |
16
+ | `execute <operation>` | Run any raw AXL operation |
17
+ | `operations` | List available operations with `--filter` and `--type crud\|action` |
18
+ | `describe <operation>` | Show tag schema with `--detailed` for required/optional/type info |
19
+ | `doctor` | Check AXL connectivity and configuration health |
20
+
21
+ ## Operation Discovery
22
+
23
+ ```bash
24
+ # Discover available operations
25
+ cisco-axl operations --filter phone
26
+ cisco-axl operations --type action --filter phone
27
+
28
+ # Describe what tags an operation needs
29
+ cisco-axl describe getPhone --detailed
30
+
31
+ # Execute any AXL operation
32
+ cisco-axl execute doLdapSync --tags '{"name":"LDAP_Main"}'
33
+ ```
34
+
35
+ ## Bulk Operations from CSV
36
+
37
+ Requires optional packages: `npm install json-variables csv-parse`
38
+
39
+ ```bash
40
+ # Bulk add phones from template + CSV
41
+ cisco-axl add Phone --template phone-template.json --csv phones.csv
42
+ cisco-axl add Phone --template phone-template.json --csv phones.csv --dry-run # preview first
43
+
44
+ # Single template with inline vars
45
+ cisco-axl add Phone --template phone-template.json --vars '{"mac":"001122334455","dp":"DP_HQ"}'
46
+ ```
47
+
48
+ Template file (`phone-template.json`):
49
+ ```json
50
+ {
51
+ "name": "SEP%%mac%%",
52
+ "devicePoolName": "%%devicePool%%",
53
+ "description": "%%description%%",
54
+ "protocol": "SIP"
55
+ }
56
+ ```
57
+
58
+ ## Command Chaining
59
+
60
+ Shell `&&` chains commands sequentially — each waits for the previous to complete, and the chain stops on the first failure:
61
+
62
+ ```bash
63
+ # Create a partition, CSS, and line in order
64
+ cisco-axl add RoutePartition --data '{"name":"PT_INTERNAL","description":"Internal"}' && \
65
+ cisco-axl add Css --data '{"name":"CSS_INTERNAL","members":{"member":{"routePartitionName":"PT_INTERNAL","index":"1"}}}' && \
66
+ cisco-axl add Line --data '{"pattern":"1000","routePartitionName":"PT_INTERNAL"}'
67
+ ```
68
+
69
+ ## Piping with --stdin
70
+
71
+ Use `--stdin` to pipe JSON between commands or from other tools like `jq`:
72
+
73
+ ```bash
74
+ # Get a phone's config, modify it with jq, update it
75
+ cisco-axl get Phone SEP001122334455 --format json | \
76
+ jq '.description = "Updated via pipe"' | \
77
+ cisco-axl update Phone SEP001122334455 --stdin
78
+
79
+ # Pipe JSON from a file
80
+ cat phone-config.json | cisco-axl add Phone --stdin
81
+
82
+ # Discover tags, fill them in, execute
83
+ cisco-axl describe applyPhone --format json | \
84
+ jq '.name = "SEP001122334455"' | \
85
+ cisco-axl execute applyPhone --stdin
86
+ ```
87
+
88
+ The `--stdin` flag is available on `add`, `update`, and `execute`. It is mutually exclusive with `--data`/`--tags` and `--template`.
89
+
90
+ ## Global Flags
91
+
92
+ ```
93
+ --format table|json|toon|csv Output format (default: table)
94
+ --insecure Skip TLS certificate verification
95
+ --clean Remove empty/null values from results
96
+ --no-attributes Remove XML attributes from results
97
+ --read-only Restrict to read-only operations
98
+ --no-audit Disable audit logging for this command
99
+ --debug Enable debug logging
100
+ ```
101
+
102
+ ## Output Formats
103
+
104
+ ```bash
105
+ cisco-axl list Phone --search "name=SEP%" --format table # default, human-readable
106
+ cisco-axl list Phone --search "name=SEP%" --format json # structured JSON
107
+ cisco-axl list Phone --search "name=SEP%" --format toon # token-efficient for AI agents
108
+ cisco-axl list Phone --search "name=SEP%" --format csv # spreadsheet export
109
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cisco-axl",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "A library and CLI for Cisco CUCM AXL operations",
5
5
  "main": "dist/index.js",
6
6
  "module": "main.mjs",
@@ -50,7 +50,7 @@
50
50
  "cli-table3": "^0.6.5",
51
51
  "commander": "^14.0.3",
52
52
  "csv-stringify": "^6.7.0",
53
- "strong-soap": "^5.0.8",
53
+ "strong-soap": "5.0.10",
54
54
  "update-notifier": "^7.3.1"
55
55
  },
56
56
  "devDependencies": {
@@ -1,13 +1,17 @@
1
1
  ---
2
2
  name: cisco-axl-cli
3
3
  description: Use when managing Cisco CUCM via the cisco-axl CLI — phones, lines, route patterns, partitions, calling search spaces, SIP profiles, and any AXL operation. Covers CRUD operations, SQL queries, operation discovery, bulk provisioning from CSV, and raw AXL execute commands.
4
+ license: MIT
5
+ metadata:
6
+ author: sieteunoseis
7
+ version: "1.0.0"
4
8
  ---
5
9
 
6
10
  # Cisco AXL CLI
7
11
 
8
12
  A CLI for Cisco Unified Communications Manager (CUCM) Administrative XML (AXL) operations.
9
13
 
10
- ## Prerequisites
14
+ ## Setup
11
15
 
12
16
  The CLI must be available. Either:
13
17
 
@@ -22,20 +26,22 @@ npm install -g cisco-axl
22
26
  If using npx, prefix all commands with `npx`: `npx cisco-axl list Phone ...`
23
27
  If installed globally, use directly: `cisco-axl list Phone ...`
24
28
 
25
- ## Setup
29
+ ### Configuration
26
30
 
27
- Configure a CUCM cluster:
31
+ Configure a CUCM cluster (interactive prompt for password — never pass credentials on the command line):
28
32
 
29
33
  ```bash
30
- cisco-axl config add <name> --host <hostname> --username <user> --password <pass> --cucm-version <ver> --insecure
34
+ cisco-axl config add <name> --host <hostname> --username <user> --cucm-version <ver> --insecure
35
+ # You will be prompted securely for the password
31
36
  ```
32
37
 
33
38
  Valid versions: 11.0, 11.5, 12.0, 12.5, 14.0, 15.0. Use `--insecure` for self-signed certificates (common in CUCM).
34
39
 
35
- Or use environment variables:
40
+ Or use environment variables (set via your shell profile, a `.env` file, or a secrets manager — never hardcode credentials):
36
41
 
37
42
  ```bash
38
- export CUCM_HOST=10.0.0.1 CUCM_USERNAME=admin CUCM_PASSWORD=secret CUCM_VERSION=14.0
43
+ # These should be set securely, e.g. via dotenv, vault, or shell profile
44
+ # CUCM_HOST, CUCM_USERNAME, CUCM_PASSWORD, CUCM_VERSION
39
45
  ```
40
46
 
41
47
  Test the connection:
@@ -44,7 +50,7 @@ Test the connection:
44
50
  cisco-axl config test
45
51
  ```
46
52
 
47
- ## Common Operations
53
+ ## Common Workflows
48
54
 
49
55
  ### Get a single item
50
56
 
@@ -90,7 +96,7 @@ cisco-axl sql query "SELECT name, description FROM device WHERE name LIKE 'SEP%'
90
96
  cisco-axl sql update "UPDATE device SET description='test' WHERE name='SEP001122334455'"
91
97
  ```
92
98
 
93
- ## Discovering Operations
99
+ ### Discovering Operations
94
100
 
95
101
  This is the CLI's most powerful feature. Discover and use ANY AXL operation dynamically — no static command definitions.
96
102
 
@@ -117,7 +123,7 @@ cisco-axl execute doLdapSync --tags '{"name":"LDAP_Main"}'
117
123
  cisco-axl execute applyPhone --tags '{"name":"SEP001122334455"}'
118
124
  ```
119
125
 
120
- ## Bulk Operations from CSV
126
+ ### Bulk Operations from CSV
121
127
 
122
128
  For provisioning multiple items, use templates with CSV files. Requires optional packages: `npm install json-variables csv-parse`
123
129
 
@@ -173,16 +179,17 @@ Use `--format` to control output:
173
179
 
174
180
  **For AI agents:** Use `--format toon` for list queries to reduce token usage. Use `--format json` when you need to parse nested structures.
175
181
 
176
- ## Multiple Clusters
182
+ ### Multiple Clusters
177
183
 
178
184
  ```bash
179
- cisco-axl config add lab --host 10.0.0.1 --username admin --password pass --cucm-version 14.0 --insecure
180
- cisco-axl config add prod --host 10.0.0.2 --username axladmin --password pass --cucm-version 15.0 --insecure
185
+ cisco-axl config add lab --host 10.0.0.1 --username admin --cucm-version 14.0 --insecure
186
+ cisco-axl config add prod --host 10.0.0.2 --username axladmin --cucm-version 15.0 --insecure
187
+ # You will be prompted securely for each password
181
188
  cisco-axl config use prod
182
189
  cisco-axl list Phone --search "name=SEP%" --cluster lab # override per-command
183
190
  ```
184
191
 
185
- ## Command Chaining
192
+ ### Command Chaining
186
193
 
187
194
  Shell `&&` chains commands sequentially — each waits for the previous to complete, and the chain stops on the first failure:
188
195
 
@@ -193,7 +200,7 @@ cisco-axl add Css --data '{"name":"CSS_INTERNAL","members":{"member":{"routePart
193
200
  cisco-axl add Line --data '{"pattern":"1000","routePartitionName":"PT_INTERNAL"}'
194
201
  ```
195
202
 
196
- ## Piping with --stdin
203
+ ### Piping with --stdin
197
204
 
198
205
  Use `--stdin` to pipe JSON between commands or from other tools:
199
206
 
@@ -210,7 +217,7 @@ cisco-axl describe applyPhone --format json | jq '.name = "SEP001122334455"' | c
210
217
 
211
218
  The `--stdin` flag is available on `add`, `update`, and `execute` commands. It is mutually exclusive with `--data`/`--tags` and `--template`.
212
219
 
213
- ## Tips
220
+ ## Global Flags
214
221
 
215
222
  - Item types are PascalCase matching AXL: `Phone`, `Line`, `RoutePartition`, `Css`, `DevicePool`, `SipTrunk`, `TransPattern`, `RouteGroup`, `RouteList`, etc.
216
223
  - Use `cisco-axl operations` to discover exact type names.